/* match3.css — 三消模組樣式（Dark Launch・A1）
   手機直式優先、安全區避讓；棋盤最大、置中；暖米托盤。
   棋子＝絕對定位，位置由 JS 用 transform 控制（交換／掉落／補棋皆走 transition）。
   ⚠️ 只由 match3-test.html 載入；正式玩家端不引入。 */

.m3-wrap {
  position: relative;
  max-width: 520px;
  margin: 0 auto;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  /* ★左右留白由 12px 收到 8px★：棋盤寬度＝可用寬度，cellSize 直接跟著變大
     （390px 機型：366 → 374，格子 52.3 → 53.4px）。棋子本來就偏小，這是免費的放大。 */
  padding:
    calc(env(safe-area-inset-top) + 8px)
    8px
    calc(env(safe-area-inset-bottom) + 8px);
  box-sizing: border-box;
  /* ★2026-08-28 Responsive Gate 修正★：.m3-holedeco 的邊界裝飾層（苔蘚／碎石／根系）
     依設計「刻意不裁切」（見 match3.css .m3-holedeco 規則的說明：要跨過禁區邊界壓住鄰格
     直角，才能吃掉矩形輪廓——這是把地形畫得自然的關鍵手法，不能動）。
     但這層裝飾實際掛在 .m3-boardwrap 之下、不是 .m3-board 之內，所以 .m3-board 自己的
     overflow:hidden 完全罩不到它；當禁區貼著棋盤外緣時，裝飾會真的畫出視窗外
     （實測 375/390/430px 窄螢幕：最多溢出 ~24px，document 出現可橫向捲動的空間，
     且全站 body/html/#app/.wrap 都沒有 overflow-x:hidden 兜底）。
     這裡只在「頁面」這一層收斂溢出：完全不動 .m3-holedeco/.m3-edge 本身的裁切設定，
     地形裝飾照樣可以跨格自然暈開，只是不再讓玩家在窄螢幕上真的橫向捲出整塊棋盤外。
     m3-wrap 本身只用 position:relative（無 transform/filter），HUD／彈窗等 fixed 定位
     元素不受此裁切影響，一併以真頁 375/390/430px 截圖＋DOM 量測驗證過。 */
  overflow-x: hidden;
}

/* ═══ 關卡 HUD ═══════════════════════════════════════════════════════════
   ★改版理由★：舊版是三塊各自獨立的粉紅／米黃膠囊（關卡名／目標／步數）散在
   一條上，資訊沒有層級、彼此爭搶注意力，在手機上讀起來像 debug 面板。
   現在收成★一整條柔和石材面板、三個分區★，符合神祕石碑／古遺跡的主題：
     左 Lv.N ＋ 關卡副標　中 本關目標（視覺核心）　右 剩餘步數（數字當主角）
   ★不是糖果遊戲的粉紅塑膠 UI★：暖金描邊、半透明石面、低對比柔影。 */
.m3-hud {
  display: flex;
  align-items: stretch;
  gap: 10px;
  margin: 6px 0 10px;
  padding: 8px 12px;
  border-radius: 16px;
  /* 柔和石材／羊皮紙面板：半透明暖米，壓在深藍黑背景上 */
  background: linear-gradient(180deg, rgba(74,63,48,.92), rgba(52,44,34,.92));
  border: 1px solid rgba(226,192,122,.42);            /* 暖金描邊 */
  box-shadow: 0 4px 14px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,240,205,.14);
}
/* 三個分區：左右固定內容寬、中間目標吃掉剩餘空間（多目標時才有地方長） */
.m3-hud-lv    { display: flex; flex-direction: column; justify-content: center; min-width: 0; flex: 0 1 auto; }
/* ⚡ 探險能量：★放在 Header 標題左側★（店長定案）
   它是整個三消共用的資源，不屬於某一關 —— 所以不該待在 Lv 卡片裡。
   ★只顯示 ⚡ 與數字★：手機空間有限，不常駐「探險能量」四個字，
   也不常駐倒數（那會讓 Header 很吵）。0/3 要開新局時才另外出提示。
   ★不得增加 Header 高度、不得做成第二列、不得做大型 badge。★ */
.m3-energy-chip {
  flex: 0 0 auto;
  font-size: 13px; font-weight: 800; line-height: 1; letter-spacing: .3px;
  font-variant-numeric: tabular-nums;      /* 3/3 → 2/3 不跳動 */
  color: #ffd98a;
  cursor: pointer; white-space: nowrap; user-select: none;
}
.m3-energy-chip.empty { color: #ff9f7a; }
.m3-energy-chip[hidden] { display: none; }
/* 標題與能量視為中央 group，整體仍接近置中；★不加高 Header★ */
#caveScene .cave-m3-title,
.cave-m3-title { display: flex; align-items: baseline; justify-content: center; gap: 7px; }
@media (max-width: 374px) {
  /* 360px：縮 gap 與字級 —— ★不得用 display:none 讓返回／標題／音量／能量消失★ */
  #caveScene .cave-m3-title, .cave-m3-title { gap: 5px; font-size: 16px; }
  .m3-energy-chip { font-size: 12px; }
}
.m3-hud-goal  { display: flex; flex-direction: column; align-items: center; justify-content: center;
                flex: 1 1 auto; min-width: 0; }
.m3-hud-moves { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 0 0 auto;
                position: relative; }   /* ⏳ 「+3 步」浮字的定位參考點 */
/* ⏳ 時光沙漏的數值回饋：★absolute，不進文件流★
   店長真機：數字默默從 20 變 23，玩家不知道發生了什麼。
   這裡讓「+N 步」在剩餘步數旁邊浮起淡出 —— 不推 HUD、不改任何幾何。
   ★N 由 boosterTimeGlass() 從 server 回傳值帶進來，不是寫死的 3★。 */
.m3-moves-bonus{
  position:absolute; right:100%; top:6px; margin-right:6px;
  font-size:14px; font-weight:800; white-space:nowrap;
  color:#ffe6a6; text-shadow:0 0 8px rgba(255,214,130,.85), 0 1px 2px rgba(0,0,0,.5);
  pointer-events:none;
  animation:m3MovesBonus 1.15s cubic-bezier(.2,.8,.3,1) forwards;
}
@keyframes m3MovesBonus{
  0%  { opacity:0; transform:translateY(6px) scale(.86); }
  18% { opacity:1; transform:translateY(0) scale(1.06); }
  62% { opacity:1; transform:translateY(-6px) scale(1); }
  100%{ opacity:0; transform:translateY(-14px) scale(.96); }
}
/* 步數本體強調一下，讓「20 → 23」這件事被看見 */
.m3-hud-moves b.m3-moves-bump{ animation:m3MovesBump .6s cubic-bezier(.2,.9,.3,1); }
@keyframes m3MovesBump{
  0%{ transform:scale(1); } 30%{ transform:scale(1.22); color:#fff3c9; } 100%{ transform:scale(1); }
}
.m3-reduced .m3-moves-bonus,.m3-reduced .m3-hud-moves b.m3-moves-bump{ animation-duration:.01s; }
@media (prefers-reduced-motion:reduce){
  .m3-moves-bonus{ animation:none; opacity:1; }
  .m3-hud-moves b.m3-moves-bump{ animation:none; }
}
/* 分區之間用細分隔線，而不是每塊各自做膠囊（避免版面零碎） */
.m3-hud-goal  { border-left: 1px solid rgba(226,192,122,.20); border-right: 1px solid rgba(226,192,122,.20); padding: 0 10px; }

.m3-lvno   { font-size: 17px; font-weight: 800; color: #f5d98f; line-height: 1.1; letter-spacing: .3px; }
/* ★關卡名不截字★（店長真頁回報：「文字沒有完整呈現」）
   舊寫法 white-space:nowrap + text-overflow:ellipsis + max-width:8.5em
   ⇒ 8 個中文字就開始吃字，Lv100「團購小萌寵·遺跡之心」被截成「團購小萌寵·遺…」。
   ★縮小字級不是解法★（契約：50+ 玩家優先，字要清楚）。
   改成允許折成兩行（clamp 2）：短名字完全不受影響，長名字完整讀得到。
   line-height 壓到 1.15、字級不動 ⇒ 兩行時 Lv 區只長高約 13px，
   仍低於目標區（caption + 16px 數字）的高度 ⇒ ★HUD 總高不變、無 layout shift★。 */
.m3-lvname { font-size: 11.5px; font-weight: 500; color: #cbbb98; line-height: 1.15; margin-top: 2px;
             max-width: 10.5em; overflow: hidden; white-space: normal; word-break: break-word;
             display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
/* 標籤一律縮小、低彩度 —— 數字與圖示才是主角 */
/* ★行高固定★：選格時字級要放大（10px 太小，50+ 玩家看不清楚），
   但 HUD 高度一格都不能變 ⇒ 用固定的 16px 行框，字在裡面置中。 */
.m3-hud-cap { font-size: 10px; font-weight: 500; color: #a89a80; letter-spacing: .5px;
              height:16px; line-height:16px; margin-bottom: 3px;
              display:flex; align-items:center; justify-content:center; white-space:nowrap; }

/* 目標：可橫排 2–3 個，超過就自己縮；★依 goals 資料 render，不為單一關寫死★ */
.m3-goals { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: nowrap; min-width: 0; }
.m3-goal  { display: flex; align-items: center; gap: 5px; font-size: 16px; font-weight: 800;
            color: #f6ecd4; text-shadow: 0 1px 2px rgba(0,0,0,.5); }
/* 數字用等寬，位數變化（9→10）不會把 HUD 推開 */
.m3-goal-n { font-variant-numeric: tabular-nums; letter-spacing: .2px; }
.m3-goal-n i { font-style: normal; opacity: .5; margin: 0 1px; font-weight: 500; }
.m3-goal-n .gd { opacity: .72; font-weight: 600; }   /* 分母略淡：分子才是玩家在追的數字 */
/* ★✓ 永遠佔位★：未完成時只是透明，所以打勾的瞬間不會造成 layout shift */
/* ★CSS 打勾★（店長 2026-08-30：正式 UI 嚴禁 Emoji／系統圖示字元）
   原本是文字 U+2713，各平台字型長相不同、部分平台還會渲染成彩色 emoji。
   改成用兩條邊框畫的勾：任何裝置長一樣，也不吃字型。
   ★仍是獨立元素且永遠存在★（未完成 opacity 0）⇒ 不造成 layout shift。 */
.m3-goal-ok { opacity: 0; transition: opacity .25s ease;
              position: relative; display: inline-block;
              width: 8px; height: 12px; flex: 0 0 auto; }
.m3-goal-ok::after { content: ''; position: absolute; left: 1px; top: 1px;
              width: 5px; height: 9px;
              border: solid #9fd6a0; border-width: 0 2px 2px 0;
              transform: rotate(42deg); }
.m3-goal.done .m3-goal-ok { opacity: 1; }
.m3-goal.done .m3-goal-n { color: #9fd6a0; }         /* 達成＝柔和綠，不用刺眼色 */
.m3-goal-ic { width: 24px; height: 24px; object-fit: contain; display: block; flex: 0 0 auto; }
/* 3 個以上目標時自動縮，確保塞得下（Lv88 四目標／Lv100 終章都吃這條） */
.m3-goals:has(.m3-goal:nth-child(3)) .m3-goal { font-size: 15px; gap: 3px; }
.m3-goals:has(.m3-goal:nth-child(3)) .m3-goal-ic { width: 20px; height: 20px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal { font-size: 13px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ic { width: 17px; height: 17px; }

/* ══ 🫐 v0.61.28e Combo 目標徽章（拖珠關）══════════════════════════════
   Lv151 起的通關條件是「累積 Combo」，沒有棋子圖可以放。
   ★不用 Emoji、不用系統圖示字元★（店長定案的 UI 規則）：
   改成一顆純 CSS 徽章＋COMBO 三個英文字，各平台長相完全一致，
   而且和棋子圖示★一樣寬★ ⇒ 不會造成 HUD 的 layout shift。 */
.m3-goal-cbic{
  width:24px; height:24px; flex:0 0 auto; display:flex;
  align-items:center; justify-content:center;
  /* ★字要待在框裡★：COMBO 五個字母在 24px 的框裡，7.5px 會溢出邊角。
     6px ＋ 負字距，並用 overflow:hidden 當最後一道保險。 */
  font-size:6px; font-weight:900; letter-spacing:-.2px; line-height:1;
  overflow:hidden; padding:0 1px;
  color:#3a1e2c; border-radius:7px;
  background:linear-gradient(180deg,#ffd98a,#f0a64e);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6), 0 1px 2px rgba(0,0,0,.42);
}
.m3-goals:has(.m3-goal:nth-child(3)) .m3-goal-cbic{ width:20px; height:20px; font-size:5px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-cbic{ width:17px; height:17px; font-size:4.2px; }
.m3-goal.done .m3-goal-cbic{ opacity:.72; }
/* 結算畫面的那一條（和 .m3-rgoal img 同尺寸，排版不變） */
.m3-rgoal .m3-goal-cbic{ width:28px; height:28px; font-size:7px; border-radius:8px; }

/* 步數：數字當主角、標籤縮小 */
.m3-hud-moves b { font-size: 26px; font-weight: 800; color: #ffe9b0; line-height: 1;
                  text-shadow: 0 1px 3px rgba(0,0,0,.55); font-variant-numeric: tabular-nums; }
/* ★低步數只做溫和視覺加壓★：不閃爍、不震動、不紅色狂跳（那會讓 50+ 玩家焦慮） */
.m3-hud.low  .m3-hud-moves b { color: #ffc98a; }
.m3-hud.crit .m3-hud-moves b { color: #ff9f7a; }
.m3-hud.crit { border-color: rgba(255,159,122,.45); }

/* 棋盤：相對定位容器；高度由 JS 依 cellSize×rows 設定（不依賴 aspect-ratio，相容較舊 iOS） */
.m3-board {
  position: relative;
  width: 100%;
  margin: 0 auto;
  background: #b6a480 url('match3/board-stone.webp') center / cover no-repeat;   /* 古老石盤材質（中高明度、被喚醒的漂亮神秘石盤，非古墓） */
  border-radius: 18px;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45),       /* 外陰影＝浮起 */
              inset 0 1px 0 rgba(255, 247, 232, 0.28),    /* 頂部高光調弱：不再洗掉上邊金框（讓 .m3-frame 上邊讀得出來） */
              inset 0 -14px 26px rgba(120, 80, 30, 0.08);   /* 底部內陰影＝一點厚度/景深；外緣金框改由 .m3-frame overlay 提供 */
  overflow: hidden;
  touch-action: none;              /* 讓手指滑動用於交換，不觸發捲動 */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* 凹槽層：同色系、極淡、靜態；不仔細看不會注意到，但棋子放上去就「有位置」（非白色按鈕） */
.m3-socket {
  position: absolute;
  top: 0; left: 0;
  /* ★真頁實測調整★：原本 26% 圓角讓每一格讀成一顆獨立的圓角按鈕，
     整盤看起來像「64 個 UI 卡片」而不是一片古石板地面。
     改成 13%（接近方形石板的倒角）＋ 更淡的接縫 ⇒
     ★先看到「一整片石板地」，第二眼才看到「這裡有格子」★。
     ★刻意不取消格線★：50+ 玩家仍必須一眼判斷棋子在哪一格、哪裡可交換。 */
  border-radius: 13%;
  background: rgba(70, 56, 34, 0.055);
  box-shadow: inset 0 1px 2px rgba(50, 40, 22, 0.085),   /* 接縫（不是按鈕陰影） */
              inset 0 -1px 1px rgba(255, 248, 228, 0.10);
  pointer-events: none;
}

/* Outer Golden Frame：石盤內縮一小圈的單一乾淨金色圓角框（獨立 overlay，依附 .m3-board）。
   ★恢復第一版原位＝inset:1px、border-radius:17px（板 radius18 內縮 1px）；金框外側保留約 1px 石盤材質，不貼齊最外緣。
   ★單一實心金線、不用髮絲陰影＝移除「第二條線」錯覺來源；石盤本身不加任何金線、不恢復舊內圈古紋 overlay。
   ★保留獨立 element：Combo 發光階段再由此圈負責（本輪不接發光）。 */
.m3-frame{
  position:absolute; inset:1px; box-sizing:border-box; pointer-events:none; z-index:5;
  border:2px solid rgba(228,180,90,0.98); border-radius:17px;   /* 恢復第一版原位：內縮 1px、外側留約 1px 石盤；單一實心金線、無 box-shadow 髮絲 */
  transition:border-color .22s ease, box-shadow .22s ease;      /* 預留給日後 Combo 發光；本輪無 box-shadow（★封存 transition 不改） */
}
/* ═══════ Outer Golden Frame Breath（基礎呼吸・新增效果 2026/8/11・非幾何回歸、非 Bug 修復）═══════
   只讓既有 .m3-frame 金線做極輕微緩慢明暗呼吸：★僅動 opacity（不加第二層/box-shadow/filter/尺寸）。
   封存幾何完全不動（inset:1px／border:2px solid／border-radius:17px／pointer-events:none／z-index:5 皆維持）。
   純 CSS、無 JS 迴圈；元素隨棋盤移除即停、無殘留、不另建計時器。不動封存 transition（未來 Combo 可暫停本呼吸並接管更亮效果）。 */
.m3-frame{ animation:m3frameBreath 3.5s ease-in-out infinite; }
@keyframes m3frameBreath{ 0%,100%{ opacity:1; } 50%{ opacity:.78; } }
.m3-reduced .m3-frame{ animation:none !important; opacity:1; }                 /* 遊戲省電模式：關閉呼吸、靜止 */
@media (prefers-reduced-motion: reduce){ .m3-frame{ animation:none; opacity:1; } }   /* 系統減少動態：關閉呼吸、靜止 */

/* 特殊棋 overlay：正式透明 WebP 疊在原棋子上（美術＝「它是什麼」；CSS 只負責動態）。
   ★用 background-image；橫向 Line 直接用 sp-line-h.webp（非執行時旋轉）。觸發爆裂沿用既有 .m3-fx，不在此重做。 */
.m3-sp{ position:absolute; inset:0; pointer-events:none; z-index:3;
        background-repeat:no-repeat; background-position:center; background-size:contain; }
/* Line：★2026-08-30 視覺退件重做★
   舊做法是把 sp-line-h/v.webp ★非等比拉成 240%★ 當貫穿條，
   加上 .m3-spdir 的青白直線與 .sp-line::after 的圓角外框 ——
   實機看起來是「一根發光尺／準星／UI debug guide 壓在棋子上」，
   跟手繪魔法療癒世界觀完全不合，店長判定 Visual Release Gate = FAIL。
   ★新做法★：不再貼任何貫穿條貼圖，方向感改由 .m3-spdir 的
   「棋子上下／左右兩股柔和魔法流光」承擔（見本檔後段）。
   ★玩法一行未改★：橫向仍只清整列、直向仍只清整欄。 */
.m3-piece.sp-lineh .m3-sp,
.m3-piece.sp-linev .m3-sp{ background-image:none; animation:none; }
/* Bomb：不旋轉；待機＝核心蓄能／裂縫亮暗呼吸（偏青藍）
   🔧 v0.60.788【手機上辨識度不足】爆炸棋縮到 ~50px 時只剩一團模糊發光球。
      ★不改 gameplay identity、不換素材★，只補兩層 CSS 讓造型讀得出來：
        ‧ ::before ＝ 暖金外殼環（外圈輪廓，和一般棋子一眼分得開）
        ‧ ::after  ＝ 青綠能量核心（中心亮點，帶呼吸感）
      兩層都是 radial-gradient ＋ 受控 box-shadow，★不用 blur／backdrop-filter★。 */
.m3-piece.sp-bomb  .m3-sp{ background-image:url('match3/sp-bomb.webp');   animation:m3spBomb 1.5s ease-in-out infinite; }
.m3-piece.sp-bomb .m3-sp::before{ content:""; position:absolute; inset:9%; border-radius:50%;
  border:1.5px solid rgba(255,214,130,.85);
  box-shadow:0 0 7px rgba(255,196,96,.7), inset 0 0 6px rgba(255,226,160,.45);
  pointer-events:none; }
.m3-piece.sp-bomb .m3-sp::after{ content:""; position:absolute; left:50%; top:50%;
  width:34%; height:34%; transform:translate(-50%,-50%); border-radius:50%;
  background:radial-gradient(circle,rgba(214,255,250,1) 0%,rgba(104,226,216,.9) 42%,rgba(70,200,196,0) 76%);
  box-shadow:0 0 9px rgba(130,238,228,.9);
  animation:m3spBombCore 1.5s ease-in-out infinite; pointer-events:none; }
@keyframes m3spBombCore{ 0%,100%{ opacity:.78; transform:translate(-50%,-50%) scale(.92) }
                          50%{ opacity:1;   transform:translate(-50%,-50%) scale(1.08) } }
.m3-piece.sp-color .m3-ico{ opacity:0; }                                  /* 星核取代原棋子外觀 */
/* Color：待機＝慢速旋轉（金色星核＋旋轉光環）＝與 Bomb 一眼可分（一個轉、一個呼吸） */
.m3-piece.sp-color .m3-sp{ background-image:url('match3/sp-color.webp');  animation:m3spSpin 7s linear infinite; }
@keyframes m3spGlow{ 0%,100%{filter:brightness(1)} 50%{filter:brightness(1.3) drop-shadow(0 0 3px rgba(255,225,150,.75))} }
@keyframes m3spBomb{ 0%,100%{filter:brightness(0.94) drop-shadow(0 0 2px rgba(120,235,245,.35))} 50%{filter:brightness(1.22) drop-shadow(0 0 8px rgba(120,235,245,.9))} }
@keyframes m3spSpin{ from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
.m3-reduced .m3-sp{ animation:none !important; transform:none !important; filter:none !important; }

/* 特殊棋爆發特效（限時、限棋盤內） */
.m3-fx{ position:absolute; pointer-events:none; z-index:4; }
.m3-fx.fx-row{ left:0; right:0; background:linear-gradient(90deg,rgba(255,225,150,0),rgba(255,235,185,.95),rgba(255,225,150,0)); box-shadow:0 0 16px rgba(231,178,74,.85); animation:m3beam .38s ease-out forwards; }
.m3-fx.fx-col{ top:0; bottom:0; background:linear-gradient(0deg,rgba(255,225,150,0),rgba(255,235,185,.95),rgba(255,225,150,0)); box-shadow:0 0 16px rgba(231,178,74,.85); animation:m3beam .38s ease-out forwards; }

/* ═══ ↔↕ 直線特殊棋 發動 FX v2（店長 2026-08-30 退件重做）═══════════════════
   ★退件的是什麼★：一條直條瞬間出現在整列／整欄上，像雷射不像魔法。
   ★新做法★：★有寬度、有漸層、有前緣亮點的魔法能量波★從棋子往兩側掃出，
     經過每一格時才亮起該格的命中 flash（依距離錯開＝真的「掃過去」）。
   ★雙同向★：`is-dual` 只把能量波加厚加亮、命中回饋加強、粒子加量，
     ★清除範圍一格都沒有變★（不變十字、不擴大方向）。
   ★效能★：只用 transform／opacity／gradient；不用 blur 動畫、不用 mix-blend-mode。 */
.m3-linefx{ position:absolute; inset:0; pointer-events:none; z-index:5; }
.m3-linefx > *{ position:absolute; pointer-events:none; }

/* 核心亮起 ＋ 中央 flash */
.m3-lf-core{ border-radius:50%;
  background:radial-gradient(circle,rgba(255,255,240,1) 0%,rgba(255,232,170,.9) 30%,rgba(198,240,255,.4) 58%,rgba(198,240,255,0) 78%);
  box-shadow:0 0 16px rgba(255,222,150,.9);
  animation:m3lfCore .34s cubic-bezier(.25,.9,.25,1) forwards; }
.m3-linefx.is-dual .m3-lf-core{
  background:radial-gradient(circle,rgba(255,255,255,1) 0%,rgba(255,238,190,.95) 28%,rgba(214,190,255,.45) 58%,rgba(214,190,255,0) 80%);
  box-shadow:0 0 26px rgba(255,232,180,1); animation-duration:.42s; }
@keyframes m3lfCore{ 0%{opacity:0;transform:scale(.5)} 26%{opacity:1;transform:scale(1.12)} 100%{opacity:0;transform:scale(1.3)} }

/* 能量波：從棋子往外展開（scaleX/scaleY ⇒ 合成器友善），前緣最亮、尾端漸淡 */
.m3-lf-wave{ opacity:0; }
.m3-linefx.dir-row .m3-lf-wave{
  background:linear-gradient(90deg,rgba(255,246,214,0) 0%,rgba(255,236,180,.42) 34%,rgba(255,250,225,.92) 82%,rgba(255,255,245,1) 100%);
  box-shadow:0 0 14px rgba(255,224,160,.7);
  animation:m3lfWaveX .34s cubic-bezier(.2,.75,.3,1) forwards; }
.m3-linefx.dir-row .m3-lf-wave.s0{
  background:linear-gradient(270deg,rgba(255,246,214,0) 0%,rgba(255,236,180,.42) 34%,rgba(255,250,225,.92) 82%,rgba(255,255,245,1) 100%); }
.m3-linefx.dir-col .m3-lf-wave{
  background:linear-gradient(180deg,rgba(255,246,214,0) 0%,rgba(255,236,180,.42) 34%,rgba(255,250,225,.92) 82%,rgba(255,255,245,1) 100%);
  box-shadow:0 0 14px rgba(255,224,160,.7);
  animation:m3lfWaveY .34s cubic-bezier(.2,.75,.3,1) forwards; }
.m3-linefx.dir-col .m3-lf-wave.s0{
  background:linear-gradient(0deg,rgba(255,246,214,0) 0%,rgba(255,236,180,.42) 34%,rgba(255,250,225,.92) 82%,rgba(255,255,245,1) 100%); }
.m3-linefx.is-dual .m3-lf-wave{ box-shadow:0 0 22px rgba(255,232,180,.95); animation-duration:.42s; }
@keyframes m3lfWaveX{ 0%{opacity:0;transform:scaleX(0) scaleY(.42)} 14%{opacity:1;transform:scaleX(.35) scaleY(.9)}
                       62%{opacity:.95;transform:scaleX(1) scaleY(1)} 100%{opacity:0;transform:scaleX(1) scaleY(.72)} }
@keyframes m3lfWaveY{ 0%{opacity:0;transform:scaleY(0) scaleX(.42)} 14%{opacity:1;transform:scaleY(.35) scaleX(.9)}
                       62%{opacity:.95;transform:scaleY(1) scaleX(1)} 100%{opacity:0;transform:scaleY(1) scaleX(.72)} }

/* ★逐格命中框已整個移除（店長第二次退件）★
   舊做法是每一格放一個圓角框依序亮 ⇒ 玩家讀到「7、8 個獨立方塊被逐一清除」，
   像 DEBUG 命中格展示。現在命中回饋改成★讓棋子本身被能量帶走★，見 .m3-sweep。 */

/* 星塵 */
.m3-lf-p{ border-radius:50%; opacity:0;
  background:radial-gradient(circle,rgba(255,250,225,1),rgba(255,214,140,.6) 58%,rgba(255,200,110,0));
  box-shadow:0 0 7px rgba(255,224,160,.85);
  animation:m3lfP .38s ease-out forwards; }
.m3-linefx.is-dual .m3-lf-p{ animation-duration:.5s; box-shadow:0 0 9px rgba(255,238,190,.95); }
@keyframes m3lfP{ 0%{opacity:0;transform:translate(0,0) scale(.5)}
                  18%{opacity:1;transform:translate(calc(var(--dx) * .3),calc(var(--dy) * .3)) scale(1.05)}
                  100%{opacity:0;transform:translate(var(--dx),var(--dy)) scale(.35)} }

/* 雙核共鳴：光環 ＋ 能量連結 */
.m3-lf-link{ border-radius:999px; opacity:0;
  background:linear-gradient(90deg,rgba(255,236,180,0),rgba(255,250,225,.95) 22%,rgba(255,238,190,1) 50%,rgba(255,250,225,.95) 78%,rgba(255,236,180,0));
  box-shadow:0 0 12px rgba(255,232,170,.9);
  animation:m3lfLink .3s ease-out forwards; }
@keyframes m3lfLink{ 0%{opacity:0;transform:scaleX(.12)} 42%{opacity:1;transform:scaleX(1)} 100%{opacity:0;transform:scaleX(1)} }
.m3-lf-reson{ border-radius:50%; opacity:0;
  border:2px solid rgba(255,240,200,.9);
  box-shadow:0 0 14px rgba(255,226,160,.8), inset 0 0 10px rgba(255,246,214,.5);
  animation:m3lfReson .3s ease-out forwards; }
@keyframes m3lfReson{ 0%{opacity:0;transform:scale(.55)} 46%{opacity:1;transform:scale(1.05)} 100%{opacity:0;transform:scale(1.25)} }

@media (prefers-reduced-motion:reduce){
  .m3-linefx > *{ animation-duration:.01ms !important; }
}
.m3-fx.fx-ring{ border-radius:50%; border:3px solid rgba(120,235,245,.9); box-shadow:0 0 18px rgba(120,235,245,.75); animation:m3ring .4s ease-out forwards; }
@keyframes m3beam{ 0%{opacity:0} 25%{opacity:1} 100%{opacity:0} }
@keyframes m3ring{ 0%{opacity:.95;transform:scale(.4)} 100%{opacity:0;transform:scale(2.7)} }

/* ═══ 異種 Combo 融合（垂直＋橫向／垂直＋爆炸／橫向＋爆炸）═══════════════
   ★店長 2026-08-30★：這三種必須讀成「兩種能力一起被強化觸發」，
   不是兩段普通動畫各播一次。所以：
     ‧ 兩顆的 FX ★同時播★（時間對齊在 playActivation 做）
     ‧ 中間拉一條共鳴連結（沿用 .m3-lf-link／.m3-lf-reson）
     ‧ 核心更亮、粒子加量（JS 端 +6／+7 顆）、命中回饋更重
   ★清除範圍一格都沒有改★ —— 資料層本來就是兩種效果的聯集。 */
.m3-linefx.is-fused .m3-lf-core,
.m3-blast.is-fused .m3-bl-core{ filter:brightness(1.25); }
.m3-linefx.is-fused .m3-lf-wave{ box-shadow:0 0 26px rgba(255,238,190,1); }
.m3-blast.is-fused .m3-bl-disc{ box-shadow:0 0 34px rgba(255,230,170,.72); }
.m3-linefx.is-fused .m3-lf-p,
.m3-blast.is-fused .m3-bl-p{ box-shadow:0 0 11px rgba(255,244,206,1); }

/* ═══ 特殊棋消除：棋子碎散（取代逐格 overlay）═══════════════════════════
   ★「資料可以逐格判定，視覺絕對不能逐格播放」★
   命中回饋不是格子上的框，而是★那顆棋子被能量帶走★：縮亮 → 碎散 → 消失。
   --swd 是它離能量源幾格換算出來的延遲（每格 14–18ms），
   整列 7 格頭尾差不到 0.1 秒 ⇒ 讀起來是同一股能量掃過去，不是一格一格輪流消失。
   ★不新增任何 DOM★：class 加在既有的 .m3-piece 上。 */
.m3-piece.m3-sweep{ animation:m3SweepGone .26s cubic-bezier(.3,.7,.3,1) var(--swd,0ms) both; }
.m3-piece.m3-sweep .m3-ico{ animation:m3SweepIco .26s ease-out var(--swd,0ms) both; }
.m3-piece.m3-sweep-strong .m3-ico{ animation-duration:.3s; }
@keyframes m3SweepGone{ 0%{ } 34%{ } 100%{ opacity:0 } }
@keyframes m3SweepIco{
  0%{ transform:scale(1); filter:none }
  30%{ transform:scale(1.16); filter:brightness(1.9) saturate(1.15) }
  100%{ transform:scale(.28) rotate(8deg); filter:brightness(2.4) saturate(.6); opacity:0 } }
.m3-reduced .m3-piece.m3-sweep,.m3-reduced .m3-piece.m3-sweep .m3-ico{ animation-duration:.01ms !important; }
@media (prefers-reduced-motion:reduce){
  .m3-piece.m3-sweep,.m3-piece.m3-sweep .m3-ico{ animation-duration:.01ms !important; } }

/* ═══ 💣 爆炸特殊棋 FX v2（店長 2026-08-30 概念圖：視覺全面升級）═══════════
   ★v1 為什麼被退回★
     v1 是「9／25 個半透明色塊 ＋ 兩個粗同心圓」。範圍讀得出來，
     但那是 QA 的 debug hitbox overlay，不是爆炸。
   ★v2 的原則★
     ‧ 命中格★只給邊緣金光 ＋ 中心小閃★，不鋪厚重色塊、不長時間遮住棋子
     ‧ 衝擊波是★細、亮、帶 glow★的環，快速擴張淡出，不是粗實心圓框
     ‧ 打擊感來自「核心壓縮 → 強閃 → streak → 逐圈命中 → 碎片」的時間敘事
   ★成本紀律（海邊平板那一輪的教訓）★
     ‧ 不用 filter:blur 動畫、不用 backdrop-filter、不用 mix-blend-mode
     ‧ 只用 transform / opacity / radial-gradient / box-shadow（面積受控）
     ‧ 全部節點掛在單一 .m3-blast wrapper 底下，一次移除即完全清乾淨
   ★資料與視覺同源★：衝擊圓盤與棋子碎散的範圍，都直接吃 gameplay 最終座標，FX 不自己重算。
   ★逐格 overlay 已整個移除★（店長第二次退件：那看起來像 DEBUG 命中格展示）。 */
.m3-blast{ position:absolute; inset:0; pointer-events:none; z-index:5; }
.m3-blast > *{ position:absolute; pointer-events:none; }

/* T0/T1 爆心：壓縮蓄能 → 強閃（青綠核心 ＋ 暖金外圈） */
.m3-bl-core{ border-radius:50%;
  background:radial-gradient(circle,rgba(190,255,246,1) 0%,rgba(120,236,226,.92) 22%,rgba(255,214,120,.75) 46%,rgba(255,176,70,0) 72%);
  box-shadow:0 0 18px rgba(140,240,230,.85), 0 0 34px rgba(255,196,96,.55);
  animation:m3blCore .34s cubic-bezier(.3,.9,.2,1) forwards; }
.m3-blast.is-combo .m3-bl-core{
  background:radial-gradient(circle,rgba(255,255,255,1) 0%,rgba(226,190,255,.95) 24%,rgba(255,214,140,.8) 48%,rgba(200,140,255,0) 74%);
  box-shadow:0 0 26px rgba(226,178,255,.95), 0 0 52px rgba(255,206,120,.6);
  animation-duration:.46s; }
@keyframes m3blCore{
  0%{ opacity:.85; transform:scale(1.35) }
  38%{ opacity:1; transform:scale(.52) }      /* 壓縮蓄能 */
  56%{ opacity:1; transform:scale(1.15) }     /* 引爆 */
  100%{ opacity:0; transform:scale(1.5) } }

.m3-bl-flash{ border-radius:50%;
  background:radial-gradient(circle,rgba(255,255,255,.95) 0%,rgba(255,240,200,.6) 26%,rgba(255,220,150,0) 62%);
  opacity:0; animation:m3blFlash .3s ease-out .12s forwards; }
.m3-blast.is-combo .m3-bl-flash{
  background:radial-gradient(circle,rgba(255,255,255,1) 0%,rgba(238,214,255,.7) 28%,rgba(214,158,255,0) 64%);
  animation-duration:.38s; }
@keyframes m3blFlash{ 0%{opacity:0;transform:scale(.5)} 18%{opacity:1;transform:scale(1)} 100%{opacity:0;transform:scale(1.35)} }

/* T2 衝擊波：細環 ＋ glow，快速外推 */
.m3-bl-wave{ border-radius:50%; border:2px solid rgba(255,236,180,.95);
  box-shadow:0 0 12px rgba(255,214,130,.9), inset 0 0 8px rgba(255,240,200,.7);
  opacity:0; animation:m3blWave .40s cubic-bezier(.15,.75,.3,1) forwards; }
.m3-blast.is-combo .m3-bl-wave.w0{ border-color:rgba(255,232,168,.98); animation-duration:.44s; }
.m3-blast.is-combo .m3-bl-wave.w1{ border-color:rgba(214,170,255,.98); border-width:2.5px;
  box-shadow:0 0 18px rgba(206,150,255,.95), inset 0 0 10px rgba(238,214,255,.6);
  animation:m3blWave2 .52s cubic-bezier(.15,.75,.3,1) forwards; }
@keyframes m3blWave{ 0%{opacity:0;transform:scale(.35)} 12%{opacity:1} 100%{opacity:0;transform:scale(2.9)} }
@keyframes m3blWave2{ 0%{opacity:0;transform:scale(.4)} 10%{opacity:1} 100%{opacity:0;transform:scale(4.6)} }

/* T3 八方向能量 streak */
.m3-bl-streak{ border-radius:999px; transform-origin:0 50%;
  background:linear-gradient(90deg,rgba(255,255,235,.95),rgba(255,206,110,.7) 45%,rgba(255,190,80,0) 100%);
  opacity:0; animation:m3blStreak .3s ease-out forwards; }
.m3-blast.is-combo .m3-bl-streak{
  background:linear-gradient(90deg,rgba(255,255,255,1),rgba(226,178,255,.75) 45%,rgba(190,130,255,0) 100%);
  animation-duration:.38s; }
@keyframes m3blStreak{
  0%{ opacity:0; transform:rotate(var(--deg)) translateX(0) scaleX(.15) }
  22%{ opacity:1; transform:rotate(var(--deg)) translateX(6%) scaleX(.85) }
  100%{ opacity:0; transform:rotate(var(--deg)) translateX(34%) scaleX(1) } }

/* T4 命中：★一個從爆心擴張出去的衝擊圓盤★（逐格命中框已整個移除）
   大小依「實際受影響的最遠一圈」算 ⇒ 邊角自然變小，仍與資料一致。 */
.m3-bl-disc{ border-radius:50%; opacity:0;
  background:radial-gradient(circle,rgba(255,250,225,.55) 0%,rgba(255,214,130,.34) 42%,rgba(255,196,96,.14) 70%,rgba(255,190,80,0) 88%);
  box-shadow:0 0 22px rgba(255,214,130,.5);
  animation:m3blDisc .38s cubic-bezier(.18,.8,.3,1) forwards; }
.m3-blast.is-combo .m3-bl-disc{
  background:radial-gradient(circle,rgba(252,244,255,.6) 0%,rgba(214,170,255,.38) 42%,rgba(190,130,255,.16) 70%,rgba(178,110,255,0) 88%);
  box-shadow:0 0 32px rgba(214,158,255,.6); animation-duration:.46s; }
@keyframes m3blDisc{ 0%{opacity:0;transform:scale(.24)} 26%{opacity:1;transform:scale(.9)} 100%{opacity:0;transform:scale(1.06)} }

/* T6 碎片：青綠晶片／暖金星屑／小四角星 */
.m3-bl-p{ opacity:0; animation:m3blP .42s ease-out forwards; }
.m3-bl-p.is-chip{ border-radius:2px;
  background:linear-gradient(135deg,rgba(190,255,248,1),rgba(96,214,206,.85));
  box-shadow:0 0 8px rgba(130,238,228,.9); }
.m3-bl-p.is-dust{ border-radius:50%;
  background:radial-gradient(circle,rgba(255,246,210,1),rgba(255,196,96,.6) 60%,rgba(255,180,70,0));
  box-shadow:0 0 7px rgba(255,206,110,.85); }
.m3-bl-p.is-star{ border-radius:1px;
  background:conic-gradient(from 0deg,rgba(255,255,255,.95),rgba(255,224,150,.2) 25%,rgba(255,255,255,.95) 50%,rgba(255,224,150,.2) 75%,rgba(255,255,255,.95));
  box-shadow:0 0 8px rgba(255,232,170,.9); }
.m3-blast.is-combo .m3-bl-p{ animation-duration:.56s; }
.m3-blast.is-combo .m3-bl-p.is-dust{
  background:radial-gradient(circle,rgba(255,250,255,1),rgba(214,170,255,.6) 60%,rgba(190,130,255,0));
  box-shadow:0 0 9px rgba(214,170,255,.9); }
@keyframes m3blP{
  0%{ opacity:0; transform:translate(0,0) scale(.5) rotate(0) }
  16%{ opacity:1; transform:translate(calc(var(--dx) * .28),calc(var(--dy) * .28)) scale(1.1) rotate(calc(var(--spin) * .3)) }
  100%{ opacity:0; transform:translate(var(--dx),var(--dy)) scale(.35) rotate(var(--spin)) } }

/* Combo 前導：兩核蓄能 ＋ 紫金能量連線 */
.m3-bl-link{ border-radius:999px; opacity:0;
  background:linear-gradient(90deg,rgba(255,236,180,0),rgba(255,246,220,.95) 20%,rgba(226,178,255,1) 50%,rgba(255,246,220,.95) 80%,rgba(255,236,180,0));
  box-shadow:0 0 14px rgba(226,178,255,.9);
  animation:m3blLink .26s ease-out forwards; }
@keyframes m3blLink{ 0%{opacity:0;transform:scaleX(.15)} 40%{opacity:1;transform:scaleX(1)} 100%{opacity:0;transform:scaleX(1)} }
.m3-bl-charge{ border-radius:50%; opacity:0;
  background:radial-gradient(circle,rgba(255,255,255,.85),rgba(226,178,255,.45) 42%,rgba(200,140,255,0) 70%);
  animation:m3blCharge .26s ease-out forwards; }
@keyframes m3blCharge{ 0%{opacity:0;transform:scale(.6)} 45%{opacity:1;transform:scale(1.1)} 100%{opacity:0;transform:scale(.9)} }

/* 無障礙：減少動態時改成「亮一下就好」，★但仍然逐格亮★
   —— 範圍讀得出來屬於資訊，不是裝飾，不能一起關掉。 */
@media (prefers-reduced-motion:reduce){
  .m3-blast > *{ animation-duration:.01ms !important; }
}

/* Combo 里程碑中央文字（純 CSS+DOM，可換文案；不擋操作、不重排） */
.m3-combo{ position:absolute; left:50%; top:42%; transform:translate(-50%,-50%); z-index:6; pointer-events:none;
  font-family:-apple-system,"PingFang TC","Noto Sans TC","Microsoft JhengHei",sans-serif; font-weight:800; white-space:nowrap;
  color:#fff5d0; text-shadow:0 2px 0 #b5761a, 0 0 10px rgba(231,178,74,.9), 0 0 18px rgba(231,178,74,.55); animation:m3combo 1s ease forwards; }
.m3-combo.cm3{ font-size:30px; }
.m3-combo.cm5{ font-size:40px; }
.m3-combo.cm8{ font-size:48px; color:#eafcff; text-shadow:0 2px 0 #2f7fb0, 0 0 12px rgba(120,235,245,.95), 0 0 20px rgba(231,178,74,.7); }
.m3-combo.cmx{ font-size:56px; color:#fffbe6; text-shadow:0 3px 0 #b5761a, 0 0 16px rgba(120,235,245,1), 0 0 28px rgba(231,178,74,.9); }
.m3-combo .m3-cparticle{ position:absolute; left:50%; top:50%; width:7px; height:7px; border-radius:50%; background:radial-gradient(circle,#ffe6a0,#e7b24a); }
@keyframes m3combo{ 0%{opacity:0;transform:translate(-50%,-50%) scale(.8);} 22%{opacity:1;transform:translate(-50%,-50%) scale(1.15);} 38%{transform:translate(-50%,-50%) scale(1.0);} 72%{opacity:1;transform:translate(-50%,-54%) scale(1.0);} 100%{opacity:0;transform:translate(-50%,-66%) scale(1.0);} }
@keyframes m3cpart{ 0%{opacity:1;transform:translate(-50%,-50%) scale(1)} 100%{opacity:0;transform:translate(calc(-50% + var(--tx)),calc(-50% + var(--ty))) scale(.4)} }
.m3-reduced .m3-combo{ animation-duration:.5s; }
.m3-board.m3-shake{ animation:m3shake .3s ease; }
@keyframes m3shake{ 0%,100%{transform:translate(0,0)} 20%{transform:translate(-3px,1px)} 40%{transform:translate(3px,-1px)} 60%{transform:translate(-2px,1px)} 80%{transform:translate(2px,0)} }

/* 棋子容器：位置＝transform translate；交換/掉落走此 transition；flex 置中棋子圖 */
.m3-piece {
  position: absolute;
  top: 0; left: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  will-change: transform;
  transition: transform 0.16s cubic-bezier(0.2, 0.7, 0.3, 1);   /* 交換位移 160ms < swap 閘門 170ms（避免慢機重疊） */
}
/* Cascade 掉落：與交換脫鉤。★這裡只是「沒拿到 inline 距離時的保底」★——
   正式路徑由 JS 依掉落距離逐顆寫 inline transition-duration（base+k·√dist）。
   曲線 (.34,.12,.5,1)＝放手→加速→煞停；★刻意不是 overshoot 曲線★
   （舊值 y1=1.32 會衝過目的地再彈回＝氣球／飄的來源）。 */
.m3-board.m3-dropping .m3-piece { transition: transform 0.2s cubic-bezier(.34, .12, .5, 1); }
/* 落地壓縮：以底部為支點、132ms 一次、不位移座標 ⇒ 有重量但不是果凍彈跳。
   animation-delay 由 JS 寫成 --land-delay（＝該顆的抵達時刻）⇒ 不需要任何 JS 計時器。
   fill-mode 用 backwards（不是 both）：動畫結束後必須把 transform 交還給
   .m3-ico 的一般規則，否則下一輪 charge/gone 會被停在最後一格的 animation 蓋住。 */
@keyframes m3Land {
  0%   { transform: translate(var(--ico-tx, 0px), var(--ico-ty, 0px)) scale(1, 1); }
  40%  { transform: translate(var(--ico-tx, 0px), var(--ico-ty, 0px)) scale(1.07, .90); }
  100% { transform: translate(var(--ico-tx, 0px), var(--ico-ty, 0px)) scale(1, 1); }
}
.m3-piece.m3-land .m3-ico {
  transform-origin: 50% 92%;
  animation: m3Land 132ms cubic-bezier(.3, .1, .3, 1) var(--land-delay, 0ms) backwards;
}
.m3-ico {
  width: 90%;                       /* 預設；實際由 JS 依 WEIGHT 視覺重量校正覆寫 */
  height: 90%;
  object-fit: contain;
  display: block;
  pointer-events: none;            /* 事件由棋盤接手；棋子本身不攔截 */
  -webkit-user-drag: none;
  user-select: none;
  filter: drop-shadow(0 3px 2px rgba(70, 45, 18, 0.34));   /* 柔和落地陰影＝坐在盤上、不浮貼 */
  transition: transform 0.17s ease, opacity 0.17s ease;
}

/* 消除：微膨脹 → 啵掉（淡出縮沒） */
.m3-piece.m3-gone { pointer-events: none; }
.m3-piece.m3-gone .m3-ico { transform: translate(var(--ico-tx, 0px), var(--ico-ty, 0px)) scale(1.18); opacity: 0; }

/* Cascade 辨識/蓄力：消除前輕微放大＋提亮，幫助認出本輪這組；90ms < charge 閘門 100ms */
.m3-piece.m3-charge .m3-ico { transform: translate(var(--ico-tx, 0px), var(--ico-ty, 0px)) scale(1.10); filter: drop-shadow(0 3px 2px rgba(70,45,18,0.34)) brightness(1.18); transition: transform 0.09s ease-out, filter 0.09s ease-out; }

/* Reduced Motion：所有位移／消除動畫關閉（QA 可切換）＝只移除等待與動畫，不改判定順序 */
.m3-reduced .m3-piece,
.m3-reduced .m3-ico { transition: none !important; }
.m3-reduced .m3-piece.m3-charge .m3-ico { transform: none; filter: none; }   /* 蓄力 no-op */
.m3-reduced .m3-piece.m3-land .m3-ico { animation: none !important; }         /* 落地壓縮 no-op */
@media (prefers-reduced-motion: reduce) { .m3-piece.m3-land .m3-ico { animation: none !important; } }

/* 預留：最後幾步狀態（A1 不做正式緊張演出，僅掛旗標供 A2 使用） */
.m3-low { /* reserved for A2 tension */ }

.m3-note {
  text-align: center;
  color: #cdbfa0;
  font-size: 12px;
  margin-top: 10px;
  text-shadow: 0 1px 2px rgba(0,0,0,.5);
}

/* ===== 勝敗／目標迴圈（純前端；不碰封存 Timing/Cascade/外框） ===== */
/* HUD 目標達成狀態 */
/* ★已移除★ 舊的淡綠底 .m3-goal.done —— 它蓋過石材 HUD 的樣式，
   在正式畫面上變成一塊像 debug box 的淡綠矩形。目標樣式統一由上方 HUD 區塊負責。 */

/* 過關／失敗結算：全螢幕山洞儀式（fixed；徽章 HERO、不塞回卡片；純視覺，不改判定/計數/步數/鎖/重玩/封存項） */
.m3-result{ position:fixed; inset:0; z-index:40; display:flex; align-items:center; justify-content:center; pointer-events:auto; }
.m3-rbackdrop{ position:absolute; inset:0; opacity:0; animation:m3rfade .3s ease forwards; }
.m3-result.win  .m3-rbackdrop{ background:radial-gradient(125% 105% at 50% 42%, rgba(16,10,4,.76), rgba(3,2,1,.95)); }
.m3-result.lose .m3-rbackdrop{ background:radial-gradient(125% 105% at 50% 46%, rgba(20,27,38,.80), rgba(5,8,13,.95)); }  /* 柔和深藍灰、無紅色警告、壓暗約 80% */
.m3-rstage{ position:relative; z-index:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px;
  width:100%; max-width:360px; box-sizing:border-box;
  padding:calc(env(safe-area-inset-top) + 10px) 18px calc(env(safe-area-inset-bottom) + 12px); }  /* iPhone 直式安全區；gap 收緊 */
/* 徽章舞台（HERO） */
.m3-remblem-wrap{ position:relative; display:flex; align-items:center; justify-content:center; }
.m3-result.win  .m3-remblem-wrap{ width:min(66vw,272px); height:min(66vw,272px); }   /* 勝利放大 ~12% */
.m3-result.lose .m3-remblem-wrap{ width:min(53vw,216px); height:min(53vw,216px); }   /* 失敗略小 */
.m3-remblem{ width:100%; height:100%; object-fit:contain; display:block; position:relative; z-index:2;
  opacity:0; transform:scale(.6); animation:m3rEmblem .5s cubic-bezier(.2,1.5,.5,1) .25s both; }
.m3-result.win  .m3-remblem{ filter:drop-shadow(0 6px 14px rgba(0,0,0,.5)) drop-shadow(0 0 12px rgba(255,214,120,.5)); }
.m3-result.lose .m3-remblem{ opacity:0; transform:translateY(-8px); animation:m3rEmblemLose .42s ease .3s both;
  filter:grayscale(.18) brightness(.72) saturate(.78) drop-shadow(0 0 8px rgba(90,150,170,.35)); }  /* 沉睡但看得清細節 */
/* 後方光：透明多層徑向（金＋青藍）自然融入背景、往下延伸接標題；持久（無實心圓盤、動畫結束不空掉） */
.m3-rglow{ position:absolute; left:50%; top:46%; transform:translate(-50%,-50%); z-index:0; pointer-events:none; }
.m3-result.win  .m3-rglow{ width:172%; height:212%;
  background:radial-gradient(closest-side, rgba(255,226,150,.5), rgba(255,226,150,0) 70%),
             radial-gradient(closest-side, rgba(120,235,245,.3), rgba(120,235,245,0) 60%);
  filter:blur(3px); opacity:0; animation:m3rGlowIn .6s ease .2s both; }
.m3-result.lose .m3-rglow{ width:118%; height:120%; top:56%;
  background:radial-gradient(closest-side, rgba(96,158,178,.22), rgba(96,158,178,0) 72%);
  filter:blur(4px); opacity:0; animation:m3rGlowIn .6s ease .3s both; }        /* 失敗：非常淡的青藍沉睡光暈 */
.m3-rhalo{ position:absolute; inset:6%; z-index:1; border-radius:50%; border:2px solid rgba(255,226,150,.7); opacity:0;
  animation:m3rHalo .6s ease-out .3s both; }                    /* 擴散光環（win 進場） */
.m3-rsparkles{ position:absolute; inset:-8%; z-index:3; pointer-events:none; }
.m3-rsparkles i{ position:absolute; width:4px; height:4px; margin:-2px; border-radius:50%; background:#fff2c8;
  box-shadow:0 0 6px 1px rgba(255,224,150,.9); opacity:.85; animation:m3rTwinkle 2.4s ease-in-out infinite; }  /* 少量持久星點 */

/* 標題 */
.m3-rtitle{ font-family:-apple-system,"PingFang TC","Noto Sans TC",sans-serif; font-weight:900; letter-spacing:1px; margin-top:2px;
  opacity:0; animation:m3rUp .32s ease .78s both; }
.m3-result.win  .m3-rtitle{ font-size:32px; color:#ffe9a8; text-shadow:0 2px 0 #a5701f, 0 0 16px rgba(255,214,120,.85); }
.m3-result.lose .m3-rtitle{ font-size:24px; color:#dfe6ee; text-shadow:0 1px 4px rgba(0,0,0,.55); }
/* D 資訊：半透明深色玻璃條＋細金/青藍線（勝利 12/12 為主、步數為次；失敗一行清楚） */
.m3-rinfo{ display:flex; flex-direction:column; align-items:center; gap:2px; margin-top:8px; opacity:0; animation:m3rUp .3s ease 1.06s both;
  background:rgba(10,14,20,.42); border:1px solid rgba(255,224,150,.35); border-radius:16px; padding:9px 22px;
  box-shadow:inset 0 0 14px rgba(120,235,245,.08); }
.m3-result.lose .m3-rinfo{ border-color:rgba(150,190,215,.4); box-shadow:inset 0 0 14px rgba(90,150,180,.1); }
.m3-rgoal{ display:flex; align-items:center; gap:9px; font-weight:800; color:#fff3d6; }
.m3-result.win  .m3-rgoal{ font-size:23px; }
.m3-result.lose .m3-rgoal{ font-size:19px; color:#eef3f8; }
.m3-rgoal img{ width:28px; height:28px; object-fit:contain; display:block; }
.m3-rmoves{ font-size:12px; color:#c9b892; opacity:.9; }
/* E 按鈕：細框＋柔光、無粗黑底影；勝利青藍深＋金框金光、失敗沉靜藍灰＋亮邊 */
.m3-rbtns{ display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:10px; opacity:0; animation:m3rUp .3s ease 1.32s both; }
.m3-rbtn{ border:1px solid rgba(255,224,150,.55); border-radius:14px; padding:13px 24px; font-size:16px; font-weight:800; cursor:pointer;
  color:#fff6de; background:linear-gradient(180deg,#1f6b82,#123f4e); box-shadow:0 0 14px rgba(255,214,120,.32), inset 0 1px 0 rgba(255,255,255,.15); }
.m3-rbtn.next{ background:linear-gradient(180deg,#e7b24a,#c98a2b); color:#4a2f0a; border-color:rgba(255,240,200,.7); box-shadow:0 0 14px rgba(255,214,120,.45), inset 0 1px 0 rgba(255,255,255,.35); }
.m3-result.lose .m3-rbtn.retry{ background:linear-gradient(180deg,#3d5772,#26384e); border-color:rgba(165,195,220,.5); color:#eaf1f8; box-shadow:0 0 10px rgba(120,180,210,.25), inset 0 1px 0 rgba(255,255,255,.12); }
.m3-rbtn:active{ transform:translateY(1px); filter:brightness(1.06); }
@keyframes m3rfade{ to{opacity:1} }
@keyframes m3rEmblem{ 0%{opacity:0;transform:scale(.6)} 60%{opacity:1;transform:scale(1.08)} 78%{transform:scale(.97)} 100%{opacity:1;transform:scale(1)} }
@keyframes m3rEmblemLose{ from{opacity:0;transform:translateY(-8px)} to{opacity:1;transform:translateY(0)} }
@keyframes m3rGlowIn{ to{opacity:1} }
@keyframes m3rHalo{ 0%{opacity:.85;transform:scale(.55)} 100%{opacity:0;transform:scale(1.7)} }
@keyframes m3rTwinkle{ 0%,100%{opacity:.25;transform:scale(.7)} 50%{opacity:.95;transform:scale(1)} }
@keyframes m3rUp{ from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} }
/* reduced-motion：兩來源 → 最終定格、關閉閃爍/光環、持久金光與失敗暗化保留 */
.m3-reduced .m3-result *{ animation:none !important; }
.m3-reduced .m3-result .m3-rbackdrop,.m3-reduced .m3-result .m3-rglow{ opacity:1; }
.m3-reduced .m3-result .m3-remblem{ opacity:1; transform:none; }
.m3-reduced .m3-result .m3-rtitle,.m3-reduced .m3-result .m3-rinfo,.m3-reduced .m3-result .m3-rbtns{ opacity:1; transform:none; }
.m3-reduced .m3-result .m3-rhalo{ display:none; }
.m3-reduced .m3-result .m3-rsparkles i{ opacity:.7; }
@media (prefers-reduced-motion: reduce){
  .m3-result *{ animation:none !important; }
  .m3-result .m3-rbackdrop,.m3-result .m3-rglow{ opacity:1; }
  .m3-result .m3-remblem{ opacity:1; transform:none; }
  .m3-result .m3-rtitle,.m3-result .m3-rinfo,.m3-result .m3-rbtns{ opacity:1; transform:none; }
  .m3-result .m3-rhalo{ display:none; }
  .m3-result .m3-rsparkles i{ opacity:.7; }
}

/* ===================== 首次遊玩教學（批1）：聚光 + 手勢 + 泡泡文案 =====================
   .m3-tut 整層 pointer-events:none（聚光/手指純視覺、不擋棋盤，閘門由 JS 控制）；只有泡泡/按鈕可點。 */
.m3-tut{ position:fixed; inset:0; z-index:38; pointer-events:none; }
.m3-tut-hole{ position:fixed; left:0; top:0; width:0; height:0; border-radius:12px;
  box-shadow:0 0 0 9999px rgba(6,10,22,.62);
  outline:2px solid rgba(255,214,120,.92); outline-offset:-1px;
  transition:left .28s ease, top .28s ease, width .28s ease, height .28s ease;
  animation:m3tutpulse 1.5s ease-in-out infinite; }
@keyframes m3tutpulse{ 0%,100%{ outline-color:rgba(255,214,120,.55); } 50%{ outline-color:rgba(255,224,150,1); } }
.m3-tut.noanim .m3-tut-hole{ transition:none; animation:none; }

.m3-tut-hand{ position:fixed; width:36px; height:36px; border-radius:50%;
  background:radial-gradient(circle at 38% 34%, rgba(255,255,255,.95), rgba(255,226,150,.55) 60%, rgba(255,205,110,0) 72%);
  box-shadow:0 0 0 3px rgba(255,255,255,.55), 0 4px 12px rgba(0,0,0,.35);
  animation:m3tuthand 1.7s ease-in-out infinite; }
@keyframes m3tuthand{
  0%{ transform:translate(0,0) scale(1); opacity:0; }
  12%{ opacity:1; }
  50%{ transform:translate(var(--dx,0),var(--dy,0)) scale(.9); opacity:1; }
  78%{ transform:translate(var(--dx,0),var(--dy,0)) scale(.9); opacity:1; }
  100%{ transform:translate(var(--dx,0),var(--dy,0)) scale(.9); opacity:0; } }
.m3-tut.noanim .m3-tut-hand{ display:none !important; }

.m3-tut-bubble{ position:fixed; left:50%; transform:translateX(-50%); z-index:39; pointer-events:auto;
  width:min(88vw,420px); box-sizing:border-box;
  background:linear-gradient(180deg, rgba(28,22,14,.96), rgba(18,14,9,.97));
  border:1px solid rgba(255,214,120,.42); border-radius:16px;
  box-shadow:0 10px 30px rgba(0,0,0,.42); padding:14px 16px; color:#fbf3e2; }
.m3-tut-bubble.bottom{ bottom:calc(env(safe-area-inset-bottom) + 20px); top:auto; }
.m3-tut-bubble.top{ top:calc(env(safe-area-inset-top) + 20px); bottom:auto; }
.m3-tut-msg{ font-size:15px; line-height:1.6; text-align:center; }
.m3-tut-msg b{ color:#ffd97a; font-weight:700; }
.m3-tut-btns{ display:flex; gap:10px; justify-content:center; margin-top:12px; }
.m3-tut-btn{ cursor:pointer; border-radius:11px; padding:9px 18px; font-size:14px; font-weight:700;
  font-family:inherit; border:1px solid transparent; }
.m3-tut-btn.primary{ background:linear-gradient(180deg,#f4c869,#d99b34); color:#3a2708;
  border-color:rgba(255,236,190,.7); box-shadow:0 3px 10px rgba(180,120,30,.4); }
.m3-tut-btn.ghost{ background:rgba(255,255,255,.06); color:#d9cbb0; border-color:rgba(255,214,120,.3); }
.m3-tut-btn:active{ transform:scale(.96); }

/* 特殊棋情境教學（批2）：沿用批1 聚光/泡泡版面（不新增 Bomb 角標）；聚光環改青綠脈動以區別「新玩法解說」 */
.m3-tut2 .m3-tut-hole{ outline-color:rgba(150,235,190,.95); animation-name:m3tut2pulse; }
@keyframes m3tut2pulse{ 0%,100%{ outline-color:rgba(150,235,190,.5); } 50%{ outline-color:rgba(190,255,215,1); } }
/* 批2 教學為 modal 性質，需蓋在所有 QA 工具之上：泡泡「知道了」必須永遠可點、文字不被 QA 工具列(z-index:9999)遮住（正式玩家無 QA 列、不受影響）。 */
.m3-tut2{ z-index:10001; }
.m3-tut2 .m3-tut-bubble{ z-index:10002; }

/* ===================== 🔋 批1 效能地基：暫停態（hidden / overlay / pause） =====================
   原則：不是把動畫關掉，而是「按下暫停鍵」——遊玩時效果完整，被遮住或切到背景就整層停算，回來原地續播。
   只動 animation-play-state，不動 transition（進行中的棋子位移讓它自然走完，避免視覺瞬移）。
   實機已證實：暫停 CSS 動畫比讓它在背景空轉明顯不燙。 */
.m3-paused,
.m3-paused *{ animation-play-state:paused !important; }

/* ═══════════════ 批10 關卡系統：障礙／掉落物／不規則棋盤 ═══════════════ */
/* 🌿 古藤 overlay：疊在棋子之上；★底下棋子仍看得見、仍可 match★（規格 §N1） */
/* ═══ v0.61.20 地板層（秋葉地毯／盛開的花叢）═══
   ★關鍵：z-index 1 ⇒ 永遠在棋子（.m3-piece z-index:2）★下面★。★
   這一層什麼都不擋：不攔事件、不進 match、不進重力。它只是畫在石盤上的裝飾，
   由 match3-level-engine.js 的 board.__floor 決定畫不畫、畫哪一階。
   沒有 floors 的關卡永遠不會有 .m3-floor 元素 ⇒ Lv1～110 視覺一個像素都不變。 */
/* 🌿 v0.61.23 花叢「包覆」共用視覺契約：BACK 背景葉片 → 棋子 → FRONT 前緣葉片
   ══════════════════════════════════════════════════════════════════════════
   ★店長 v0.61.22 真人視覺驗收 FAIL★
     「花叢遮住棋子過多」「像一整坨花叢圖片蓋在棋子前面，不是棋子被花叢包住」。
   ★root cause（真 Chromium 合成、逐像素量出來，★不是調 opacity 猜的★）★
     花苞障礙從頭到尾只有★一個★元素：.m3-obs（z-index 4）＝一整張不透光的灌木，
     ★100% 畫在棋子（z-index 2）前面★，後面一層都沒有（後景可見面積 ＝ 0）。
     於是唯一可調的旋鈕只剩「整叢往下推多少」，而這一個旋鈕同時控制兩件
     互相打架的事：
       推得高 ⇒ 困住感有了，但棋子中央主體被吃掉 ⇒ 認不出是哪一顆
       推得低 ⇒ 認得出來了，但變成「一叢草站在棋子前面」
     v0.61.22 實測（360px／格 48px／alpha≥128 才算不透明；量測工具見
     tools/m3-bloom-occlusion-gate.mjs，同一支 Gate 常駐）：
       中央主體辨識區可見率 lv2：草莓 0.0%／布丁 0.0%／高麗菜 0.0%／牛奶 0.0%／薰衣草 1.3%
       棋子上緣之上有葉子的欄位比例：0.0%～14.5%（＝幾乎沒有「棋子後面的葉子」）
     ⇒ ★結構問題★：少了後景層，包覆感在數學上就做不出來，
       把 opacity 調小、把圖縮小、把棋子搬到最上面都只是換一種難看法。
   ★修法：沿用專案內★已經驗證過★的分層契約（秋葉 .m3-floor ＋ .m3-floor-over）★
     同一張 bloom-2.webp 畫兩次，兩次★讀同一組幾何變數★（--bl-size / --bl-y）：
       .m3-obs-back（z-index 1，棋子後面）：整叢、略壓暗 ⇒ 背景葉片
       .m3-obs     （z-index 4，棋子前面）：遮罩只留下緣一段 ⇒ 前緣葉片
     棋子夾在中間 ⇒ 臉留著（認得出哪一顆），下半身被葉子埋住（困住）。
   ★共用契約，不是逐顆棋子補丁★：整段沒有任何 [data-key] 選擇器，
     五種棋子（草莓／布丁／高麗菜／牛奶／薰衣草）走完全相同的路徑。
   ★門檻不是憑空指定的★：先量五顆棋子 × 兩個階段 × 四種寬度，
     再由實測分佈訂出上下兩邊的界線（遮太多＝認不出、遮太少＝不像被困住），
     寫在 tools/m3-bloom-occlusion-gate.mjs 的 CONTRACT 裡。
   ══════════════════════════════════════════════════════════════════════════ */
/* ── 幾何變數：★前景與後景讀的是同一組★（任何一邊單獨被改都會被 Gate 咬到）──
   size 刻意 < 100%：留一點邊界餘裕，貼圖不會被格子切出方形硬邊。
   lv2（完整花苞）比 lv1（被打過一次、束縛變鬆）叢更大、位置更高、前緣更早開始
   ⇒ 兩階段的「露出多少」是★單調★的，打一下真的看得出鬆了。 */
.m3-obs.m3-obs-bloom.m3-obs-lv2,
.m3-obs-back.m3-obs-back-lv2{
  --bl-size: 96%;      /* 叢的大小（格寬的 %） */
  --bl-y: 78%;         /* background-position 的 Y；96% ⇒ 可移動 4% ⇒ 上緣落在 3.1% */
  --bl-rim: 46%;       /* 前緣葉片從這裡開始出現 */
  --bl-rim-mid: 60%;   /* 半透明的過渡點（避免出現一條直線邊） */
  --bl-rim-full: 80%;  /* 前緣葉片完全不透明 */
}
.m3-obs.m3-obs-bloom.m3-obs-lv1,
.m3-obs-back.m3-obs-back-lv1{
  --bl-size: 90%;
  --bl-y: 118%;        /* 90% ⇒ 可移動 10% ⇒ 上緣落在 11.8%：叢往下沉一點 */
  /* ★v0.61.24 同步上移 2%★：棋子因為落地陰影補償往上移了 0.84px（≈1.8% 格高），
     前緣葉片若留在原處，蓋到棋子的比例就會跟著變少 —— 實測 320px 的高麗菜
     visibleRatio 從 92% 升到 96.6%，踩破 E-12「不得幾乎沒被遮」的下界。
     ★這是把前緣跟著棋子一起移，不是放寬 E-12 的門檻。★ */
  --bl-rim: 58%;
  --bl-rim-mid: 72%;
  --bl-rim-full: 90%;
}
/* ── 後景：整叢畫在棋子★後面★ ────────────────────────────────────────
   ★z-index 1★＝和地板層同一層。本章關卡資料實測：★沒有任何一格同時有
   障礙層與地板層★（gen 出來的 Lv111～120 逐格比對 = 0 筆），所以兩者
   不會在同一格互相蓋住；Gate 也直接量「後景實際可見面積 > 0」，
   萬一未來被地形或任何一層蓋掉，Gate 會紅。
   略壓暗 ＋ 稍微降飽和 ＝ 讀得出「這是後面那一層」，★不是整體調 opacity★。 */
.m3-obs-back{
  position:absolute; z-index:1; pointer-events:none;
  background-repeat:no-repeat;
  background-size: var(--bl-size) var(--bl-size);
  background-position: 50% var(--bl-y);
  filter: brightness(.82) saturate(.92)
          drop-shadow(0 1px 2px rgba(18,34,10,.42));
}
/* ── 前景：★同一張圖、同一組幾何★，只留下緣一段畫在棋子前面 ──────────
   遮罩是一條由上而下的漸層 ⇒ 沒有任何直線硬邊；
   棋子的臉（中央主體辨識區）永遠在 --bl-rim 之上 ⇒ 認得出是哪一顆。 */
.m3-obs.m3-obs-bloom{
  background-size: var(--bl-size) var(--bl-size);
  background-position: 50% var(--bl-y);
  -webkit-mask-image: linear-gradient(to bottom, transparent var(--bl-rim),
          rgba(0,0,0,.62) var(--bl-rim-mid), #000 var(--bl-rim-full));
          mask-image: linear-gradient(to bottom, transparent var(--bl-rim),
          rgba(0,0,0,.62) var(--bl-rim-mid), #000 var(--bl-rim-full));
  /* 接觸陰影：讓前緣葉片明確「壓在棋子上面」。
     ★往上的那一道是給高麗菜用的★ —— 同樣是綠色，沒有這條暗邊就會跟花叢糊在一起。 */
  filter: drop-shadow(0 2px 3px rgba(18,34,10,.58))
          drop-shadow(0 -1px 1px rgba(14,26,8,.55))
          drop-shadow(0 0 2px rgba(18,34,10,.46));
}
.m3-obs.m3-obs-bloom.m3-obs-lv1{
  filter: drop-shadow(0 2px 3px rgba(18,34,10,.5))
          drop-shadow(0 -1px 1px rgba(14,26,8,.45)) brightness(1.08) saturate(1.04);
}

/* 🍯 v0.61.20 糖罐需求徽章：畫在糖罐貼圖之上的小棋子圖
   幾何（left/top/width/height）由 JS 依量測值寫 inline，這裡只給材質。 */
.m3-need{ position:absolute; object-fit:contain; pointer-events:none;
  filter:drop-shadow(0 1px 1px rgba(40,26,10,.55)); }

.m3-floor{ position:absolute; inset:0; z-index:1; pointer-events:none;
  background-repeat:no-repeat; background-position:center; background-size:contain; }
/* 🌸 v0.61.20-a 盛開的花叢（地板層）：★必須和棋子共用同一個視覺中心★
   店長：「粉紅花圈沒有真正包住格內棋子…像花圈浮在棋子旁邊／外面。」
   舊版花圈只有 contain（≒100%），和棋子差不多大 ⇒ 兩者互相疊、誰也不包誰。
   改成 112% 並嚴格置中：花圈外緣被格子邊切掉一點，內圈剛好框住棋子
   ⇒ 讀起來是「這一格還帶著花」，而不是「旁邊多了一朵花」。 */
.m3-floor.m3-floor-t-bloom{
  background-size: 112% 112%;
  /* ★v0.61.24：花圈的「內圈中心」要對準格中心，不是把整張圖置中★
     店長說「粉紅花圈那張最明顯」。實測 bloom-1.webp 的★內圈（框內留白）中心★
     在畫布的 y −0.59%（偏上）—— 整張圖置中時，玩家看到的「相框」就偏上 0.28px，
     棋子相對它就永遠偏下。
     ★算式（不是手調）★：background-size 112% ⇒ 可移動範圍 −12%；
       目前 offset = −12% × 50% = −6%；要把圖往下移 0.59% ⇒ offset = −5.41%
       ⇒ p = −5.41 / −12 = 45.08%。
     ★Gate 鎖住的是結果★：批89 會量「環內緣中心 − 格中心」，
     任何人改動這個值或換素材，量出來不對就會紅。 */
  background-position: 50% 45.08%;
}
/* 🍁 v0.61.20-a 落葉覆蓋層（★純表現、z-index 3、pointer-events:none★）
   店長：「布丁站在秋葉格時仍然像完整裸露在上面，沒有被落葉覆蓋／困住的感覺。」
   root cause：地板層永遠在棋子★下面★（z1 < 棋子 z2），所以不管葉子畫多滿，
   棋子永遠是完整壓在最上面的那一層 —— 矮而圓的布丁尤其明顯。
   修法：同一張葉子圖再畫一份在棋子★上面★，但用遮罩只留下緣那一段，
   ⇒ 棋子的臉永遠不被遮，腳邊卻真的埋在落葉裡。
   ★不碰規則★：不進 match、不擋事件、不影響 __floor 狀態，純 cosmetic。 */
.m3-floor-over{ position:absolute; z-index:3; pointer-events:none;
  background-repeat:no-repeat; background-position:center; background-size:contain;
  -webkit-mask-image: linear-gradient(to bottom, transparent 46%, rgba(0,0,0,.55) 62%, #000 82%);
          mask-image: linear-gradient(to bottom, transparent 46%, rgba(0,0,0,.55) 62%, #000 82%);
  opacity:.92;
  filter: drop-shadow(0 1px 2px rgba(18,11,3,.55)) saturate(1.12) brightness(1.04); }
.m3-floor-over.m3-floor-gone{ animation:m3FloorSweep .35s ease-out both; }
.m3-reduced .m3-floor-over{ animation:none !important; }
/* 掃掉一層時往外淡出散開（0.35s，不彈跳、不碎裂） */
@keyframes m3FloorSweep{ 0%{ opacity:1; transform:scale(1); } 100%{ opacity:0; transform:scale(1.14); } }
.m3-floor.m3-floor-gone{ animation:m3FloorSweep .35s ease-out both; }
/* 花苞→盛開：綻放 ＋ 輕微彈跳（0.4s） */
@keyframes m3BloomOpen{ 0%{ opacity:0; transform:scale(.72); } 60%{ opacity:1; transform:scale(1.08); } 100%{ opacity:1; transform:scale(1); } }
.m3-floor.m3-floor-bloom{ animation:m3BloomOpen .4s cubic-bezier(.3,.8,.35,1) both; }
.m3-reduced .m3-floor{ animation:none !important; }

.m3-obs{ position:absolute; inset:0; z-index:4; pointer-events:none;
  background-repeat:no-repeat; background-position:center; background-size:contain;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.45)); }
.m3-piece.m3-vine .m3-ico{ filter:drop-shadow(0 3px 2px rgba(70,45,18,.34)) brightness(.82) saturate(.9); }
.m3-piece.m3-vine .m3-obs{ animation:m3vineBreath 2.6s ease-in-out infinite; }
@keyframes m3vineBreath{ 0%,100%{ filter:drop-shadow(0 1px 2px rgba(0,0,0,.45)) brightness(1); }
                         50%{ filter:drop-shadow(0 1px 3px rgba(0,0,0,.5)) brightness(1.12); } }
.m3-reduced .m3-piece.m3-vine .m3-obs{ animation:none; }

/* 🗿 古代石片：★不可消、不可交換★——用靜態微光與略大尺寸讓玩家一眼看出「這不是棋子」 */
.m3-relic .m3-ico{ width:96% !important; height:96% !important;
  filter:drop-shadow(0 3px 4px rgba(0,0,0,.5)) drop-shadow(0 0 7px rgba(120,235,245,.55)); }
.m3-relic{ z-index:3; }
.m3-relic::after{ content:""; position:absolute; inset:8%; border-radius:22%; pointer-events:none;
  box-shadow:inset 0 0 12px rgba(120,235,245,.28); }

/* 🚪 底部出口：標示石片要送到哪裡（規格 §N11 依該欄最底 playable cell 定位） */
.m3-exit{ position:absolute; z-index:1; pointer-events:none;
  background:url('match3/drop-exit.webp') center bottom/contain no-repeat; opacity:.9; }

/* ⬛ 不規則棋盤：mask hole 不畫凹槽、不接受事件（本來就沒有 cell，這裡只確保視覺乾淨） */
/* ═══ ★A1★ non-playable（mask=false）視覺語意 ═══
   有格子底板＝可玩區域；沒有格子底板＝棋盤不存在。
   這兩種狀態★不可共用同一種 empty 外觀★——否則玩家會以為只是暫時沒棋子、等一下會掉進來。
   ★不用紅框／X／鎖頭／文字／黑塊★，靠棋盤本身的形狀表達。 */
.m3-socket.m3-hole{ display:none; }

/* ══════════════════════════════════════════════════════════════════════════
   ⚠ TEMP PLACEHOLDER — 禁區地形（.m3-zone / .m3-edge / .m3-holefill::before）
   ─────────────────────────────────────────────────────────────────────────
   ★這一整套 CSS 漸層地形不是正式版，不得計入 VISUAL COHESION PASS★

   真頁逐層量測（Lv72，共 98 個繪製圖層）證明目前的呈現方式是：
     ① .m3-board 用一張「羽化過的 SVG mask」在禁區位置挖一個洞
     ② 洞下面露出 .m3-holefill（另一個色值的填色層）
     ③ 再往上疊 .m3-zone::before/::after（region bbox 整片鋪色，
        opacity .28/.30、mix-blend-mode: soft-light、7 個 radial-gradient）
     ④ 最後撒 ~90 個 .m3-edge 色塊（每個 1–2 個 radial-gradient + blur .25–2.4px）

   ★root cause★：①②本身就是「柔邊遮罩露出另一塊顏色」——
   結構上就是一塊軟邊矩形，這正是玩家說的「像被 Photoshop mask 挖掉」。
   ③④只是在那塊軟邊矩形上面撒裝飾，不可能改變底層的形狀語意。
   實測把 ③④ 全部關掉，剩下的就是一塊乾淨的淡橄欖色軟邊矩形。

   ⇒ 所以：不再用更多 CSS 漸層去疊。正式版必須改成
      connected region → boundary → terrain component 貼圖組合，
      CSS 漸層退為「墊底防止露黑」的輔助層。
      等 outputs/match3-art-redraw-prompts.html 的 7 張 terrain component
      進 repo 後才做 renderer integration。
   ══════════════════════════════════════════════════════════════════════════ */

/* ★障礙是格子的圖層★：依座標定位在棋盤上，棋子掉落不會帶著它跑 */
.m3-obs-cell{ position:absolute; top:0; left:0; z-index:4; pointer-events:none;
  background-size:contain; background-position:center; background-repeat:no-repeat; }

/* ══════════════════════════════════════════════════════════════════════════
   障礙 CSS 造型 fallback（type-generic）
   ‧ 貼圖是 background-image，★畫在 ::before 的造型永遠在它下面★
     ⇒ 有貼圖時看不見這一層；貼圖 404 時仍然讀得出「這格被東西擋住」，
       不會出現破圖或空白格（與禁區同一條「永不露黑／永不像壞圖」原則）
   ‧ 耐久用 .m3-obs-lv<N> 區分深淺 ⇒ ★受損程度一眼可辨★
   ══════════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   障礙 CSS 造型 fallback（type-generic）— ★第三版，前兩版都錯了★

   ▎第一版：fallback 畫在 ::before + z-index:-1
       父層一加 filter 就建立 stacking context ⇒ 負 z-index 偽元素被畫到
       父層背景「之上」，把機關美術整個蓋掉。

   ▎第二版：fallback 改成 background-image 的「下層」
       background-image: url(貼圖), var(--obs-fb)
       ★這一版的錯更嚴重★：CSS 的多層背景是★每一層都永遠會畫★，
       不是「上層失效才畫下層」。貼圖有 42.7% 是透明的（四角全空），
       所以下層那道棕色漸層就從透明處整片透出來，
       在每個 crate 格子後面畫出一塊 82% 大的實心棕色方板。
       ⇒ 使用者看到的「木箱被貼在一張棕色方形卡片上」就是這個。
       它從來不是 404 fallback，而是一塊常駐的底板。

   ▎第三版（本版）：fallback ★只在貼圖真的載入失敗時才存在★
       ‧ 正常狀態：background-image 只有一層貼圖，其餘完全透明
         ⇒ 木箱周圍直接看到棋盤地面，visible rectangular backing = 0
       ‧ JS 用 new Image() 探測；onerror 才加上 .m3-obs-fallback
       ‧ 連 fallback 本身也不是矩形（有機圓角 + 內縮），
         就算真的 404 也不會出現方形卡片
   ══════════════════════════════════════════════════════════════════════════ */
.m3-obs-cell{
  --obs-fb: none;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}
/* ★只有真的載不到貼圖時才會有這個 class★（見 match3.js 的 obsSpriteProbe）
   內縮 9% + 有機圓角 ⇒ 即使 fallback 生效，也不是一塊填滿格子的方板。 */
.m3-obs-cell.m3-obs-fallback::after{
  content:"";
  position:absolute; inset:9%;
  border-radius:44% 56% 52% 48% / 50% 46% 54% 50%;
  background: var(--obs-fb);
  box-shadow: inset 0 2px 8px rgba(0,0,0,.28);
}
/* 🌿 古藤（既有）：深綠交錯 */
.m3-obs-vine{
  --obs-fb: repeating-linear-gradient(42deg, rgba(74,102,46,.92) 0 5px, rgba(52,76,32,.92) 5px 11px);
}
/* 📦 石箱：木紋＋鐵角 */
.m3-obs-crate{
  --obs-fb: linear-gradient(160deg, #a5763f 0%, #7d5628 100%);
}
/* ❄ 冰晶：半透明藍白 */
.m3-obs-ice{
  --obs-fb: linear-gradient(135deg, rgba(255,255,255,.55) 0%, rgba(150,214,246,.42) 48%, rgba(96,168,214,.5) 100%);
}
/* ▨ 泥苔石 */
.m3-obs-mud{
  --obs-fb: radial-gradient(56% 52% at 46% 42%, rgba(122,152,78,.75) 0%, rgba(78,88,52,.85) 62%, rgba(62,70,42,.9) 100%);
}
/* ❄ v0.61.25 霜鎖鏈 chain ═════════════════════════════════════════════
   ★這一段只作用在 .m3-obs-chain / .m3-obs-chain-end / .m3-obs-chain-mid★
   ⇒ Lv1～110 與 Lv111～120 一個像素都不變（那些關卡沒有 chain 這個型別）。

   ▎為什麼 background-size / position 寫在 JS 不寫在這裡
     多格連成一條鏈需要「第 i 節顯示條帶的第 i 段」，那是★逐格不同的位移★，
     CSS 沒有辦法只用類別表達。幾何常數（帶中心 0.5303 / 0.5846）是
     對 match3/chain-*.webp 逐像素量出來的，寫在 match3.js 的 CHAIN_GEO，
     並由 tools/m3-chain-gate.mjs 對帳 —— ★換素材必須重量，不可以沿用★。

   ▎端點的「受傷」怎麼表現
     店長企劃 §7 明訂 chain-end 不是第四種耐久 ⇒ 端點永遠是 chain-end.webp，
     level 2 → 1 的差別由 .m3-obs-lv1 表現（霜光變弱、略微下沉）。
     這與花叢「障礙層永遠是 bloom-2.webp，level 差別交給 CSS」是同一個既有先例。 */
.m3-obs-chain{
  /* 金屬＋冰：貼圖 404 時仍讀得出「這格被鍊住了」 */
  --obs-fb: linear-gradient(180deg, rgba(196,214,232,.55) 0%, rgba(120,150,184,.72) 52%, rgba(74,100,136,.78) 100%);
  /* ★不要外光★（與 ice 同一條理由）：外溢的 glow 會讓玩家覺得機關超出格子 */
  filter: drop-shadow(0 1px 2px rgba(24,46,72,.38));
}
/* 中段：★不可攻擊★。壓低一點彩度與亮度，讓「亮的是兩頭」成為畫面上的事實，
   而不是只寫在教學文字裡。★這不是把中段藏起來★——仍然完全看得見。 */
.m3-obs-chain.m3-obs-chain-mid{
  filter: drop-shadow(0 1px 2px rgba(24,46,72,.38)) saturate(.88) brightness(.94);
}
/* 端點：★可攻擊★。比中段亮一點、外加一圈很淡的冷藍光。
   ★幅度刻意小★：玩家第一眼要先認出「這是鎖鏈的頭」，才注意到它在發光。 */
.m3-obs-chain.m3-obs-chain-end{
  filter: drop-shadow(0 1px 2px rgba(24,46,72,.38))
          drop-shadow(0 0 3px rgba(150,214,255,.50)) brightness(1.06);
}
/* 端點被打過一次（level 1）：霜光變弱、略微下沉 —— 同一張圖、不同狀態 */
.m3-obs-chain.m3-obs-chain-end.m3-obs-lv1{
  filter: drop-shadow(0 1px 2px rgba(24,46,72,.38)) saturate(.82) brightness(.90);
  transform-origin: 50% 50%;
}
/* 被鎖鏈壓住的棋子：與既有「被藤蔓罩住」同一種語意（變暗），不另創一套 */
.m3-piece.m3-under-chain .m3-ico{ filter: drop-shadow(0 3px 2px rgba(30,52,80,.34)) brightness(.84) saturate(.92); }
.m3-reduced .m3-obs-chain{ animation:none; }

/* 新機關的可讀性修正（★只動 crate/ice/mud，不碰既有 vine 外觀★）：
   crate/mud 貼圖平均亮度只有 57–74，在明亮石盤上會讀成一塊深色方塊。
   ★filter 現在可以安全使用★ —— fallback 已經不是偽元素，不會被 stacking context 反轉。 */
/* ★真頁抓到的「機關超出格子」root cause 之二★
   drop-shadow 是 filter，★會畫在元素框「外面」★（background 會被裁、filter 不會）。
   原本冰塊有 0 0 3px 的外光 ⇒ 一圈亮青色暈開到隔壁格，看起來就是「冰塊貼邊／超界」。
   改成只往下的短落影（0 1px 2px），完全落在 safe-area 內，不會外溢。 */
.m3-obs-crate, .m3-obs-mud{
  filter: brightness(1.16) saturate(1.08) contrast(1.03)
          drop-shadow(0 1px 2px rgba(40,26,10,.40));
}
.m3-obs-ice{
  /* ★不要外光★：冰的質感由貼圖本身表現，不靠 glow 描邊 */
  filter: drop-shadow(0 1px 2px rgba(20,40,60,.32));
}
/* ★opacity 補丁已移除★
   這裡原本有 .m3-obs-ice{opacity:.76} / .m3-obs-lv1{opacity:.62}，
   理由是「讓底下棋子讀得出來」（實測 ice-2 遮蔽 76%、ice-1 遮蔽 98%）。
   但那實際上是在★用透明度掩蓋一張畫錯的素材★ —— ice-2 的 alpha 輪廓
   本身就是一塊填滿的圓角方形冰磚（四角填滿 72–78%），
   調淡只是讓方框變淡，方框還在。
   正確做法是重畫素材（見 outputs/match3-art-redraw-prompts.html：
   邊緣冰霜厚、中央高透明），所以這裡不再用 opacity 假裝解決。
   ⇒ 在新 ice 素材到位之前，冰塊會維持「看得出是方的」，這是誠實的現況。 */
/* 被冰包住的棋子稍微提高彩度，確保仍是最高閱讀層 */
.m3-piece.m3-under-ice .m3-ico{ filter: saturate(1.28) brightness(1.08)
  drop-shadow(0 2px 2px rgba(30,50,70,.35)); }
/* ★死碼已刪★：原本這裡有 .m3-obs-<type>::before 的整套造型（木紋／冰裂／苔蘚）。
   .m3-obs-cell::before 早就沒有 content 宣告 ⇒ 偽元素根本不會生成，這批規則一行都畫不出來。
   留著只會讓人以為「方塊來自偽元素」而查錯方向 —— 棕色方板的唯一來源是
   --obs-fb 那一層常駐背景（見上方第三版說明）。fallback 造型現在統一由
   .m3-obs-cell.m3-obs-fallback::after 一處提供。 */

/* 石盤外層：異形關的剪影邊緣與陰影畫在這裡（filter 早於 mask 套用，必須在外層） */
/* ★棋盤垂直置中★（消除「上面散、下面一大塊黑」）
   ─ 根因不是棋盤太小，是 7×8 的長寬比在高螢幕上必然有剩餘高度，而 .m3-wrap 是
     flex-start ⇒ 剩下的高度全部堆在棋盤下方。iPhone 14（390×844）實測：
       safe-top 47 + 8 ＋ header 60 ＋ HUD 約 60 ＋ 棋盤 418 ＝ 593
       844 − 593 − (safe-bottom 34 + 8) ＝ ★底部約 200px 純黑★
   ─ 棋盤已經受寬度限制（cellSize = 可用寬 ÷ 7），再拉高只會超出螢幕寬，
     所以正解是★把剩餘空間平均分到棋盤上下★，而不是硬撐棋盤。
   ─ margin:auto 0 讓 boardwrap 在剩餘高度裡置中；未來接上底部道具列時，
     道具列排在它後面，剩餘空間會自動重新分配，不需要再改這裡。 */
.m3-boardwrap{ position:relative; margin: auto 0; }
/* ★filter 不能掛在 .m3-boardwrap★（STEP 2 真頁抓到）
   filter 會作用在★整個子樹合成後的 alpha★上。地形層 .m3-terrain 是 .m3-board 的
   子節點 ⇒ 每一塊岩石都會各自長出一份 `0 12px 26px rgba(0,0,0,.5)` 的投影，
   在棋盤上讀成「岩石後面一坨髒灰色」（店長回報：「後面黑黑的是什麼」）。
   金色 rim 也一樣會描在每塊岩石外緣，讓地形邊緣發黃。
   ⇒ 改掛在 .m3-board-bg —— 它就是「被 mask 切成關卡形狀的石盤本體」，
     剪影正是我們要描邊與投影的對象，而且它 z-index:0、不含任何地形或棋子。 */
.m3-board.m3-shaped > .m3-board-bg{
  filter: drop-shadow(0 0 1px rgba(228,180,90,.95))      /* 沿著剪影的金色細邊＝取代矩形金框 */
          drop-shadow(0 0 3px rgba(228,180,90,.5))
          drop-shadow(0 12px 26px rgba(0,0,0,.5));       /* 厚度／浮起 */
}
/* ══ 🗿 v0.60.778 MASK ARCHITECTURE MIGRATION ══════════════════════════════
   遮罩改掛在 .m3-board-bg（專責底圖層），不再掛在 .m3-board 容器本身。
   ★為什麼★：mask-image 掛在容器上會①裁掉整個 subtree ②讓容器自成
   stacking context。逐 playable cell 的 <rect> 遮罩在禁區位置是一個
   ★邊界貼齊格線的矩形洞★，任何地形層掛在裡面就直接被裁成 0 像素，
   墊在下面則只能從矩形洞露出來 ⇒ 崩解羽化消失、90° 硬切回來。
   分層之後：.m3-board 不再有 mask，地形可以自由跨越格線羽化。 */
.m3-board-bg{
  position:absolute; inset:0; z-index:0;   /* 最底層；不影響 board 的 layout 尺寸 */
  pointer-events:none;
  border-radius: inherit;                  /* 非異形關維持原本的 18px 圓角 */
  background: inherit;                     /* ★底色／底圖沿用 .m3-board 的宣告，不另建第二份★ */
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;     mask-size: 100% 100%;
  -webkit-mask-position: 0 0;       mask-position: 0 0;
}
/* 異形關：石盤本體被 mask 切成關卡真正的形狀 —— 但只切「底圖層」 */
.m3-board.m3-shaped{
  border-radius: 0;                 /* 形狀由 mask 決定，不再是圓角矩形 */
  box-shadow: none;                 /* 矩形陰影會被 mask 切掉；改由 .m3-boardwrap 的 drop-shadow 提供 */
  /* ★容器自己不再畫背景★：否則沒被 mask 的矩形底色會整片露在剪影外面。
     底色／底圖改由 .m3-board-bg 承擔（它用 background:inherit 拿到同一份宣告，
     所以下面這條 none 必須★晚於★ .m3-board-bg 的 inherit 求值 —— CSS 的
     inherit 取的是父層 computed value，父層被設成 none 就一起沒了 ⇒
     改成在 shaped 時直接把同一組背景明寫在 bg 層，見下一條規則）。 */
  background: none;
}
.m3-board.m3-shaped > .m3-board-bg{
  background: #b6a480 url('match3/board-stone.webp') center / cover no-repeat;
}
.m3-ch2 .m3-board.m3-shaped > .m3-board-bg{
  background: #7d7a5c url('match3/board-stone.webp') center / cover no-repeat;
}
/* 矩形金框在異形盤上一定是錯的（會被切成碎片）→ 異形關直接不畫，改用上面的剪影 rim */
.m3-board.m3-shaped .m3-frame{ display:none; }

/* ══ ★v0.60.787 清除舊膠囊 HUD 的殘留規則★ ═══════════════════════════════
   這裡原本有兩組舊的 `.m3-goals` / `.m3-goal` 宣告（膠囊式 HUD 時代留下的），
   位置在新石材 HUD 之後 ⇒ ★後來居上，把新 HUD 蓋掉★。實際後果就是店長一直看到的三件事：
     ① `flex-wrap:wrap` ⇒ 目標放不下時★換到第二行★ ⇒ HUD 整條變高 ⇒ 畫面跳動
     ② `.m3-goal{ padding:4px 9px }` ⇒ 每個目標又變回一個「小方塊」⇒ 醜矩形感
     ③ `.m3-lvname{ font-size:13px }` ⇒ 關卡名跟 Lv 數字搶視覺重量
   ★正確做法是刪掉，不是再疊一層 !important★ —— 疊上去只會讓下一個人更難查。
   目標／關卡名的樣式★單一來源＝本檔最上方的 HUD 區塊★。
   這裡只保留「不屬於 HUD 版面」的那一條：障礙目標的圖示。 */
.m3-goal .m3-goal-ic.obs{ background:url('match3/vine-1.webp') center/contain no-repeat; }
.m3-level{ max-width:38vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* ★同上：關卡名不截字★（見 .m3-lvname 說明），這裡把舊的 nowrap 覆寫掉 */
.m3-lvname{ white-space:normal !important; text-overflow:clip !important; }

/* ⭐ 星等晉升演出：★依序點亮、逐顆加強、三星共鳴★（禁止三顆同時直接顯示結果） */
.m3-rstars{ display:flex; gap:8px; justify-content:center; margin-bottom:6px; }
.m3-rstars .m3-star{ width:46px; height:46px; object-fit:contain; opacity:.8; transform:scale(.84); }
/* 之前就有的星：直接就是亮的，只做很輕的淡入，★不假裝重新取得★ */
.m3-rstars .m3-star.old{ opacity:1; transform:scale(1); filter:drop-shadow(0 0 6px rgba(255,214,120,.5));
  animation:m3StarFade .3s ease both; }
/* 本局新取得的星：依序彈出，第 2 顆更強、第 3 顆最高潮 */
.m3-rstars .m3-star.new{ opacity:0; }
.m3-rstars .m3-star.new[data-n="1"]{ animation:m3StarPop1 .5s cubic-bezier(.2,1.6,.45,1) .15s both; }
.m3-rstars .m3-star.new[data-n="2"]{ animation:m3StarPop2 .55s cubic-bezier(.2,1.7,.45,1) .55s both; }
.m3-rstars .m3-star.new[data-n="3"]{ animation:m3StarPop3 .62s cubic-bezier(.2,1.85,.45,1) .98s both; }
@keyframes m3StarFade{ from{opacity:0;transform:scale(.9)} to{opacity:1;transform:scale(1)} }
@keyframes m3StarPop1{ 0%{opacity:0;transform:scale(.35) rotate(-22deg);filter:none}
  62%{opacity:1;transform:scale(1.20) rotate(4deg);filter:drop-shadow(0 0 14px rgba(255,214,120,.9))}
  100%{opacity:1;transform:scale(1);filter:drop-shadow(0 0 8px rgba(255,214,120,.75))} }
@keyframes m3StarPop2{ 0%{opacity:0;transform:scale(.3) rotate(-26deg);filter:none}
  58%{opacity:1;transform:scale(1.30) rotate(6deg);filter:drop-shadow(0 0 20px rgba(255,224,150,1))}
  100%{opacity:1;transform:scale(1);filter:drop-shadow(0 0 11px rgba(255,214,120,.85))} }
@keyframes m3StarPop3{ 0%{opacity:0;transform:scale(.25) rotate(-32deg);filter:none}
  55%{opacity:1;transform:scale(1.44) rotate(9deg);filter:drop-shadow(0 0 28px rgba(255,238,180,1)) drop-shadow(0 0 44px rgba(120,235,245,.75))}
  78%{transform:scale(.96) rotate(-2deg)}
  100%{opacity:1;transform:scale(1);filter:drop-shadow(0 0 14px rgba(255,224,150,.95))} }
/* 3★ 完美過關：三顆最後一起共鳴 */
/* ★★v0.60.788 P0-2：3★ 通關時三顆星全部看不見★★
   移除的那一行是：
       .m3-rstars.lv3 .m3-star.on{ animation-name:var(--m3sn,initial); }
   它排在 `.m3-star.new[data-n="N"]{ animation:m3StarPopN ... }` 之後、特異度相同（0,4,0），
   所以★把星星的 pop 動畫名蓋成 initial＝none★；而 --m3sn 這個變數★從來沒有任何地方定義★。
   於是 `.m3-star.new{ opacity:0 }` 就永遠停在 0 —— 星星有 DOM、有圖、但一顆都看不到。
   ★只在 3★（.lv3）時發生★，也就是玩家表現最好的那一次結算最難看。
   本機 Chrome 實測 computed：animation-name:none／opacity:0／natW 256（圖是好的）。
   同批一併移除旁邊幾條沒有作用的空殼規則（::after content:""、空 block、
   animation-composition:replace），它們只會讓下一個人更難查。
   保留：animation-fill-mode 與 position（有實際作用）。 */
.m3-rstars.lv3 .m3-star{ animation-fill-mode:both; }
.m3-rstars.lv3 .m3-star.on{ position:relative; }
.m3-rstars.lv3{ animation:m3StarsResonate .7s ease 1.62s 1 both; }
@keyframes m3StarsResonate{ 0%{transform:scale(1);filter:none}
  40%{transform:scale(1.10);filter:drop-shadow(0 0 26px rgba(255,238,180,.95))}
  100%{transform:scale(1);filter:none} }
/* 晉升字樣 */
.m3-rupgrade{ text-align:center; font-size:13.5px; font-weight:700; color:#c9b892; margin:-2px 0 6px;
  animation:m3StarFade .35s ease 1.35s both; }
.m3-rupgrade b{ color:#ffd97a; font-size:15px; }
/* ★無動畫 fallback：全部直接呈現最終結果★ */
.m3-reduced .m3-rstars,.m3-reduced .m3-rstars .m3-star,.m3-reduced .m3-rupgrade{ animation:none !important; }
.m3-reduced .m3-rstars .m3-star.on{ opacity:1; transform:scale(1); }
.m3-reduced .m3-rstars .m3-star.new{ opacity:1; }
@media (prefers-reduced-motion:reduce){
  .m3-rstars,.m3-rstars .m3-star,.m3-rupgrade{ animation:none !important; }
  .m3-rstars .m3-star.on{ opacity:1; transform:scale(1); }
  .m3-rstars .m3-star.new{ opacity:1; } }

/* 結算多目標逐條 */
.m3-rinfo .m3-rgoal{ gap:8px; }
.m3-rinfo .m3-rgoal.ok span{ color:#a8e6c0; }
.m3-rtick{ font-style:normal; color:#7fdba0; font-weight:900; }

/* 🏁 Pack End：★不導向不存在的關卡★ */
.m3-packend{ margin-top:10px; padding:10px 16px; border-radius:14px; text-align:center;
  background:rgba(10,14,20,.5); border:1px solid rgba(255,224,150,.35);
  color:#ffe9a8; font-size:15.5px; font-weight:800; line-height:1.6; }
.m3-packend span{ display:block; font-size:13px; font-weight:600; color:#c9b892; margin-top:2px; }

/* ═══ 批10-G 新機關情境導引（短、直接、在正式棋盤上示範）═══ */
.m3-guiding .m3-board{ position:relative; }
/* ★2026-08-28 Motion Gate 修正★：這條規則只想「把非聚焦棋子調暗」，但用 transition
   簡寫整條覆蓋掉了 .m3-piece 原本的 `transition:transform .16s …`——變成非聚焦棋子在
   導引期間只剩 opacity 會轉場，transform（交換／彈回／掉落位移）完全沒有 transition，
   直接瞬移。實測：Lv24（vine2 導引關）首次進場時，無效交換的彈回動畫對非聚焦棋子
   會整格瞬間跳回，不是滑回去。
   ★修法★：把 transform 轉場原樣加回去（與 .m3-piece 基礎規則同一條 cubic-bezier），
   只補回遺漏的屬性，不改暗化本身、不改任何幾何／判定，導引聚焦棋子（.m3-gfocus）
   的呈現完全不受影響。 */
.m3-guiding .m3-piece:not(.m3-gfocus){ opacity:.42; filter:saturate(.7);
  transition:opacity .25s ease, transform .16s cubic-bezier(0.2, 0.7, 0.3, 1); }
.m3-piece.m3-gfocus{ z-index:6; }
.m3-piece.m3-gfocus::after{ content:""; position:absolute; inset:-4%; border-radius:26%; pointer-events:none;
  outline:2px solid rgba(255,224,150,.95); outline-offset:-2px;
  box-shadow:0 0 14px 3px rgba(255,214,120,.6); animation:m3GuidePulse 1.4s ease-in-out infinite; }
@keyframes m3GuidePulse{ 0%,100%{ box-shadow:0 0 10px 2px rgba(255,214,120,.45); } 50%{ box-shadow:0 0 18px 5px rgba(255,224,150,.85); } }
.m3-guide{ position:absolute; left:50%; transform:translateX(-50%);
  bottom:calc(env(safe-area-inset-bottom,0px) + 16px); z-index:37; pointer-events:none;
  width:min(90vw,420px); box-sizing:border-box; padding:13px 18px; border-radius:16px;
  background:linear-gradient(180deg,rgba(28,22,14,.97),rgba(18,14,9,.98));
  border:1px solid rgba(255,214,120,.5); box-shadow:0 10px 30px rgba(0,0,0,.45); }
.m3-guide p{ margin:0; text-align:center; color:#fbf3e2; font-size:16px; line-height:1.65; font-weight:600; }
.m3-guide p b{ color:#ffd97a; font-weight:800; }
/* ★v0.61.22★「知道了」：卡本體是 pointer-events:none（不擋棋盤），
   所以只把按鈕打開。字夠大、點擊區 ≥44px，婆婆媽媽按得到。 */
.m3-guide{ pointer-events:none; }
.m3-guide-go{ pointer-events:auto; display:block; margin:10px auto 0; min-height:44px; padding:0 22px;
  border-radius:999px; border:1px solid rgba(255,214,120,.65); cursor:pointer;
  background:linear-gradient(180deg,#f0c775,#d09a3c); color:#2b1d07;
  font-size:16px; font-weight:800; letter-spacing:.5px;
  font-family:-apple-system,"PingFang TC","Noto Sans TC","Microsoft JhengHei",sans-serif; }
.m3-guide-go:active{ filter:brightness(.94); }
@media (max-width:360px){ .m3-guide-go{ font-size:15px; padding:0 18px; } }
@media (max-width:360px){ .m3-guide p{ font-size:15px; } }
.m3-goal.m3-gpulse{ animation:m3GoalPulse .8s ease-in-out 2; }
@keyframes m3GoalPulse{ 0%,100%{ transform:scale(1); } 50%{ transform:scale(1.14); box-shadow:0 0 12px rgba(255,214,120,.8); } }
.m3-reduced .m3-piece.m3-gfocus::after,.m3-reduced .m3-goal.m3-gpulse{ animation:none; }
@media (prefers-reduced-motion:reduce){ .m3-piece.m3-gfocus::after,.m3-goal.m3-gpulse{ animation:none; } }

/* 洗牌提示 */
.m3-shuffle{ position:absolute; left:50%; top:46%; transform:translate(-50%,-50%); z-index:36; pointer-events:none;
  padding:10px 18px; border-radius:999px; background:rgba(9,16,28,.9); border:1px solid rgba(210,165,78,.5);
  color:#f2e4c6; font-size:15px; font-weight:700; white-space:nowrap; animation:m3ShufIn .25s ease both; }
@keyframes m3ShufIn{ from{opacity:0;transform:translate(-50%,-46%)} to{opacity:1;transform:translate(-50%,-50%)} }

/* ⚗️ QA-only 選關列（正式站不會輸出這段 DOM，樣式留著無害） */
.cave-m3qa{ display:flex; gap:6px; align-items:center; flex-wrap:wrap; padding:6px 10px;
  background:rgba(120,20,20,.85); color:#ffe; font-size:12.5px; font-weight:700; }
.cave-m3qa select{ font-size:12.5px; max-width:46vw; }
.cave-m3qa button{ font-size:12px; padding:4px 8px; border-radius:6px; border:0; background:#ffd97a; color:#3a2708; font-weight:800; }


/* ═══ 機關破解回饋（Obstacle Feedback Pass）═══
   ★純表現層★：不影響 lifecycle／座標／傷害次數／cascade。單株 0.35–0.5s，與 cascade 並行。 */
.m3-obs-cell.m3-obs-hit{ animation:m3ObsHit .38s cubic-bezier(.36,.07,.19,.97) both; }
@keyframes m3ObsHit{
  0%{ transform:translate(var(--ox,0),var(--oy,0)); filter:none; }
  15%{ filter:brightness(1.9) drop-shadow(0 0 6px rgba(180,255,190,.9)); }
  30%{ filter:brightness(1.25) drop-shadow(0 0 3px rgba(180,255,190,.5)); }
  100%{ filter:none; }
}
.m3-obs-cell.m3-obs-hit{ animation-name:m3ObsHit, m3ObsShake; animation-duration:.38s,.3s; }
@keyframes m3ObsShake{ 0%,100%{ margin:0 } 20%{ margin:0 0 0 -3px } 40%{ margin:0 0 0 3px } 60%{ margin:0 0 0 -2px } 80%{ margin:0 0 0 2px } }
.m3-obs-cell.m3-obs-break{ animation:m3ObsBreak .5s ease-out both; }
@keyframes m3ObsBreak{
  0%{ opacity:1; transform:scale(1); filter:none; }
  25%{ opacity:1; transform:scale(1.14); filter:brightness(2.2) drop-shadow(0 0 12px rgba(255,224,150,.95)); }
  100%{ opacity:0; transform:scale(1.45); filter:brightness(1.6) drop-shadow(0 0 18px rgba(140,255,190,.6)); }
}
/* 碎片層 */
.m3-obsfx{ position:absolute; top:0; left:0; z-index:6; pointer-events:none; }
.m3-obsfx .ofx-frag{ position:absolute; left:50%; top:50%; width:22%; height:22%; border-radius:40% 55% 45% 50%;
  transform:translate(-50%,-50%) rotate(var(--a)) translateY(0) scale(1); opacity:0; }
.ofx-hit .ofx-frag{ background:linear-gradient(160deg,#7fd88f,#3f8f5c); animation:m3Frag .46s ease-out both; }
.ofx-break .ofx-frag{ background:linear-gradient(160deg,#ffe79a,#8fe6b6); animation:m3FragBig .62s ease-out both; }
.ofx-relic .ofx-frag{ background:linear-gradient(160deg,#ffe79a,#e4b45a); animation:m3FragBig .68s ease-out both; }
@keyframes m3Frag{ 0%{opacity:.95; transform:translate(-50%,-50%) rotate(var(--a)) translateY(0) scale(.6)}
  100%{opacity:0; transform:translate(-50%,-50%) rotate(var(--a)) translateY(-140%) scale(.25)} }
@keyframes m3FragBig{ 0%{opacity:1; transform:translate(-50%,-50%) rotate(var(--a)) translateY(0) scale(.7)}
  100%{opacity:0; transform:translate(-50%,-50%) rotate(var(--a)) translateY(-220%) scale(.2)} }
/* 解封光圈（Lv1 → remove） */
.ofx-break::after,.ofx-relic::after{ content:""; position:absolute; inset:6%; border-radius:50%;
  border:2px solid rgba(255,232,170,.95); animation:m3Unseal .58s ease-out both; }
@keyframes m3Unseal{ 0%{ transform:scale(.35); opacity:1 } 100%{ transform:scale(1.6); opacity:0 } }
/* 多株同時破解＝整盤共鳴 */
.m3-board.m3-obs-resonate{ animation:m3Resonate .7s ease-out; }
@keyframes m3Resonate{ 0%,100%{ filter:none } 30%{ filter:brightness(1.16) saturate(1.12) } }
/* 石片：落地／接近出口／送達 */
.m3-piece.m3-relic-land{ animation:m3RelicLand .3s cubic-bezier(.34,1.56,.64,1) both; }
@keyframes m3RelicLand{ 0%{ transform-origin:50% 100% } 40%{ filter:brightness(1.3) } 100%{ filter:none } }
.m3-socket.m3-exit-hint{ animation:m3ExitHint .9s ease-in-out 2; }
@keyframes m3ExitHint{ 0%,100%{ box-shadow:inset 0 2px 3px rgba(50,40,22,.11) }
  50%{ box-shadow:inset 0 0 0 2px rgba(255,224,150,.85), 0 0 10px rgba(255,214,120,.6) } }
/* 省電／減少動態：全部關閉，行為完全不變 */
.m3-reduced .m3-obs-cell.m3-obs-hit,.m3-reduced .m3-obs-cell.m3-obs-break,
.m3-reduced .m3-board.m3-obs-resonate,.m3-reduced .m3-piece.m3-relic-land,.m3-reduced .m3-socket.m3-exit-hint{ animation:none !important; }
@media (prefers-reduced-motion:reduce){
  .m3-obs-cell.m3-obs-hit,.m3-obs-cell.m3-obs-break,.m3-board.m3-obs-resonate,
  .m3-piece.m3-relic-land,.m3-socket.m3-exit-hint,.m3-obsfx{ animation:none !important; }
}


/* ═══ 直線特殊棋：待機外觀 v2（店長 2026-08-30 退件重做）═══════════════════
   ★退件的是什麼★
     一根筆直的青白發光棒直接蓋在棋子中央，加上圓角外框 ——
     看起來像尺／準星／UI debug guide／科幻雷射定位器。
   ★新的視覺語言★
     ‧ 保留食物本體：牛奶還是牛奶、草莓還是草莓，FX 只附著在★周圍★
     ‧ 方向感由「棋子上下（或左右）兩股柔和魔法流光」自然形成
     ‧ 靠近棋子最亮，離開棋子迅速變細、變透明，尾端散成星塵
     ‧ ★沒有★菱形箭頭、尺規刻度、筆直矩形光柱、機械線條、選取外框
   ★待機必須收斂★：常駐只有很輕的呼吸，華麗留給發動的那幾百毫秒。
   ★效能★：純 gradient ＋ box-shadow 星點，★不用 blur／mix-blend-mode／backdrop-filter★，
   也不新增任何常駐 DOM particle（海邊平板那一輪的教訓）。 */
.m3-spdir{ position:absolute; inset:0; z-index:4; pointer-events:none; overflow:visible; }
/* 兩股流光：::before ＝ 上／左，::after ＝ 下／右。
   用 radial-gradient 的橢圓自然收尖，不需要 blur。 */
.m3-spdir::before,.m3-spdir::after{ content:""; position:absolute; pointer-events:none;
  animation:m3wispBreath 2.6s ease-in-out infinite; }

/* 直向：上下兩股 */
.m3-piece.sp-linev .m3-spdir::before{
  left:31%; width:38%; bottom:50%; height:72%;
  background:radial-gradient(ellipse 58% 100% at 50% 100%,
    rgba(255,252,232,1) 0%, rgba(255,236,176,.92) 14%, rgba(255,214,140,.6) 34%, rgba(190,240,255,.34) 62%, rgba(190,240,255,0) 100%);
  box-shadow:0 -7px 12px -6px rgba(255,226,160,.7); }
.m3-piece.sp-linev .m3-spdir::after{
  left:31%; width:38%; top:50%; height:72%;
  background:radial-gradient(ellipse 58% 100% at 50% 0%,
    rgba(255,252,232,1) 0%, rgba(255,236,176,.92) 14%, rgba(255,214,140,.6) 34%, rgba(190,240,255,.34) 62%, rgba(190,240,255,0) 100%);
  box-shadow:0 7px 12px -6px rgba(255,226,160,.7); animation-delay:-1.3s; }

/* 橫向：左右兩股（★不是把同一根尺轉 90°★，是同一套流光語言的另一個方向） */
.m3-piece.sp-lineh .m3-spdir::before{
  top:31%; height:38%; right:50%; width:72%;
  background:radial-gradient(ellipse 100% 58% at 100% 50%,
    rgba(255,252,232,1) 0%, rgba(255,236,176,.92) 14%, rgba(255,214,140,.6) 34%, rgba(190,240,255,.34) 62%, rgba(190,240,255,0) 100%);
  box-shadow:-7px 0 12px -6px rgba(255,226,160,.7); }
.m3-piece.sp-lineh .m3-spdir::after{
  top:31%; height:38%; left:50%; width:72%;
  background:radial-gradient(ellipse 100% 58% at 0% 50%,
    rgba(255,252,232,1) 0%, rgba(255,236,176,.92) 14%, rgba(255,214,140,.6) 34%, rgba(190,240,255,.34) 62%, rgba(190,240,255,0) 100%);
  box-shadow:7px 0 12px -6px rgba(255,226,160,.7); animation-delay:-1.3s; }

/* 常駐動態：只有很輕的明暗呼吸與長度微變，不位移棋子、不閃爍 */
@keyframes m3wispBreath{ 0%,100%{ opacity:.78; transform:scale(.95) } 50%{ opacity:1; transform:scale(1) } }

/* 星塵點：用 box-shadow 疊在流光尾端，★0 額外 DOM★ */
.m3-piece.sp-line .m3-sp{ overflow:visible; }
.m3-piece.sp-line .m3-sp::after{ content:""; position:absolute; left:50%; top:50%;
  width:3px; height:3px; margin:-1.5px 0 0 -1.5px; border-radius:50%;
  background:rgba(255,252,235,1); pointer-events:none;
  animation:m3wispSpark 2.6s ease-in-out infinite; }
.m3-piece.sp-linev .m3-sp::after{
  box-shadow: -6px -24px 0 0 rgba(255,246,212,1), 5px -34px 0 -0.5px rgba(202,242,255,.9),
               7px  24px 0 0 rgba(255,246,212,1), -6px  34px 0 -0.5px rgba(202,242,255,.9); }
.m3-piece.sp-lineh .m3-sp::after{
  box-shadow: -24px -6px 0 0 rgba(255,246,212,1), -34px 5px 0 -0.5px rgba(202,242,255,.9),
               24px  7px 0 0 rgba(255,246,212,1),  34px -6px 0 -0.5px rgba(202,242,255,.9); }
@keyframes m3wispSpark{ 0%,100%{ opacity:.5 } 50%{ opacity:1 } }

/* 棋子本體：只做很輕的暖光提亮，★不加外框★（外框會讓整顆變成 UI 選取狀態） */
.m3-piece.sp-line{ z-index:5; }
.m3-piece.sp-line .m3-ico{ filter:brightness(1.12) saturate(1.06) drop-shadow(0 0 5px rgba(255,226,168,.7)); }

/* 省電／減少動態：保留方向辨識（兩股流光都在），只關掉動態 */
.m3-reduced .m3-spdir::before,.m3-reduced .m3-spdir::after,
.m3-reduced .m3-piece.sp-line .m3-sp::after{ animation:none !important; opacity:.92 !important; transform:none !important; }
@media (prefers-reduced-motion:reduce){
  .m3-spdir::before,.m3-spdir::after,.m3-piece.sp-line .m3-sp::after{
    animation:none !important; opacity:.92 !important; transform:none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
   🗿 v0.60.777【A】禁區＝神祕石碑遺跡（★純表現層，0 gameplay 變更★）
   ─────────────────────────────────────────────────────────────────────────
   ★root cause★：.m3-board.m3-shaped 用 mask-image 只保留 playable 格，
     blocked 格的石盤材質被切掉 ⇒ 露出後面 #caveScene .cave-m3 的 #0a0f1a
     ⇒ 玩家看到「純黑矩形」，讀起來像圖沒載入／UI 壞掉。
   ★修法★：在 .m3-board ★下面★再墊一層 .m3-holefill，套「反向 mask」
     （只顯示 blocked 格），把遺跡質感畫在洞裡。
     ‧ 形狀由同一份 level.mask 產生 ⇒ ★28 個異形關全部自動適用★，不需逐關美術
     ‧ 底色是 CSS 漸層（石質暖灰）⇒ ★就算 PNG 404 也永遠不會變回純黑★
     ‧ blocked topology／gravity／refill／swap／match 一律不碰
   ══════════════════════════════════════════════════════════════════════════ */
/* ── 遺跡地形（Ancient Ruins Ground）──────────────────────────────────
   ★不是「灰色方塊」★：暖灰褐石材 ＋ 石板接縫 ＋ 青苔 ＋ 碎石 ＋ 極淡金色紋路。
   邊界處理：inset 陰影（凹陷感）＋ 底部暖光（反射）＋ ::before 的苔色暈染，
   讓 blocked／playable 的交界讀起來像「石板破損邊」而不是 CSS 矩形。
   ★所有裝飾都被 mask 夾在 blocked 格內，絕不侵入 playable tile★。 */
.m3-holefill{
  position:absolute; left:0; top:0; width:100%; height:100%;
  z-index:0;                      /* 在 .m3-board 之下（board 會被 mask 切出洞） */
  border-radius:18px;
  pointer-events:none;
  /* ★fallback 底色：暖砂岩古地面，永遠不是黑色★
     真頁實測調整：原本偏橄欖冷灰、且亮度均勻 ⇒ 讀起來像一塊「灰色 UI 方塊」。
     改成①暖砂岩色階 ②中央亮、四周暗的內暈影 ⇒ 有立體凹陷感，
     跟暖米色石盤同一個色系，但明顯更舊、更暗，一眼就知道「這裡不能放棋」。 */
  /* ★真頁第三次修正：色差才是「看得出矩形」的主因★
     原本 #9c8b6b→#6d6045 比棋盤(#d8cdbc 一帶)暗太多、又偏橄欖，
     色塊邊界即使羽化也藏不住 —— 玩家一眼就看到那個矩形。
     改成「只比棋盤暗一點點的同色系舊石地」：色差小到邊界自然消失，
     ★「這裡不能放棋」改由地標、苔蘚碎石、內凹暈影去傳達★，
     不再依賴「一塊明顯較暗的方塊」。 */
  background:
    radial-gradient(74% 64% at 46% 40%, rgba(255,250,232,.16) 0%, rgba(0,0,0,0) 76%),
    radial-gradient(126% 116% at 50% 50%, rgba(0,0,0,0) 52%, rgba(74,58,34,.22) 100%),
    linear-gradient(176deg, #cabb9f 0%, #bcab8d 48%, #a8956f 100%);
  background-color:#bdad90;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-size:100% 100%;    mask-size:100% 100%;
  -webkit-mask-position:0 0;      mask-position:0 0;
  /* 內凹感保留但大幅減弱，避免形成一圈清楚的深色框 */
  box-shadow: inset 0 5px 13px rgba(60,46,24,.20),
              inset 0 -3px 10px rgba(255,246,214,.10);
}
/* 石板接縫＋青苔＋碎石＋淡金紋路（純 CSS，0 額外請求） */
.m3-holefill::before{
  content:""; position:absolute; inset:0; pointer-events:none;
  background:
    /* 極小碎石 / 斑點 */
    radial-gradient(1.6px 1.6px at 18% 28%, rgba(255,246,214,.24) 0%, rgba(0,0,0,0) 100%),
    radial-gradient(1.4px 1.4px at 72% 61%, rgba(255,246,214,.20) 0%, rgba(0,0,0,0) 100%),
    radial-gradient(1.8px 1.8px at 41% 79%, rgba(30,24,14,.28) 0%, rgba(0,0,0,0) 100%),
    radial-gradient(1.5px 1.5px at 86% 22%, rgba(30,24,14,.24) 0%, rgba(0,0,0,0) 100%),
    /* 青苔：邊角偏綠 */
    radial-gradient(46% 40% at 6% 96%, rgba(108,138,74,.30) 0%, rgba(0,0,0,0) 72%),
    radial-gradient(40% 36% at 96% 8%, rgba(108,138,74,.24) 0%, rgba(0,0,0,0) 72%),
    /* ★真頁實測後移除 repeating-linear-gradient 的雙向接縫★
       原本用兩個方向的重複線條模擬「石板接縫」，在大面積異形禁區上
       會疊成明顯的交叉格紋，讀起來像地毯／debug texture／缺 tile 的區域，
       完全不像遊戲世界。改成不規則的岩層斑塊 —— 沒有任何重複週期，
       所以放大到整片異形區也不會出現規律格線。 */
    radial-gradient(38% 26% at 22% 18%, rgba(28,22,12,.20) 0%, rgba(0,0,0,0) 70%),
    radial-gradient(30% 44% at 78% 34%, rgba(28,22,12,.16) 0%, rgba(0,0,0,0) 72%),
    radial-gradient(44% 22% at 56% 72%, rgba(28,22,12,.18) 0%, rgba(0,0,0,0) 70%),
    radial-gradient(26% 34% at 12% 62%, rgba(255,244,214,.10) 0%, rgba(0,0,0,0) 74%),
    radial-gradient(34% 28% at 88% 82%, rgba(255,244,214,.08) 0%, rgba(0,0,0,0) 74%);
  mix-blend-mode:soft-light; opacity:.95;
}
/* 遺跡貼圖（可有可無）：載入失敗時 .m3-holefill 的漸層仍在 ⇒ 不會變黑 */
/* 洞裡的石質底＝沿用既有 board-stone.webp，壓暗＋降飽和 ⇒ 讀得出「這裡是遺跡、不能放棋」
   但又跟盤面同一種石頭。載入失敗只是少一層貼圖，底下漸層仍在。 */
.m3-holefill-tex{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; opacity:.46; pointer-events:none;
  /* ★真頁實測調整★：原本 brightness(.62)+multiply 讓禁區壓成偏冷的灰色矩形，
     跟暖米色石盤脫節、讀起來像「一塊灰色 UI 方塊」。改成輕微壓暗＋保留暖度，
     並用 soft-light 疊，讓它變成「同一塊石頭上比較舊、比較暗的一段」。 */
  filter:brightness(.86) saturate(.92) contrast(1.02) sepia(.12);
  mix-blend-mode:soft-light;
}
/* ══════════════════════════════════════════════════════════════════════════
   🗿💧🌿 禁區主題層 .m3-zone —— A 石碑／B 泉水／C 樹根
   ‧ ★每一個禁區 region 都會拿到一層 .m3-zone★（見 match3.js landmarkRegions）
     ⇒ 不存在「沒被處理到、露出純黑」的禁區
   ‧ .m3-zone-lm 才額外掛 PNG；長條與異形只吃主題 CSS 地形，
     ★不硬塞方形 PNG★（形狀由 gameplay 決定，美術去適應）
   ══════════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   🌿 第二章「失落庭園」外緣溶解（★只在 .m3-ch2，第一章 50 關零影響★）
   ─────────────────────────────────────────────────────────────────────────
   真頁量到的四個「像 UI 卡片」的結構性來源，逐一拆掉：
     ① .m3-boardwrap 的金色 drop-shadow 描邊 → 換成柔和的地面接觸陰影
     ② .m3-frame 的金色實線邊框            → 第二章不顯示
     ③ .m3-board 不透明矩形                → 由 ch2BoardMaskUrl() 給不規則羽化外緣
     ④ 56 個 socket 的卡片感               → 再降一階，讀成石板接縫
   ══════════════════════════════════════════════════════════════════════════ */
/* ★同 .m3-boardwrap.m3-shaped：filter 不能掛在 wrap★（店長回報「後面黑黑的是什麼」）
   filter 作用在整個子樹合成後的 alpha 上 ⇒ .m3-terrain 裡每一塊岩石都各自
   長出一份 `0 10px 18px rgba(6,12,10,.42)` 的投影，在棋盤上讀成一坨髒灰色。
   ⇒ 落地陰影改掛 .m3-board-bg（被 mask 切成關卡形狀的石板本體）。 */
.m3-ch2 .m3-board.m3-shaped > .m3-board-bg{
  /* ★拿掉金色描邊★：那條亮金線就是「這是一塊 UI 面板」最強的訊號。
     換成很柔的落地陰影 —— 棋盤像是「放在地上的一片石板」，不是浮在畫面上的卡片。 */
  filter: drop-shadow(0 10px 18px rgba(6,12,10,.42))
          drop-shadow(0 2px 4px rgba(6,12,10,.28)) !important;
}
/* 非異形（無 mask）第二章關卡：沒有 .m3-board-bg 剪影可用，仍掛在 board 本身
   —— 那時 board 是一個矩形，投影就是矩形，且地形層在 board 內、不受影響。 */
.m3-ch2 .m3-board:not(.m3-shaped){
  box-shadow: 0 10px 18px rgba(6,12,10,.42), 0 2px 4px rgba(6,12,10,.28);
}
.m3-ch2 .m3-frame{ display:none !important; }        /* 第二章不要金框 */
.m3-ch2 .m3-board{
  border-radius: 0;                                   /* 形狀改由 mask 決定 */
  /* ★真頁量到的最後一個「卡片感」主因：明度與色相跟場景差太多★
     原本 #b6a480（亮米色）浮在深綠洞窟上 ⇒ 讀成「白色卡片貼在暗場景上」，
     金框拿掉之後反而更明顯。第二章把石板拉進洞窟的色域：偏綠的暖石、
     明度降到接近場景的中段。★對比交給棋子★ —— 飽和鮮豔的草莓／薰衣草
     在中暗底上反而更跳，可讀性不減反增。
     用 background-blend-mode 只染貼圖，★不影響任何子元素（棋子/機關/HUD）★。 */
  background: #7d7a5c url('match3/board-stone.webp') center / cover no-repeat;
  background-blend-mode: multiply;
  box-shadow: inset 0 -18px 30px rgba(40,44,26,.22);  /* 只留一點厚度，拿掉頂部高光 */
}
/* 禁區地貌跟著一起降到同一個色域，否則又會冒出一塊亮色矩形 */
.m3-ch2 .m3-holefill{
  background-color:#6f6d51;
  background-image:
    radial-gradient(74% 64% at 46% 40%, rgba(226,232,190,.12) 0%, rgba(0,0,0,0) 76%),
    radial-gradient(126% 116% at 50% 50%, rgba(0,0,0,0) 52%, rgba(24,26,14,.26) 100%),
    linear-gradient(176deg, #7d7a5c 0%, #6f6d51 48%, #5e5c44 100%);
}
/* 地標周圍的羽化補丁在第二章也要跟著壓暗，不然 PNG 外圍會浮出一塊亮色 */
/* ★★ 第二章：禁區不得再有「鋪滿 bbox 的底板」★★
   ─────────────────────────────────────────────────────────────
   root cause（真頁截圖 + CSS 逐層對照）：
     .m3-zone 的 .m3-zone-blend 會鋪滿整個 region box。它本來靠自己的
     radial mask 變成有機形狀，但那段用了
          這組前綴／非前綴語法在瀏覽器實作不一致，只要其中一行被丟棄，
     整條 mask 宣告就失效 ⇒ 它直接畫成一塊完整矩形，
     而且比棋盤亮 ⇒ 就是截圖裡古井下面那條灰綠長方形。
   修法（架構性，不是調色）：
     ★第二章直接不畫任何「填滿盒子」的底★。
     禁區的「地面不一樣」交給 .m3-holefill（在棋盤之下、已羽化），
     「自然邊界」交給 .m3-edge 碎石苔蘚（本質就是不規則的小塊），
     「視覺焦點」交給 A/B/C 地標 PNG（alpha 已實測乾淨）。
     ⇒ 結構上不存在可以退化成矩形的圖層。 */
.m3-ch2 .m3-zone-blend{ display:none !important; }
/* 地標區連主題地形層也不要（古井本身就是水與石，底下不需要再鋪一層） */
.m3-ch2 .m3-zone-lm{ background:none !important; }
.m3-ch2 .m3-zone-terrain::before{ opacity:.28; }   /* 純地形區只留很淡的地貌，不成塊 */
/* ══════════════════════════════════════════════════════════════════════════
   ★真頁回報（第一章 Lv1-50）同一個「矩形補丁／淡黃背景」缺陷★
   ─────────────────────────────────────────────────────────────────────────
   root cause 與上面第二章那段完全同一顆：.m3-zone-blend 鋪滿整個 region
   bbox，加上非地標禁區的主題 ::before 也用全 opacity 鋪滿同一個 bbox，
   兩層同色系（暖褐／橄欖）疊在一起，讀起來就是一塊「有邊界的矩形色塊」，
   不是自然地面。第二章早就診斷並修過這個問題（見上方 .m3-ch2 專屬規則），
   但當初只套用在 .m3-ch2，第一章 50 關從未套用 ⇒ Lv11/Lv12 等關卡的
   淡黃／橄欖矩形就是這裡。
   修法：把同一個「架構性移除多餘鋪滿底板」的做法，從 .m3-ch2 專屬
   ★推廣成全域（第一章＋第二章通用）★，不再限定章節：
     ‧ 地貌一律交給 .m3-holefill（棋盤下方、已用 holeMaskUrl 羽化）
     ‧ 邊界過渡交給 .m3-edge（本質是不規則小塊，見下方密度修正）
     ‧ .m3-zone-blend 這一層「整盒填色」不再需要，全面關閉
     ‧ 非地標禁區的主題 ::before 大幅降到 .28（地標仍用既有 .34/.5）
   ★不是調色／不是加遮罩／不是縮小地標★：純粹拿掉一層本來就多餘、
   且會鋪滿 bounding box 的圖層，讓真正有機的 .m3-holefill / .m3-edge
   來表達「這裡是禁區」。Mutation 驗證見 tests/。 */
.m3-zone-blend{ display:none !important; }
.m3-zone-terrain::before{ opacity:.28; }
/* ── 小型禁區（1–2 格）：土坑，不是深色按鈕 ─────────────────────
   root cause：1×1 的 region 套「主題地磚 + 矩形細胞遮罩」，在 70px 的
   格子裡一定讀成一顆有邊框的深色圓角方塊 ＝ UI button。
   改法：拿掉細胞遮罩的方形感，用不對稱 border-radius ＋ 由外而內加深的
   徑向陰影，畫成「地面上一個塌陷的小土坑」。
   ★仍然一眼可辨★：中心明顯較暗＋內緣陰影 ⇒ 玩家知道不能放棋子，
   但沒有邊框、沒有 bevel、不像可以按的東西。 */
.m3-zone-tiny{ -webkit-mask-image:none !important; mask-image:none !important; }
.m3-zone-tiny .m3-zone-blend{ display:none; }
.m3-zone-tiny::before{
  content:""; position:absolute; inset:var(--ins,0); pointer-events:none;
  border-radius: 56% 44% 62% 38% / 48% 58% 42% 52%;
  background:
    radial-gradient(58% 54% at 46% 42%, rgba(48,40,24,.52) 0%, rgba(60,52,32,.30) 58%, rgba(60,52,32,0) 100%);
  box-shadow: inset 0 3px 7px rgba(26,20,10,.34);
  filter: blur(1.4px);
  opacity:1 !important;
}
.m3-zone-tiny::after{ display:none !important; }
.m3-zone-spring.m3-zone-tiny::before{
  background:radial-gradient(58% 54% at 46% 42%, rgba(38,62,60,.50) 0%, rgba(52,78,74,.28) 58%, rgba(52,78,74,0) 100%);
}
.m3-zone-roots.m3-zone-tiny::before{
  background:radial-gradient(58% 54% at 46% 42%, rgba(52,40,22,.54) 0%, rgba(70,56,32,.30) 58%, rgba(70,56,32,0) 100%);
}

/* ★有地標時，主題地形層幾乎全退★
   真頁看到的最後一塊矩形就是它：.m3-zone-spring::before 把整個 3×4 region
   鋪滿水色 ⇒ 井的四周浮出一圈淡青方框。地標本身已經是「水」了，
   底下不需要再鋪一層水 —— 只留極淡的濕痕當作井外溢的水氣。
   ★沒有地標的 region 不受影響★：那裡的地形層本來就是主角。 */
/* ★地標區的主題地形層整個關掉★
   真頁看到的「古井後面那塊方形底」就是它：.m3-zone-spring::before 把整個
   3×4 region 鋪滿水色 ⇒ 井的四周浮出一圈矩形。
   古井本身就是水、石碑本身就是石 —— 底下不需要再鋪一層同主題的方形色塊。
   融合改由：羽化補丁(.m3-zone-blend) ＋ 邊界侵蝕(.m3-edge) ＋ 接地陰影 完成，
   這三者都是不規則的，不會產生 bounding box。 */
.m3-ch2 .m3-zone-lm::before{ display:none; }
.m3-ch2 .m3-zone-lm::after{ opacity:.42; }
/* ── 第二章棋盤格：從「填色方塊」改成「石板接縫」──────────────────
   root cause：每一格都畫自己的底色＋內陰影 ⇒ 56 顆一模一樣的圓角方塊
   ＝ 玩家先看到「一排排 UI button」，才看到石板地面。
   改法：★格子本身不填色★，只在右邊與下邊留一道極細的接縫線，
   像真實石板地的縫。整片地面因此是連續的，但格位仍然讀得出來。
   ★可讀性不犧牲★：接縫 ＋ 棋子本身的落影已足以定位；
   50+ 玩家判斷「棋子在哪一格、可不可以換」不受影響。 */
.m3-ch2 .m3-socket{
  border-radius: 0;
  background: transparent;
  box-shadow:
    inset -1px 0 0 rgba(46,38,20,.10),      /* 右接縫 */
    inset 0 -1px 0 rgba(46,38,20,.10),      /* 下接縫 */
    inset 1px 0 0 rgba(255,248,228,.045),   /* 接縫另一側的極淡受光 */
    inset 0 1px 0 rgba(255,248,228,.045);
}
/* 選取／提示狀態仍然要非常清楚（這是 gameplay 判讀，不能為了自然感犧牲） */
.m3-ch2 .m3-socket.m3-exit-hint{ box-shadow:none; }
/* 棋盤外緣的地面過渡：泥土／苔蘚從外面咬進來，讓邊界不是一條線。
   ★pointer-events:none★，而且刻意畫在 board 之上、棋子之下。 */
/* 棋盤外緣 → 洞窟地面 的過渡：石板 → 破損石緣 → 泥土 → 苔蘚碎石 → 場景。
   ★沿著整圈鋪★（不是只有六個點），而且每一邊的疏密／色相都不一樣，
   所以四邊不會形成四條可以描出來的直線。 */
.m3-ch2 .m3-holedeco::before{
  content:""; position:absolute; inset:-9%; pointer-events:none; z-index:0;
  background:
    /* 上緣：苔蘚為主，疏 */
    radial-gradient(22% 11% at 12% 3%,  rgba(96,110,58,.40) 0%, rgba(96,110,58,0) 72%),
    radial-gradient(15% 9%  at 38% 1%,  rgba(104,84,52,.34) 0%, rgba(104,84,52,0) 74%),
    radial-gradient(19% 10% at 66% 4%,  rgba(96,110,58,.32) 0%, rgba(96,110,58,0) 72%),
    radial-gradient(13% 8%  at 88% 2%,  rgba(104,84,52,.30) 0%, rgba(104,84,52,0) 74%),
    /* 左緣：泥土為主，密 */
    radial-gradient(11% 17% at 1% 22%,  rgba(104,84,52,.40) 0%, rgba(104,84,52,0) 72%),
    radial-gradient(9%  13% at 3% 46%,  rgba(96,110,58,.34) 0%, rgba(96,110,58,0) 74%),
    radial-gradient(12% 19% at 0% 70%,  rgba(104,84,52,.38) 0%, rgba(104,84,52,0) 72%),
    radial-gradient(8%  11% at 4% 88%,  rgba(96,110,58,.30) 0%, rgba(96,110,58,0) 74%),
    /* 右緣：苔蘚＋碎石，中等 */
    radial-gradient(10% 15% at 99% 18%, rgba(96,110,58,.36) 0%, rgba(96,110,58,0) 72%),
    radial-gradient(12% 20% at 100% 44%,rgba(104,84,52,.38) 0%, rgba(104,84,52,0) 72%),
    radial-gradient(9%  12% at 97% 68%, rgba(96,110,58,.32) 0%, rgba(96,110,58,0) 74%),
    radial-gradient(11% 16% at 100% 90%,rgba(104,84,52,.34) 0%, rgba(104,84,52,0) 72%),
    /* 下緣：泥土最重（地面堆積） */
    radial-gradient(24% 13% at 18% 99%, rgba(104,84,52,.44) 0%, rgba(104,84,52,0) 70%),
    radial-gradient(18% 11% at 46% 100%,rgba(96,110,58,.36) 0%, rgba(96,110,58,0) 72%),
    radial-gradient(21% 12% at 72% 98%, rgba(104,84,52,.40) 0%, rgba(104,84,52,0) 70%),
    radial-gradient(14% 9%  at 93% 100%,rgba(96,110,58,.32) 0%, rgba(96,110,58,0) 74%);
  filter: blur(.8px);
}

/* ══════════════════════════════════════════════════════════════════════════
   .m3-holedeco — ★掛在 .m3-board 之上的純裝飾層★
   ─────────────────────────────────────────────────────────────────────────
   為什麼需要它：.m3-holefill 在 board 底下、又被銳利的 mask 切成矩形，
   任何往外的羽化都會被上面的棋盤蓋掉 ⇒ 邊界永遠是 90° 直角，
   玩家看到的就是「一塊矩形補丁」。
   這一層在 board 之上，所以苔蘚／碎石／根系／接地陰影可以★跨過邊界★
   壓到旁邊 tile 的角落，把幾何輪廓吃掉。
   ★安全契約★：pointer-events:none —— 旁邊的 tile 仍然完全可點、可交換、
   可消除，hitbox 與掉落路徑一格都沒變。overflow 也刻意不裁切。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-holedeco{
  position:absolute; left:0; top:0; width:100%; height:100%;
  z-index:5;                       /* 在 .m3-board 之上、在結算/教學 overlay 之下 */
  pointer-events:none;             /* ★絕對不可拿掉★（M18 mutation 驗這一條） */
  overflow:visible;
}
.m3-zone{
  position:absolute; pointer-events:none; overflow:visible;
  display:flex; align-items:center; justify-content:center;
}
/* ── 羽化地貌補丁：從棋盤上方蓋過禁區邊界，把 90° 直角溶掉 ──────────
   中央不透明＝禁區的地貌；往外用「兩層不同中心的橢圓遮罩」淡出到 0，
   ★刻意不用單一置中圓★ —— 那會產生規律的橢圓輪廓，一樣看得出幾何。
   兩層錯位疊出來的形狀是不對稱的，讀起來才像自然地面。 */
.m3-zone-blend{
  position:absolute; inset:0; pointer-events:none; display:block; z-index:0;
  background:
    radial-gradient(62% 54% at 44% 40%, rgba(150,132,98,.55) 0%, rgba(150,132,98,0) 100%),
    radial-gradient(58% 62% at 62% 66%, rgba(122,106,76,.5) 0%, rgba(122,106,76,0) 100%),
    radial-gradient(70% 64% at 50% 50%, rgba(138,122,90,.62) 0%, rgba(138,122,90,0) 96%);
  filter:blur(1.2px);
}
.m3-zone-spring .m3-zone-blend{
  background:
    radial-gradient(62% 54% at 44% 40%, rgba(112,146,138,.5) 0%, rgba(112,146,138,0) 100%),
    radial-gradient(58% 62% at 62% 66%, rgba(92,124,118,.46) 0%, rgba(92,124,118,0) 100%),
    radial-gradient(70% 64% at 50% 50%, rgba(104,134,126,.56) 0%, rgba(104,134,126,0) 96%);
}
.m3-zone-roots .m3-zone-blend{
  background:
    radial-gradient(62% 54% at 44% 40%, rgba(124,104,70,.55) 0%, rgba(124,104,70,0) 100%),
    radial-gradient(58% 62% at 62% 66%, rgba(96,80,52,.5) 0%, rgba(96,80,52,0) 100%),
    radial-gradient(70% 64% at 50% 50%, rgba(110,94,62,.6) 0%, rgba(110,94,62,0) 96%);
}
/* 地標本體：略微降銳利與對比，跟柔和圓潤的棋盤同一種美術語言，
   不要成為整個畫面唯一超銳利的物件。★只改 runtime 呈現，不動原始素材★ */
.m3-zone-art{
  width:100%; height:100%; object-fit:contain; display:block;
  filter:saturate(.9) contrast(.94)
         drop-shadow(0 3px 5px rgba(38,28,14,.34));   /* 接地陰影＝不再像浮在圖上 */
}
/* 地標的接地暈影（Ambient Occlusion）：壓在地標底部，讓它「長在地面上」 */
.m3-zone-lm::after{
  content:""; position:absolute; left:16%; right:16%; bottom:14%; height:22%;
  border-radius:50%;
  background:radial-gradient(50% 50% at 50% 50%, rgba(34,24,12,.34) 0%, rgba(34,24,12,0) 72%);
  filter:blur(2px);
}

/* ── 邊界侵蝕裝飾（由 buildEdgeDressing 逐顆產生，橫跨禁區邊界）────── */
.m3-edge{
  position:absolute; pointer-events:none; display:block;
  opacity:var(--a,.6); transform:rotate(var(--rot,0deg));
}
/* 苔蘚：軟邊、不規則、偏綠 */
.m3-edge-moss{
  border-radius:52% 48% 44% 56% / 58% 42% 56% 44%;
  background:radial-gradient(58% 58% at 42% 38%, rgba(126,158,86,.92) 0%, rgba(92,124,62,.62) 56%, rgba(92,124,62,0) 100%);
  filter:blur(.6px);
}
/* 碎石：硬一點、暖灰、有一點高光 */
.m3-edge-pebble{
  border-radius:46% 54% 58% 42% / 52% 46% 54% 48%;
  background:
    radial-gradient(46% 40% at 36% 32%, rgba(232,220,192,.9) 0%, rgba(232,220,192,0) 62%),
    radial-gradient(62% 62% at 54% 58%, rgba(150,134,106,.92) 0%, rgba(112,98,74,.5) 70%, rgba(112,98,74,0) 100%);
  filter:blur(.25px) drop-shadow(0 1px 1px rgba(30,22,10,.4));
}
/* 土斑：最糊，用來把直角整段抹掉 */
.m3-edge-dirt{
  border-radius:50%;
  background:radial-gradient(50% 50% at 50% 50%, rgba(104,84,56,.66) 0%, rgba(104,84,56,0) 74%);
  filter:blur(2.4px);
}
/* 依主題換色：B 泉水偏濕苔＋水痕；C 樹根偏落葉＋深土 */
.m3-zone-spring .m3-edge-moss{
  background:radial-gradient(58% 58% at 42% 38%, rgba(104,168,150,.85) 0%, rgba(72,128,116,.55) 56%, rgba(72,128,116,0) 100%);
}
.m3-zone-spring .m3-edge-dirt{
  background:radial-gradient(50% 50% at 50% 50%, rgba(70,116,110,.5) 0%, rgba(70,116,110,0) 74%);
}
.m3-zone-roots .m3-edge-moss{
  background:radial-gradient(58% 58% at 42% 38%, rgba(140,158,78,.88) 0%, rgba(96,116,52,.6) 56%, rgba(96,116,52,0) 100%);
}
.m3-zone-roots .m3-edge-dirt{
  background:radial-gradient(50% 50% at 50% 50%, rgba(84,62,38,.66) 0%, rgba(84,62,38,0) 74%);
}
/* reduced-motion / 低階裝置：裝飾仍在（它是靜態的），只是不加 blur 省一點 */
@media (prefers-reduced-motion: reduce){
  .m3-edge{ filter:none !important; }
}
/* ★真頁實測調整★：有地標 PNG 時，主題地形層要退到背景。
   否則 PNG 的透明邊會露出整片飽和的主題色（例如泉水的青綠被 screen 疊在
   暖褐底上 → 變成一塊橄欖綠矩形），看起來像色塊而不是地面。
   沒有地標的 region 不受影響 —— 那裡的地形層本來就是主角。 */
.m3-zone-lm::before{ opacity:.34; }
.m3-zone-lm::after{ opacity:.5; }

/* ── A 石碑遺跡：暖砂岩＋刻印金光 ───────────────────────────────── */
.m3-zone-stele::before{
  content:""; position:absolute; inset:var(--ins,0);
  background:
    radial-gradient(70% 60% at 50% 40%, rgba(214,178,110,.20) 0%, rgba(214,178,110,0) 72%),
    /* ★真頁實測移除重複線★：94° 的 repeating-gradient 在大禁區上會變成規律條紋。
       改成不規則的古石紋理斑塊，無週期、放大也不會出現網格。 */
    radial-gradient(26% 14% at 30% 26%, rgba(255,255,255,.09) 0%, rgba(255,255,255,0) 74%),
    radial-gradient(18% 22% at 72% 44%, rgba(255,255,255,.07) 0%, rgba(255,255,255,0) 76%),
    radial-gradient(30% 12% at 48% 74%, rgba(60,46,26,.14) 0%, rgba(60,46,26,0) 72%),
    linear-gradient(168deg, rgba(150,132,96,.34) 0%, rgba(96,84,62,.30) 100%);
  mix-blend-mode:soft-light;
}
/* 刻印微光：極輕的呼吸，不搶棋子注意力 */
.m3-zone-stele::after{
  content:""; position:absolute; left:calc(var(--ins,0px) + 12%); right:calc(var(--ins,0px) + 12%); top:calc(var(--ins,0px) + 18%); bottom:calc(var(--ins,0px) + 14%);
  border-radius:22%;
  background:radial-gradient(60% 55% at 50% 45%, rgba(255,206,120,.22) 0%, rgba(255,206,120,0) 70%);
  animation:m3SteleBreath 5.2s ease-in-out infinite;
}
@keyframes m3SteleBreath{ 0%,100%{opacity:.45;} 50%{opacity:.95;} }

/* ── B 神祕泉水：青碧水面＋緩慢漣漪（長條禁區＝水渠，只有這層，無 PNG）── */
.m3-zone-spring::before{
  content:""; position:absolute; inset:var(--ins,0);
  background:
    radial-gradient(58% 46% at 50% 52%, rgba(120,222,214,.30) 0%, rgba(60,150,158,.16) 55%, rgba(0,0,0,0) 80%),
    /* ★真頁實測移除重複線★：0° 每 9px 一條的水紋在大禁區上是規律橫條。
       改成不規則水痕／濕斑，讀起來才像自然的水面反光。 */
    radial-gradient(34% 10% at 38% 34%, rgba(190,242,238,.14) 0%, rgba(190,242,238,0) 76%),
    radial-gradient(24% 8% at 68% 56%, rgba(190,242,238,.11) 0%, rgba(190,242,238,0) 78%),
    radial-gradient(30% 9% at 50% 76%, rgba(190,242,238,.09) 0%, rgba(190,242,238,0) 76%),
    linear-gradient(176deg, rgba(46,104,110,.42) 0%, rgba(26,66,74,.40) 100%);
  mix-blend-mode:screen; opacity:.9;
}
.m3-zone-spring::after{
  content:""; position:absolute; left:calc(var(--ins,0px) + 14%); right:calc(var(--ins,0px) + 14%); top:calc(var(--ins,0px) + 22%); bottom:calc(var(--ins,0px) + 18%);
  border-radius:50%;
  background:radial-gradient(52% 48% at 50% 50%, rgba(178,255,247,.26) 0%, rgba(178,255,247,0) 72%);
  animation:m3SpringRipple 6.4s ease-in-out infinite;
}
@keyframes m3SpringRipple{ 0%,100%{opacity:.35; transform:scale(.92);} 50%{opacity:.9; transform:scale(1.05);} }

/* ── C 古代生命樹根：深棕樹根＋青苔＋生命微光（異形禁區的預設語言）──── */
.m3-zone-roots::before{
  content:""; position:absolute; inset:var(--ins,0);
  background:
    radial-gradient(46% 38% at 32% 30%, rgba(112,140,74,.30) 0%, rgba(112,140,74,0) 62%),
    radial-gradient(40% 34% at 72% 68%, rgba(96,126,62,.26) 0%, rgba(96,126,62,0) 60%),
    /* ★真頁實測移除★：原本兩條 38° / -46° 的 repeating-gradient 交叉之後，
       在大片異形禁區上會疊成非常明顯的菱形格紋，看起來像地毯／debug texture。
       改成不規則根系斑塊 —— 無重複週期，放大也不會出現規律網格。 */
    radial-gradient(30% 18% at 26% 30%, rgba(70,48,26,.30) 0%, rgba(70,48,26,0) 72%),
    radial-gradient(22% 30% at 68% 22%, rgba(70,48,26,.24) 0%, rgba(70,48,26,0) 74%),
    radial-gradient(34% 16% at 52% 64%, rgba(58,40,22,.26) 0%, rgba(58,40,22,0) 72%),
    radial-gradient(18% 26% at 84% 70%, rgba(58,40,22,.22) 0%, rgba(58,40,22,0) 74%),
    linear-gradient(160deg, rgba(104,80,50,.34) 0%, rgba(62,48,32,.34) 100%);
  mix-blend-mode:soft-light;
}
.m3-zone-roots::after{
  content:""; position:absolute; left:calc(var(--ins,0px) + 18%); right:calc(var(--ins,0px) + 18%); top:calc(var(--ins,0px) + 24%); bottom:calc(var(--ins,0px) + 22%);
  border-radius:44%;
  background:radial-gradient(56% 50% at 50% 50%, rgba(186,244,142,.18) 0%, rgba(186,244,142,0) 74%);
  animation:m3RootsBreath 7.1s ease-in-out infinite;
}
@keyframes m3RootsBreath{ 0%,100%{opacity:.30;} 50%{opacity:.78;} }

/* 全消同色發動時，地標刻印回應一下（★純視覺，不改 gameplay★） */
.m3-zone.m3-stele-pulse::after{ animation:m3StelePulse .5s ease-out 1 !important; }
@keyframes m3StelePulse{ 0%{opacity:.5; transform:scale(1);} 40%{opacity:1; transform:scale(1.06);} 100%{opacity:.5; transform:scale(1);} }

/* ══════════════════════════════════════════════════════════════════════════
   ⚡ v0.60.777【B】全消同色：閃電能量鏈（★純表現層★）
   ‧ 目標集合★直接沿用 gameplay 的 specialEffectCoords 結果★，不重新掃盤
   ‧ SVG overlay：一次性建立、動畫結束即整層移除，0 常駐 DOM
   ‧ 閃電可以視覺跨過禁區（它是能量），但棋子移動永遠不穿越 blocked topology
   ══════════════════════════════════════════════════════════════════════════ */
.m3-bolt{
  position:absolute; left:0; top:0; width:100%; height:100%;
  z-index:6; pointer-events:none; overflow:visible;
}
.m3-bolt path{
  fill:none; stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:var(--len,220); stroke-dashoffset:var(--len,220);
  animation:m3BoltDraw var(--dur,190ms) cubic-bezier(.2,.7,.3,1) forwards;
  animation-delay:var(--d,0ms);
}
/* ★店長 2026-08-30：閃電「粗一點、慢一點」★
   原本 glow 6.5 / core 1.5 在手機上細得像頭髮，加上整條鏈 170ms 畫完，
   玩家只看到一閃 —— 讀不出「能量從中心竄出去」。
   外光暈 6.5→10、核心 1.5→2.6、暖色 .9→1.6、分岔 1.1→1.8，
   時間軸放慢見 match3.js 的 --dur / STEP。 */
/* ★--w＝這一段的粗細係數★（由 JS 依鏈上位置給，越後面越細）
   等粗是店長真機判 FAIL 的主因之一：那讀起來是「畫的線」，不是「放電」。 */
.m3-bolt .bolt-glow{ stroke:rgba(96,178,255,.62); stroke-width:calc(10px * var(--w,1)); filter:blur(3.2px); }
.m3-bolt .bolt-core{ stroke:#f4fbff; stroke-width:calc(2.6px * var(--w,1)); }
.m3-bolt .bolt-warm{ stroke:rgba(255,209,128,.66); stroke-width:calc(1.6px * var(--w,1)); }
.m3-bolt .bolt-branch{ stroke:rgba(214,240,255,.9); stroke-width:1.8; }
/* ⚡ 移動中的電流頭：這一顆就是玩家看到的「正在旅行的光」。
   位置由 <animateMotion> 沿著同一條 path 帶著走 —— 不自己算座標，
   所以不可能出現「光跑到別的地方」那類 origin bug。 */
.m3-bolt .bolt-head{
  fill:#ffffff;
  filter:drop-shadow(0 0 5px rgba(190,230,255,.95)) drop-shadow(0 0 11px rgba(110,190,255,.75));
  opacity:0; animation:m3BoltHead 300ms ease-out forwards; animation-delay:var(--d,0ms);
}
@keyframes m3BoltHead{
  0%{ opacity:0; transform:scale(.5); }
  12%{ opacity:1; transform:scale(1.15); }
  76%{ opacity:1; transform:scale(1); }
  100%{ opacity:0; transform:scale(.7); }
}
/* 抵達點的局部照明：★只照約一格★，不是整張棋盤一起白閃 */
.m3-bolt .bolt-spill{
  fill:radial-gradient(circle, rgba(190,230,255,.5), rgba(190,230,255,0) 70%);
  fill:rgba(170,220,255,.28);
  opacity:0; animation:m3BoltSpill 340ms ease-out forwards; animation-delay:var(--d,0ms);
}
@keyframes m3BoltSpill{
  0%{ opacity:0; transform:scale(.35); transform-box:fill-box; transform-origin:center; }
  22%{ opacity:.9; }
  100%{ opacity:0; transform:scale(1.25); transform-box:fill-box; transform-origin:center; }
}
@keyframes m3BoltDraw{
  0%  { stroke-dashoffset:var(--len,220); opacity:0; }
  14% { opacity:1; }
  62% { stroke-dashoffset:0; opacity:1; }   /* 畫完之後★多停一下★再散，看得清楚 */
  82% { stroke-dashoffset:0; opacity:.9; }
  100%{ stroke-dashoffset:0; opacity:0; }
}
/* 目標棋子短暫發光（不遮住圖案，只加外圈） */
.m3-piece.m3-zap{ animation:m3Zap .42s ease-out 1; }   /* 跟著閃電放慢 */
@keyframes m3Zap{
  0%{ filter:none; }
  35%{ filter:drop-shadow(0 0 7px rgba(160,215,255,.95)) drop-shadow(0 0 14px rgba(120,190,255,.6)) brightness(1.22); }
  100%{ filter:none; }
}
/* 中心特殊棋先亮起 */
.m3-piece.m3-zap-src{ animation:m3ZapSrc .26s ease-out 1; }
@keyframes m3ZapSrc{
  0%{ filter:none; transform:var(--m3t,none); }
  40%{ filter:drop-shadow(0 0 12px rgba(200,235,255,1)) brightness(1.35); }
  100%{ filter:none; }
}
/* ♿ 減少動態：不做高頻閃爍；閃電整層不畫，只保留一次極短的目標微光 */
.m3-reduced .m3-bolt{ display:none !important; }
.m3-reduced .m3-bolt .bolt-head,.m3-reduced .m3-bolt .bolt-spill{ animation:none !important; opacity:0 !important; }
.m3-reduced .m3-piece.m3-zap,.m3-reduced .m3-piece.m3-zap-src{ animation:none !important; }
.m3-reduced .m3-zone::after{ animation:none !important; opacity:.6 !important; }
@media (prefers-reduced-motion:reduce){
  .m3-bolt{ display:none !important; }
  .m3-piece.m3-zap,.m3-piece.m3-zap-src{ animation:none !important; }
  .m3-zone::after{ animation:none !important; opacity:.6 !important; }
}

/* ══ 素材可視中心對齊（由 scripts/audit-asset-geometry.mjs 產生，請勿手改）══
   ★數學置中 ≠ 視覺置中★：每張素材的透明留白不同，畫布中心與可視主體
   中心可以差到 7% 以上（實測草莓 x -7.49% / y -5.81%）。
   ★v0.61.24：改用「不透明像素質心」，不再用 alpha bbox 中心★——
   bbox 會被面積很小但伸得很遠的部位拉走（草莓的綠蒂），人眼看的是質心。
   這裡把實測位移輸出成 CSS 變數，由 CSS 統一補償。
   ★共用契約★：換素材只要重跑產生器，不需要逐張加 top/left 補丁。 */
.m3-piece[data-key="strawberry"]{ --vc-x:-7.49%; --vc-y:-5.81%; }
.m3-piece[data-key="lavender"]{ --vc-x:-0.5%; --vc-y:-0.25%; }
.m3-piece[data-key="milk"]{ --vc-x:-0.43%; --vc-y:0.53%; }
.m3-piece[data-key="pudding"]{ --vc-x:-0.43%; --vc-y:1.29%; }
.m3-piece[data-key="cabbage"]{ --vc-x:-0.81%; --vc-y:-0.13%; }
.m3-obs-crate.m3-obs-lv2{ --ob-x:0.08%; --ob-y:-0.39%; --ob-scale:1.038; }
.m3-obs-crate.m3-obs-lv1{ --ob-x:0.21%; --ob-y:0.45%; --ob-scale:0.963; }
.m3-obs-ice.m3-obs-lv2{ --ob-x:-0.02%; --ob-y:-0.29%; --ob-scale:0.997; }
.m3-obs-ice.m3-obs-lv1{ --ob-x:0.43%; --ob-y:-0.31%; --ob-scale:0.999; }
.m3-obs-mud.m3-obs-lv3{ --ob-x:-0.14%; --ob-y:1.67%; --ob-scale:0.971; }
.m3-obs-mud.m3-obs-lv2{ --ob-x:-0.03%; --ob-y:2.06%; --ob-scale:0.893; }
.m3-obs-mud.m3-obs-lv1{ --ob-x:-0.19%; --ob-y:1.66%; --ob-scale:1.001; }
.m3-zone-stele{ --lm-x:-1.95%; --lm-y:7.99%; }
.m3-zone-spring{ --lm-x:0.12%; --lm-y:0.37%; }
.m3-zone-roots{ --lm-x:0.29%; --lm-y:0.4%; }
/* ══ 素材可視中心對齊 END ══ */

/* ── 對齊表的「消費端」：把上面量到的位移與階段大小真的套上去 ──────────
   ★機關★：background-position 依 --ob-x/--ob-y 補償可視中心；
           background-size 依 --ob-scale 讓各 HP 階段的可視大小★單調遞減★
           （實測 mud 原本是 90.6% → 92.6% → 77.7%，第 2 階比第 1 階還大，
             打一下就會「忽大忽小」；正規化後變成一階一階被磨掉）。
   ★地標★：依 --lm-x/--lm-y 把可視主體對回禁區中心
           （例：石碑的可視內容偏左 1.76%，若不補償，井/碑會偏一側）。
   ★全部只影響呈現★：不改 HP、不改判定、不改座標、不改 hitbox。 */
.m3-obs-cell{
  /* ★單層★：background-image 只有貼圖一層，所以這裡也只能有一組值。
     （舊版寫成兩組是為了搭配常駐的 --obs-fb 下層 —— 那一層就是棕色方板的來源，已移除。） */
  background-position: calc(50% - var(--ob-x, 0%)) calc(50% - var(--ob-y, 0%));
  background-size: calc(100% * var(--ob-scale, 1)) calc(100% * var(--ob-scale, 1));
}
.m3-zone-art{
  /* 地標：先做可視中心補償，再疊既有的接地陰影（drop-shadow 在 .m3-zone-art 本體） */
  transform: translate(calc(var(--lm-x, 0%) * -1), calc(var(--lm-y, 0%) * -1));
}

/* ── 被冰凍的棋子：以「可視主體中心」對準冰塊視覺中心 ────────────────
   ★共用契約，不是單一素材補丁★
     ‧ --vc-x / --vc-y 由 audit-asset-geometry.mjs 實測產生（見上方對齊表）
     ‧ 這裡反向補償 ⇒ 五種棋子在冰塊裡都上下左右留白均衡
     ‧ 位移以 .m3-ico 自身尺寸為基準（%）⇒ Desktop / Tablet / Mobile
       換了 cellSize 之後仍然維持置中
   ★只作用在冰凍狀態★：
     ‧ 一般棋子完全不受影響（位置、hitbox、掉落、消除判定全部不變）
     ‧ 石箱／泥苔不透明，棋子看不見，不需要位移
     ‧ 藤蔓是第一章既有行為，一律不動
   ★不影響 gameplay★：位移只加在 .m3-ico（圖片），
     .m3-piece 的座標與 hitbox 才是 gameplay 的唯一來源，一格都沒動。
   ★解凍不跳位★：冰塊消失時 class 一併移除，.m3-ico 既有的 transition
     會讓它平順回到原位。
   ★這一段刻意放在產生器標記區塊「之外」★ —— 之前放在裡面，
     重跑產生器就被整段洗掉了。 */
/* ★分層寫法（本批 §9）★：對齊位移不再由每一條狀態規則各自重寫一遍
   （那會讓「狀態 × 對齊」變成 N×M 條規則，任何新狀態都會忘記帶上位移而跳一下）。
   改成：對齊只寫進 --ico-tx / --ico-ty 兩個變數（自訂屬性會繼承），
   所有 .m3-ico 的 transform 一律組成 translate(對齊) × scale(狀態)。
   ⇒ charge / gone / land 之後再加任何狀態，都自動保有對齊、不需要再補規則。 */
/* ★v0.61.22 FAIL①：這條對齊契約原本★只挂在冰凍★，現在改成共用★
   店長實玩：「草莓沒有位於陷阱正中央」。實測（360/390/430px 共10 關）：
     地板層／覆蓋層／障礙層的矩形中心 vs 格中心：★全部 0.00px★（陷阱沒畫錯）
     棋子的★視覺中心★ vs 格中心：草莓 最大 4.00px（≈6.9% 格寬），其他棋子 ≤ 0.41px
   因為 piece-strawberry.webp 的不透明像素在自己 256×256 畫布上的中心是 (42.4%, 45.9%)。
   這個偏移專案早就量過也寫在上面的對齊表了（--vc-x:-7.62%; --vc-y:-4.1%），
   只是★只有冰凍狀態在用★。v0.61.20 新增的秋葉地毯／花叢 A+／糖罐
   同樣是「以格中心包住棋子」的機關，沒帶上它 ⇒ 草莓就明顯不在中心。
   ★不得逐顆棋子 hardcode 一組偏移★：這裡只多一個選擇器，數值一律來自量測表。 */
.m3-board{
  /* ══ 🍓 v0.61.24 落地陰影視覺補償（★單一共用常數，不是逐顆棋子的表★）══════
     ★店長 v0.61.23 真人驗收 FAIL：「棋子在圓形地板裝飾裡看起來沒有真正同心」★
     ★root cause（真 Chromium 最終合成像素，逐像素量出來）★
       .m3-piece 與 .m3-ico 各掛了一道★向下★的落地陰影
       （drop-shadow(0 2px 3px …) ＋ drop-shadow(0 3px 2px …)，
        就是「棋子坐在盤上、不浮貼」的那個效果）。
       玩家看到的是★帶陰影的合成結果★，它的不透明像素質心被往下拉。
       而 --vc-x/--vc-y 是在★素材原圖（沒有陰影）★上量的 ⇒ 這一段永遠補不到。
       實測（五顆棋子 × 320/360/390/430 × 四種包覆狀態，共 80 組）：
         把 --vc 改用質心之後，五顆棋子的殘差收斂成同一個值 +0.63～+0.94px，
         平均 +0.84px —— ★跟是哪一顆棋子無關★，因為陰影是共用的 CSS。
     ⇒ 所以這裡只需要★一個常數★，不是一顆棋子一個數字。
     ★為什麼陰影是固定 px 而不是 %★：drop-shadow 的 offset 寫死 2px/3px，
       不隨格子大小縮放 ⇒ 補償也必須是固定 px，四種寬度共用同一個值。
     ★只作用在包覆狀態★（下面只有 .m3-under-ice / .m3-wrapped 會讀它）：
       一般格沒有外框可以對照，「坐在盤上」本來就是要的，一格都不動 ⇒ Lv1～110 零影響。 */
  --ico-shadow-lift: -0.84px;
}
.m3-piece.m3-under-ice,
.m3-piece.m3-wrapped{
  --ico-tx: calc(var(--vc-x, 0%) * -1);
  --ico-ty: calc(var(--vc-y, 0%) * -1 + var(--ico-shadow-lift, 0px) + var(--vc-fruit-lift, 0%));
}
/* ═══ 🍓 v0.61.26 盛開花圈裡的草莓：對齊「果肉本體」而不是「整體輪廓」 ═══════
   ── 店長 v0.61.25 真人 QA ──
     「Lv129 花圈裡的草莓沒有置中」。
   ── 量測（白／黑背板 alpha 還原，真 Chromium 最終合成像素，172 個花圈態樣本，
      4 種寬度 × 8 列 × 7 欄 × 乾淨隔離／真 Lv129／四面鄰居／貼邊，量尺 A～K 自驗通過）──
     ‧ 草莓★整體輪廓★的 alpha 質心 vs 花圈開口中心 ＝ −0.03／−0.49／+0.08／+0.08 px
       ⇒ ★產品對自己既有的契約（v0.61.24 --vc 表）是達標的，沒有壞掉。★
     ‧ 但草莓★紅色果肉本體★的 alpha 質心，比整體輪廓低
         5.289％／5.365％／5.330％／5.500％（圖示框的百分比，172 樣本平均 5.371%）
       ★跨四種寬度是常數★ ⇒ 純粹是素材內部幾何，不是排版問題。
       離線直接量素材檔（PIL，完全不經瀏覽器）：紅相質心 − alpha 質心 ＝ +5.43%，
       ★兩條完全獨立的路徑對到 0.06 個百分點★。
     ‧ 成因：綠蒂佔不透明面積 19.7%、質心在畫布 22.2%（很高）⇒ 它把「整體輪廓質心」
       往上拉，於是「輪廓對準環心」時，紅色果肉必然落在環心下方 ≈4.8% 格寬
       （52px 的格子＝2.5px）。玩家的眼睛讀的是那一坨紅色，不是含綠蒂的質心。
   ── 修法 ──
     ★只在「盛開花圈（地板層 bloom）包住草莓」這一個狀態★多加一個位移項，
     值＝上面量到的 5.371%，★單一常數、不分列／不分欄／不分寬度／不分 DPR★。
     其他四顆棋子不設這個變數 ⇒ 預設 0% ⇒ 一個像素都不動；
     花苞態（障礙層 bloom）不帶這個 class ⇒ 批88 的前緣遮蔽契約完全不受影響；
     一般格（沒有 m3-wrapped）本來就讀不到 --ico-ty ⇒ Lv1～110 零影響。
   ★這個值不是手調的★：見 tools/m3-sb-measure.mjs（量測）與
     tools/m3-sb-center-gate.mjs（逐樣本驗收＋反向 mutation）。 */
.m3-piece.m3-wrap-bloom-open[data-key="strawberry"]{ --vc-fruit-lift: -5.37%; }
/* ★為什麼把陰影補償併進 --ico-ty 本身★（而不是另外再改每一條 transform 規則）：
   m3-gone / m3-sweep / m3-land 等狀態都是組 translate(--ico-tx, --ico-ty)，
   併在變數裡 ⇒ 所有狀態自動帶上，不會有哪個狀態忘了帶而跳一下。
   ★上面那兩行必須相鄰★：批63 M20e2 逐字檢查「對齊位移只有這一個來源」。 */
.m3-piece.m3-under-ice .m3-ico,
.m3-piece.m3-wrapped .m3-ico{
  transform: translate(var(--ico-tx, 0px), var(--ico-ty, 0px));
}
.m3-piece.m3-under-ice .m3-ico{
  width:74%; height:74%;              /* 稍微縮小，確保完整包在冰塊內、不碰邊 */
}


/* ══════════════════════════════════════════════════════════════════════════
   TERRAIN ASSEMBLY 圖層（match3-terrain.js）
   ★.m3-terrain 是正式圖層；.m3-terrain-debug 只在 QA 頁開 debug 時加上★
   正式玩家頁不載入 match3-terrain.js，所以這整段在玩家端永遠沒有元素套用。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-terrain{ position:absolute; left:0; top:0; width:100%; height:100%;
  pointer-events:none;
  /* ★必須低於棋子★：.m3-piece 是 z-index 2。
     edge component 依設計會侵入可玩格 0.62 cell（崩解羽化側），
     若地形疊在棋子上，那些棋子就會被地形蓋住 —— 視覺層級直接反了。
     地形是「地面」，永遠在棋子底下；機關（.m3-obs-cell z:4）才在棋子之上。 */
  z-index:1; overflow:visible; }
/* ★CONTRACT AUDIT 抓到的 root cause：background-size 必須是 100% 100%，不能是 contain★
   plan 算出來的 footprint 是 len×thick，而且 len 會被 fitChain（±25%）與
   variant 的非等比縮放（±8~30%）改動 ⇒ 元素框的長寬比會隨每一片而變。
   `contain` 會★保持素材原始長寬比★把圖縮進框內：
     ‧ edge 設計比例 2 : 0.62 ＝ 3.23:1，若素材畫成正方形畫布，
       contain 會把它畫成 44×44 的小方塊放在 142px 寬的框中央 ⇒ 整條 run 斷開
     ‧ 就算畫布比例正確，fitChain 一改 len，contain 就會留出上下／左右空白
       ⇒ 重疊 25% 的接縫數學全部落空，實際畫面會出現縫
   ★placeholder 永遠測不到這件事★：debug 用 background-color + clip-path，
   一定填滿 100%，所以這個 bug 只會在接上真素材那一刻才爆出來。
   ⇒ 這裡改成 100% 100%：★元素框 ＝ 素材 footprint，兩者永遠是同一件事★，
     代價是素材必須容忍契約規定範圍內的長寬比形變（見 material proof contract）。 */
/* ══ GATE B｜terrain-cover（純呈現層，z 在棋子之上）══════════════════════
   層序：board-bg 0 → terrain(base) 1 → socket auto → piece 2 → cover 3 → obstacle 4
   ★cover 只收「本體完全落在禁區格內」的片★（見 match3-terrain.js coverSafe），
   所以它的輪廓就是素材自己的不規則 alpha ——
   ★不是矩形遮罩、不是整格色塊、不是霧層、不用 opacity hack★。
   base 的自然羽化完全保留，兩層是同一份 placements 畫兩次。 */
.m3-tc{ position:absolute; pointer-events:none;
  background-repeat:no-repeat; background-position:center; background-size:100% 100%; }
/* debug placeholder：半透明色塊 ＋ 細框，看得出每一片的實際 footprint 與重疊 */
.m3-terrain-debug .m3-tc{ border-radius:2px; }
.m3-terrain-debug .m3-tc::after{
  content:attr(data-l); position:absolute; left:2px; top:1px;
  font:700 8px/1 ui-monospace,monospace; color:rgba(0,0,0,.62); letter-spacing:.3px; }


/* ══════════════════════════════════════════════════════════════════════════
   SINGLE SOURCE OF VISUAL TRUTH
   新 terrain assembly 接管地形視覺時，舊的 CSS 地形層一律停止繪製。
   ★保留 DOM 結構★（gameplay / mask bookkeeping 仍需要），只關掉 paint。
   兩套一起畫的實測後果：.m3-zone 1 層 ＋ .m3-holedeco::before 16 個
   radial 光斑同時疊在新地形底下，就是使用者看到的柔焦圓斑。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-terrain-active .m3-zone,
.m3-terrain-active .m3-zone::before,
.m3-terrain-active .m3-zone::after,
.m3-terrain-active .m3-zone-art,
.m3-terrain-active .m3-edge,
.m3-terrain-active .m3-holedeco::before,
.m3-terrain-active .m3-holefill::before{
  background:none !important; background-image:none !important;
  box-shadow:none !important; filter:none !important; opacity:0 !important;
  content:none !important;
}
/* ★opacity:0 不夠★（Lv100 真頁實測）
   舊的 .m3-edge 邊緣裝飾是 position:absolute 的節點，opacity:0 只是看不見，
   ★仍然參與版面★ —— 它們是照「上一次 render 的 cell 尺寸」擺的，
   於是 .m3-wrap 的 scrollHeight 被撐到 1104/900（超出 204px），
   computeCell() 的無捲軸收斂迴圈以為內容真的超出，一路把 cell 砍到下限 18，
   棋盤只剩 144px 高，捲軸卻還在。
   新地形（.m3-terrain）已經完全接管邊緣視覺 ⇒ 這些節點要 display:none，
   ★徹底退出版面★，不只是隱形。
   （.m3-zone 本身保留 opacity:0：B1 gate E-10 以 opacity 判定「不得雙 runtime 同時畫」。） */
.m3-terrain-active .m3-edge,
.m3-terrain-active .m3-zone-art{ display:none !important; }


/* ══ GATE B｜地形層的地面 ═════════════════════════════════════════════════
   .m3-terrain 是 z-index:1 的 stacking context。這塊地面放在它的 z-index:0，
   於是：地形素材（z1–3）在它之上、進場中的棋子（board context z0）在它之下。
   遮罩＝holeMaskUrl 的同一套反向遮罩（只留 non-playable 格）⇒
   一格都不會蓋到可玩區，已在可玩格的棋子永遠不會被遮。

   ★配色不是新調的，是直接沿用 .m3-holefill 的同一組宣告★（見上方 rule 的
   選擇器已把 .m3-terrain-floor 併進去）。理由：.m3-holefill 就墊在棋盤底下、
   透過 board 的洞露出來，玩家現在看到的禁區地面★就是它★。
   若這裡另外配一個較暗的顏色，等於在禁區外緣憑空製造一圈色差硬邊 ——
   .m3-holefill 的註解已經記載「色差才是『看得出矩形』的主因」。
   ⇒ floor 用同一組色 ⇒ 疊上去之後畫面★零變化★，只是把同樣的地面
     搬到「進場中棋子的上方」，遮蔽是層序帶來的，不是新畫的東西。
   ★實作方式：element 直接掛 class="m3-holefill m3-terrain-floor"★
     —— 顏色只有一份宣告（上面那條 .m3-holefill rule），這裡只覆寫位置相關的幾項。
     不複製一份色票，就不會有「兩邊各改一次、慢慢對不起來」的漂移。 */
.m3-terrain-floor{
  border-radius:0;                       /* 在 board 內，圓角由 board 決定 */
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-size:100% 100%;  mask-size:100% 100%;
  -webkit-mask-position:0 0;    mask-position:0 0;
}

/* ══ STEP 2｜地面語意改為「岩石之間的陰影地面」════════════════════════════
   ★為什麼要改（N1「olive floor 色塊」的最後一塊）★
     .m3-holefill 的底色是「只比棋盤暗一點點的暖砂岩」——那是為了
     ★舊架構★調的：當時禁區沒有地形素材，這塊地面自己就是禁區的樣子，
     所以必須跟棋盤同色系才不會讀成「一塊灰色 UI 方塊」。
     新架構下它的角色完全相反：地形素材才是禁區的樣子（覆蓋率 82–93%），
     地面只是素材縫隙間露出的那 7–18%。用「亮度接近棋盤的砂岩」去填那些縫隙，
     每一塊縫隙就都讀成「一小片淺色色塊」——那正是玩家說的橄欖／砂岩塊。
   ★改法★：只在 .m3-terrain-active（新地形接管）時壓暗＋降飽和，
     讓露出的部分讀成「岩石之間的陰影泥地」。
     ‧ .m3-holefill 與 .m3-terrain-floor ★同一條規則★ ⇒ 兩層永遠同色，
       不會在禁區外緣憑空製造色差硬邊（沿用原註解的理由）
     ‧ 不動非 terrain-active 的舊路徑 ⇒ 未載入 match3-terrain.js 時 0 影響 */
.m3-terrain-active .m3-holefill,
.m3-terrain-active .m3-terrain-floor{
  background:
    radial-gradient(74% 64% at 46% 40%, rgba(255,250,232,.05) 0%, rgba(0,0,0,0) 76%),
    radial-gradient(126% 116% at 50% 50%, rgba(0,0,0,0) 40%, rgba(18,13,6,.44) 100%),
    linear-gradient(176deg, #4a4232 0%, #3d3628 48%, #2f2a1f 100%);
  background-color:#3d3628;
  box-shadow: inset 0 6px 16px rgba(0,0,0,.42),
              inset 0 -3px 10px rgba(255,246,214,.04);
}
.m3-terrain-active .m3-holefill-tex{
  opacity:.30; filter:brightness(.52) saturate(.70) contrast(1.04);
  mix-blend-mode:normal;
}

/* ══ GATE B｜進場遮蔽（per-piece，時間界定）══════════════════════════════
   棋子從 segment 入口正上方那一格進場時，z-index 降到 terrain-base（z1）之下，
   於是被地形自然遮住；跨過入口的那一幀立刻恢復（見 match3.js enterGuard）。
   ★不是 opacity、不是 clip、不是 mask、不是矩形遮罩★ —— 只有層序，
   所以不會產生任何切邊，也不會多遮住已進入可玩格的棋子。 */
.m3-piece.m3-entering{ z-index:0 !important; }


/* ══════════════════════════════════════════════════════════════════════════
   🔨🔀✨ 局內道具（Booster）｜道具列 ＋ 使用動畫特效層
   ──────────────────────────────────────────────────────────────────────────
   設計依據：docs/PRODUCT_CONTRACT.md「Booster（定案）」與「PRIMARY CTA」
     ‧ 棋盤下方★一條★精簡道具列（不是三顆大按鈕佔掉半個畫面）
     ‧ 亮＝可按／暗＝不可按，與「開始挑戰」同一套語言
     ‧ 0 個一律 disabled ＋ pointer-events:none（★不可假成功★）
     ‧ 特效層與道具本體分離 ⇒ 特效能疊在棋盤任意一格
   ★所有圖都是正式 .webp★，沒有 emoji／Unicode／臨時 SVG。
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 道具列本體 ─────────────────────────────────────────────────────── */
.m3-boosterhost{
  flex:0 0 auto;
  display:flex; align-items:stretch; justify-content:center;
  gap:10px; padding:8px 10px 2px;
  position:relative;
}
.m3-booster{
  -webkit-appearance:none; appearance:none;
  flex:1 1 0; min-width:0; max-width:132px;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:2px; padding:6px 6px 5px;
  border:1px solid rgba(214,178,106,.42);
  border-radius:13px;
  background:
    linear-gradient(180deg, rgba(38,52,74,.94) 0%, rgba(24,34,52,.96) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,236,196,.14),
    inset 0 -2px 6px rgba(0,0,0,.34),
    0 2px 6px rgba(0,0,0,.34);
  color:#f2e3c4;
  font:inherit; line-height:1.1;
  cursor:pointer;
  transition:transform .12s ease, filter .16s ease, box-shadow .16s ease, border-color .16s ease;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
}
.m3-booster-art{
  display:block; width:38px; height:38px; position:relative;
  filter:drop-shadow(0 2px 3px rgba(0,0,0,.45));
}
.m3-booster-art img{ width:100%; height:100%; object-fit:contain; display:block; }
.m3-booster-name{
  font-size:12px; font-weight:700; letter-spacing:.4px;
  color:#f6e8ca; white-space:nowrap;
}
.m3-booster-qty{
  font-size:11px; font-weight:700; color:#d9c396; letter-spacing:.2px;
}
.m3-booster-qty b{ font-size:13px; color:#ffe6a6; }

/* ENABLED：亮（與「開始挑戰」同一套蜂蜜金語言，但道具列不搶主 CTA 的視覺重量） */
.m3-booster[data-state="idle"]{ border-color:rgba(233,199,124,.6); }
.m3-booster[data-state="idle"]:hover{ filter:brightness(1.06); }

/* PRESS：下沉，要有實體回饋 */
.m3-booster:active:not(:disabled){ transform:translateY(1px); filter:brightness(.94); }

/* FOCUS：鍵盤外框，清楚但不刺眼 */
.m3-booster:focus-visible{ outline:2px solid rgba(255,230,166,.85); outline-offset:2px; }

/* PICKING：正在等玩家選格 —— 這顆亮起來，其他顆退到背景 */
.m3-booster[data-state="picking"]{
  border-color:#ffe6a6;
  background:linear-gradient(180deg, rgba(84,68,36,.96) 0%, rgba(52,42,24,.96) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,242,214,.24),
    0 0 0 1px rgba(255,230,166,.34),
    0 0 14px rgba(255,206,110,.34);
}

/* DISABLED（含 0 個）：明顯變暗＋去飽和＋點不動 —— ★不可假成功★ */
.m3-booster:disabled,
.m3-booster[data-state="disabled"]{
  filter:saturate(.45) brightness(.62);
  opacity:.72;
  cursor:default;
  pointer-events:none;
  border-color:rgba(150,132,98,.32);
  box-shadow:inset 0 -2px 5px rgba(0,0,0,.4);
}
/* 動畫進行中：不改尺寸（不得造成版面跳動），只壓暗 */
.m3-booster[data-state="busy"]{ filter:brightness(.8); }
.m3-booster[data-anim="play"] .m3-booster-art{ animation:m3BxPop .34s ease-out; }
@keyframes m3BxPop{ 0%{ transform:scale(1);} 38%{ transform:scale(1.16);} 100%{ transform:scale(1);} }

/* 選格提示條：告訴玩家現在要點哪裡，並給明確的取消（★不讓玩家猜下一步★） */
/* ★選格提示條放在「棋盤上方的空位」★（店長真頁回報）
   ── 為什麼不是下方 ──
     原本 bottom:calc(100% + 4px) 是浮在道具列上方 ⇒ 直接壓住棋盤最後一列，
     玩家反而看不到自己要點的那一格；改成佔用道具列的保留高度，
     又會壓到三顆道具的圖示，而且把整組版面拉長（可能生出捲軸）。
   ── 正解 ──
     棋盤是 margin:auto 0 垂直置中的，HUD 與棋盤之間本來就有一段空白。
     提示條掛進 .m3-boardwrap（position:relative）並 bottom:calc(100% + 6px)
     ⇒ 落在那段既有空白裡：
        ‧ 不覆蓋任何棋格
        ‧ 不覆蓋任何道具按鈕
        ‧ ★不佔用 flow 高度★ ⇒ 版面完全不變長、不會多出捲軸
     元素改掛在 .m3-boardwrap 由 match3-booster.js 的 render() 負責（見該處說明）。 */
.m3-boardwrap > .m3-booster-hint{
  position:absolute; left:0; right:0; bottom:calc(100% + 6px); top:auto;
  display:flex; align-items:center; justify-content:center; gap:10px;
  padding:5px 10px; border-radius:11px;
  background:linear-gradient(180deg, rgba(46,38,20,.96), rgba(30,25,14,.96));
  border:1px solid rgba(255,214,128,.5);
  box-shadow:0 3px 10px rgba(0,0,0,.42);
  font-size:12.5px; font-weight:700; color:#ffe9bd; letter-spacing:.3px;
  z-index:6;
}
.m3-booster-hint[hidden]{ display:none; }
.m3-booster-cancel{
  -webkit-appearance:none; appearance:none;
  border:1px solid rgba(255,225,160,.5); border-radius:8px;
  background:rgba(0,0,0,.3); color:#ffe6a6;
  font:700 12px/1 inherit; padding:4px 10px; cursor:pointer;
}
.m3-booster-cancel:focus-visible{ outline:2px solid rgba(255,230,166,.85); outline-offset:2px; }

/* 選格模式：棋盤給一個「現在是在選目標」的訊號（不改版面、不擋輸入） */
.m3-wrap.m3-picking .m3-boardwrap{
  box-shadow:0 0 0 2px rgba(255,214,128,.42), 0 0 22px rgba(255,196,92,.2);
  border-radius:14px;
}
.m3-wrap.m3-picking .m3-board{ cursor:crosshair; }

/* ── 特效層：疊在 .m3-boardwrap，永不吃輸入 ──────────────────────── */
.m3-bxfx{
  position:absolute; inset:0;
  pointer-events:none;
  /* ★必須裁切在棋盤範圍內★（STEP 4 真頁截圖抓到）
     道具本體是一張 224px 的大圖，位置由 anim-contract 的 anchor 決定：
     命中點／瓶口要對準玩家點的那一格，所以圖本身一定會往那一格的外側伸出去。
     選到★邊角格★時（例如 (0,0)），藥瓶會往上伸出 0.82×224 ≒ 184px
     —— 直接蓋在 HUD 面板上，把「Lv.12」整個遮住（實測截圖）。
     ★不能改成「把圖夾回畫面內」★：那會讓命中點離開玩家點的格子，
       「敲哪格就打哪格」的手感是這個道具最核心的東西。
     ⇒ 位置維持不動，改成把超出棋盤的部分裁掉：
       工具從盤外揮進來、在盤內命中，是正確的物理讀法，也永遠不會污染 HUD。 */
  overflow:hidden;
  /* ★邊緣柔化★（店長真頁回報「槌子超出畫面」）
     只做 overflow:hidden 的話，靠邊格的道具會被一刀切平，讀起來像圖破了。
     四邊各加 16px 的淡出，工具就變成「從盤外揮進來、邊緣自然消失」。
     ★不改變任何位置★ ⇒ 命中點仍精準對齊玩家點的那一格。 */
  -webkit-mask-image:
    linear-gradient(to right, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
  mask-image:
    linear-gradient(to right, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
  -webkit-mask-composite:source-in; mask-composite:intersect;
  z-index:9;                       /* 在棋子與地形之上、在結算畫面之下 */
}
.m3-bxfx img{ position:absolute; display:block; will-change:transform,opacity; }
/* ★★這一行是星星魔杖「十字固定在左上」的真因★★
   上面那條只把 <img> 絕對定位。十字光束、羽毛軌跡這些是 <div>，
   於是它們 position:static ⇒ JS 寫的 inline left/top ★完全無效★ ⇒
   兩條光束掉回文件流的左上角，不管玩家點哪一格都一樣。
   真頁量到：點 (5,4) 應在 wrap (240,294)，實際 row beam 在 (176,13)。
   ★不是動畫 anchor 調偏，是根本沒有定位脈絡★。 */
.m3-bxfx .m3-bx-fx{ position:absolute; }

/* 道具本體（槌子／藥瓶／葉片）：位置由 JS 依 anim-contract 的 anchor 算，
   ★CSS 不再自己旋轉★ —— 角度已經畫在動作幀上，再轉一次會繞錯支點。 */
.m3-bx-tool{ filter:drop-shadow(0 4px 8px rgba(0,0,0,.5)); z-index:2; }
.m3-bx-hammer{ animation:m3BxIn .12s ease-out; }
.m3-bx-bottle{ animation:m3BxIn .16s ease-out; }
@keyframes m3BxIn{ from{ opacity:0; } to{ opacity:1; } }

/* 命中碎光：從中心向外放射（契約：scale .6→1.6 並淡出） */
.m3-bx-burst{ z-index:1; transform-origin:50% 50%; animation:m3BxBurst .42s ease-out forwards; }
@keyframes m3BxBurst{
  0%  { transform:scale(.6); opacity:.05; }
  22% { opacity:1; }
  100%{ transform:scale(1.6); opacity:0; }
}

/* 洗牌漩渦：正圓環旋轉，中央留空讓玩家看得見棋盤在重排 */
.m3-bx-swirl{ z-index:1; transform-origin:50% 50%; animation:m3BxSwirl .75s cubic-bezier(.3,.05,.2,1) forwards; }
@keyframes m3BxSwirl{
  0%  { transform:rotate(0deg)   scale(.72); opacity:0; }
  16% { opacity:.95; }
  70% { transform:rotate(560deg) scale(1.04); opacity:.9; }
  100%{ transform:rotate(720deg) scale(1.12); opacity:0; }
}
.m3-bx-swirl.settle{ animation-timing-function:cubic-bezier(.15,.85,.25,1); }
.m3-bx-shuffleleaf{ z-index:2; transform-origin:50% 50%; animation:m3BxLeaf .75s cubic-bezier(.3,.05,.2,1) forwards; }
@keyframes m3BxLeaf{
  0%  { transform:rotate(0deg)   scale(.86); opacity:0; }
  14% { opacity:1; }
  74% { transform:rotate(700deg) scale(1.06); opacity:1; }
  100%{ transform:rotate(720deg) scale(.9);  opacity:0; }
}
/* 洗牌時整盤的動勢：先被吸住，再落定（★只有 transform／filter，不改版面★） */
.m3-boardwrap.m3-bx-swirling .m3-board{ animation:m3BxSuck .34s ease-in both; }
@keyframes m3BxSuck{ from{ transform:scale(1);} to{ transform:scale(.965) rotate(-.6deg);} }
.m3-boardwrap.m3-bx-reseat .m3-board{ animation:m3BxReseat .42s cubic-bezier(.2,1.5,.4,1) both; }
@keyframes m3BxReseat{ 0%{ transform:scale(.965) rotate(-.6deg);} 100%{ transform:none; } }

/* 敲擊時棋盤的一下震動（很輕，不做成手遊式狂震） */
.m3-boardwrap.m3-bx-shake .m3-board{ animation:m3BxShake .22s ease-out; }
@keyframes m3BxShake{
  0%{ transform:none; } 28%{ transform:translateY(2px); }
  58%{ transform:translateY(-1px); } 100%{ transform:none; }
}

/* 星塵：從瓶口飛向每一顆同類棋子（--dx/--dy 由 JS 依實際格心給） */
.m3-bx-stardust{
  z-index:1; transform-origin:50% 50%;
  animation:m3BxDust .5s cubic-bezier(.35,.1,.2,1) forwards;
}
@keyframes m3BxDust{
  0%  { transform:translate(0,0) scale(.42); opacity:0; }
  18% { opacity:1; }
  72% { transform:translate(var(--dx,0), var(--dy,0)) scale(1); opacity:1; }
  100%{ transform:translate(var(--dx,0), var(--dy,0)) scale(1.35); opacity:0; }
}

/* ══════════════════════════════════════════════════════════════════════════
   星光瓶「吸收」FX（店長：玩法是「收走一種」，方向必須是 棋子 → 瓶口）
   ──────────────────────────────────────────────────────────────────────────
   質感基準：手繪水彩繪本＋色鉛筆，暖金（#ffd79a）＋淡紫（#c9a8ff）魔法感。
   ★不做★霓虹雷射、大面積白色爆閃、滿畫面粒子污染。
   每顆粒子帶自己的 --tx/--ty（終點位移）與 --cx/--cy（弧線控制點），
   所以左上／右上／左下／右下／中央來源都成立，不是單一固定 transform。
   ══════════════════════════════════════════════════════════════════════════ */
/* ② 全場同類棋子的標記：暖金＋淡紫 halo，輕微 pulse；★棋子本體不動、不消失★ */
.m3-bx-absorbmark{
  position:absolute; z-index:1; border-radius:50%; pointer-events:none;
  background:
    radial-gradient(circle at 50% 50%, rgba(255,222,168,.42) 0%, rgba(255,222,168,.16) 44%, rgba(255,222,168,0) 66%),
    radial-gradient(circle at 50% 50%, rgba(201,168,255,.30) 0%, rgba(201,168,255,0) 72%);
  animation:m3BxMark .9s ease-out forwards;
}
@keyframes m3BxMark{
  0%  { opacity:0; transform:scale(.72); }
  22% { opacity:1; transform:scale(1.04); }
  55% { opacity:.85; transform:scale(.98); }
  100%{ opacity:0; transform:scale(1.12); }
}
/* ③ 瓶口吸力環：柔和旋轉，不是霓虹圈 */
.m3-bx-suction{
  position:absolute; z-index:2; border-radius:50%; pointer-events:none;
  border:1.5px solid rgba(255,222,168,.55);
  box-shadow:0 0 10px rgba(201,168,255,.45) inset, 0 0 8px rgba(255,222,168,.35);
  animation:m3BxSuction .72s cubic-bezier(.4,.05,.3,1) forwards;
}
@keyframes m3BxSuction{
  0%  { opacity:0; transform:rotate(0deg) scale(1.35); }
  25% { opacity:.9; }
  100%{ opacity:0; transform:rotate(220deg) scale(.42); }
}
/* ⑤⑥ 吸收粒子：★origin = 棋子，destination = 瓶口★
   走 起點 →（往盤內凸的弧線控制點）→ 瓶口，抵達時縮小＝被吸進去 */
.m3-bx-absorb{
  z-index:3; transform-origin:50% 50%;
  animation:m3BxAbsorb .42s cubic-bezier(.32,.02,.24,1) forwards;
  filter:drop-shadow(0 0 4px rgba(255,215,154,.75)) drop-shadow(0 0 7px rgba(201,168,255,.5));
}
.m3-bx-absorb-dot{ filter:drop-shadow(0 0 3px rgba(201,168,255,.8)); opacity:.9; }
@keyframes m3BxAbsorb{
  0%  { transform:translate(0,0) scale(.34) rotate(0deg); opacity:0; }
  14% { transform:translate(calc(var(--cx,0px) * .3), calc(var(--cy,0px) * .3)) scale(1) rotate(40deg); opacity:1; }
  56% { transform:translate(var(--cx,0px), var(--cy,0px)) scale(1.02) rotate(140deg); opacity:1; }
  86% { transform:translate(var(--tx,0px), var(--ty,0px)) scale(.58) rotate(240deg); opacity:.95; }
  100%{ transform:translate(var(--tx,0px), var(--ty,0px)) scale(.14) rotate(280deg); opacity:0; }
}
/* ③ 瓶子浮起／⑧ 收瓶回彈 */
.m3-bx-bottle-float{ animation:m3BxFloat .34s cubic-bezier(.3,1.2,.5,1) forwards; }
@keyframes m3BxFloat{ 0%{ opacity:0; } 100%{ opacity:1; } }
.m3-bx-bottle-seal{ animation:m3BxSeal .30s cubic-bezier(.3,1.5,.5,1) forwards; }
@keyframes m3BxSeal{ 0%{ } 45%{ filter:brightness(1.22) saturate(1.1); } 100%{ } }
.m3-bx-sealflash{ z-index:4; transform-origin:50% 50%; animation:m3BxSealFlash .34s ease-out forwards; }
@keyframes m3BxSealFlash{
  0%  { transform:scale(.34); opacity:0; }
  38% { transform:scale(1); opacity:.92; }
  100%{ transform:scale(1.18); opacity:0; }
}

/* reduced-motion 降級：★保留一次明確回饋，不是整個取消★
   （完全沒有回饋 ⇒ 玩家不知道道具到底有沒有生效） */
.m3-bx-flash{ z-index:1; transform-origin:50% 50%; animation:m3BxFlash .2s ease-out forwards; }
@keyframes m3BxFlash{ 0%{ transform:scale(.8); opacity:0;} 40%{ opacity:.95;} 100%{ transform:scale(1.15); opacity:0;} }
@media (prefers-reduced-motion: reduce){
  .m3-bx-hammer, .m3-bx-bottle, .m3-bx-burst, .m3-bx-swirl,
  .m3-bx-shuffleleaf, .m3-bx-stardust,
  .m3-bx-absorb, .m3-bx-absorbmark, .m3-bx-suction, .m3-bx-sealflash, .m3-bx-bottle-float, .m3-bx-bottle-seal,
  .m3-boardwrap.m3-bx-swirling .m3-board,
  .m3-boardwrap.m3-bx-reseat .m3-board,
  .m3-boardwrap.m3-bx-shake .m3-board,
  .m3-booster[data-anim="play"] .m3-booster-art{ animation:none !important; }
  .m3-bx-flash{ animation-duration:.14s; }
}

/* ── 窄機身：375 仍要放得下三顆＋不擠壓 ──────────────────────────── */
@media (max-width:400px){
  .m3-boosterhost{ gap:8px; padding:6px 8px 2px; }
  .m3-booster{ padding:5px 4px 4px; border-radius:12px; }
  .m3-booster-art{ width:34px; height:34px; }
  .m3-booster-name{ font-size:11.5px; }
}
@media (max-width:360px){
  .m3-booster-art{ width:31px; height:31px; }
  .m3-booster-name{ font-size:11px; letter-spacing:0; }
}
/* 極矮畫面（iPhone SE 橫幅／鍵盤彈出）：道具列不得把棋盤擠掉 */
@media (max-height:640px){
  .m3-booster-art{ width:30px; height:30px; }
  .m3-boosterhost{ padding:4px 8px 2px; }
}
/* 桌機：道具列不要拉成一整條橫幅 */
@media (min-width:768px){
  .m3-boosterhost{ gap:14px; padding:12px 10px 4px; }
  .m3-booster{ max-width:150px; }
  .m3-booster-art{ width:44px; height:44px; }
  .m3-booster-name{ font-size:13px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   🏁 結算演出精緻化（v0.60.787）
   ──────────────────────────────────────────────────────────────────────────
   問題：原本結算只有「挑戰成功」四個字 —— 連續闖關十關，十次結算長得一模一樣，
   玩家不知道自己剛過第幾關、下一關是第幾關，沒有推進感。
   失敗時也只有「再試一次」，打不過的玩家等於被關在這一關裡出不去。
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 關卡身分列：Lv.N ＋ 關卡名（在標題之上，字級刻意小於標題）── */
.m3-rident{
  display:flex; align-items:center; justify-content:center; gap:9px;
  margin-top:2px; opacity:0; animation:m3rUp .34s ease 1.02s both;
}
.m3-rlv{
  font:900 13px/1 -apple-system,"PingFang TC","Noto Sans TC",sans-serif;
  letter-spacing:1.2px; padding:4px 9px; border-radius:8px;
  color:#3d2606; background:linear-gradient(180deg,#ffe6a6,#e2b45f);
  box-shadow:inset 0 1px 0 rgba(255,252,236,.85), 0 1px 3px rgba(0,0,0,.35);
}
.m3-rname{
  font:800 15px/1.2 -apple-system,"PingFang TC","Noto Sans TC",sans-serif;
  letter-spacing:1.6px; color:#f0dcb0; text-shadow:0 1px 3px rgba(0,0,0,.55);
  max-width:56vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.m3-result.lose .m3-rlv{ background:linear-gradient(180deg,#cfdae6,#93a7bb); color:#1e2a38; }
.m3-result.lose .m3-rname{ color:#dbe4ee; }

/* ── 次要按鈕列：回神祕山洞（★層級明顯低於主要 CTA★，不搶「下一關」）── */
.m3-rbtns2{
  display:flex; justify-content:center; margin-top:8px;
  opacity:0; animation:m3rUp .3s ease 1.5s both;
}
.m3-rbtn.exit{
  background:transparent;
  border:1px solid rgba(224,206,168,.4);
  color:#d9cbaa;
  padding:9px 18px; font-size:14px; font-weight:700; border-radius:12px;
  box-shadow:none;
}
.m3-rbtn.exit:hover{ background:rgba(255,240,208,.08); }
.m3-result.lose .m3-rbtn.exit{ border-color:rgba(180,205,225,.38); color:#c6d4e2; }

/* 下一關按鈕帶了關卡編號，字距要收一點才不會換行 */
.m3-rbtn.next{ letter-spacing:.6px; white-space:nowrap; }

@media (prefers-reduced-motion: reduce){
  .m3-rident, .m3-rbtns2{ animation:none !important; opacity:1 !important; }
}
.m3-reduced .m3-rident, .m3-reduced .m3-rbtns2{ animation:none !important; opacity:1 !important; }


/* ══════════════════════════════════════════════════════════════════════════
   🖥 桌機舞台構圖（v0.60.787）
   ──────────────────────────────────────────────────────────────────────────
   問題：`.m3-wrap` 是 max-width:520px 置中的一條窄欄，
   在 1440/1920 的桌機上，左右各剩下 400–700px 完全沒有構圖 ——
   背景圖雖然在（1672×941 橫式，不是純黑），但整層被 brightness(.86)
   加上兩層暗角壓過，中央又刻意低對比，讀起來就是「一大片黑」。

   ★做法★：不是「把背景調亮」（那會搶走棋盤），而是替兩側做出「舞台」：
     ① 遊戲欄本身給一個石材檯面＋古金細邊 ⇒ 眼睛讀得出「這是一座祭壇」
     ② 檯面底部一圈橢圓地光 ⇒ 棋盤是「站在」場景裡，不是浮在空中
     ③ 兩側對稱的柔和光池（極低不透明度）⇒ 空白處有層次，但不搶辨識度
   ★禁止★：純黑底、把棋盤放大到失去比例、在兩側塞資訊欄。
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width:900px){
  .m3-wrap{
    max-width:560px;
    /* 石材檯面：與 HUD 同一組石色，透明度很低 ⇒ 只是「有東西」，不是一塊面板 */
    background:
      linear-gradient(180deg, rgba(38,52,68,.30) 0%, rgba(24,34,48,.36) 46%, rgba(16,24,35,.42) 100%);
    border-left:1px solid rgba(205,168,96,.20);
    border-right:1px solid rgba(205,168,96,.20);
    box-shadow:
      0 0 0 1px rgba(0,0,0,.22),
      0 0 46px 12px rgba(0,0,0,.34),
      inset 0 1px 0 rgba(255,238,200,.07);
  }
  /* 檯面底部地光：讓遊戲欄「落」在場景裡 */
  /* ★bottom 不得為負★（店長二審：捲軸必須是「真的沒有溢出」，不是藏起來）
     這層是純裝飾的地面光暈（radial-gradient 到全透明）。
     原本 bottom:-2px 讓它★超出 .m3-wrap 2px★ —— 而 .m3-wrap 是 overflow-y:auto，
     於是 scrollHeight = clientHeight + 2，Chrome 就畫出一條 15px 的捲軸。
     實測：把三個子元素全部 display:none，scrollHeight 仍是 846（因為溢出的是這個
     偽元素，不是內容）⇒ 調棋盤高度、留安全邊全都無效。
     改成 0 之後光暈外觀零差異（漸層在邊緣早已接近全透明），溢出直接歸零。 */
  .m3-wrap::after{
    content:""; position:absolute; left:50%; bottom:0; transform:translateX(-50%);
    width:132%; height:120px; pointer-events:none; z-index:-1;
    background:radial-gradient(58% 100% at 50% 100%, rgba(255,206,120,.14) 0%, rgba(255,190,96,.06) 42%, rgba(0,0,0,0) 100%);
  }
  /* 兩側光池：對稱、極淡，只負責「不要是空的」 */
  #caveScene .cave-m3-root::after{
    background:
      radial-gradient(30% 46% at 15% 44%, rgba(120,170,190,.10) 0%, rgba(0,0,0,0) 100%),
      radial-gradient(30% 46% at 85% 44%, rgba(120,170,190,.10) 0%, rgba(0,0,0,0) 100%),
      radial-gradient(40% 44% at 50% 48%, rgba(4,10,16,.30) 0%, rgba(4,10,16,.15) 62%, rgba(0,0,0,0) 100%),
      radial-gradient(94% 80% at 50% 48%, rgba(0,0,0,0) 66%, rgba(6,14,20,.14) 86%, rgba(6,14,20,.22) 100%);
  }
  /* 背景本體在寬螢幕上不要再壓那麼暗（中央低對比帶會被拉開） */
  #caveScene .cave-m3-root::before{
    filter:brightness(.94) saturate(1.0) contrast(1.0);
    background-position:center, center 46%;
  }
  /* 標題列在桌機上不要橫跨整個寬度變成一條長條 */
  #caveScene .cave-m3-bar{ justify-content:center; }
  #caveScene .cave-m3-title{ flex:0 1 auto; min-width:320px; }
}
/* 超寬螢幕：遊戲欄再寬一點點，但★不超過 600★ —— 再寬棋子會大到失去盤面感 */
@media (min-width:1440px){
  .m3-wrap{ max-width:600px; }
}
/* 矮螢幕（桌機視窗被壓扁／手機橫放）：道具列與 HUD 讓位給棋盤 */
@media (max-height:560px){
  .m3-wrap{ padding-top:calc(env(safe-area-inset-top) + 4px); padding-bottom:calc(env(safe-area-inset-bottom) + 4px); }
  .m3-boosterhost{ padding:3px 8px 1px; }
  .m3-booster-art{ width:26px; height:26px; }
  .m3-booster-name{ font-size:10.5px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   ■ HUD 精緻化（v0.60.787）
   ──────────────────────────────────────────────────────────────────────────
   ★單一來源★：目標與關卡名的樣式只在這一區與本檔最上方的 HUD 區塊出現。
   下面這些是「把資訊層級做出來」的補強，不是又一套覆蓋。

   50+ 玩家一眼要能回答四件事：我第幾關／叫什麼／要做什麼／還剩幾步。
   所以視覺重量的順序是：目標數字 > 剩餘步數 > Lv.N > 關卡名。
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 絕不換行、絕不長高：HUD 高度必須是常數（換行＝畫面跳動）── */
.m3-goals{ flex-wrap:nowrap !important; }
.m3-hud{ min-height:56px; }

/* ── 目標：達成時用「柔和綠 ＋ 一個勾」，★不加底色方塊★ ── */
.m3-goal{
  transition:color .22s ease, opacity .22s ease;
  white-space:nowrap;
}
.m3-goal-ic{ filter:drop-shadow(0 1px 2px rgba(0,0,0,.42)); }
/* 達成的那一刻只讓「勾」淡入，數字換色 —— 不做彈跳、不改尺寸（避免 layout shift） */
.m3-goal-ok{ transition:opacity .24s ease; }
.m3-goal.done .m3-goal-ic{ opacity:.72; }

/* ── 4 個目標：字級與圖示再收一階，但★不得小到讀不出來★ ── */
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal{ gap:2px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ok{ font-size:11px; }
/* 不支援 :has() 的舊瀏覽器：用 gap 收斂當保底，不會爆版 */
@supports not selector(:has(*)){
  .m3-goals{ gap:5px; }
  .m3-goal{ font-size:14px; }
  .m3-goal-ic{ width:19px; height:19px; }
}

/* ── 關卡名：明確次要（不搶目標）── */
.m3-lvname{
  font-size:11.5px; font-weight:500; color:#c7b795; opacity:.92;
  /* ★不截字★：改折兩行（見本檔上方 .m3-lvname 的完整說明）。
     22vw 在 390px 上只有 86px ＝ 7 個中文字 ⇒ 長關卡名一定被吃掉。 */
  max-width:min(30vw, 11em); overflow:hidden; white-space:normal; word-break:break-word;
  line-height:1.15; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
}
.m3-lvno{ text-shadow:0 1px 2px rgba(0,0,0,.5); }

/* ── 剩餘步數：數字是主角、標籤退位 ── */
.m3-hud-moves b{ font-variant-numeric:tabular-nums; }   /* 20→19 時寬度不變＝不跳動 */
.m3-hud-cap{ text-transform:none; }
/* 選格中：HUD 中央標題暫時變成指示文字（見 match3-booster.js render() 的說明）。
   ★只改顏色與字重，不改字級／行高／margin★ ⇒ HUD 高度完全不變、不會有 layout shift。 */
.m3-hud-cap.m3-hud-cap-pick{
  color:#ffe6b0; font-weight:800; font-size:14px; letter-spacing:.2px;
  text-shadow:0 1px 2px rgba(0,0,0,.55);
  /* height 仍是 16px（見上方）⇒ 放大字級不會把 HUD 撐高 */
}
@media (max-width:400px){ .m3-hud-cap.m3-hud-cap-pick{ font-size:12.5px; } }

/* ── 窄機身（375）：三分區都要塞得下，優先犧牲關卡名 ── */
@media (max-width:392px){
  .m3-hud{ min-height:52px; }
  .m3-lvname{ max-width:17vw; }
  .m3-goals{ gap:7px; }
}
@media (max-width:360px){
  .m3-lvname{ display:none; }        /* ★寧可不顯示關卡名，也不要擠壓目標★ */
  .m3-hud-goal{ padding:0 6px; }
}
/* ── 桌機：HUD 跟著遊戲欄變寬，字級略放大（不是等比放大整條）── */
@media (min-width:900px){
  .m3-hud{ min-height:62px; }
  .m3-lvno{ font-size:19px; }
  .m3-lvname{ font-size:12.5px; max-width:150px; }
  .m3-goal{ font-size:17px; }
  .m3-goal-ic{ width:26px; height:26px; }
  .m3-hud-moves b{ font-size:29px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   🛡 覆蓋層定位硬化（v0.60.788｜P0 修復的第二道防線）
   ──────────────────────────────────────────────────────────────────────────
   ★為什麼需要 !important★（本專案平常禁用，這裡是有意識的例外）

   宿主頁面只要寫出一條像
       #someScene .some-root > * { position: relative; }
   的規則（特異度 1-1-0），就會蓋掉 `.m3-result{position:fixed}`（0-1-0），
   於是結算層被排進正常流、掉到棋盤下方、再被父層 overflow:hidden 裁掉 ——
   ★DOM 有、玩家看不到、盤面永遠停在達標畫面★。
   v0.60.787 的正式站就是這樣（cave-scene 的 `> *` 規則），本機 Chrome 實測抓到。

   宿主那條已經修好了（只抬 .m3-wrap）。但「結算畫面能不能被看見」是整個
   遊戲迴圈的命脈，不能只靠「宿主永遠不要寫錯」。所以這裡再上一道鎖：
   ★任何宿主規則都不得把這些覆蓋層降級成正常流★。

   同一條理由適用於教學／導引／提示層 —— 它們被降級時症狀比較不致命
   （提示跑版），但成因完全相同，一起鎖。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-result{ position:fixed !important; inset:0 !important; z-index:60 !important; }
.m3-tut,
.m3-tut-hole,
.m3-tut-hand,
.m3-tut-bubble{ position:fixed !important; }
.m3-guide,
.m3-shuffle{ position:absolute !important; }


/* ══════════════════════════════════════════════════════════════════════════
   ■ 結算可讀性與 CTA 層級（v0.60.788｜本機 Chrome 真頁 QA 修正）
   ──────────────────────────────────────────────────────────────────────────
   真頁看到的兩件事：
   ① 資訊面板底色只有 rgba(10,14,20,.42)，棋盤直接透出來 ——
      「12 / 12」壓在草莓與高麗菜上，★50+ 玩家讀起來很吃力★。
   ② 勝利畫面的「再玩一次」是飽和藍（#1f6b82→#123f4e），
      在蜂蜜金的世界裡像另一個系統的按鈕，而且會跟金色主 CTA 搶注意力。
      藍灰只該用在★失敗★畫面（那裡它是主要動作，語意成立）。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-rinfo{
  background:rgba(8,12,18,.78);
  box-shadow:inset 0 0 18px rgba(0,0,0,.35), 0 6px 18px rgba(0,0,0,.34);
}
.m3-result.lose .m3-rinfo{ background:rgba(10,16,24,.80); }

/* 勝利畫面的次要動作：石材中性色，明確低於金色主 CTA 一階 */
.m3-result.win .m3-rbtn.retry{
  background:linear-gradient(180deg, rgba(62,72,88,.96), rgba(38,46,60,.96));
  border-color:rgba(214,192,150,.42);
  color:#eadfc6;
  box-shadow:inset 0 1px 0 rgba(255,246,222,.14), 0 2px 8px rgba(0,0,0,.34);
}
.m3-result.win .m3-rbtn.retry:hover{ filter:brightness(1.08); }


/* ══════════════════════════════════════════════════════════════════════════
   ■ 遊戲舞台垂直配置（v0.60.788｜本機 Chrome 真頁量測後修正）
   ──────────────────────────────────────────────────────────────────────────
   ★真頁看到的問題★（桌機最明顯）：
   `.m3-boardwrap{ margin: auto 0 }` 讓★棋盤自己★垂直置中，於是視窗剩下的高度
   全部堆在「棋盤底 → 道具列頂」之間。桌機 1440/1920 上那段空白長達兩三百 px：
     ‧ 棋盤與道具的視覺關係被切斷
     ‧ 玩家第一眼不會發現這關有道具可用
     ‧ 用道具時滑鼠要跑很遠
     ‧ 整個畫面重心下沉

   ★修法★：置中的單位不是「棋盤」，而是「棋盤＋道具列」這一組。
   做法是把剩餘空間丟到★這一組的上方與下方★：
       .m3-boardwrap  margin-top: auto     ← 上方吸收剩餘空間
       .m3-boosterhost margin-bottom: auto ← 下方吸收剩餘空間
   兩者之間只留一個明確、固定的呼吸間距（不黏死、也不斷開）。
   HUD 仍固定在最上方。

   ★手機不照搬桌機★：手機高度本來就緊，剩餘空間很少甚至為 0，
   同一組規則會自然退化成「HUD→棋盤→道具列」緊湊排列，
   ★不會去縮小棋盤、也不會把道具列推出畫面★（下方仍有 safe-area padding）。
   間距值另外依螢幕高度分級（見下方 media query）。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-boardwrap{ margin-top:auto !important; margin-bottom:0 !important; }
.m3-boosterhost{ margin-bottom:auto; }

/* 棋盤 → 道具列 的呼吸間距（唯一來源，依高度分級） */
/* padding-top 同時是「選格提示條」的常駐保留高度（見 .m3-booster-hint） */
.m3-boosterhost{ padding-top:12px; }
@media (min-height:760px){ .m3-boosterhost{ padding-top:16px; } }
@media (max-height:700px){ .m3-boosterhost{ padding-top:9px; } }
/* 提示條在矮螢幕再壓縮一點，仍保持可讀 */
@media (max-height:700px){ .m3-booster-hint{ font-size:11.5px; padding:0 8px; gap:8px; } }
@media (max-height:620px){ .m3-boosterhost{ padding-top:6px; } }
/* 桌機：舞台是「HUD＋棋盤＋道具列」一整組，間距略放大但★不超過一個格子高★ */
@media (min-width:900px){
  .m3-boosterhost{ padding-top:18px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   ■ 遊戲欄高度必須跟著宿主，而不是整個 viewport（v0.60.788）
   ──────────────────────────────────────────────────────────────────────────
   `.m3-wrap{ min-height:100dvh }` 是給「整頁就是遊戲」的情境用的。
   但在神祕山洞裡，三消是掛在 `.cave-m3-root` 之下，上面還有一條 52px 的標題列
   （再加 env(safe-area-inset-top)）。100dvh 完全沒有把那條算進去 ⇒
   ★遊戲欄比容器高 50–90px，最下面的道具列直接被推出可視範圍★。
   本機 Chrome 實測 430×718：道具列底部超出容器 3px，越矮越寬超得越多。

   ⇒ 宿主有明確高度時（山洞、驗收台），遊戲欄一律跟著宿主填滿，不再自己讀 viewport。
   沒有宿主高度的獨立頁面仍走 100dvh 的舊行為，不受影響。
   ══════════════════════════════════════════════════════════════════════════ */
/* ★v0.61.19：正式站與本機 QA 共用同一份場景層★
   背景圖本身早就寫在這個檔案裡（`.cave-m3-root.m3-scene::before/::after`，見下方場景區），
   ★只差「宿主的最小幾何」★ —— 那兩行原本只存在於 cave-scene.js 注入的
   `#caveScene .cave-m3-root{...}`，所以沒有 cave 外殼的 match3-test.html 看不到背景。
   這裡補上★不帶 #caveScene 前綴★的版本：
     ‧ 正式站：cave-scene.js 的 ID 版特異性較高，仍然勝出 ⇒ **正式畫面 0 變更**
     ‧ QA 頁：沒有 ID 版，就靠這一份 ⇒ 兩邊的背景來源與 mapping 完全是同一份
   ★刻意不動 cave-scene.js★（GPT 指令：不要順手重構）。 */
.cave-m3-root{ position:relative; overflow:hidden; min-height:0; }
.cave-m3-root > .m3-wrap{ min-height:0; height:100%; position:relative; z-index:2; }

/* ★遊戲欄永遠不得超出宿主（v0.60.788）★
   min-height:100dvh 只在「整頁就是遊戲」時正確。宿主一旦有自己的高度
   （山洞的標題列、驗收台的工具列、桌機視窗），100dvh 就會讓遊戲欄比容器高，
   多出來的部分就是玩家看到的「道具被切掉 ＋ 右側捲軸」。
   max-height:100% 在宿主高度未定義時會被忽略（等於不影響獨立頁面），
   宿主有定義時則強制收斂 ⇒ 兩種情境都正確。 */
.m3-wrap{ max-height:100%; }


/* ══════════════════════════════════════════════════════════════════════════
   ■ 四目標 HUD（Lv100）不得橫向溢出（v0.60.788｜本機 Chrome 量測）
   ──────────────────────────────────────────────────────────────────────────
   實測 Lv100（4 個目標）：`.m3-goals` 需要 235px，但 `.m3-hud-goal` 只有 187px
   ⇒ ★橫向溢出 48px★。因為 flex-wrap 已鎖 nowrap，不會換行，但會被擠出去／被裁。
   修法有兩層：
     ① 目標區優先取得寬度（其他兩區可以讓）
     ② 4 目標時整組再收一階，並讓出關卡名（★資訊優先序：目標 > 步數 > Lv > 關卡名★）
   ══════════════════════════════════════════════════════════════════════════ */
.m3-hud-goal{ flex:1 1 auto; min-width:0; }
.m3-hud-lv{ flex:0 1 auto; min-width:0; }
.m3-hud-moves{ flex:0 0 auto; }

/* 4 個目標：整組收一階，並隱藏關卡名（Lv 數字保留） */
.m3-hud:has(.m3-goal:nth-child(4)) .m3-lvname{ display:none; }
.m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-lv{ max-width:64px; }
.m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-goal{ padding:0 6px; }
.m3-goals:has(.m3-goal:nth-child(4)){ gap:6px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal{ font-size:12px; gap:2px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ic{ width:15px; height:15px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ok{ font-size:10px; }
/* 3 個目標也先收半階，避免 375 機身剛好卡邊 */
.m3-goals:has(.m3-goal:nth-child(3)){ gap:7px; }
/* 桌機空間夠，4 目標可以放大回來一點 */
@media (min-width:900px){
  .m3-hud:has(.m3-goal:nth-child(4)) .m3-lvname{ display:-webkit-box; }
  .m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-lv{ max-width:120px; }
  .m3-goals:has(.m3-goal:nth-child(4)) .m3-goal{ font-size:14px; }
  .m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ic{ width:19px; height:19px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   ■ 結算層必須自己適應 viewport 高度（v0.60.788｜真 Chrome 截圖 NG 修正）
   ──────────────────────────────────────────────────────────────────────────
   實測 NG：徽章 min(66vw,272px) ＋ 標題 32px ＋ 星等 46px ＋ 資訊卡 ＋ 兩顆主 CTA
   ＋ 回山洞，在 812/844 這種常見手機高度會★超出 viewport★，於是出現瀏覽器捲軸，
   而且「下一關」被推到畫面外。
   ★修法不是 overflow:hidden 裁掉★（那是遮 bug），而是讓整組結算依
   ★viewport 高度★等比收斂：徽章 → 標題 → 星等 → 資訊 → CTA 全部用 vh 夾住上下限。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-rstage{
  max-height:100dvh;
  padding:10px 12px;
  box-sizing:border-box;
  justify-content:center;
}
.m3-result.win  .m3-remblem-wrap{ width:min(58vw, 26vh, 260px); height:min(58vw, 26vh, 260px); }
.m3-result.lose .m3-remblem-wrap{ width:min(46vw, 21vh, 208px); height:min(46vw, 21vh, 208px); }
.m3-result.win  .m3-rtitle{ font-size:clamp(21px, 4.0vh, 32px); }
.m3-result.lose .m3-rtitle{ font-size:clamp(17px, 3.1vh, 24px); }
.m3-rident{ margin-top:0; }
.m3-rname{ font-size:clamp(12px, 1.9vh, 15px); }
.m3-rstars{ gap:clamp(5px, 1.1vh, 8px); margin-bottom:clamp(3px, .7vh, 6px); }
.m3-rstars .m3-star{ width:clamp(26px, 5.0vh, 46px); height:clamp(26px, 5.0vh, 46px); }
.m3-rinfo{ padding:clamp(7px, 1.3vh, 12px) clamp(12px, 3vw, 18px); gap:clamp(2px,.5vh,6px); }
.m3-result.win  .m3-rgoal{ font-size:clamp(16px, 2.7vh, 23px); }
.m3-result.lose .m3-rgoal{ font-size:clamp(14px, 2.3vh, 19px); }
.m3-rgoal img,
.m3-rgoal .m3-goal-cbic{ width:clamp(20px, 3.2vh, 28px); height:clamp(20px, 3.2vh, 28px); }
.m3-rbtns{ margin-top:clamp(5px, 1.2vh, 10px); gap:clamp(7px,1.4vw,10px); }
.m3-rbtn{ padding:clamp(9px, 1.6vh, 13px) clamp(15px, 3.4vw, 24px); font-size:clamp(14px, 2.0vh, 16px); }
.m3-rbtns2{ margin-top:clamp(5px, 1.0vh, 8px); }
.m3-rbtn.exit{ padding:clamp(7px,1.2vh,9px) clamp(13px,3vw,18px); font-size:clamp(12px,1.7vh,14px); }
/* 極矮（手機橫放／桌機視窗壓扁）：徽章讓位，優先保住 CTA */
@media (max-height:560px){
  .m3-result.win .m3-remblem-wrap,
  .m3-result.lose .m3-remblem-wrap{ width:min(30vw,150px); height:min(30vw,150px); }
  .m3-rident{ display:none; }
}


/* ══════════════════════════════════════════════════════════════════════════
   ■ 四目標 HUD 正式版面（v0.60.788｜Lv100 真頁 NG 修正）
   ──────────────────────────────────────────────────────────────────────────
   真頁 NG：4 個目標硬塞一列 ⇒ 擠在一起、最右目標貼上「剩餘步數」、
   關卡名被截成「團購小萌寵·…」。★縮到看不清楚不是解法★。
   資訊優先序（50+ 玩家）：本關目標 > 剩餘步數 > Lv > 關卡名。
   ⇒ 4 目標時：關卡名讓位、目標區取得最大寬度、目標之間有明確分隔、
     並與步數區之間保留固定安全距離。桌機空間夠才把關卡名放回來。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-hud:has(.m3-goal:nth-child(4)) .m3-lvname{ display:none !important; }
.m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-lv{ max-width:58px; }
.m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-goal{ padding:0 10px; }
.m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-moves{ padding-left:4px; }
.m3-goals:has(.m3-goal:nth-child(4)){ gap:9px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal{ font-size:12.5px; gap:3px; }
.m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ic{ width:16px; height:16px; }
/* ★桌機不再把 4 目標放大回去★ —— 那正是真頁擠爆的原因；只放回關卡名 */
@media (min-width:900px){
  /* 4 目標桌機：關卡名放回來，寬度給到與 Lv 區同寬並允許兩行（不再截字） */
  .m3-hud:has(.m3-goal:nth-child(4)) .m3-lvname{ display:-webkit-box !important; max-width:118px; }
  .m3-hud:has(.m3-goal:nth-child(4)) .m3-hud-lv{ max-width:118px; }
  .m3-goals:has(.m3-goal:nth-child(4)) .m3-goal{ font-size:13.5px; }
  .m3-goals:has(.m3-goal:nth-child(4)) .m3-goal-ic{ width:18px; height:18px; }
}

/* ══ GATE B v2｜進場遮蔽層（.m3-terrain-occl）══════════════════════════════
   ★這一層的設計目標是「看不見」★
     前三版都在「畫一個形狀去補洞」：深色矩形 → 深色有機橢圓 → 岩石圓盤，
     每一版都被看出來（店長：「不可以有黑色那個」「橢圓弧不要露出來」）。
     只要那個形狀會出現在畫面上，它就一定讀成幾何圖形。
   ★作法★：背景宣告與 .m3-board.m3-shaped > .m3-board-bg ★完全相同★
     （同一張 board-stone、同一個 center/cover、同一個底色），
     尺寸也與棋盤相同 ⇒ 疊上去之後每一像素都等於它底下的棋盤石面，
     ★不管遮罩形狀是什麼都看不出來★。
   ★但對遮蔽而言它是實心的★：它在 .m3-terrain（z-index:1）內，
     進場中的棋子是 z-index:0 ⇒ 100% 被遮住。
   ⇒ 遮蔽保證與視覺完全解耦。 */
.m3-terrain-occl{
  position:absolute; left:0; top:0; width:100%; height:100%;
  z-index:0; pointer-events:none;
  background:#b6a480 url('match3/board-stone.webp') center / cover no-repeat;
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-size:100% 100%;  mask-size:100% 100%;
  -webkit-mask-position:0 0;    mask-position:0 0;
}
.m3-ch2 .m3-terrain-occl{ background:#7d7a5c url('match3/board-stone.webp') center / cover no-repeat; }

/* ══ 版面：★不得出現捲軸★（店長真頁回報三次）════════════════════════════
   ── 真因（實測）──
     .m3-wrap 是 overflow-y:auto。內容合計 713.43px、clientHeight 也是 713.43px，
     ★剛好貼齊★；但容器高度本身是小數（#stagewrap = 747.43 − 34），
     Chrome 的 scrollHeight 會進位成 715 ⇒ 只差 2px 就畫出一條 15px 的灰色捲軸。
     這 2px 不是任何一個元素多出來的，是次像素進位，靠調高度調不掉
     （已另外在 computeCell() 留 6px 安全邊，實測 scrollHeight 仍是 715）。
   ── 修法 ──
     ★保留 overflow:auto 作為真正溢出時的安全網，但不畫捲軸★。
     不改成 hidden／clip 的理由：那會在真的溢出時把道具列直接吃掉，
     而「道具列被推出畫面」是本專案發生過的事故，不能為了美觀把安全網拆掉。
   ⇒ 玩家永遠看不到捲軸；萬一某台裝置真的溢出，內容仍然滑得到。 */
/* ★不再隱藏捲軸外觀★：真因（.m3-wrap::after 的 bottom:-2px）已修掉，
   內容是真的塞得下。留著 scrollbar-width:none 只會遮掩未來真正的溢出。
   overflow:auto 保留作安全網 —— 真的溢出時要看得到捲軸，才抓得到問題。 */

/* ══ Booster 三態：locked / empty / 可用 ═══════════════════════════════════
   ★契約：玩家必須一眼分得出「還沒解鎖」與「已解鎖但用完了」★
   locked：整顆壓暗去飽和 ＋ 鎖頭字樣，讀起來是「還不能用」
   empty ：道具本體維持原色（它是玩家已經擁有的東西），只有數量變 ×0
           並露出「取得方式」連結 ⇒ 讀起來是「用完了，可以再拿」 */
.m3-booster[data-state="locked"]{ filter:grayscale(.85) brightness(.62); }
/* ★純 CSS 鎖頭★：不用 emoji（那是系統字型，各平台長相不同，且被列為 placeholder） */
.m3-lockicon{
  display:inline-block; width:8px; height:7px; margin-right:4px; position:relative;
  border-radius:1.5px; background:currentColor; vertical-align:-1px;
}
.m3-lockicon::before{
  content:''; position:absolute; left:1.5px; top:-4px; width:5px; height:5px;
  border:1.5px solid currentColor; border-bottom:0;
  border-radius:3px 3px 0 0; box-sizing:border-box;
}
.m3-booster[data-state="locked"] .m3-booster-qty{ color:#c9b98f; font-weight:700; letter-spacing:.2px; }
.m3-booster[data-state="empty"]{ filter:none; opacity:.96; }
.m3-booster[data-state="empty"] .m3-booster-qty b{ color:#e8b98a; }
.m3-booster-how{ text-decoration:underline; opacity:.92; }
/* 取得方式說明：★置中對話框★，不是浮在道具列上方的貼條
   ── 為什麼不是貼條 ──
     貼在道具列上方會壓到棋盤最下排（店長已明令不可）；貼在道具列裡會蓋住道具圖示。
     這是玩家「自己點開」的說明，本來就該是一個明確的對話框：
     置中 ＋ 暗底 ⇒ 一眼知道是暫時的說明，關掉就回到遊戲。
   ── 版面安全 ──
     用 position:fixed ＋ inset:0 的暗底，內容 max-height:70dvh 可捲，
     ★不佔任何 flow 高度、不改變棋盤尺寸、不產生頁面捲軸★。 */
.m3-booster-how-box{
  position:fixed; inset:0; z-index:60;
  display:flex; align-items:center; justify-content:center;
  background:rgba(6,10,14,.62); backdrop-filter:blur(2px);
  padding:16px; box-sizing:border-box;
}
.m3-booster-how-box > *{ pointer-events:auto; }
.m3-booster-how-box .m3-bh-card{
  width:min(320px, 88vw); max-height:70dvh; overflow:auto;
  background:linear-gradient(180deg, rgba(46,38,20,.99), rgba(28,23,13,.99));
  border:1px solid rgba(255,214,128,.55); border-radius:14px; padding:14px 16px;
  box-shadow:0 10px 30px rgba(0,0,0,.6); color:#ffe9bd; text-align:left;
}
.m3-booster-how-box .m3-bh-t{ font-size:15px; font-weight:800; margin-bottom:8px; }
.m3-booster-how-box .m3-bh-l{ margin:0 0 10px; padding-left:18px; font-size:13.5px; line-height:1.7; }
.m3-booster-how-box .m3-bh-close{
  -webkit-appearance:none; appearance:none; display:block; margin-left:auto;
  border:1px solid rgba(255,225,160,.5); border-radius:8px; background:rgba(0,0,0,.3);
  color:#ffe6a6; font:700 12px/1 inherit; padding:5px 12px; cursor:pointer;
}

/* ★不用 ✓ emoji★（店長 2026-08-30：正式 UI 嚴禁 Emoji，各平台長相不同）
   目標完成的勾勾改由 CSS 畫：兩條線旋轉 45°，任何平台都長一樣。 */
.m3-hud-goal.done .m3-hud-num{ position:relative; min-width:14px; }
.m3-hud-goal.done .m3-hud-num::after{
  content:''; position:absolute; left:50%; top:50%;
  width:5px; height:10px; margin:-7px 0 0 -3px;
  border:solid #ffe6a6; border-width:0 2px 2px 0;
  transform:rotate(42deg);
}

/* ══════════════════════════════════════════════════════════════════════════
   ⭐[羽]⏳ 新三款 Booster 使用動畫（2026-08-30）
   ★成本紀律與既有三款一致★：只用 transform / opacity / gradient，
   ★不用 filter:blur 動畫、不用 backdrop-filter、不用 mix-blend-mode★
   （海邊平板那一輪的教訓；由 58-booster-ui 與真頁 Gate 一起鎖）。
   ══════════════════════════════════════════════════════════════════════════ */

/* ⭐ 星星魔杖：杖身浮起 → 揮出；十字光束一橫一豎，★不是逐格框★ */
.m3-bx-starwand{ z-index:3; transform-origin:50% 84%; animation:m3BxWandIn .24s cubic-bezier(.3,1.1,.5,1) forwards; }
@keyframes m3BxWandIn{ 0%{ opacity:0; transform:translateY(6%) scale(.92);} 100%{ opacity:1; transform:none;} }
.m3-bx-wandbeam{
  z-index:2; pointer-events:none; opacity:0; border-radius:999px;
  animation:m3BxBeam .52s cubic-bezier(.2,.7,.3,1) forwards;
}
.m3-bx-wandbeam.is-row{
  transform-origin:50% 50%;
  background:linear-gradient(90deg,rgba(255,246,214,0) 0%,rgba(255,236,178,.85) 18%,rgba(255,255,255,.95) 50%,rgba(255,236,178,.85) 82%,rgba(255,246,214,0) 100%);
  box-shadow:0 0 14px rgba(255,226,150,.55);
}
.m3-bx-wandbeam.is-col{
  transform-origin:50% 50%;
  background:linear-gradient(180deg,rgba(255,246,214,0) 0%,rgba(255,236,178,.85) 18%,rgba(255,255,255,.95) 50%,rgba(255,236,178,.85) 82%,rgba(255,246,214,0) 100%);
  box-shadow:0 0 14px rgba(255,226,150,.55);
}
.m3-bx-wandbeam.is-row{ animation-name:m3BxBeamRow; }
.m3-bx-wandbeam.is-col{ animation-name:m3BxBeamCol; }
@keyframes m3BxBeamRow{
  0%{ opacity:0; transform:scaleX(.06) scaleY(.5);} 26%{ opacity:1; transform:scaleX(1) scaleY(1);}
  70%{ opacity:.9;} 100%{ opacity:0; transform:scaleX(1) scaleY(.7);}
}
@keyframes m3BxBeamCol{
  0%{ opacity:0; transform:scaleY(.06) scaleX(.5);} 26%{ opacity:1; transform:scaleY(1) scaleX(1);}
  70%{ opacity:.9;} 100%{ opacity:0; transform:scaleY(1) scaleX(.7);}
}

/* ✨ 月光羽毛選中的第一顆：★必須一眼看得出來★
   （店長真機：「沒有選中第一顆的明顯特徵」——原本棋盤上完全沒有任何表示）
   月白光環 ＋ 緩慢呼吸，讓玩家知道「這顆已經被羽毛勾住了，再選一顆就交換」。
   ★只加 filter 與 outline，不碰 transform★（transform 屬於 place() 與飛行動畫）。 */
.m3-piece.m3-pair-first{
  z-index:6;
  filter:drop-shadow(0 0 8px rgba(226,236,255,.98))
         drop-shadow(0 0 16px rgba(168,198,255,.72))
         brightness(1.10);
  animation:m3PairFirst 1.15s ease-in-out infinite;
}
@keyframes m3PairFirst{
  0%,100%{ filter:drop-shadow(0 0 7px rgba(226,236,255,.86)) drop-shadow(0 0 14px rgba(168,198,255,.55)) brightness(1.06); }
  50%    { filter:drop-shadow(0 0 12px rgba(240,246,255,1)) drop-shadow(0 0 22px rgba(186,212,255,.85)) brightness(1.16); }
}
.m3-reduced .m3-piece.m3-pair-first{ animation:none; }
@media (prefers-reduced-motion:reduce){ .m3-piece.m3-pair-first{ animation:none; } }

/* ✨ 被羽毛「抓起來」飛行中的那兩顆：抬到最上層＋月白接地光暈，
   讓玩家看得出它們正在被帶著走（而不是瞬間換位）。
   ★只加 z-index 與 filter，不碰 transform★ —— transform 由 WAAPI 全權負責。 */
.m3-piece.m3-feather-fly{
  z-index:7;
  filter:drop-shadow(0 6px 10px rgba(20,24,48,.45))
         drop-shadow(0 0 9px rgba(214,226,255,.75));
}
/* [羽] 月光羽毛：★三顆裡最柔★ —— 位移小、透明度低、沒有強光 */
.m3-bx-feather{ z-index:3; transform-origin:50% 78%; animation:m3BxFeatherIn .22s cubic-bezier(.3,1.05,.5,1) forwards; }
@keyframes m3BxFeatherIn{ 0%{ opacity:0; transform:translateY(4%) rotate(-4deg);} 100%{ opacity:1; transform:none;} }
.m3-bx-feathertrail{
  z-index:2; pointer-events:none; opacity:0; border-radius:999px;
  background:linear-gradient(90deg,rgba(226,236,255,0) 0%,rgba(228,238,255,.55) 30%,rgba(255,255,255,.8) 60%,rgba(226,236,255,0) 100%);
  box-shadow:0 0 10px rgba(206,224,255,.45);
  animation:m3BxTrail .46s cubic-bezier(.25,.75,.3,1) forwards;
}
@keyframes m3BxTrail{
  0%{ opacity:0; transform:rotate(var(--deg,0deg)) scaleX(.05);}
  30%{ opacity:.9;} 100%{ opacity:0; transform:rotate(var(--deg,0deg)) scaleX(1);}
}

/* ⏳ 時光沙漏：翻轉 → ★逆流（整個棋盤輕微回捲）★ → 回正 */
.m3-bx-timeglass{ z-index:3; transform-origin:50% 50%; animation:m3BxGlassIn .26s cubic-bezier(.3,1.1,.5,1) forwards; }
@keyframes m3BxGlassIn{ 0%{ opacity:0; transform:scale(.9);} 100%{ opacity:1; transform:none;} }
.m3-bx-timeglass.is-flip{ animation:m3BxGlassFlip .30s cubic-bezier(.35,.05,.2,1) forwards; }
@keyframes m3BxGlassFlip{ 0%{ transform:rotate(0deg);} 100%{ transform:rotate(180deg);} }
.m3-bx-timeglass.is-reverse{ animation:m3BxGlassRev .34s cubic-bezier(.2,.8,.3,1) forwards; }
@keyframes m3BxGlassRev{
  0%{ transform:rotate(180deg) scale(1);} 50%{ transform:rotate(180deg) scale(1.06);}
  100%{ transform:rotate(180deg) scale(1);}
}
/* 「時間被拉回去」：棋盤★極輕微★逆向回捲一下（不是抖動、不影響命中判定） */
.m3-boardwrap.m3-bx-rewind{ animation:m3BxRewind .34s cubic-bezier(.25,.7,.3,1); }
@keyframes m3BxRewind{
  0%{ transform:none;} 45%{ transform:rotate(-.45deg) scale(.992);} 100%{ transform:none;}
}

@media (prefers-reduced-motion: reduce){
  .m3-bx-starwand, .m3-bx-wandbeam, .m3-bx-feather, .m3-bx-feathertrail,
  .m3-bx-timeglass, .m3-boardwrap.m3-bx-rewind{ animation-duration:.01ms !important; }
}

/* ⏳ 時光沙漏「這一關已經用過」：★與 locked／empty 都不同的第三種狀態★
   （用過 ≠ 沒解鎖 ≠ 用完；三者混成同一種灰，玩家會以為道具壞了） */
.m3-booster[data-state="used"]{ opacity:.55; }
.m3-booster[data-state="used"] .m3-booster-art{ filter:grayscale(.55); }
.m3-booster[data-state="used"] .m3-booster-qty{ color:#9a8f7d; font-size:.82em; }

/* ★六款道具之後：qty 那一行絕對不可以換行★
   ── 為什麼 ──
     三款時每顆按鈕寬 ~90px，「再按取消」放得下；
     擴充到六款之後每顆只剩 45–54px（375/390/430），
     「再按取消」會折成兩行 ⇒ 道具列高 112→121px ⇒ ★棋盤被往上推 4.9px★。
     真頁實測三個手機 viewport 都會發生，是新增三顆造成的 layout shift。
   ── 作法 ──
     nowrap 保證永遠一行；字級用 clamp 依按鈕寬度縮，仍維持 50+ 可讀。
     ★不改按鈕高度、不改棋盤尺寸★，所以 0 layout shift。 */
.m3-booster-qty{ white-space:nowrap; }
.m3-booster[data-state="picking"] .m3-booster-qty,
.m3-booster[data-state="used"] .m3-booster-qty{
  font-size:clamp(9px, 2.6vw, 12px);
  letter-spacing:-.2px;
}
.m3-booster[data-state="empty"] .m3-booster-qty{ font-size:clamp(9px, 2.5vw, 12px); }

/* ══════════════════════════════════════════════════════════════════════════
   🎒 6 選 3 入場 Loadout（PRODUCT_CONTRACT §528）
   ★棋盤底部永遠只有三格★；六款在這個選裝畫面挑。
   50+ 可讀性：名稱大、四種狀態一眼分得出來（可選／已選／未解鎖／用完）。
   ══════════════════════════════════════════════════════════════════════════ */
/* 空的道具格：看得出「這裡可以放東西」，但不搶戲 */
.m3-booster.is-slotempty{ opacity:.5; border-style:dashed; }
.m3-booster.is-slotempty .m3-booster-art{ background:none; }
.m3-booster.is-slotempty .m3-booster-name{ color:#c9b98f; }

.m3-loadout-box{
  position:fixed; inset:0; z-index:60; display:flex; align-items:center; justify-content:center;
  background:rgba(24,18,12,.62); padding:16px;
}
.m3-lo-card-wrap{
  width:min(420px, 94vw); max-height:88vh; overflow:auto;
  background:linear-gradient(180deg,#2e2519,#241d14);
  border:1px solid rgba(255,226,150,.28); border-radius:16px;
  padding:16px 14px 14px; box-shadow:0 12px 40px rgba(0,0,0,.45);
}
.m3-lo-t{ font-size:18px; font-weight:800; color:#ffe6a6; text-align:center; }
.m3-lo-sub2{ font-size:13px; color:#c9b98f; text-align:center; margin:4px 0 12px; }
.m3-lo-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
.m3-lo-card{
  position:relative; display:flex; flex-direction:column; align-items:center; gap:2px;
  padding:8px 4px 6px; border-radius:12px; cursor:pointer;
  background:rgba(255,255,255,.04); border:2px solid transparent; color:#e8dcc0;
}
.m3-lo-art{ width:44px; height:44px; display:block; }
.m3-lo-art img{ width:100%; height:100%; object-fit:contain; }
.m3-lo-name{ font-size:12px; font-weight:700; white-space:nowrap; }
.m3-lo-sub{ font-size:11px; color:#c9b98f; white-space:nowrap; }
.m3-lo-tick{
  position:absolute; top:4px; right:4px; width:18px; height:18px; border-radius:50%;
  background:#7bc47f; opacity:0; transform:scale(.6); transition:opacity .12s, transform .12s;
}
.m3-lo-tick::after{
  content:''; position:absolute; left:6px; top:3px; width:4px; height:8px;
  border:solid #fff; border-width:0 2px 2px 0; transform:rotate(42deg);
}
.m3-lo-card[data-st="on"]{ border-color:#7bc47f; background:rgba(123,196,127,.14); }
.m3-lo-card[data-st="on"] .m3-lo-tick{ opacity:1; transform:scale(1); }
.m3-lo-card[data-st="locked"]{ opacity:.5; }
.m3-lo-card[data-st="locked"] .m3-lo-art img{ filter:grayscale(1); }
.m3-lo-card[data-st="empty"]{ opacity:.72; }
.m3-lo-card[data-st="empty"] .m3-lo-sub{ color:#e8b98a; }
.m3-lo-go{
  display:block; width:100%; margin-top:14px; padding:12px; border:0; border-radius:12px;
  font-size:16px; font-weight:800; color:#3b2a12; cursor:pointer;
  background:linear-gradient(180deg,#ffd98a,#f0b95f);
}
@media (prefers-reduced-motion: reduce){ .m3-lo-tick{ transition:none; } }

/* ══════════════════════════════════════════════════════════════════════════
   🔷 NORMAL STAGE｜半透明霧面玻璃棋盤（Lv1–100 全部關卡）
   ──────────────────────────────────────────────────────────────────────────
   ★店長定案★：每一關的場景美術都要成為畫面的一部分，
   不是被一塊不透明米灰石板蓋掉 —— 玩家看棋盤時仍要知道自己在森林／海邊／遺跡。

   ★不可以對整個 board 下 opacity★
     那會連棋子、特殊棋、地形、障礙、FX、選取框一起變透明。
     這裡★只處理底板與格面★，棋子那一層一個字都不碰：
       場景背景 → 半透明底板 → 半透明格面 → 地形/障礙 → 棋子 → 特殊棋 → FX

   ★同一片區域只准一層玻璃在畫★
     .m3-board / .m3-board-bg / .m3-holefill 是重疊全幅的三層；
     每層各鋪一次，疊起來就會變回一片灰板（水晶洞穴那邊實測過）。
     所以 shaped 時只有 .m3-board-bg 在畫，其餘透明。

   ★兩套 skin 分離★
     這裡是 NORMAL（中性暖灰霧面玻璃，能適應各種背景）；
     Hidden Crystal 用自己的月光藍水晶玻璃覆蓋在後面。
     反向 Gate：把 crystal skin 套到 normal stage 必須 FAIL。

   ★只改材質，不改幾何★：cell size、board rect、HUD、Booster bar 全部不動。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-board.m3-shaped,
.m3-holefill {
  background: transparent;
}
.m3-board:not(.m3-shaped),
.m3-board.m3-shaped > .m3-board-bg {
  /* ★真機 REOPEN 後重訂★
     第一版用「暖白 0.28 的區域提亮」當玻璃 —— 疊在偏暗的場景上，
     整片棋盤反而變成一塊發亮的米褐板，背景細節全被洗掉。
     ★合成後的 pixels 才是驗收標準★，CSS 有 alpha 不等於看得到背景。

     改法：區域填色壓到幾乎沒有（合計約 0.08，而且是中性色不是暖白），
     棋盤的「存在感」改由★邊界與格線★負責 —— 那些只佔幾個 pixel 寬，
     不會把整片場景蓋掉。 */
  background:
    linear-gradient(170deg, rgba(255, 253, 250, .05) 0%, rgba(232, 230, 226, .03) 55%,
                            rgba(170, 168, 164, .05) 100%),
    rgba(96, 94, 90, .03);
  /* 玻璃的厚度感全部收在邊緣，不進中央 */
  box-shadow:
    inset 0 0 0 1px rgba(255, 252, 244, .20),
    inset 0 1px 0 rgba(255, 252, 244, .26),
    0 10px 26px rgba(0, 0, 0, .34);
}
/* 格子：非常薄的玻璃分隔 —— 看得出格位，但不要每格變成一顆獨立按鈕 */
/* 格子：★不填色★，只留一道細接縫。
   64 格各鋪一次淡色，加起來就是又一層全幅遮蔽 —— 那正是第一版失敗的原因之一。 */
.m3-socket {
  background: transparent;
  box-shadow:
    inset -1px 0 0 rgba(255, 252, 244, .20),
    inset 0 -1px 0 rgba(255, 252, 244, .20),
    inset 1px 0 0 rgba(40, 34, 26, .16),
    inset 0 1px 0 rgba(40, 34, 26, .16);
}
/* ★ch2 石板接縫版本會以 0,2,0 特異性蓋掉上面這條★，
   而它原本是為「不透明石盤」設計的暗色接縫，在透明玻璃上幾乎看不見。
   這裡把它一併換成玻璃接縫，否則第二章關卡的格線會消失。 */
.m3-ch2 .m3-socket {
  background: transparent;
  box-shadow:
    inset -1px 0 0 rgba(255, 252, 244, .20),
    inset 0 -1px 0 rgba(255, 252, 244, .20),
    inset 1px 0 0 rgba(40, 34, 26, .16),
    inset 0 1px 0 rgba(40, 34, 26, .16);
}
/* ★holefill 在第二章有 0,2,0 的實心石盤宣告★（.m3-ch2 .m3-holefill），
   我原本那條 .m3-holefill 是 0,1,0 ⇒ 打不過它 ⇒ 那一層仍然是實心的。
   這就是「CSS 看起來改了、真機卻沒變」的其中一個真因。 */
.m3-ch2 .m3-holefill,
.m3-ch2 .m3-board.m3-shaped > .m3-board-bg { background: transparent; }
/* ★棋子辨識度是硬條件★
   背景變成真實風景之後，牛奶（白）與高麗菜（淺綠）最容易融進去。
   解法是保護棋子本身，不是把整張棋盤又加回不透明底。 */
.m3-piece {
  filter: drop-shadow(0 2px 3px rgba(38, 28, 14, .42))
          drop-shadow(0 0 1px rgba(38, 28, 14, .34));
}
/* 地形／障礙必須清楚壓在玻璃之上，不得跟著透明 */
.m3-terrain, .m3-obs-cell:not(.m3-obs-crystal) {
  filter: drop-shadow(0 2px 4px rgba(30, 22, 10, .38));
}
/* 🍁 v0.61.20 地板層同理 —— 而且它比障礙更需要這一層。
   ★實測問題★：楓林背景本身就是滿滿的橘紅楓葉，秋葉地毯放上去會「融進背景」。
   解法沿用這裡既有的做法（描邊式陰影把素材壓在玻璃之上），
   ★不是退回不透明米灰底板★ —— 那會把整張背景蓋掉，失去做背景的意義。
   地板層在棋子下面，所以陰影要比障礙再輕一點，避免棋子周圍出現黑圈。
   ★這是基礎版，實際套用在「盛開的花叢」(bloom-1)★ —— 粉色花環本來就跳，
   不需要、也不應該跟著秋葉一起加強，否則會變髒。 */
.m3-floor {
  filter: drop-shadow(0 1px 3px rgba(30, 22, 10, .46))
          drop-shadow(0 0 1px rgba(30, 22, 10, .40));
}
/* ══ 🍯 蜜糖罐專屬分離度（GPT 第二次 Release Gate 指定項目）════════════════
   ★問題★：糖罐本體是金黃／琥珀，maple 背景也是大量橘紅、金黃與暖色燈光。
   112px 單獨看很漂亮，但縮到正式 38px 棋盤，★Lv1 金蓋★的外輪廓會部分融進背景。
   （Lv2 紅蓋比較不會，因為紅蠟封本身就跟背景分開。）
   這不是 need 圖示的問題 —— need 已 PASS —— 是「整顆 obstacle 與場景的前後景分離」。

   ★作法：走暗側，不走亮側★
   糖罐是金色，再加金色／黃色 glow 只會讓它跟楓林背景更融。
   所以分離感全部來自「落地接觸陰影 ＋ 暗輪廓 ＋ 很小幅對比」。
     ❌ 無金色發光　❌ 無 halo　❌ 無粗黑框／白框　❌ 無方形／圓形底板
     ❌ 不改罐身主色　❌ 不動 need 位置　❌ 不壓暗棋盤　❌ 不動共用棋盤 CSS

   ★A/B/C 實測（正式 maple ＋ 正式透明棋盤 ＋ 38px 整盤，4x 取樣）★
     外圈色距（糖罐 vs 同列背景格）：A 14.2　B 14.2　★C 19.0★
     罐內對比（結構有沒有被壓出來）：A 80.1　B 83.3　★C 85.5★
     目視：A 在 38px 要「找」才看得到金蓋罐；B 略好；★C 是掃一眼就數得出幾顆★。
   ★採用 C★。

   ★「不改主色」有量過★：A ⇒ C 的罐身平均色
     ΔH 0.55°、ΔS +0.027、ΔV +0.015、ΔRGB 距離 5.9（0–255）
     ⇒ 色相幾乎沒動，只是暗部更紮實一點，不是換色。

   ★刻意不加 saturate／brightness★：filter 會作用在整個子樹，
   need 徽章（.m3-need）是這個元素的子節點，加了就會一起被改到。
   只用 contrast(1.09) 是因為它對「已經很亮的白色標籤」影響最小。

   ★只作用在 .m3-obs-syrup★
     ‧ 木箱／冰晶／古藤／泥苔／花苞 一律維持原本的 .m3-obs-cell 通用陰影
     ‧ 秋葉（.m3-floor-t-leaf）維持它自己的 C 版，不受影響
     ‧ Lv1～110 沒有 syrup 這個型別 ⇒ 零影響
   ══════════════════════════════════════════════════════════════════════════ */
.m3-obs.m3-obs-syrup {
  filter: drop-shadow(0 3px 4px rgba(16, 9, 2, .66))
          drop-shadow(0 0 2px rgba(16, 9, 2, .62))
          drop-shadow(0 0 5px rgba(16, 9, 2, .30))
          contrast(1.09);
}

/* ══ 🍁 秋葉專屬分離度（GPT 第二審指定項目）══════════════════════════════
   ★問題（店長＋GPT 都指出、我自己報告裡也承認）★
     楓林背景本身就是滿滿的橘紅楓葉。38px 縮小之後，玩家看到一大片橘紅，
     要花時間辨認「哪些是我要清的落葉、哪些只是背景」。主客群是婆婆媽媽，
     ★一眼看懂優先★，所以這一條在封版前解掉。

   ★作法★：做了 A（現況）／B（輕度）／C（中度）三版，在正式 maple 背景
   ＋正式透明棋盤 ＋38px 實景下對照，**採用 C**。
     實測分離度（葉格 vs 同列無葉格的平均色距／外圈色距）：
       A 70.8 / 54.9　　B 76.1 / 58.5　　★C 79.5 / 64.6★
     目視：A 在 38px 會融進背景；B 可讀；C 是唯一「掃一眼就知道哪幾格要清」的。

   ★分離感的來源是「落地接觸陰影」，不是框★ ——
   它是地板層，本來就該有接觸陰影；再加很小幅的色彩提升。
   ❌ 沒有粗黑框　❌ 沒有白框　❌ 沒有發光　❌ 沒有底板　❌ 沒有 UI icon 感
   ★只作用在 .m3-floor-t-leaf★ ⇒ bloom-1 不受影響、Lv1～110 根本不會有這個 class。
   若店長覺得太搶，改回 B 只要換掉這一組 filter。 */
.m3-floor.m3-floor-t-leaf {
  filter: drop-shadow(0 2px 3px rgba(18, 11, 3, .66))
          drop-shadow(0 0 2px rgba(18, 11, 3, .66))
          drop-shadow(0 0 4px rgba(18, 11, 3, .34))
          saturate(1.18) brightness(1.08) contrast(1.06);
}

/* ══════════════════════════════════════════════════════════════════════════
   💎 HIDDEN CRYSTAL STAGE｜被封住的道具（obstacle type = crystal）
   ──────────────────────────────────────────────────────────────────────────
   ★為什麼是 CSS 畫的而不是貼圖★
     既有障礙走 `<type>-<level>.webp`，貼圖 404 時才落到 .m3-obs-fallback 的 CSS 造型。
     水晶目前★沒有專屬貼圖★，所以這裡直接把 crystal 的四態做成正式 CSS 造型
     ——不是「等它 404 再說」，而是本來就以 CSS 為準；
     日後若補上 crystal-1/2/3.webp，貼圖會自動蓋在上面，這段仍是保底。

   四態（level 3→1，0 就移除了）
     3 FROZEN   完全封住、霧面厚實
     2 CRACKED  出現裂痕
     1 BREAKING 快碎了、內部透光
   ══════════════════════════════════════════════════════════════════════════ */
/* ★格子裡放的是「那一顆被冰封的道具」本尊★（inline background-image 由 JS 指定），
   所以這裡★不能用 background 簡寫★ —— 簡寫會把 image 層一起重設，道具就不見了。
   冰的部分改用 background-color ＋ ::before/::after 疊在道具上面。 */
.m3-obs.m3-obs-crystal {
  border-radius: 14%;
  background-color: rgba(150, 205, 240, .34);
  background-size: 72% 72%;           /* 道具縮在格子中央，四周留出冰殼 */
  background-position: center;
  background-repeat: no-repeat;
  box-shadow:
    inset 0 0 0 1.5px rgba(255,255,255,.72),
    inset 0 -22% 26% rgba(60,120,190,.38),
    0 4% 12% rgba(40,90,150,.35);
  backdrop-filter: none;              /* ★不用 backdrop-filter★：那是海邊那包實測過的效能地雷 */
}
/* 內部的稜面切割：用兩道 clip 過的高光疊出「多面體」感，不加任何濾鏡 */
.m3-obs.m3-obs-crystal::before,
.m3-obs.m3-obs-crystal::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
}
.m3-obs.m3-obs-crystal::before {
  background: linear-gradient(115deg, transparent 34%, rgba(255,255,255,.55) 42%, transparent 50%),
              linear-gradient(-70deg, transparent 58%, rgba(255,255,255,.34) 66%, transparent 73%);
}
/* 3 FROZEN：霧面最厚 —— ★但仍要看得出「裡面封了一顆道具」★。
   完全遮住的話，玩家讀到的只是「一格白方塊」，整個玩法動機就沒了。 */
.m3-obs.m3-obs-crystal.m3-obs-lv3::after {
  background:
    radial-gradient(78% 78% at 50% 48%, rgba(226,244,255,.20), rgba(226,244,255,.52) 78%),
    linear-gradient(160deg, rgba(255,255,255,.30), rgba(168,214,244,.34));
}
/* 2 CRACKED：裂痕出現 */
.m3-obs.m3-obs-crystal.m3-obs-lv2::after {
  background:
    linear-gradient(198deg, transparent 46%, rgba(255,255,255,.92) 47.5%, transparent 49%),
    linear-gradient(-24deg, transparent 60%, rgba(255,255,255,.72) 61.4%, transparent 63%);
}
/* 1 BREAKING：裂得更開、內部透出暖光（＝道具快出來了） */
.m3-obs.m3-obs-crystal.m3-obs-lv1::after {
  background:
    radial-gradient(60% 60% at 50% 55%, rgba(255,226,150,.55), rgba(255,226,150,0) 70%),
    linear-gradient(198deg, transparent 40%, rgba(255,255,255,.95) 42%, transparent 44%),
    linear-gradient(-24deg, transparent 52%, rgba(255,255,255,.88) 54%, transparent 56%),
    linear-gradient(86deg, transparent 66%, rgba(255,255,255,.8) 67.6%, transparent 69%);
  animation: m3CrystalPulse 1.6s ease-in-out infinite;
}
@keyframes m3CrystalPulse { 0%, 100% { opacity: .85 } 50% { opacity: 1 } }
@media (prefers-reduced-motion: reduce) {
  .m3-obs.m3-obs-crystal.m3-obs-lv1::after { animation: none }
}

/* 隱藏關卡的整體場景：只換背景，★不加任何全螢幕濾鏡★ */
.m3-hidden-stage {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* ★場景層原本的洞窟圖必須讓位★
   .cave-m3-root::before 是 z-index:0 的實心場景圖，蓋在元素自身背景之上；
   不關掉它，換上去的水晶洞穴背景就永遠被壓在下面 ⇒ 玩家看不到自己進了洞穴。
   ::after 是原場景的暗角，一併關掉，避免兩層氛圍互打。 */
#caveScene .cave-m3-root.m3-hidden-stage::before,
#caveScene .cave-m3-root.m3-hidden-stage::after,
.cave-m3-root.m3-hidden-stage::before,
.cave-m3-root.m3-hidden-stage::after { display: none; }
/* 水晶洞穴的氛圍：一層很淡的冷色暈，讓棋盤仍然清楚可讀 */
.cave-m3-root.m3-hidden-stage {
  background-color: #0d2230;
  box-shadow: inset 0 0 22% 6% rgba(8, 26, 40, .55);
}

/* ══════════════════════════════════════════════════════════════════════════
   💎 HIDDEN CRYSTAL｜半透明水晶玻璃棋盤（★只在水晶洞穴生效★）
   ──────────────────────────────────────────────────────────────────────────
   ★問題根因是 BOARD OCCLUSION，不是背景圖★
     水晶洞穴背景本身是對的，但正常關卡的棋盤是一整片不透明米灰石盤
     （#b6a480 ＋ board-stone.webp），把中央 70–80% 的洞穴整個蓋掉 ——
     玩家只在棋盤四周看得到洞穴，Hidden Crystal 最大的視覺特色被浪費。

   ★不可以對整個 board 下 opacity★
     那會連棋子、障礙、冰封道具、FX、選取框一起變半透明 ⇒ 全部看不清。
     所以這裡★只處理底板與格面★，棋子那一層一個字都不碰：
       水晶洞穴背景 → 半透明底板 → 半透明格面 → 地形／冰封物 → 棋子 → FX

   ★scope★
     全部掛在 .m3-hidden-stage 之下。正常 Lv1–100 的棋盤 skin 完全不受影響
     （tests/hidden-crystal-wiring.mjs 有反向 Gate：拿掉 scope 必須轉紅）。

   ★這裡只改視覺 skin★：不動 geometry、不動 topology、不動 playable area。
   ══════════════════════════════════════════════════════════════════════════ */
/* ★同一片區域只准鋪一層玻璃★
   .m3-board / .m3-board-bg / .m3-holefill 三層是重疊全幅的；
   三層各 0.26 疊起來 ≈ 0.59 ⇒ 中央又變回一片藍灰板（真頁量到的就是這個）。
   所以：底圖層只留 .m3-board-bg 這一層在畫，另外兩層在洞穴裡一律透明。 */
.m3-hidden-stage .m3-board.m3-shaped,
.m3-hidden-stage .m3-holefill {
  background: transparent;
  box-shadow: none;
}
.m3-hidden-stage .m3-board:not(.m3-shaped),
.m3-hidden-stage .m3-board.m3-shaped > .m3-board-bg {
  /* 深月光藍 ＋ 藍灰水晶玻璃，微量紫色折射；★不放石盤貼圖★。
     真頁量測：這一層合計約 0.20 alpha，大型晶體、光束、景深都仍讀得到。 */
  background:
    linear-gradient(168deg, rgba(126, 176, 224, .09) 0%, rgba(86, 122, 186, .06) 46%,
                            rgba(120, 104, 186, .08) 100%),
    rgba(18, 42, 68, .10);
  box-shadow:
    inset 0 1px 0 rgba(220, 240, 255, .28),
    inset 0 -14px 26px rgba(20, 44, 78, .14),
    0 12px 30px rgba(4, 14, 26, .40);
}
/* 每一格：非常輕的藍玻璃分隔，★不要每格一條明顯白線★（會變 Candy Crush 塑膠感） */
.m3-hidden-stage .m3-socket {
  background: rgba(150, 196, 238, .05);
  box-shadow:
    inset -1px 0 0 rgba(196, 226, 255, .10),
    inset 0 -1px 0 rgba(196, 226, 255, .10),
    inset 1px 0 0 rgba(10, 30, 56, .09),
    inset 0 1px 0 rgba(10, 30, 56, .09);
}
/* ★棋子辨識度不犧牲★
   背景變亮變花之後，牛奶（白）與高麗菜（淺綠）最容易融進藍光。
   解法不是把棋盤重新加深一大片，而是給★每一顆棋子自己★一點接地陰影與描邊，
   讓輪廓從背景裡浮出來。這一層只作用在棋子，不影響 geometry。 */
.m3-hidden-stage .m3-piece {
  filter: drop-shadow(0 2px 3px rgba(4, 16, 30, .55))
          drop-shadow(0 0 1px rgba(6, 20, 36, .70));
}
/* 洞穴裡的接地陰影改成冷色（normal skin 已提供暖色版，這裡只換色調） */
.m3-hidden-stage .m3-terrain,
.m3-hidden-stage .m3-obs-cell:not(.m3-obs-crystal) {
  filter: drop-shadow(0 2px 4px rgba(4, 16, 30, .45));
}

/* ══════════════════════════════════════════════════════════════════════════
   💎 探險能量｜★平常完全不顯示★
   ──────────────────────────────────────────────────────────────────────────
   店長定案：探險能量是遊玩規則，★不得為了它改動已經定稿的三消版型★。
   所以這裡★沒有任何常駐元件★——不加 HUD、不加常駐列、不推棋盤、
   不移動 Moves／目標／道具列。有能量時扣點是安靜完成的。

   只有「0/3 被擋下來」那一刻才出現下面這個提示：
     ‧ position: fixed ⇒ ★完全不進文件流★，底層 geometry 一個像素都不動
     ‧ 關掉之後畫面完全恢復原狀（元素直接移除）
     ‧ 倒數只是 server state 的投影，不是真值
   ══════════════════════════════════════════════════════════════════════════ */
.m3-energy-notice {
  position: fixed; left: 50%; transform: translateX(-50%) translateY(6px);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 96px);
  z-index: 2400; pointer-events: auto; cursor: pointer;
  max-width: min(88vw, 360px); text-align: center;
  padding: 10px 18px; border-radius: 14px;
  background: rgba(10, 22, 34, .92);
  border: 1px solid rgba(150, 220, 255, .34);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
  color: #dff1ff; opacity: 0; transition: opacity .18s ease, transform .18s ease;
}
.m3-energy-notice.on { opacity: 1; transform: translateX(-50%) translateY(0); }
.m3-energy-notice-t1 { font-size: clamp(13px, 3.6vw, 15px); font-weight: 700; letter-spacing: .5px; }
.m3-energy-notice-t2 { margin-top: 3px; font-size: clamp(11px, 3vw, 12.5px); color: #a9c9dd;
  font-variant-numeric: tabular-nums; }   /* ★等寬數字★：29:59 → 29:58 不會抖動 */
@media (prefers-reduced-motion: reduce) {
  .m3-energy-notice { transition: none; }
}


/* ══════════════════════════════════════════════════════════════════════════
   🎒 六選三的入口徽章
   ──────────────────────────────────────────────────────────────────────────
   ★為什麼需要它★：原本開啟選裝畫面的唯一入口是「空的道具格」，
   但預設 loadout 一開始就填滿三格 ⇒ 解鎖三款以上的玩家永遠沒有空格，
   ★也就永遠點不到六選三★（正式站實際發生過）。
   ★絕對定位在既有按鈕內★ ⇒ 不新增列、不推棋盤、不改任何既有幾何。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-booster { position: relative; }
/* 🔁 「換道具」入口
   ★店長真機：太不明顯，怕玩家不好按★
   原本是 18×18px、11px 字的小圓點 —— 遠低於手指可靠命中的尺寸。
   改法（★不動棋盤與道具列的既有幾何★）：
     ‧ 字改成「換」＋圖示，膠囊放大到 26px 高、字 12px
     ‧ 用 ::after 撐出 ★44×44 的隱形觸控區★（Apple／Google 的最小建議值），
       視覺沒有變大，但手指按得到
     ‧ 進場時做三次很輕的呼吸，讓玩家第一次就注意到它存在（之後不再動） */
.m3-booster-swap {
  /* ★往外掛在卡片右上角★（不是壓在圖上）
     ‧ 三張卡各自一顆（每張卡 121px 寬），若把 50px 的膠囊放進卡片內，
       道具圖會被吃掉一半 —— 店長要的是「看得到、按得到」，不是「蓋住道具」。
     ‧ 負位移讓它坐在卡片邊框上，道具圖完全不被遮，辨識度反而更高。 */
  position: absolute; top: -7px; right: -4px;
  min-width: 26px; height: 22px; padding: 0 7px;
  display: flex; align-items: center; justify-content: center; gap: 2px;
  border-radius: 999px; font-size: 11.5px; line-height: 1; font-weight: 800;
  color: #10233a; background: linear-gradient(180deg, #ffe9b0, #f5cf7d);
  border: 1px solid rgba(120, 88, 24, .5);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .42), inset 0 1px 0 rgba(255, 255, 255, .6);
  animation: m3SwapHint 1.5s ease-in-out 3;      /* 只提示三次，不長期閃 */
}
/* ★隱形觸控區★：視覺 26px，實際可按 44px */
.m3-booster-swap::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
@keyframes m3SwapHint {
  0%, 100% { transform: scale(1); box-shadow: 0 2px 6px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.6); }
  50%      { transform: scale(1.12); box-shadow: 0 2px 12px rgba(255,214,130,.85), inset 0 1px 0 rgba(255,255,255,.7); }
}
.m3-reduced .m3-booster-swap { animation: none; }
@media (prefers-reduced-motion: reduce) { .m3-booster-swap { animation: none; } }
.m3-booster.is-slotempty .m3-booster-swap { display: none; }   /* 空格本身就是入口 */
@media (max-width: 400px) { .m3-booster-swap { min-width: 24px; height: 21px; padding: 0 6px; font-size: 11px; } }

/* ══════════════════════════════════════════════════════════════════════════
   🏞 Lv1–100 場景背景（THEME SCENES）
   ──────────────────────────────────────────────────────────────────────────
   ★為什麼是分層 CSS 而不是點陣圖★
     ‧ 解析度無關：375 到平板都銳利，不必準備三種尺寸
     ‧ 體積接近 0：不會再發生「素材沒進版控 ⇒ 正式站全站 404」那類事故
     ‧ 日後換成畫好的插畫時，只要在 match3-scenes.js 的表填 image 欄位，
       下面的 --m3-scene-img 就會蓋過分層漸層，★關卡對應完全不用重寫★

   ★分層順序★（由遠到近，全部畫在同一個 ::before，不新增 DOM）
     天空 → 遠山／遠景 → 中景（樹冠／浪／柱列）→ 地面 → 大氣光

   ★這一層永遠在棋盤之下★（z-index:0），而且 pointer-events:none。
   棋盤本身是半透明玻璃，所以這些層會被玩家真的看到 —— 這正是目的。
   ══════════════════════════════════════════════════════════════════════════ */
/* ★特異性必須壓過 `#caveScene .cave-m3-root::before`★（cave-scene.js 注入的 ID 選擇器）
   —— 這正是「class 套上了、畫面卻還是舊洞窟」的原因，與 .m3-ch2 .m3-holefill 同一類。 */
#caveScene .cave-m3-root.m3-scene::before,
.cave-m3-root.m3-scene::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background-image:var(--m3-scene-img, var(--m3-scene-layers));
  background-size:var(--m3-scene-size, cover);
  background-position:var(--m3-scene-pos, center);
  background-repeat:no-repeat;
  /* ★不降銳利度★：只做很輕的整體收斂，讓棋子仍是視覺主角 */
  filter:brightness(.97) saturate(1.02);
}
/* 棋盤背後的柔和暗 halo（沿用原本的作法：不用大面積壓黑去凸顯棋盤） */
#caveScene .cave-m3-root.m3-scene::after,
.cave-m3-root.m3-scene::after{
  content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  /* ★v0.61.28：把兩層光暈的顏色抽成變數，預設值與原本逐字相同★
     —— Lv1–100 的場景全部是深色（洞窟／夜空／海岸），壓一層冷暗很合理；
     夢莓花園是粉彩高明度，同一層冷暗會變成一塊灰髒。
     ★不新增選擇器也不改結構★：只讓場景可以覆寫自己的光暈顏色。 */
  background:
    radial-gradient(40% 44% at 50% 48%, var(--m3-halo-1, rgba(8,14,22,.20)) 0%, var(--m3-halo-2, rgba(8,14,22,.10)) 62%, rgba(0,0,0,0) 100%),
    radial-gradient(94% 78% at 50% 48%, rgba(0,0,0,0) 64%, var(--m3-halo-3, rgba(10,16,24,.12)) 86%, var(--m3-halo-4, rgba(10,16,24,.22)) 100%);
}

/* 🌲 晨光森林 Lv1–20：清晨、暖陽、低矮花叢 */
.m3-scene-dawn{
  --m3-scene-layers:
    radial-gradient(58% 26% at 76% 12%, rgba(255,246,214,.85), rgba(255,246,214,0) 70%),
    radial-gradient(120% 42% at 50% 100%, #8fbf6a 0%, #7fae5e 42%, rgba(127,174,94,0) 100%),
    radial-gradient(80% 24% at 18% 74%, rgba(214,232,170,.75), rgba(214,232,170,0) 72%),
    radial-gradient(70% 22% at 82% 78%, rgba(198,224,158,.70), rgba(198,224,158,0) 74%),
    linear-gradient(180deg, #a8dcf0 0%, #c7e9f2 34%, #e6f2dc 62%, #cbe3a8 82%, #a9cd7e 100%);
  background-color:#bfe0ee;
}
/* 🌸 溪谷花園 Lv21–40：樹冠篩光、苔綠、深景 */
.m3-scene-valley{
  --m3-scene-layers:
    linear-gradient(196deg, rgba(255,247,200,.30) 6%, rgba(255,247,200,0) 34%),
    radial-gradient(46% 30% at 22% 6%, rgba(120,168,86,.85), rgba(120,168,86,0) 72%),
    radial-gradient(52% 32% at 80% 4%, rgba(96,146,72,.88), rgba(96,146,72,0) 74%),
    radial-gradient(130% 40% at 50% 104%, #3f6b3a 0%, #4d7a41 46%, rgba(77,122,65,0) 100%),
    linear-gradient(180deg, #9fd0a8 0%, #7fb98d 26%, #5f9a6e 56%, #4a7d55 80%, #3c6647 100%);
  background-color:#5f9a6e;
}
/* 🌙 月光海岸 Lv41–60：藍綠海水、浪線、沙灘 */
.m3-scene-coast{
  --m3-scene-layers:
    radial-gradient(52% 24% at 84% 10%, rgba(255,250,226,.82), rgba(255,250,226,0) 72%),
    linear-gradient(180deg, rgba(255,255,255,0) 52%, rgba(255,255,255,.34) 55%, rgba(255,255,255,0) 58%),
    linear-gradient(180deg, rgba(255,255,255,0) 63%, rgba(255,255,255,.26) 66%, rgba(255,255,255,0) 69%),
    radial-gradient(130% 30% at 50% 106%, #e8d9b0 0%, #dccda2 44%, rgba(220,205,162,0) 100%),
    linear-gradient(180deg, #9fd8ef 0%, #bfe8f5 26%, #6fc0d8 48%, #3f9cbd 72%, #d9caa0 100%);
  background-color:#6fc0d8;
}
/* 🏛 古代遺跡 Lv61–80
   ★這裡原本指向 match3_cave_ruins_bg.webp —— 而那張★從來沒進版控★。
   正式產物是從 git 出的，所以它在正式站根本不存在，玩家看到的是下面
   background-color 的回退色 —— 這極可能就是店長說的「單調暖褐牆面」。
   已把該檔補進版控，同時這一層改用店長新交付的正式美術。 */
.m3-scene-ruins{
  background-color:#12232c;   /* 圖載不到時的回退色，永遠不會變純黑 */
}
/* ✨ 星夜高地 Lv81–100：夜空、月光、湖面反光 */
.m3-scene-highlands{
  --m3-scene-layers:
    radial-gradient(16% 9% at 78% 14%, rgba(255,252,232,.95), rgba(255,252,232,0) 70%),
    radial-gradient(60% 26% at 78% 16%, rgba(196,214,255,.34), rgba(196,214,255,0) 74%),
    linear-gradient(180deg, rgba(255,255,255,0) 66%, rgba(190,214,255,.20) 70%, rgba(255,255,255,0) 74%),
    radial-gradient(130% 34% at 50% 104%, #1d2b52 0%, #24345f 46%, rgba(36,52,95,0) 100%),
    linear-gradient(180deg, #101a38 0%, #1b2a52 30%, #2b3f6e 58%, #35507f 78%, #24345f 100%);
  background-color:#1b2a52;
}

/* 🫐 夢莓花園 Lv151–175：粉彩夕陽、莓果花圃
   ★背景圖是主角的反面★：這一區的背景刻意畫成「中央低細節」，
   所以這裡只做兩件事 ——
     ① 回退色：圖沒載到也不會變黑（用背景圖自己的中段粉色）
     ② 光暈：把預設的冷暗光暈換成同色系的暖莓色，
        既保留「棋盤浮在場景上」的層次，又不會在粉彩上壓出一塊灰。 */
.m3-scene-dreamberry{
  background-color:#f3dfe6;
  --m3-halo-1: rgba(122, 64, 92, .17);
  --m3-halo-2: rgba(122, 64, 92, .08);
  --m3-halo-3: rgba(112, 58, 86, .10);
  --m3-halo-4: rgba(112, 58, 86, .19);
}
/* ── 棋盤要和夢莓花園是★同一款遊戲★，不是兩張圖疊在一起 ─────────────────
   ★沿用第二章已經驗證過的同一招★（見上面 .m3-ch2 .m3-board 的註解）：
   石盤貼圖不換、只用 background-blend-mode:multiply 把它染進場景的色域。
   第二章把石盤從亮米色 #b6a480 拉到洞窟的暗綠 #7d7a5c；
   夢莓花園是粉彩高明度場景，同一塊暗綠石板浮在粉紅夕陽上，
   讀起來就是「一塊橄欖綠 UI 面板貼在插畫上」——
   這裡把它拉到同色系的暖莓灰，明度仍★低於背景中段★，
   棋盤還是「凹進場景裡的一塊地」，對比一樣交給飽和的棋子。
   ★只改呈現★：不動幾何、不動 socket 座標、不動 hitbox、不動任何判定。
   ★作用範圍只有 .m3-scene-dreamberry（Lv151–175）★ ⇒ Lv1–150 零影響。 */
.m3-scene-dreamberry .m3-board{
  background:#d9b3c4 url('match3/board-stone.webp') center / cover no-repeat;
  background-blend-mode:multiply;
  box-shadow: inset 0 -18px 30px rgba(120,70,95,.20);
}
.m3-scene-dreamberry .m3-board:not(.m3-shaped){
  /* 第二章那組冷黑落地陰影（rgba(6,12,10,.42)）在粉彩上是一圈髒灰；
     換成同色系、更柔的接觸陰影，棋盤一樣「坐在花園裡」。 */
  box-shadow: inset 0 -18px 30px rgba(120,70,95,.20),
              0 10px 22px rgba(120,66,94,.26), 0 2px 5px rgba(120,66,94,.18);
}
.m3-scene-dreamberry .m3-socket{
  /* 接縫跟著換成暖莓色，避免在粉底上出現偏綠的格線 */
  background: rgba(104, 58, 80, 0.055);
  box-shadow: inset 0 1px 2px rgba(96, 52, 74, 0.085),
              inset 0 -1px 1px rgba(255, 246, 250, 0.12);
}

/* ══════════════════════════════════════════════════════════════════════════
   🫐 v0.61.29c【Lv151+ 整個宿主外殼都屬於夢莓花園】
   ──────────────────────────────────────────────────────────────────────────
   ── 店長實機 ──
     「Lv151 上半部是夢莓花園，往下延伸卻露出舊的深藍洞窟／月霜雪原。」
   ── 真因（★不是 Victory CSS 的問題★）──
     場景背景畫在 .cave-m3-root::before，而 .cave-m3-root 只是宿主裡
     flex:1 的那一段；它外面的 .cave-m3 自己帶一層寫死的
     radial-gradient(#1d3a45 → #0b141b)＝深藍洞窟色（cave-scene.js）。
     標題列背後、以及結算 overlay（fixed inset:0 蓋滿 viewport）背後，
     看到的都是那一層 ⇒ 同一張畫面出現兩個區域。
   ── 修法 ──
     M3Scenes.apply() 現在也把 .m3-host-<theme> 標到宿主外殼上，
     ★這裡只為夢莓花園定義外殼背景★ —— 用的就是同一張 --m3-scene-img，
     不是另外做一張假背景、更不是用黑色遮罩把問題蓋掉。
     Lv1–150 的 theme 沒有對應規則 ⇒ 外殼一個像素都沒變。
   ── 特異性 ──
     cave-scene.js 的 `#caveScene .cave-m3{background:…}` 是 (1,1,0)，
     所以這裡沿用本檔既有前例（見 .cave-m3-root.m3-scene::before）寫兩份：
     帶 #caveScene 的正式站版本，與不帶前綴的驗收台版本。
   ══════════════════════════════════════════════════════════════════════════ */
#caveScene .cave-m3.m3-host-dreamberry,
.m3-host-dreamberry{
  background-color:#f3dfe6;
  background-image:var(--m3-scene-img);
  background-size:cover;
  background-position:center 38%;
  background-repeat:no-repeat;
}
/* 外殼那一張是「整頁的夢莓花園」，不該和棋盤後面那一張一樣搶焦點：
   ★同一張圖、往後退一階★（略降亮度與飽和），讓中央的 gameplay 仍是主角。 */
#caveScene .cave-m3.m3-host-dreamberry::before,
.m3-host-dreamberry::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(86% 62% at 50% 40%, rgba(255,242,246,0) 0%, rgba(120,66,94,.10) 72%, rgba(104,54,80,.20) 100%),
    linear-gradient(180deg, rgba(255,246,248,.10) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 72%, rgba(112,58,86,.14) 100%);
}
/* 標題列與遊戲欄一律在外殼背景之上（原本 .cave-m3 是 flex 容器、子元素沒有
   stacking context，補一層 z-index 就好，★不動任何幾何★）。 */
#caveScene .cave-m3.m3-host-dreamberry > *,
.m3-host-dreamberry > *{ position:relative; z-index:1; }
/* .cave-m3 本來就是 position:absolute（★不可覆寫★）；
   驗收台那種「外殼就是頁面容器」的情況才需要補定位脈絡給 ::before。 */
.m3-host-dreamberry:not(.cave-m3){ position:relative; }
/* ★整頁只鋪一張圖★：外殼已經把夢莓花園鋪滿整個可視範圍，
   gameplay viewport 若再鋪一次同一張圖，兩次 cover 的裁切位置不同
   ⇒ 棋盤下緣會出現一條明顯的接縫（實機截圖抓到）。
   這裡把 gameplay 那一層的底圖與回退色收掉，讓同一張圖連續貫穿整個畫面。
   ‧ 回退色改由外殼負責（.m3-host-dreamberry 的 background-color）⇒
     圖 404 時仍然是粉色花園，不會變黑，fail-safe 沒有退化。
   ‧ 棋盤背後的柔和暗化（::after 的 halo）★保留★，棋盤仍然浮在場景上。 */
#caveScene .cave-m3.m3-host-dreamberry .cave-m3-root.m3-scene::before,
.m3-host-dreamberry .cave-m3-root.m3-scene::before{
  background-image:none; background-color:transparent;
}
#caveScene .cave-m3.m3-host-dreamberry .cave-m3-root.m3-scene-dreamberry,
.m3-host-dreamberry .cave-m3-root.m3-scene-dreamberry{ background-color:transparent; }

/* 夢莓區的標題列不要再是洞窟的深藍玻璃：換成同色系的暖莓深色。
   ★只在夢莓外殼底下生效★ ⇒ 其他區域的標題列一個字沒改。 */
#caveScene .cave-m3.m3-host-dreamberry .cave-m3-bar,
.m3-host-dreamberry .cave-m3-bar{
  background:rgba(58,28,40,.88);
  border-bottom:1px solid rgba(246,214,224,.42);
}

/* ══ 🫐 夢莓花園美術組（ART SET）的可視中心對齊 ══════════════════════════
   ★上面那張對齊表是「預設美術組」的★：--vc-x/--vc-y 是★逐張素材量出來的★，
   換一張圖就是換一組數字，不可以沿用。
   新版草莓 piece-db-strawberry.webp 的構圖和舊草莓完全不同
   （舊的留白多、綠蒂把質心往左上拉到 -7.49%/-5.81%；新的幾乎正置中），
   沿用舊數字會讓拖珠教學的選取圈明顯偏一邊。
   ★數字來源 tools/gen-lv151-art-assets.py 的量測輸出，不是手調的。★
   ★作用範圍只有 data-artset 的棋盤★ ⇒ Lv1–150 讀不到這條規則。 */
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="strawberry"]{
  --vc-x:-0.15%; --vc-y:-0.70%;
}
/* 🐾 v0.61.28c 四顆正式棋子（熊掌／貓爪／鳥嘴／龜殼）的可視中心
   ★全部由 tools/gen-lv151-art-assets.py 量測輸出，不是手調的★
   四顆的質心本來就都在畫布中心 ±1.1% 內（母檔構圖很乾淨），
   這裡只是把那一點點殘差補掉，讓拖珠教學的亮框真的套在棋子正中央。 */
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="bearpaw"]{ --vc-x:-0.28%; --vc-y:0.34%; }
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="catpaw"]{ --vc-x:-0.20%; --vc-y:0.92%; }
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="beak"]{ --vc-x:-0.19%; --vc-y:0.77%; }
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="shell"]{ --vc-x:-0.10%; --vc-y:1.09%; }
/* v0.61.28g 素材庫擴充：紅、藍兩色（同樣由 gen-lv151-art-assets.py 量測輸出）
   瓢蟲殼的母檔主體原本偏下 +2.22%，★已在匯出正式 WebP 時用平移置中★
   （母檔一個位元都沒動，只是平移，不重畫、不縮放），所以這裡只剩 0.13%。 */
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="ladybug"]{ --vc-x:0.13%; --vc-y:-0.13%; }
.m3-board[data-artset="dreamberry"] .m3-piece[data-key="butterfly"]{ --vc-x:-0.16%; --vc-y:-1.36%; }

/* ══ ✂️ v0.61.28c 拖曳中的棋子不得被棋盤上緣切掉 ═════════════════════════
   店長實機：「抓最上排往上拖，浮起的棋子被切掉一角」。
   ★root cause 與修法的完整說明寫在 match3.js 的 dgSetHold()★（那裡才是
   決定何時掛這個 class 的地方，說明放在一起才不會兩邊各寫一半）。
   ‧ .m3-dg-dragging ★只有拖珠關、只有按住的那一刻★掛得上 ⇒ Lv1–150 零影響
   ‧ 只改 overflow 一個屬性：幾何、遮罩、圓角、hitbox、判定一律不動
   ‧ 浮起的棋子本來就有 z-index:8（見 .m3-piece.m3-dg-hold），
     這裡再把 .m3-frame 的金框壓到它下面，避免「跑出盤面卻被框線橫切」 */
.m3-board.m3-dg-dragging{ overflow:visible; }
.m3-board.m3-dg-dragging .m3-frame{ z-index:1; }

/* ══════════════════════════════════════════════════════════════════════════
   🚫 三消 UI 不得被文字框選（UI-SEL contract）
   ──────────────────────────────────────────────────────────────────────────
   店長真頁截圖：滑動棋盤時 Lv.12／本關目標／剩餘步數／探險能量／
   Booster 名稱與數量全部被瀏覽器藍色反白，還會出現文字 caret。
   ★這不是純美觀★ —— 反白會干擾滑動與 booster 選格的操作感。

   ★scope 只鎖三消 gameplay shell★
   不做 html, body 全域 user-select:none —— 專案其他頁面還有表單、
   管理頁、可複製內容，全域鎖死會把那些一起弄壞。
   實際 root（依 repo 真實 class，不是猜的）：
     .m3-wrap        gameplay 容器（HUD ＋ 棋盤 ＋ 特效層）
     .m3-boosterhost Booster bar 的宿主
     .m3-bxfx        道具特效層
     .m3-energy-notice / .m3-result / .m3-booster-how-box  覆蓋層

   ★輸入型元素一律例外★：未來若三消裡出現 input／textarea／contenteditable，
   它們必須仍然可以選取與輸入。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-wrap, .m3-wrap *,
.m3-boosterhost, .m3-boosterhost *,
.m3-bxfx, .m3-bxfx *,
.m3-energy-notice, .m3-energy-notice *,
.m3-result, .m3-result *,
.m3-booster-how-box, .m3-booster-how-box *,
/* ★冒險收藏冊★（v0.60.792）：它是掛在 body 底下的獨立覆蓋層，
   不在 .m3-wrap 裡面 ⇒ 原本這一整段完全沒有涵蓋到它，
   風景／寶物／徽章大圖可以被框選、長按跳「拷貝圖片」。
   右鍵與拖圖那一半由 app.js 的 __tgxcGuard 攔（show() 建立時呼叫）。 */
.m3c-overlay, .m3c-overlay *{
  -webkit-user-select: none;
  user-select: none;
  /* iOS Safari：長按不要跳出「複製／查詢」與文字選取把手 */
  -webkit-touch-callout: none;
}
/* ★例外：輸入型元素不得被鎖死★ */
.m3-wrap input, .m3-wrap textarea, .m3-wrap [contenteditable="true"],
.m3-boosterhost input, .m3-boosterhost textarea, .m3-boosterhost [contenteditable="true"]{
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
/* 棋子與道具圖不得被瀏覽器當成圖片拖走（ghost drag image 會蓋住棋盤） */
.m3-wrap img, .m3-boosterhost img, .m3-bxfx img{
  -webkit-user-drag: none;
  user-drag: none;
  pointer-events: none;      /* 手勢一律由容器處理，圖片本身不吃事件 */
}

/* ══════════════════════════════════════════════════════════════════════════
   ⬛ 透明棋盤之後的視覺層級（TERRAIN-VIS / SEAL-VIS contract）
   ──────────────────────────────────────────────────────────────────────────
   ★店長真頁 Lv11 判 FAIL 的問題★
     背景換成漂亮森林之後，背景本身就有大量石頭／樹木／陰影／地景紋理，
     岩石禁區的色相與它太接近 ⇒ 讀起來像「背景的一部分」，
     而不是「棋盤上不能走的地方」。

   ★修的是 terrain 的前景層級，不是 board opacity★
     ★★禁止用「把 cell 加深」或「棋盤變回不透明」來讓岩石變明顯。★★
     目標是四層同時成立：
       背景「哇好漂亮」→ 棋子「我能消什麼」→ 岩石「這裡走不通」→ 刻印「這裡有秘密」

   ★跨 Theme 都要成立★：森林、海岸、雪原、星空、水晶洞穴…
   背景明暗差很大，所以剪影同時用★暗描邊＋亮描邊★兩層，
   在亮背景靠暗邊、在暗背景靠亮邊，不會有哪一種 Theme 整個消失。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-terrain .m3-tc{
  /* ★第一版做錯的方向★：靠「加黑影＋加厚度＋提高對比」求辨識度 ——
     結果每一格各自吃一次 drop-shadow，相鄰格疊起來變成一圈黑色光暈，
     整片禁區讀成「從棋盤上豎起來的黑色岩柱」，
     比棋子、目標、背景都重，把透明棋盤的輕盈感整個毀掉。

     ★正確方向★：禁區是「自然融入棋盤的低矮地形」，不是障礙物。
     所以這裡★不做黑影、不做厚度、不放大★，只留：
       ① 極細的貼身描邊（一暗一亮）讓外輪廓在亮／暗背景都切得出來
       ② 非常輕的落地感（1px，不是 5~7px 的大陰影）
     其餘辨識度交給「blocked 區域整體壓暗一點」＋ 岩石材質本身。 */
  filter:
    drop-shadow(0 0 .5px rgba(28, 22, 12, .55))
    drop-shadow(0 1px 1px rgba(28, 22, 12, .30))
    drop-shadow(0 0 1.5px rgba(255, 250, 238, .28));
}
/* ★相鄰 blocked cells 必須讀成一整片自然岩盤★
   壓暗放在★整個地形層★（一次），不是每一格各壓一次 ——
   每格各壓就會在格線上留下深淺接縫，那正是「一塊一塊拼貼」的來源。 */
.m3-terrain{
  filter: brightness(.94) saturate(.98);
}
/* 水晶洞穴是冷色場景，描邊跟著換色，避免暖褐髒邊 */
.m3-hidden-stage .m3-terrain .m3-tc{
  filter:
    drop-shadow(0 0 .5px rgba(6, 16, 28, .55))
    drop-shadow(0 1px 1px rgba(6, 16, 28, .30))
    drop-shadow(0 0 1.5px rgba(220, 240, 255, .30));
}

/* 🔮 刻印（relic）：★要一眼看出「這個東西不一樣」★
   不是廉價大箭頭，而是很柔和的暖金 halo ＋ 數秒一次的極輕呼吸。
   ★不改 cell geometry、不遮旁邊棋子★（halo 畫在自己格內，pointer-events:none）。 */
.m3-piece.m3-relic{
  filter:
    drop-shadow(0 0 5px rgba(255, 228, 158, .85))
    drop-shadow(0 0 13px rgba(255, 205, 120, .45))
    drop-shadow(0 3px 5px rgba(20, 14, 4, .5));
  animation: m3RelicBreath 4.2s ease-in-out infinite;
}
@keyframes m3RelicBreath{
  0%, 72%, 100%{ filter:
    drop-shadow(0 0 5px rgba(255, 228, 158, .78))
    drop-shadow(0 0 13px rgba(255, 205, 120, .40))
    drop-shadow(0 3px 5px rgba(20, 14, 4, .5)); }
  86%{ filter:
    drop-shadow(0 0 9px rgba(255, 240, 196, 1))
    drop-shadow(0 0 20px rgba(255, 214, 140, .62))
    drop-shadow(0 3px 5px rgba(20, 14, 4, .5)); }
}
.m3-reduced .m3-piece.m3-relic{ animation:none; }
@media (prefers-reduced-motion:reduce){ .m3-piece.m3-relic{ animation:none; } }

/* ══════════════════════════════════════════════════════════════════════════
   📖 冒險收藏冊（COLLECTION）
   ──────────────────────────────────────────────────────────────────────────
   ★入口不得搶走五枚刻印的主要功能★：一顆小按鈕，掛在既有頁面尾端，
   不重新設計那一頁、不擠壓手機 safe-area。
   ★打開後是一本收藏冊，不是 checklist★：漂亮的卡片、剪影、???。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-entry{
  display:inline-flex; align-items:center; gap:6px;
  margin:10px auto 0; padding:9px 16px;
  font:600 14px/1 inherit; color:#f4ecd7; white-space:nowrap;
  background:linear-gradient(180deg, rgba(74,63,48,.94), rgba(52,44,34,.94));
  border:1px solid rgba(226,192,122,.46); border-radius:14px;
  box-shadow:0 4px 14px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,240,205,.14);
  cursor:pointer;
}
.m3c-entry b{ color:#ffd98a; font-variant-numeric:tabular-nums; }
.m3c-entry .m3c-new{
  font-style:normal; font-size:10px; font-weight:800; letter-spacing:.5px;
  color:#1d2b1f; background:#8fe0a8; border-radius:999px; padding:2px 6px;
}

.m3c-overlay{
  /* ★z-index 必須壓過 #caveScene 的 1400★
     這就是店長回報「按了沒出現」的真因 —— 按鈕有反應、面板卻被山洞畫面蓋住。
     ★不是按鈕壞掉，是堆疊層級★（和星星魔杖那次同一類問題）。 */
  position:fixed; inset:0; z-index:1500; display:flex; align-items:center; justify-content:center;
  background:rgba(6,10,16,.68); padding:16px;
  padding-top:max(16px, env(safe-area-inset-top));
  padding-bottom:max(16px, env(safe-area-inset-bottom));
}
.m3c-overlay[hidden]{ display:none; }
.m3c-panel{
  width:min(560px, 100%); max-height:100%; display:flex; flex-direction:column;
  background:linear-gradient(180deg, #1d2735, #141c27);
  border:1px solid rgba(226,192,122,.34); border-radius:18px; overflow:hidden;
  box-shadow:0 18px 44px rgba(0,0,0,.5);
}
.m3c-head{
  flex:0 0 auto; display:flex; align-items:center; gap:10px; padding:14px 16px;
  border-bottom:1px solid rgba(226,192,122,.2);
}
.m3c-title{ font-size:17px; font-weight:800; color:#f4ecd7; }
.m3c-total{ margin-left:auto; color:#ffd98a; font-weight:800; font-variant-numeric:tabular-nums; }
.m3c-close{
  width:32px; height:32px; border-radius:10px; cursor:pointer;
  background:rgba(12,20,34,.7); border:1px solid rgba(205,160,76,.5); color:#e7d3a0;
}
.m3c-body{ flex:1 1 auto; overflow:auto; padding:12px 16px 18px; -webkit-overflow-scrolling:touch; }
.m3c-sec{ margin-bottom:18px; }
.m3c-sechead{
  display:flex; align-items:baseline; gap:8px; margin:0 0 9px;
  font-size:14px; font-weight:700; color:#cfe0ee;
}
.m3c-secn{ margin-left:auto; font-size:12px; color:#8fa6bb; font-variant-numeric:tabular-nums; }
.m3c-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(96px, 1fr)); gap:10px; }
.m3c-card{ text-align:center; opacity:.55; }
.m3c-card.owned{ opacity:1; }
.m3c-art{
  position:relative; aspect-ratio:3/4; border-radius:12px;
  background:#0e1a26 center/cover no-repeat;
  border:1px solid rgba(226,192,122,.22);
  display:flex; align-items:center; justify-content:center;
}
.m3c-card.owned .m3c-art{ border-color:rgba(255,217,138,.5); box-shadow:0 0 12px rgba(255,205,120,.18); }

/* ══════════════════════════════════════════════════════════════════════════
   🏆 成就徽章：稀有度外框 ＋ 未取得的處理
   ──────────────────────────────────────────────────────────────────────────
   ★1 achievement = 1 artwork★（店長定案）
     未取得★不做第二張灰圖★ —— 同一張正式美術，這裡用 CSS 降彩度／降亮度；
     隱藏成就再多一層剪影，解開那一刻才第一次看到完整徽章。
     ⇒ 資產不會倍增，也不會出現「灰圖忘了跟著更新」這種漂移。
   ★稀有度視覺成長★（不寫 SSR 這種遊戲化文字）
     T1 簡潔暖金邊 → T2 多一圈植物星光 → T3 完整華麗外框（隱藏成就）
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-art.is-badgeart{
  background-size:contain; background-position:center; background-repeat:no-repeat;
  background-origin:content-box; background-clip:content-box;
  box-sizing:border-box; padding:9%;
}
/* 外框：純 CSS，跟著 tier 長大。美術本身是透明背景，所以外框不會蓋到圖 */
.m3c-art.t2::after, .m3c-art.t3::after{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
}
.m3c-art.t2::after{
  box-shadow:inset 0 0 0 1px rgba(150,210,160,.34), inset 0 0 14px rgba(150,210,160,.14);
}
.m3c-art.t3::after{
  box-shadow:inset 0 0 0 1.5px rgba(255,217,138,.62), inset 0 0 20px rgba(255,205,120,.24);
  background:radial-gradient(60% 40% at 50% 8%, rgba(255,232,180,.16), rgba(255,232,180,0) 70%);
}
/* 未取得：同一張資產，只是安靜下來 */
.m3c-art.is-dim{ filter:grayscale(.86) brightness(.62) contrast(.95); }
.m3c-art.is-dim .m3c-emblem{ opacity:.85; }
/* 隱藏成就未取得：只給神祕剪影 —— 解開才第一次看到完整徽章 */
/* 剪影要「神祕」不是「一團黑」：保留輪廓與一點冷色調，看得出形狀、看不出細節 */
.m3c-art.is-silhouette{
  filter:grayscale(1) brightness(.30) contrast(1.15)
         drop-shadow(0 0 6px rgba(150,190,255,.22));
}
.m3c-art.is-silhouette .m3c-emblem-g{ opacity:.9; }
.m3c-art.is-silhouette::before{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:radial-gradient(58% 46% at 50% 44%, rgba(150,190,255,.10), rgba(0,0,0,0) 72%);
}

/* 結算畫面上「不能前進」的誠實說明（v0.60.792 P0）
   ★不是 toast、不是彈窗★：就在按鈕上方，玩家一定看得到，且不打斷結算節奏。 */
.m3-rblocked{
  margin:10px auto 2px; max-width:min(86%, 320px);
  padding:9px 14px; border-radius:12px; text-align:center;
  font-size:13px; line-height:1.6; color:#ffdcc2;
  background:rgba(120,70,40,.30); border:1px solid rgba(220,150,110,.42);
}
.m3-rbtn[disabled]{ opacity:.45; cursor:default; filter:saturate(.6); }

/* ══════════════════════════════════════════════════════════════════════════
   🌿 旅途足跡（明信片下方）
   ──────────────────────────────────────────────────────────────────────────
   店長：不要做成資料表，要像一本旅行相簿。
   平常只有兩行（第一次來到這裡／已留下 N 次足跡），點「查看足跡」才展開每一天。
   ★沒有足跡就整段不出現★ —— 不留空殼、不假裝記得。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-trail:empty{ display:none; }
.m3c-trail{ margin-top:10px; padding-top:10px; border-top:1px solid rgba(226,192,122,.18); }
.m3c-trailtop{ font-size:13px; color:#e6d6ae; letter-spacing:.2px; }
.m3c-trailn{
  margin-top:5px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  font-size:12.5px; color:#a9c1d4;
}
.m3c-trailmore{
  -webkit-appearance:none; appearance:none; cursor:pointer;
  min-height:30px; padding:0 12px; border-radius:999px;
  border:1px solid rgba(226,192,122,.30); background:rgba(20,30,44,.8);
  color:#ffe6a6; font:inherit; font-size:12px; font-weight:700;
}
.m3c-traillist{
  list-style:none; margin:10px 0 0; padding:0;
  display:flex; flex-direction:column; gap:6px;
}
/* ★作者的 display:flex 會蓋過瀏覽器內建的 [hidden]{display:none}★
   ⇒ 少了這一條，「查看足跡」按了等於沒作用，清單從頭到尾都是展開的。 */
.m3c-traillist[hidden]{ display:none; }
.m3c-traillist li{
  display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  font-size:12.5px; color:#cbd9e4;
  padding:6px 10px; border-radius:9px; background:rgba(18,28,42,.6);
}
.m3c-traillist em{ font-style:normal; font-size:11px; color:#8fa6b8; }

/* ══════════════════════════════════════════════════════════════════════════
   🏆 冒險排行榜（收藏冊內的第二個分頁）
   ──────────────────────────────────────────────────────────────────────────
   ★世界觀優先★（店長第六條）：這是療癒／收藏／旅行感的遊戲，
   排行榜★不得★出現紅色、PK、戰力、勝敗字樣。沿用收藏冊既有的深色卡片＋暖金。
   ★手機優先★：只有 Top 10，前三名稍微亮一點，#4～#10 是精簡單列，
   最後才是「我的排名」。不做巨大 podium、不做無限捲動、不做 Load More。
   ★50+ 客群★：名次與數值字級不小於 13px，整列可點擊區高度 ≥ 44px。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-tabs{
  display:flex; gap:8px;
  /* ★店長 v0.60.794 真頁回報「切齊上面格線了」★
     原本 padding-top 是 0 ⇒ 分頁膠囊直接貼著 .m3c-head 的底線，
     兩條線黏在一起，標題「冒險收藏冊」看起來像被下一排壓住。
     真頁 390px 量到 head.bottom 與 tabs.top ★完全相等（間距 0）★。
     ⇒ 補回上方留白，讓分頁列與標題那一列各自站穩。 */
  padding:10px 14px 10px;
  border-bottom:1px solid rgba(226,192,122,.16);
}
.m3c-tab{
  -webkit-appearance:none; appearance:none; cursor:pointer;
  flex:0 0 auto; min-height:38px; padding:0 18px;
  border-radius:999px; border:1px solid rgba(226,192,122,.30);
  background:linear-gradient(180deg, rgba(38,52,74,.85), rgba(24,34,52,.9));
  color:#cbb98a; font:inherit; font-size:14px; font-weight:700;
  transition:color .16s ease, border-color .16s ease, background .16s ease;
}
.m3c-tab.on{
  color:#10233a; border-color:rgba(120,88,24,.5);
  background:linear-gradient(180deg,#ffe9b0,#f5cf7d);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6), 0 2px 8px rgba(0,0,0,.35);
}
/* 四個榜的切換：比分頁再小一級，避免兩排按鈕搶戲 */
.m3lb-picks{ display:flex; flex-wrap:wrap; gap:6px; margin:2px 0 12px; }
.m3lb-pick{
  -webkit-appearance:none; appearance:none; cursor:pointer;
  min-height:34px; padding:0 12px; border-radius:999px;
  border:1px solid rgba(226,192,122,.22);
  background:rgba(18,28,42,.75); color:#a9c1d4;
  font:inherit; font-size:13px; font-weight:700;
}
.m3lb-pick.on{ color:#ffe6a6; border-color:rgba(255,217,138,.55); background:rgba(46,62,86,.9); }

.m3lb-list{ display:flex; flex-direction:column; gap:6px; }
.m3lb-row{
  display:flex; align-items:center; gap:10px;
  min-height:44px; padding:6px 12px;          /* 44px：手指與 50+ 客群都好按 */
  border-radius:12px;
  border:1px solid rgba(226,192,122,.16);
  background:linear-gradient(180deg, rgba(30,42,60,.72), rgba(20,29,44,.78));
}
/* 前三名：只用暖金光暈，★不做大 podium★，不吃掉手機高度 */
.m3lb-row.is-top{
  border-color:rgba(255,217,138,.42);
  background:linear-gradient(180deg, rgba(52,45,30,.85), rgba(28,26,22,.86));
  box-shadow:0 0 12px rgba(255,205,120,.14);
}
.m3lb-row.is-me{
  border-color:rgba(150,220,255,.55);
  box-shadow:0 0 12px rgba(120,200,255,.18);
}
.m3lb-rank{
  flex:0 0 auto; min-width:38px; text-align:center;
  font-size:13px; font-weight:800; color:#9fb6c9; font-variant-numeric:tabular-nums;
}
.m3lb-rank.has-medal{ min-width:38px; }
.m3lb-medal{ font-style:normal; font-size:22px; line-height:1; }
.m3lb-who{ flex:1 1 auto; min-width:0; display:flex; align-items:center; gap:6px; }
.m3lb-name{
  font-size:14.5px; font-weight:700; color:#f2e3c4;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* ★v0.60.792：居民標記與第二名稱整個移除★（店長真頁回報）
   原本會顯示「居民」標籤＋居民專屬的寵物名，玩家一眼就分得出誰是真人、誰是仿真居民，
   世界觀與生命感就沒了。
   ★規則不是「把兩個字藏起來」★ —— 是玩家從排行榜 UI ★不應能判斷資料來源★，
   所以標籤、第二名稱、專屬顏色、專屬版型一律不存在，
   真人與居民走完全相同的一列。（server 端也不再回傳 src／petType／petName。） */
.m3lb-val{
  flex:0 0 auto; font-size:14px; font-weight:800; color:#ffe6a6;
  font-variant-numeric:tabular-nums;
}
/* 「我的排名」：與 Top 10 之間拉一條分隔線，玩家一眼找得到自己 */
.m3lb-mine{ margin-top:12px; padding-top:12px; border-top:1px dashed rgba(226,192,122,.28); }
.m3lb-minehead{ font-size:12px; color:#a9c1d4; margin-bottom:6px; letter-spacing:.5px; }
.m3lb-note{
  padding:26px 16px; text-align:center; line-height:1.8;
  font-size:13.5px; color:#9fb6c9;
}
.m3lb-await{ color:#cbb98a; }
.m3lb-foot{
  margin-top:14px; text-align:center; font-size:11.5px; color:#7f97a9;
}
@media (max-width: 400px){
  .m3lb-name{ font-size:13.5px; }
  .m3lb-val{ font-size:13px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   🎒 探險寶物：道具本尊要「擺進」框裡，不是「填滿」框
   ──────────────────────────────────────────────────────────────────────────
   ★店長回報★「探險寶物．道具太大．看起來不漂亮有點失真」。
   ★root cause★ 道具原圖是 256×256 的正方形，卡片框卻是 3:4，
     原本吃 .m3c-art 的 center/cover ⇒ ①左右被裁掉 ②為了填滿高度被放大超過 256px
     ⇒ 又胖又糊。這不是美術畫壞，是 cover 用錯地方。
   ★修法★ 改 contain ＋ 用 padding 把可用區縮到原圖尺寸以內（永不放大 ⇒ 永遠銳利），
     背後補一圈很淡的圓形底盤，讓道具看起來是「放在展示座上」而不是浮空。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-art.is-item, .m3c-pcart.is-item{
  box-sizing:border-box;
  background-size:contain;
  background-origin:content-box;
  background-clip:content-box;
}
.m3c-art.is-item{ padding:13%; }     /* 小卡 72–96px ⇒ 內容區 ≦ 74px，遠小於 256 原尺寸 */
.m3c-pcart.is-item{ padding:20%; }   /* 明信片最寬 360px ⇒ 內容區 ≦ 216px，仍在原圖 256px 以內 */

/* ══════════════════════════════════════════════════════════════════════════
   🖼 收藏品大圖：★不要有方形底★（店長 v0.60.792 真頁回報）
   ──────────────────────────────────────────────────────────────────────────
   ★root cause★（實查，不是猜的）
     ‧ 資產本身是透明的：booster-*.webp 與 badge-*.webp 的 alpha 範圍 0–255、
       四角 alpha = 0 ⇒ ★黑底不是烤進圖裡的★
     ‧ 黑底來自 .m3c-pcart 的 `background:#0e1a26`（給風景大圖用的底色）
       再加上 .is-item::before 的展示座漸層
     ⇒ 所以修法是「這兩類不要那層底」，★不是把黑色換成深藍色★。

   ★徽章被裁切的 root cause★
     .m3c-pcart.is-badgeart ★根本沒有任何規則★，
     於是落回 .m3c-pcart 的 aspect-ratio:3/4 ＋ background-size:cover
     ⇒ 512×512 的正方形徽章被 3:4 的框裁掉左右，金色外框整圈不見。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-pcart.is-item, .m3c-pcart.is-badgeart{
  background-color:transparent;          /* ← 方形底就是這裡來的 */
  aspect-ratio:1/1;                      /* 正方形資產不要塞進 3:4 的框 */
  border-bottom:0;
  background-size:contain;               /* ★contain★：完整顯示，永不裁切 */
  background-position:center;
  background-repeat:no-repeat;
  background-origin:content-box;
  background-clip:content-box;
  box-sizing:border-box;
}
/* 徽章大圖：★左右上下都要留安全邊★，金色外框不得碰到裁切邊界 */
.m3c-pcart.is-badgeart{ padding:9%; }
/* 桌機不要因為空間大就把徽章無限放大 */
.m3c-pcart.is-item, .m3c-pcart.is-badgeart{ max-height:min(62vh, 420px); }
/* 展示座：純漸層，0 素材、0 404 風險
   ★只給小卡（.m3c-art）★——大圖（.m3c-pcart）不得有展示座。
   ★這裡踩過一次坑，記錄下來★：原本我是在上面補一條
   `.m3c-pcart.is-item::before{ content:none; }` 想把它關掉，
   但那條規則和這裡的選擇器★特異度相同（0,2,1）★，而這裡寫在後面 ⇒ 後者勝，
   展示座其實★沒有被關掉★（真頁量到 ::before content 仍是 ""）。
   ⇒ 正確做法是把 .m3c-pcart 從這條規則的選擇器裡拿掉，
     不是再疊一層更高特異度去打架。 */
.m3c-art.is-item::before{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  background:
    radial-gradient(52% 40% at 50% 46%, rgba(255,226,166,.16), rgba(255,226,166,0) 70%),
    radial-gradient(46% 12% at 50% 82%, rgba(0,0,0,.42), rgba(0,0,0,0) 75%);
}
.m3c-pcart{ position:relative; }

/* ══════════════════════════════════════════════════════════════════════════
   🏆 榮耀徽章／奇遇珍藏：向量徽記
   ──────────────────────────────────────────────────────────────────────────
   ★店長回報★「榮譽徽章沒圖片」—— 這幾張卡本來就沒有點陣圖素材，
   artFor() 回 null，卡片就是一片空的深色框。
   ★為什麼不叫美術★ 這個專案已出過四次「素材沒進版控 ⇒ 正式站 404」；
   徽記是漸層＋一個字符，★沒有檔案就不可能 404★，而且任何尺寸都銳利。
   顏色由 JS 帶 --em-a／--em-b 進來，每一枚徽章各有自己的金／銀／冰／紫色調。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-art.is-emblem, .m3c-pcart.is-emblem{
  display:flex; align-items:center; justify-content:center; position:relative;
  background:
    radial-gradient(70% 55% at 50% 34%, rgba(255,240,200,.10), rgba(255,240,200,0) 70%),
    linear-gradient(180deg, #16232f 0%, #0d1620 100%);
}
.m3c-emblem{
  position:relative; display:flex; align-items:center; justify-content:center;
  width:58%; aspect-ratio:1/1; border-radius:50%;
  background:
    radial-gradient(60% 60% at 38% 30%, rgba(255,255,255,.55), rgba(255,255,255,0) 62%),
    linear-gradient(160deg, var(--em-a, #ffe1a6) 0%, var(--em-b, #a97a32) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),
    inset 0 -3px 8px rgba(0,0,0,.30),
    0 3px 10px rgba(0,0,0,.42);
}
/* 外圈：像獎牌的金屬齒邊。★用徽章自己的色調★（不是灰白），
   否則深色卡片上會變成一圈灰環，看起來像沒上色的佔位圖。 */
.m3c-emblem::before{
  content:""; position:absolute; inset:-9%; border-radius:50%;
  background:conic-gradient(from 210deg,
    var(--em-a, #ffe1a6), var(--em-b, #a97a32) 18%, var(--em-a, #ffe1a6) 34%,
    var(--em-b, #a97a32) 52%, var(--em-a, #ffe1a6) 68%, var(--em-b, #a97a32) 86%,
    var(--em-a, #ffe1a6));
  -webkit-mask:radial-gradient(circle, transparent 63%, #000 65%);
          mask:radial-gradient(circle, transparent 63%, #000 65%);
  filter:brightness(.86) saturate(.92);
  box-shadow:0 4px 14px rgba(0,0,0,.45);
}
.m3c-emblem-g{
  font-style:normal; line-height:1; font-size:min(46px, 46cqw);
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.45));
}
.m3c-art.is-emblem, .m3c-pcart.is-emblem{ container-type:inline-size; }
/* 沒有 container query 的瀏覽器 ⇒ 退回相對於卡片寬度的固定級距，不會變成 16px 小字 */
@supports not (font-size: 1cqw){
  .m3c-art.is-emblem .m3c-emblem-g{ font-size:30px; }
  .m3c-pcart.is-emblem .m3c-emblem-g{ font-size:64px; }
}
/* 徽章明信片★不要用 3:4★ —— 一枚圓徽章放在直式相框裡，上下會空一大片。
   改成正方形，徽章佔滿視覺重心。 */
.m3c-pcart.is-emblem{ aspect-ratio:1/1; }
.m3c-pcart.is-emblem .m3c-emblem{ width:58%; }
.m3c-pcart.is-emblem .m3c-emblem-g{ font-size:min(88px, 32cqw); }
/* 寶物明信片同理：一支道具不需要直式相框 */
.m3c-pcart.is-item{ aspect-ratio:1/1; }

/* ══════════════════════════════════════════════════════════════════════════
   🃏 小卡區（探險寶物／榮耀徽章）
   ★店長回報★「探險寶物跟榮譽徽章可以是小卡片…太大了，沒填滿反而難看」。
   風景是一幅畫，值得 3:4 的大相框；道具與徽章是一個物件，
   放進同樣大的框裡只會四周空一大片。這一區改正方形小卡，密度也高一點。
   ══════════════════════════════════════════════════════════════════════════ */
.m3c-grid.is-small{ grid-template-columns:repeat(auto-fill, minmax(72px, 1fr)); gap:8px; }
.m3c-grid.is-small .m3c-art{ aspect-ratio:1/1; border-radius:10px; }
.m3c-grid.is-small .m3c-name{ font-size:11.5px; margin-top:4px; }
.m3c-grid.is-small .m3c-sub{ font-size:10.5px; }
.m3c-grid.is-small .m3c-emblem{ width:70%; }
.m3c-grid.is-small .m3c-emblem-g{ font-size:min(34px, 42cqw); }
@supports not (font-size: 1cqw){
  .m3c-grid.is-small .m3c-emblem-g{ font-size:24px; }
}
/* 未取得：剪影／鎖定 —— ★秘密奇遇連名字都不給★，製造收集慾 */
.m3c-lock{ font-size:22px; color:#5a7086; font-weight:800; }
.m3c-name{ margin-top:6px; font-size:12px; font-weight:700; color:#e8eef6; }
.m3c-sub{ font-size:10.5px; color:#93a8bd; font-variant-numeric:tabular-nums; }

/* 🖼 明信片大圖（店長：像寵物旅行的明信片） */
.m3c-postcard{
  position:fixed; inset:0; z-index:1510;   /* 明信片再壓過收藏冊本身 */ display:flex; align-items:center; justify-content:center;
  background:rgba(4,8,14,.78); padding:18px;
  padding-top:max(18px, env(safe-area-inset-top));
  padding-bottom:max(18px, env(safe-area-inset-bottom));
}
.m3c-pc{
  position:relative; width:min(420px,100%); max-height:100%; overflow:auto;
  background:linear-gradient(180deg,#22303f,#161f2b);
  border:1px solid rgba(255,217,138,.4); border-radius:20px;
  box-shadow:0 22px 54px rgba(0,0,0,.6);
}
.m3c-pcart{
  aspect-ratio:3/4; background:#0e1a26 center/cover no-repeat;
  border-bottom:1px solid rgba(255,217,138,.26);
  border-radius:20px 20px 0 0;
}
.m3c-pcbody{ padding:14px 18px 20px; }
.m3c-pcname{ font-size:19px; font-weight:800; color:#ffd98a; margin-bottom:8px; }
.m3c-pcmeta{ font-size:13px; line-height:1.9; color:#cfe0ee; font-variant-numeric:tabular-nums; }
.m3c-pcstory{
  margin-top:12px; padding-top:12px; font-size:13px; line-height:1.8;
  color:#9fb2c6; font-style:italic; border-top:1px solid rgba(226,192,122,.18);
}
.m3c-pcclose{
  position:absolute; right:10px; top:10px; width:34px; height:34px; border-radius:12px;
  background:rgba(8,14,22,.72); border:1px solid rgba(205,160,76,.5); color:#e7d3a0; cursor:pointer;
}
.m3c-card[data-key]{ cursor:pointer; }

/* 📊 我的紀錄 */
.m3c-memo{
  background:rgba(10,18,28,.5); border:1px solid rgba(226,192,122,.18);
  border-radius:12px; padding:10px 14px;
}
.m3c-memorow{
  display:flex; align-items:baseline; gap:10px; padding:5px 0;
  font-size:13px; color:#9fb2c6;
}
.m3c-memorow + .m3c-memorow{ border-top:1px solid rgba(226,192,122,.10); }
.m3c-memorow b{ margin-left:auto; color:#ffe9b0; font-variant-numeric:tabular-nums; }

/* ══════════════════════════════════════════════════════════════════════════
   🗺 關卡足跡（v0.60.796）
   ──────────────────────────────────────────────────────────────────────────
   ★依每 10 關收合★：手機不要一次拉出 50 列。區段標題自己是一顆按鈕，
   點擊區可點面積 ≥ 44px（50+ 客群）。
   ★不做進度條、不做百分比★ —— 玩家要的是「哪幾關還沒補到三星」。
   ══════════════════════════════════════════════════════════════════════════ */
.m3fp-msg{
  margin:0 0 10px; padding:10px 12px; border-radius:12px;
  background:rgba(255,214,138,.12); border:1px solid rgba(226,192,122,.3);
  color:#ffd98a; font-size:13px; line-height:1.5;
}
.m3fp-seg{ margin-bottom:10px; }
.m3fp-seghead{
  -webkit-appearance:none; appearance:none; cursor:pointer;
  width:100%; min-height:44px; display:flex; align-items:center; gap:10px;
  padding:0 14px; border-radius:12px; font:inherit;
  border:1px solid rgba(226,192,122,.24); color:#e7d3a0;
  background:linear-gradient(180deg, rgba(38,52,74,.85), rgba(24,34,52,.9));
}
.m3fp-segname{ font-size:14px; font-weight:800; }
.m3fp-segn{ margin-left:auto; font-size:12.5px; color:#cbb98a; font-variant-numeric:tabular-nums; }
.m3fp-caret{ font-size:11px; color:#8fa6bb; }
.m3fp-list{ margin-top:8px; display:flex; flex-direction:column; gap:8px; }
.m3fp-row{
  display:flex; align-items:center; gap:10px; min-height:44px;
  padding:0 12px; border-radius:12px;
  background:rgba(16,25,38,.72); border:1px solid rgba(226,192,122,.16);
}
.m3fp-lv{ font-size:13.5px; font-weight:800; color:#e7d3a0; min-width:56px; font-variant-numeric:tabular-nums; }
.m3fp-stars{ font-size:13px; letter-spacing:1px; }
.m3fp-gem{ font-size:13px; opacity:.45; }
.m3fp-gem.on{ opacity:1; }
.m3fp-go{
  -webkit-appearance:none; appearance:none; cursor:pointer;
  margin-left:auto; min-height:34px; padding:0 14px; border-radius:999px;
  font:inherit; font-size:13px; font-weight:700;
  border:1px solid rgba(226,192,122,.34); color:#10233a;
  background:linear-gradient(180deg,#ffe9b0,#f5cf7d);
}
.m3fp-go[disabled]{ opacity:.55; cursor:default; }

/* 🎁 獲得探險寶物：結算畫面上的獲得演出（v0.60.796）
   ★不是只有庫存數字 +1★ —— 玩家要看得到「這是剛才冒險拿到的」。
   同一局多個獎勵合併在這一區，不連跳 Modal。 */
.m3-reward{
  margin:12px 0 0; padding:12px; border-radius:14px; text-align:center;
  background:linear-gradient(180deg, rgba(255,226,166,.14), rgba(255,226,166,.06));
  border:1px solid rgba(226,192,122,.38);
}
.m3-reward-t{ font-size:14px; font-weight:800; color:#ffd98a; margin-bottom:8px; }
.m3-reward-t.is-new{ color:#fff0c2; }
.m3-reward-items{ display:flex; flex-wrap:wrap; gap:10px; justify-content:center; }
.m3-reward-it{ width:92px; }
.m3-reward-art{
  width:72px; height:72px; margin:0 auto 4px; border-radius:12px;
  background-size:contain; background-position:center; background-repeat:no-repeat;
}
.m3-reward-nm{ font-size:12.5px; font-weight:700; color:#f4ecd7; }
.m3-reward-qty{ font-size:12px; color:#cbb98a; font-variant-numeric:tabular-nums; }
.m3-reward-note{ margin-top:8px; font-size:11.5px; color:#9db3c6; }

/* 📋 結算層「成績 ＋ 寶物」並排（店長 v0.60.798）
   ════════════════════════════════════════════════════════════════════════
   ★問題★：797 把「獲得探險寶物」做成獨立一張卡，直接堆在成績卡下面。
   在手機上（店長 iPhone 實機）整個結算層被撐到超過一個畫面高度，
   「下一關／再玩一次」被擠到最下面，玩家要捲動才按得到。

   ★修法★：成績卡（星星／目標／剩餘步數）在★左★、寶物卡在★右★。
   ‧ is-2col ★只有在真的有寶物時才會被加上★ ⇒ 沒有掉落的局面
     版面與 797 之前逐字相同，不為了少數情況去動每一關都會看到的畫面。
   ‧ 兩欄等寬、可各自縮，min-width:0 讓長字串不會把欄位撐爆。
   ‧ ★極窄機（<340px）自動退回直式★ —— 兩欄擠到看不清楚比捲動更糟。 */
.m3-rmid{ display:flex; justify-content:center; }
.m3-rmid.is-2col{
  align-items:stretch; gap:8px; width:100%;
  padding:0 clamp(6px, 2vw, 14px); box-sizing:border-box;
}
.m3-rmid.is-2col > .m3-rinfo{ flex:1 1 0; min-width:0; padding-left:10px; padding-right:10px; }
.m3-rmid.is-2col > .m3-reward{ flex:1 1 0; min-width:0; margin:8px 0 0; padding:9px 8px; }
/* 並排時寶物卡要瘦身，才不會把成績卡壓扁 */
.m3-rmid.is-2col .m3-reward-it{ width:auto; max-width:96px; }
.m3-rmid.is-2col .m3-reward-art{ width:clamp(44px, 13vw, 64px); height:clamp(44px, 13vw, 64px); }
.m3-rmid.is-2col .m3-reward-t{ font-size:clamp(12px, 3.2vw, 14px); margin-bottom:6px; }
.m3-rmid.is-2col .m3-reward-note{ font-size:clamp(10.5px, 2.8vw, 12px); }
@media (max-width: 339px){
  .m3-rmid.is-2col{ flex-direction:column; align-items:center; }
  .m3-rmid.is-2col > .m3-rinfo,
  .m3-rmid.is-2col > .m3-reward{ flex:0 0 auto; width:100%; }
}

/* ══════════════════════════════════════════════════════════════════════════
   P0-2｜閒置提示（idle hint）與 P0-1｜死局 recovery 面板
   --------------------------------------------------------------------------
   ★提示只是視覺★：不代替玩家交換、不改 board state、不消耗步數。
   要求「明顯但不刺眼」—— 用柔和的暖金呼吸光暈，不用高飽和外框、不閃爍。
   ══════════════════════════════════════════════════════════════════════════ */
/* ★絕對不要在這裡動 position★（v0.60.811，真頁肉眼抓到）
   .m3-piece 是用 position:absolute + transform:translate(x,y) 擺位的。
   第一版寫了 position:relative「好讓 ::after 有定位參考」，
   結果被標記的那兩格直接【掉回靜態排版位置】—— 提示會把方塊移位，
   畫面上看到的是上下相鄰兩格，engine 回的卻是同一列左右相鄰。
   ::after 本來就會以已定位的 .m3-piece 為參考，不需要再宣告 position。 */
.m3-hint{ z-index:12; }
.m3-hint::after{
  content:''; position:absolute; inset:6%;
  border-radius:22%;
  box-shadow:0 0 0 2px rgba(255,214,140,.92), 0 0 14px 4px rgba(255,196,92,.55);
  pointer-events:none;
  animation:m3HintBreath 1.5s ease-in-out infinite;
}
@keyframes m3HintBreath{
  0%,100%{ opacity:.42; transform:scale(.965); }
  50%    { opacity:1;   transform:scale(1.035); }
}
/* 手機上略微加強，但仍不刺眼 */
@media (max-width:480px){
  .m3-hint::after{ box-shadow:0 0 0 2.5px rgba(255,214,140,.95), 0 0 16px 5px rgba(255,196,92,.6); }
}
/* ★尊重系統設定★：關掉動態時只留靜態外框，不做呼吸 */
@media (prefers-reduced-motion:reduce){
  .m3-hint::after{ animation:none; opacity:.9; transform:none; }
}
/* 動畫暫停（切背景／overlay）時提示也跟著停 */
.m3-anim-paused .m3-hint::after{ animation-play-state:paused; }

/* ── 死局真的救不回來時的明確 recovery（P0-1）───────────────────────────── */
.m3-deadlock{
  position:absolute; inset:0; z-index:60; display:flex; align-items:center; justify-content:center;
  background:rgba(18,12,8,.72); backdrop-filter:blur(2px);
}
.m3-deadlock-box{
  max-width:min(86vw,320px); padding:20px 22px; border-radius:16px; text-align:center;
  background:linear-gradient(180deg,#3b2b20,#2a1e16); border:2px solid #a9503f;
  box-shadow:0 10px 30px rgba(0,0,0,.45);
}
.m3-deadlock-t1{ font-size:17px; font-weight:800; color:#ffd9cf; margin:0 0 8px; }
.m3-deadlock-t2{ font-size:13.5px; line-height:1.7; color:#f0dcc8; margin:0 0 16px; }
.m3-deadlock-go{
  width:100%; padding:12px 0; border:0; border-radius:12px; cursor:pointer;
  font-size:15px; font-weight:800; color:#3b2b20;
  background:linear-gradient(180deg,#ffd98f,#e8b45c);
}


/* ══════════════════════════════════════════════════════════════════════════
   ■ HUD 目標列不得壓到「剩餘步數」（v0.61.20-a｜真 Chromium 幾何量測）
   ──────────────────────────────────────────────────────────────────────────
   ★這是本輪第一審自己抓到的缺陷，不是店長回報的六項之一★
   Lv120 是專案第一個「5 目標」關卡。實測（320/360/390/430、真 Chromium）：
       目前 320px：Lv115(3目標) 壓 5px、Lv117/119(4目標) 壓 29px、Lv120(5目標) 壓 56px
                   ★Lv100（既有已出貨關卡，4 目標）也壓 20px★ ⇒ 這不是新關卡才有的洞
       目前 360px：Lv117/119 壓 12px、Lv120 壓 39px
       目前 390px：Lv120 壓 26px　　目前 430px：Lv120 壓 9px
   ★為什麼不是再縮字★：本檔上面「四目標 HUD 正式版面」那一段已經寫過
   「縮到看不清楚不是解法」。5 個目標要塞進 320px 的目標區（184px），
   每顆徽章只剩 32px —— 那等於把數字縮到 50+ 玩家看不見。
   ★作法★：≥4 目標時目標列改成★兩列★（字級、icon 一律不再往下縮），
   桌機（≥900px）空間夠，維持一列。
   極窄機身（≤360px）另外收半階，確保 5 目標是 2 列而不是 3 列，
   以及 3 目標在 320px 不會擦到步數。
   實測結果（同一支量測腳本）：320/360/390/430 × Lv100/111/115/117/119/120
       右壓步數 = 0（全部）　最小字級 12px　最小 icon 15px　HUD 高 70～74px　垂直溢出 0
   常駐把關：tests/browser/m3-hud-fit-gate.js（真 Chromium，資料驅動，含反向變異）
   ══════════════════════════════════════════════════════════════════════════ */
.m3-hud:has(.m3-goal:nth-child(4)) .m3-goals{ flex-wrap:wrap !important; row-gap:2px; justify-content:center; }
.m3-hud:has(.m3-goal:nth-child(4)) .m3-goals .m3-goal{ flex:0 0 auto; }
@media (min-width:900px){
  .m3-hud:has(.m3-goal:nth-child(4)) .m3-goals{ flex-wrap:nowrap !important; }
}
@media (max-width:360px){
  /* 5 目標：窄機收半階 ⇒ 2 列（不收的話會變 3 列，HUD 變高 19px） */
  .m3-goals:has(.m3-goal:nth-child(5)){ column-gap:5px; }
  .m3-goals:has(.m3-goal:nth-child(5)) .m3-goal{ font-size:12px; gap:2px; }
  .m3-goals:has(.m3-goal:nth-child(5)) .m3-goal-ic{ width:15px; height:15px; }
  /* 3 目標（Lv115 等）在 320px 也會擦到步數：收半階，仍維持一列 */
  .m3-goals:has(.m3-goal:nth-child(3)):not(:has(.m3-goal:nth-child(4))){ column-gap:5px; }
  .m3-goals:has(.m3-goal:nth-child(3)):not(:has(.m3-goal:nth-child(4))) .m3-goal{ font-size:14px; }
  .m3-goals:has(.m3-goal:nth-child(3)):not(:has(.m3-goal:nth-child(4))) .m3-goal-ic{ width:18px; height:18px; }
}
/* ★340px 以下再收一階★：14px/18px 在 320 機身仍然壓到步數 3～5px（Lv10、Lv118 實測），
   因為 3 目標的數字長度差很多（「0/9」vs「0/176」）。13px/16px 量到餘裕 +3～+8px。 */
@media (max-width:340px){
  .m3-goals:has(.m3-goal:nth-child(3)):not(:has(.m3-goal:nth-child(4))){ column-gap:4px; }
  .m3-goals:has(.m3-goal:nth-child(3)):not(:has(.m3-goal:nth-child(4))) .m3-goal{ font-size:13px; gap:2px; }
  .m3-goals:has(.m3-goal:nth-child(3)):not(:has(.m3-goal:nth-child(4))) .m3-goal-ic{ width:16px; height:16px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   🫐 v0.61.27【拖珠模式】的視覺語言
   ──────────────────────────────────────────────────────────────────────────
   ★這一段是 Prototype 視覺★：全部用 CSS 畫，★0 張新圖★、0 個外部素材。
   正式的「夢莓花園」背景與新棋子素材見
     docs/_internal/lv151-demo/夢莓花園_美術規格與生成詞.html
   在正式素材進 repo 之前，這裡讓店長先把「手感」玩對。

   ★只作用在拖珠關★：這些 class 只有 level.mode === 'drag' 時才會被建出來，
   Lv1～150 完全不會出現 ⇒ 既有關卡一個像素都不變。
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 倒數條（按住才開始跑；放手／結算完就停住）────────────────────────── */
/* ══ 🫐 倒數條：★掛在 .m3-hud 裡面★，當 HUD 的第二列 ═══════════════════
   為什麼一定要在 HUD 裡：見 match3.js 的 dgMountUI 真因說明
   （掛在 .m3-wrap 底下會把 computeCell 第一輪的 freeH 正負號翻過去，
     棋盤直接掉到 18px 地板）。
   .m3-hud-drag 只有拖珠關才加得到 ⇒ Lv1–150 的 HUD 完全不受影響。 */
/* ★第四欄★：和「剩餘回合」並排，★不讓 HUD 長高★
   （長高多少，在固定高度的宿主上棋盤就被擠掉多少 —— 這正是本輪的 bug 本體。） */
.m3-hud.m3-hud-drag .m3-hud-goal{ padding:0 8px; }
/* 欄位順序：關卡｜目標｜剩餘時間｜剩餘回合
   ★「剩餘回合」留在最右邊★ —— 和 Lv1–150 的「剩餘步數」同一個位置，
   玩家的眼睛不必重新學一次要往哪裡看。 */
.m3-hud.m3-hud-drag .m3-dgbar     { order:1; }
.m3-hud.m3-hud-drag .m3-hud-moves { order:2; }
.m3-dgbar{
  flex:0 0 auto; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:3px;
  padding:0 10px; border-right:1px solid rgba(226,192,122,.20);
  color:#e9dcc2;
}
.m3-dgbar-cap{ white-space:nowrap; }
.m3-dgbar-track{
  width:46px; height:5px; border-radius:3px; overflow:hidden;
  background:rgba(0,0,0,.4);
  box-shadow:inset 0 1px 2px rgba(0,0,0,.45);
}
.m3-dgbar-fill{
  display:block; height:100%; width:100%;
  border-radius:3px;
  background:linear-gradient(90deg,#ffb3d2,#ffd2b0);
  transition:width .1s linear, background .35s ease, box-shadow .35s ease;
}
.m3-dgbar-sec{
  font-weight:800; font-size:18px; color:#ffe9b0; line-height:1;
  font-variant-numeric:tabular-nums;
  transition:color .3s ease, font-size .3s ease, text-shadow .3s ease;
}
/* 狀態提示字（「按住一顆棋子，拖著它走」「結算中…」）：
   ★只給輔助工具讀，畫面上不佔任何版面★ ——
   要教的話教學卡已經教過了，HUD 上再擺一行字只會把棋盤往下擠。 */
.m3-dgtip{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
}
@media (max-width:360px){ .m3-dgbar-track{ width:38px; } }

/* ── 教學那一次：不倒數，整條走「安心」的綠 ─────────────────────────── */
.m3-dgbar.free .m3-dgbar-fill{ background:linear-gradient(90deg,#9fd8b4,#9fd8ef); }
.m3-dgbar.free .m3-dgbar-sec { color:#a9e0c4; }
.m3-dgbar.free .m3-dgbar-cap::after{ content:"（這次不計時）"; }

/* ══ 20 秒的漸進式緊張感（★秒數不縮短，靠節奏做★）════════════════════
     20–11 calm   柔和粉，完全不干擾
     10– 6 notice 暖一階、數字放大一級
      5– 4 warn   橘紅、更醒目、非常輕的 pulse（1.1s）
      3– 1 alarm  明顯 pulse／scale、節奏加快（0.62s）
   ★只動這一條 HUD 上的讀數，不動棋盤、不整頁閃、不擋住任何一格。★ */
.m3-dgbar.run.st-notice .m3-dgbar-fill{ background:linear-gradient(90deg,#ffc98a,#ffb3a0); }
.m3-dgbar.run.st-notice .m3-dgbar-sec { color:#ffd79a; font-size:20px; }

.m3-dgbar.run.st-warn   .m3-dgbar-fill{ background:linear-gradient(90deg,#ff9f5a,#ff7a6b);
  box-shadow:0 0 8px rgba(255,140,90,.55); }
.m3-dgbar.run.st-warn   .m3-dgbar-sec { color:#ffbb86; font-size:22px; text-shadow:0 0 10px rgba(255,150,90,.55); }
.m3-dgbar.run.st-warn   .m3-dgbar-sec { animation:m3dgSec 1.1s ease-in-out infinite; }
.m3-dgbar.run.st-warn   .m3-dgbar-cap { color:#ffbb86; }

.m3-dgbar.run.st-alarm  .m3-dgbar-fill{ background:linear-gradient(90deg,#ff6b5a,#ff9a3c);
  box-shadow:0 0 12px rgba(255,105,80,.75); }
.m3-dgbar.run.st-alarm  .m3-dgbar-sec { color:#ff9f7a; font-size:25px; text-shadow:0 0 14px rgba(255,110,80,.8); }
.m3-dgbar.run.st-alarm  .m3-dgbar-sec { animation:m3dgSec .62s cubic-bezier(.3,.9,.4,1) infinite; }
.m3-dgbar.run.st-alarm  .m3-dgbar-cap { color:#ff9f7a; }
.m3-dgbar.run.st-alarm  .m3-dgbar-track{ box-shadow:inset 0 1px 2px rgba(0,0,0,.4), 0 0 0 1px rgba(255,120,90,.45); }
@keyframes m3dgSec{
  0%,100%{ transform:scale(1); }
  46%    { transform:scale(1.16); }
}
/* ★婆婆媽媽看得清楚優先★：關掉動畫時只留顏色與字級，數字不跳。 */
.m3-reduced .m3-dgbar-fill{ transition:none; }
.m3-reduced .m3-dgbar-sec,
.m3-paused  .m3-dgbar-sec{ animation:none !important; transform:none !important; }
@media (prefers-reduced-motion: reduce){
  .m3-dgbar-sec{ animation:none !important; transform:none !important; }
}

/* ── COMBO ×N（結算時浮在棋盤上方，★不蓋住棋盤★）──────────────────── */
.m3-dgcombo{
  position:absolute; left:50%; top:4px; transform:translateX(-50%);
  z-index:9; pointer-events:none;
  font-size:26px; font-weight:900; letter-spacing:1px;
  color:#fff; text-shadow:0 2px 0 #d2618c, 0 0 14px rgba(255,150,190,.8);
  opacity:0;
}
.m3-dgcombo.pop{ animation:m3dgCombo .62s cubic-bezier(.2,.9,.3,1) both; }
@keyframes m3dgCombo{
  0%{ opacity:0; transform:translateX(-50%) scale(.6); }
  22%{ opacity:1; transform:translateX(-50%) scale(1.12); }
  70%{ opacity:1; transform:translateX(-50%) scale(1); }
  100%{ opacity:0; transform:translateX(-50%) scale(1.02); }
}
.m3-reduced .m3-dgcombo.pop{ animation:none; opacity:1; }

/* ══ 🎯 v0.61.29a【每一組消除的 +1 回饋】═════════════════════════════════
   店長實機：「COMBO 會一次加好幾點了，但看不出來是哪幾組。」
   ★一組合法消除 ＝ 盤上一個 +1★，位置就在那一組自己的中心格。
   資料來源是正式記帳用的同一份 match group（見 match3.js dgGroupChips），
   所以畫面上冒幾個 +1，HUD 就記幾點 —— 不可能一邊算一套。 */
.m3-dgfx{
  position:absolute; inset:0;
  pointer-events:none;
  overflow:visible;          /* ★不裁切★：第 0 列的 +1 要能往上飄一點點 */
  z-index:12;                /* 棋子之上、結算畫面之下 */
}
/* ★一定要在滿盤棋子上讀得出來★（實機截圖：純文字＋陰影在花紋棋子上會被吃掉）
   ⇒ 給它一個小小的深色膠囊當底，白字才有固定對比；
     膠囊本身很小、半透明、圓角，不會變成「一堆廉價數字在盤上亂飛」。 */
.m3-gchip{
  position:absolute;
  transform:translate(-50%,-50%);
  font-style:normal; font-weight:900; line-height:1;
  font-size:clamp(15px, 4.2vw, 20px); letter-spacing:.5px;
  color:#fff;
  padding:3px 8px 4px; border-radius:999px;
  background:linear-gradient(180deg, rgba(58,20,38,.88), rgba(38,12,26,.9));
  border:1px solid rgba(255,190,214,.75);
  box-shadow:0 2px 8px rgba(0,0,0,.45), 0 0 12px rgba(255,150,190,.55);
  text-shadow:0 1px 2px rgba(0,0,0,.6);
  white-space:nowrap; opacity:0;
  animation:m3gChip .82s cubic-bezier(.2,.9,.3,1) both;
}
@keyframes m3gChip{
  0%  { opacity:0; transform:translate(-50%,-50%) scale(.55); }
  20% { opacity:1; transform:translate(-50%,-62%) scale(1.14); }
  55% { opacity:1; transform:translate(-50%,-92%) scale(1); }
  100%{ opacity:0; transform:translate(-50%,-135%) scale(.98); }
}
/* Reduced Motion：★不是整個不顯示★ —— 位移與縮放拿掉，字仍然看得到。 */
.m3-reduced .m3-gchip,
.m3-paused  .m3-gchip{ animation:none !important; opacity:1; }
@media (prefers-reduced-motion:reduce){
  .m3-gchip{ animation:none !important; opacity:1; }
}

/* ── HUD 的「這一次操作拿到 +N」────────────────────────────────────────
   一次拖曳結算完才出現一次，N ＝ 真正記進 goals 的數字。
   ★教學回合不會出現★（教學不灌正式進度，見 dgUnlock）。 */
.m3-goal{ position:relative; }
.m3-goal-delta{
  position:absolute; left:50%; bottom:calc(100% + 2px);
  transform:translateX(-50%);
  font-style:normal; font-weight:900; line-height:1;
  font-size:clamp(12px, 2.6vw, 15px); white-space:nowrap;
  color:#fff; text-shadow:0 1px 0 #d2618c, 0 0 8px rgba(255,150,190,.9);
  pointer-events:none;
  animation:m3GoalDelta .9s cubic-bezier(.2,.9,.3,1) both;
}
@keyframes m3GoalDelta{
  0%  { opacity:0; transform:translate(-50%, 6px) scale(.7); }
  22% { opacity:1; transform:translate(-50%, 0) scale(1.1); }
  62% { opacity:1; transform:translate(-50%, -4px) scale(1); }
  100%{ opacity:0; transform:translate(-50%, -12px) scale(1); }
}
.m3-goal.bump .m3-goal-n{ animation:m3GoalBump .42s cubic-bezier(.3,.9,.4,1) both; display:inline-block; }
@keyframes m3GoalBump{
  0%{ transform:scale(1); } 40%{ transform:scale(1.22); } 100%{ transform:scale(1); }
}
.m3-reduced .m3-goal-delta,
.m3-paused  .m3-goal-delta{ animation:none !important; opacity:1; }
.m3-reduced .m3-goal.bump .m3-goal-n{ animation:none !important; }
@media (prefers-reduced-motion:reduce){
  .m3-goal-delta{ animation:none !important; opacity:1; }
  .m3-goal.bump .m3-goal-n{ animation:none !important; }
}

/* ══ 🫐 v0.61.27【拖珠選取圈的定位契約】★真因與修法★ ══════════════════
   ── 店長真人 QA 看到的 ──
     高麗菜、布丁的亮框沒有套在棋子正中央（草莓更明顯）。

   ── 真因（不是某一顆的偏移，是整條契約錯了）──
     ① 棋子格 .m3-piece 是「格子」，它的中心是★數學中心★。
     ② 棋子圖 .m3-ico 的大小是 cell × WEIGHT（草莓 .90／布丁 .94／牛奶 1.00…），
        而且圖檔本身的★不透明像素質心★並不在畫布中心 ——
        專案早就量過並寫在上方的對齊表（--vc-x／--vc-y）。
     ③ 一般棋子★不做★可視中心補償（--ico-tx/--ico-ty 只掛在 under-ice／wrapped 上），
        所以玩家看到的水果中心 = 格子中心 ＋ (--vc-x, --vc-y)。
     ④ 舊寫法 `inset:5%` 是以★格子★為基準畫圈 ⇒ 圈在數學中心、水果在視覺中心
        ⇒ 每一顆都偏，偏多少各自不同。這就是為什麼「只修高麗菜」一定會壞其他四顆。

   ── 修法（一條契約，不逐顆硬補）──
     讓亮框和棋子圖★用同一個盒子★：
       尺寸 = 100% × var(--ico-w)   ← 和 .m3-ico 的 width/height 同一個數（JS 寫進來）
       位移 = translate(-50% + --vc-x, -50% + --vc-y)
     因為盒子一樣大，translate 的百分比就和 --vc-x/--vc-y 量測時的分母一致，
     ★不需要任何一顆棋子的手工修正值★；日後換素材只要重跑既有的
     scripts/audit-asset-geometry.mjs 更新對齊表，亮框會自己跟著對。

   ── 影響範圍 ──
     .m3-dg-pick／.m3-dg-hold 這兩個 class ★只有拖珠關才加得到★
     ⇒ 一般交換模式、bloom／leaf／ice／syrup 的 wrapper 契約一個字都沒動。
   ══════════════════════════════════════════════════════════════════════ */
/* ══ ★產品決策（店長 v0.61.27 第二次真人 QA）★ ═══════════════════════
   【正式拖珠沒有選取圓框】
     玩家抓起任何一顆棋子時，★不准★出現白色大圓框跟著棋子跑。
     「我現在抓的是這一顆」改成★長在棋子本身★的回饋：浮起、輕微放大、柔和發光。
     這樣未來正式 4D 美術換進來也照樣成立（圓框會和精緻美術打架）。
   【亮框只屬於新手教學】
     只有教學第 2～3 步「按住這顆草莓」的★那一顆指定棋子★才有亮框，
     按下【開始挑戰】之後必須完全清掉。
   ⇒ 所以下面的置中契約★只服務 .m3-dg-pick（教學）★，
     .m3-dg-hold（正式拖珠）已經沒有任何偽元素了。
   ══════════════════════════════════════════════════════════════════════ */
.m3-piece.m3-dg-pick{
  /* 亮框要落在「玩家看到的水果中心」，不是格子的數學中心 */
  --dg-ring-x: calc(-50% + var(--vc-x, 0%));
  --dg-ring-y: calc(-50% + var(--vc-y, 0%));
}
.m3-piece.m3-dg-pick::after{
  content:""; position:absolute; left:50%; top:50%;
  width:calc(100% * var(--ico-w, .9)); height:calc(100% * var(--ico-w, .9));
  pointer-events:none;
}

/* ── 被抓住的那一顆：★只動棋子本體★（浮起 ＋ 輕微放大 ＋ 柔和外發光）──
   ★這裡刻意沒有 ::after★ —— 正式拖珠不得出現任何額外的圈／框／底板。
   效果全部掛在 .m3-ico 上 ⇒ 跟著棋子走、不會蓋到鄰格、換美術也不用重做。 */
.m3-piece.m3-dg-hold{ z-index:8; }
.m3-piece.m3-dg-hold .m3-ico{
  filter:drop-shadow(0 12px 10px rgba(70,45,18,.55))    /* 離開盤面的落影＝浮起來了 */
         drop-shadow(0 0 9px rgba(255,232,190,.9))       /* 柔和的暖色外發光 */
         brightness(1.14) saturate(1.08);
  /* ★視覺跟手★：--dg-fx/--dg-fy 由 JS 逐 frame 寫進來（見 match3.js 的 dgApplyFollow）。
     translate3d 走 compositor，不碰 layout ⇒ 快速橫掃也不掉格。
     ★transition 必須是 none★：任何補間都等於「慢半拍」，那正是店長說的不跟手。 */
  transform:translate3d(var(--dg-fx,0px), var(--dg-fy,0px), 0)
            translate(var(--ico-tx,0px), calc(var(--ico-ty,0px) - 7px)) scale(1.16);
  transition:none;
  will-change:transform;
}
/* 放手的那一下：偏移歸零 ＋ 一小段過渡 ⇒ 自然吸附回最後一格合法位置 */
.m3-piece.m3-dg-snap .m3-ico{ transition:transform .16s cubic-bezier(.22,.9,.3,1); }
/* ★Reduced Motion 不關閉跟手★：跟手是操作必要回饋，不是裝飾動畫。
   這裡只關掉「吸附」那一段補間，拖曳中的 1:1 跟隨照常。 */
.m3-reduced .m3-piece.m3-dg-snap .m3-ico{ transition:none; }
/* ── 教學指定的那一顆：呼吸亮框（「就按這一顆」）────────────────────── */
.m3-piece.m3-dg-pick{ z-index:7; }
.m3-piece.m3-dg-pick::after{
  border-radius:50%;
  box-shadow:0 0 0 3px rgba(255,214,140,.95), 0 0 16px 5px rgba(255,196,92,.7);
  transform:translate(var(--dg-ring-x), var(--dg-ring-y));
  animation:m3dgPick 1.4s ease-in-out infinite;
}
/* ★位移必須寫進每一個 keyframe★：transform 是整條覆蓋的，
   只寫 scale 會把上面那行的置中位移洗掉（舊版就是這樣偏掉的）。 */
@keyframes m3dgPick{
  0%,100%{ opacity:.55; transform:translate(var(--dg-ring-x), var(--dg-ring-y)) scale(.97); }
  50%    { opacity:1;   transform:translate(var(--dg-ring-x), var(--dg-ring-y)) scale(1.04); }
}
.m3-reduced .m3-piece.m3-dg-pick::after,
.m3-paused .m3-piece.m3-dg-pick::after{
  animation:none; opacity:.95;
  transform:translate(var(--dg-ring-x), var(--dg-ring-y));
}

/* ══ ✨ v0.61.28d 夢莓花園特效素材接線（★只有這一區、只有 CSS★）══════════
   ★零邏輯改動★：下面兩段只是把原本用 box-shadow／純文字畫的東西，
   換成店長交付的正式素材。JS 一行都沒動，class 的掛法、時機、清除
   全部沿用已經 PASS 的既有契約。
   ★作用範圍★：都掛在 .m3-scene-dreamberry（Lv151–175）底下
   ⇒ Lv1–150、以及日後其他區域的拖珠關，讀不到這兩條。 */

/* ⓪ Combo 讀數的位置：★移到棋盤上，不要壓在 HUD 上★
   .m3-dgcombo 原本是 `top:4px`（相對 .m3-wrap）—— 那個位置正好是 HUD，
   讀數與光花會蓋掉「本關目標 COMBO n/18」。
   Lv151 起 Combo 就是通關條件本身，那一行絕對不能被自己的特效蓋住。
   ⇒ 移到棋盤上緣附近；HUD 完全讓出來。 */
.m3-scene-dreamberry .m3-dgcombo{ top:30%; }

/* ① 教學指定光環：box-shadow 圓框 → dreamberry-pick-ring.webp
   ★店長的產品決策沒有被動到★：正式拖珠仍然沒有任何圈
   （.m3-dg-pick 只有教學中才掛得上，dgSetPick 裡有硬閘）。
   置中仍走既有的 --dg-ring-x/y 契約 ⇒ 不是逐顆補丁。 */
.m3-scene-dreamberry .m3-piece.m3-dg-pick::after{
  box-shadow:none;
  background:url('match3/dreamberry-pick-ring.webp') center / contain no-repeat;
  /* 素材的環本身有留白，畫得比格子大一點才對得上原本 box-shadow 的視覺半徑 */
  width:calc(124% * var(--ico-w, .9)); height:calc(124% * var(--ico-w, .9));
}

/* ② Combo 讀數後面的光花：dreamberry-combo-burst.webp
   ★刻意收斂★（生成詞裡就寫死了「不是最終大絕」）：
   只在 .pop 的那 0.62 秒跟著讀數閃一下，讀數本身完全沒被蓋住
   —— 素材中央就是留白的，而且它在讀數★下層★（z-index:-1）。 */
.m3-scene-dreamberry .m3-dgcombo{ position:absolute; }
.m3-scene-dreamberry .m3-dgcombo::before{
  content:""; position:absolute; left:50%; top:50%; z-index:-1;
  width:190px; height:190px; transform:translate(-50%,-50%);
  background:url('match3/dreamberry-combo-burst.webp') center / contain no-repeat;
  opacity:0; pointer-events:none;
}
.m3-scene-dreamberry .m3-dgcombo.pop::before{
  animation:m3dgBurst .62s cubic-bezier(.2,.9,.3,1) both;
}
@keyframes m3dgBurst{
  0%  { opacity:0;   transform:translate(-50%,-50%) scale(.45) rotate(-8deg); }
  26% { opacity:.92; transform:translate(-50%,-50%) scale(1.06) rotate(0deg); }
  70% { opacity:.55; transform:translate(-50%,-50%) scale(1.14) rotate(3deg); }
  100%{ opacity:0;   transform:translate(-50%,-50%) scale(1.22) rotate(5deg); }
}
/* 省電／系統減少動態：不閃、也不殘留（和既有 .m3-dgcombo 的處理一致） */
.m3-reduced .m3-scene-dreamberry .m3-dgcombo.pop::before,
.m3-paused  .m3-scene-dreamberry .m3-dgcombo.pop::before{ animation:none; opacity:0; }
@media (prefers-reduced-motion: reduce){
  .m3-scene-dreamberry .m3-dgcombo.pop::before{ animation:none; opacity:0; }
}

/* ══ 🌱 夢莓花圃（v0.61.27c）══════════════════════════════════════════
   一句話：「拖出 3 連鎖以上，花圃上的棋子就會變成草莓。」
   ★它不是障礙★：不擋拖曳、不擋交換、不用打、不是過關目標，
   所以視覺上要一眼看出「這是地上的一塊土」，而不是「蓋在棋子上的東西」。
   ★Prototype 階段 0 張圖檔★（Gate C-06 擋著）：純 CSS 畫。 */
.m3-socket.m3-seedbed{
  background:
    radial-gradient(70% 70% at 50% 58%, rgba(255,214,232,.34) 0%, rgba(255,214,232,0) 72%),
    radial-gradient(52% 52% at 50% 62%, rgba(120,78,52,.40) 0%, rgba(120,78,52,.16) 60%, rgba(120,78,52,0) 78%);
  box-shadow:inset 0 0 0 1.5px rgba(255,196,224,.42), inset 0 2px 6px rgba(90,55,40,.28);
}
/* 土上的小芽：兩片葉子，純 CSS，靜態（不搶棋子的注意力） */
.m3-socket.m3-seedbed::after{
  content:""; position:absolute; left:50%; bottom:16%;
  width:22%; height:22%; transform:translateX(-50%);
  background:
    radial-gradient(60% 90% at 18% 40%, #8fd8a4 0%, #8fd8a4 60%, rgba(143,216,164,0) 62%),
    radial-gradient(60% 90% at 82% 40%, #8fd8a4 0%, #8fd8a4 60%, rgba(143,216,164,0) 62%),
    linear-gradient(180deg, rgba(120,190,140,0) 40%, #78bc8c 42%, #78bc8c 100%);
  background-size:100% 60%, 100% 60%, 8% 100%;
  background-position:left top, right top, center bottom;
  background-repeat:no-repeat;
  opacity:.9; pointer-events:none;
}
/* 開花的那一下：長出來的那顆棋子輕輕彈一下（很短，不拖節奏） */
.m3-piece.m3-seed-pop .m3-ico{ animation:m3SeedPop .42s cubic-bezier(.25,.9,.3,1); }
@keyframes m3SeedPop{
  0%  { transform:translate(var(--ico-tx,0px), var(--ico-ty,0px)) scale(.55); opacity:.35; }
  55% { transform:translate(var(--ico-tx,0px), var(--ico-ty,0px)) scale(1.16); opacity:1; }
  100%{ transform:translate(var(--ico-tx,0px), var(--ico-ty,0px)) scale(1); opacity:1; }
}
.m3-reduced .m3-piece.m3-seed-pop .m3-ico{ animation:none; }
@media (prefers-reduced-motion: reduce){ .m3-piece.m3-seed-pop .m3-ico{ animation:none; } }
/* 花圃規則卡：沿用教學卡的樣式，只是換個顏色，讓它一眼看得出是「說明」不是「教學步驟」 */
.m3-dgtut.m3-seedtut .m3-dgtut-card{ border-color:rgba(140,200,160,.6); }
.m3-dgtut.m3-seedtut .m3-dgtut-card b{ color:#3f8f63; }

/* ── 第一次教學的卡片 ────────────────────────────────────────────────── */
.m3-dgtut{
  position:fixed; inset:0; z-index:80; display:flex; align-items:center; justify-content:center;
  background:rgba(40,20,32,.46); padding:18px;
}
.m3-dgtut.slim{ align-items:flex-end; background:transparent; pointer-events:none; padding-bottom:calc(env(safe-area-inset-bottom) + 14px); }
.m3-dgtut-card{
  pointer-events:auto; max-width:330px; width:100%;
  background:linear-gradient(180deg,#fff8fb,#ffeef5);
  border:1px solid rgba(214,122,160,.5); border-radius:18px;
  padding:16px 18px; color:#6d3350; line-height:1.66; font-size:15px;
  box-shadow:0 10px 26px rgba(80,30,55,.3);
}
.m3-dgtut.slim .m3-dgtut-card{ background:rgba(255,248,251,.96); box-shadow:0 6px 18px rgba(80,30,55,.28); }
.m3-dgtut-card b{ color:#c2496f; }
.m3-dgtut-card p{ margin:8px 0 0; }
.m3-dgtut-btn{
  display:block; width:100%; margin-top:14px; min-height:46px;
  border:0; border-radius:13px; cursor:pointer;
  background:linear-gradient(180deg,#ff9ec4,#e7658f); color:#fff;
  font-size:16px; font-weight:800; letter-spacing:1px;
  box-shadow:0 3px 0 #c2496f, 0 4px 10px rgba(180,70,110,.35);
}
.m3-dgtut-btn:active{ transform:translateY(2px); box-shadow:0 1px 0 #c2496f; }

/* ── 夢莓花園｜Prototype 場景色調 ★已退休（v0.61.28）★ ──────────────────
   正式背景圖 dreamberry-garden-bg*.webp 已進版控，並登記在 match3-scenes.js
   的 THEMES／MAP（Lv151–175 → dreamberry）。
   ★兩條上背景的路徑不能並存★：舊的 .m3-dream-berry::before 和
   .m3-scene::before 是★同一個虛擬元素★，留著就是互相覆蓋。
   場景的回退色與光暈調校改放在下面的 .m3-scene-dreamberry。 */

/* ══════════════════════════════════════════════════════════════════════════
   🫐 v0.61.29c｜Lv151+ DREAMBERRY VICTORY SCENE（第二版・整套重做）
   ──────────────────────────────────────────────────────────────────────────
   ── 第一版被店長退回的真因 ──
     「好醜」「像婚禮活動頁／美妝 App」。
     我把 Dreamberry 直接等號成「粉紅＋白玻璃卡」：粉紅背景、粉紅 badge、
     粉白卡片、粉紅按鈕、粉紅 glow 全部一起上 ⇒ 層次消失、遊戲感被拿掉。
     ★教訓：莓粉只能當 accent，不能當主色。★

   ── 這一版的色彩比例（視覺方向，不是精確數字）──
     葉綠／自然色 30%｜暖深莓棕 30%｜奶油／暖白 25%｜莓粉莓紅 10%｜暖金 5%
     ⇒ 底層是「花園夜色」的深莓棕，資訊是奶油色，莓紅只出現在主要 CTA，
       暖金只出現在標題與細邊 —— 不是滿畫面粉紅。

   ── 要留住 Lv150 舊結算的優點（不是它的黑金配色）──
     儀式感｜Hero 美術主體｜清楚資訊層級｜成績像遊戲成績而不是 App 卡片｜
     下一關按鈕有真正的 CTA 重量｜整體是一個完整場景，不是幾張 UI 卡疊上去。

   ── 一樣的工程約束 ──
     ★不複製第二套結算邏輯★：showResult() 一條路、DOM 一模一樣，
     Lv151+ 只是多掛 .m3-r-dreamberry（由 level.artSet 決定）。
     Lv1–150 讀不到這一段的任何一條規則。
   ══════════════════════════════════════════════════════════════════════════ */

/* ── ① 遮罩：棋盤仍辨認得出來，只是自然退後（★不要奶霧、不要粉紅濾鏡★）── */
.m3-result.win.m3-r-dreamberry .m3-rbackdrop{
  background:
    /* ★v0.61.29c：背景層已經接對（整個外殼都是夢莓花園）⇒ 這裡不需要再用
       深棕／深莓硬壓暗去掩蓋錯誤背景★。只做適量暗化＋柔和暗角，
       讓玩家仍然認得出「我剛剛就是在這個花園裡過關」。 */
    radial-gradient(104% 80% at 50% 40%, rgba(48,24,34,.30), rgba(34,16,24,.62) 58%, rgba(22,10,16,.80)),
    linear-gradient(180deg, rgba(48,30,18,.10), rgba(30,14,20,.18));
  /* blur 從 3px 降到 1px：第一版整片糊成色塊，像系統跳出的 modal。 */
  -webkit-backdrop-filter:blur(1.5px) saturate(.86) brightness(.82);
  backdrop-filter:blur(1.5px) saturate(.86) brightness(.82);
}

/* ── ② 視覺順序：花環 → 挑戰成功 → Lv.151 → ★★★ → 成績 → 行動 ───────── */
.m3-r-dreamberry .m3-remblem-wrap{ order:1; }
.m3-r-dreamberry .m3-rtitle       { order:2; }
.m3-r-dreamberry .m3-rident       { order:3; }
.m3-r-dreamberry .m3-rmid         { order:4; }
.m3-r-dreamberry .m3-packend      { order:5; }
.m3-r-dreamberry .m3-rbtns        { order:6; }
.m3-r-dreamberry .m3-rbtns2       { order:7; }
.m3-r-dreamberry .m3-rstage{ gap:clamp(3px, .8vh, 8px); }
/* 花環下緣的葉尖離標題太近（實機截圖）：只加一點呼吸，不動花環尺寸 */
.m3-result.win.m3-r-dreamberry .m3-remblem-wrap{ margin-bottom:clamp(2px, 1.1vh, 10px); }

/* ── ③ 花環：Victory 畫面的第一視覺主角 ─────────────────────────────────
   v0.61.29c 一次縮了 20%，店長實機：「矮螢幕縮得有點過頭。」
   v0.61.29d 回加約 12%（★不是回到最初那種巨大神器比例★），
   而且三段斷點★一起調★，不針對單一截圖硬湊。 */
/* v0.61.29g：換成完成印記之後回量了一次 —— 花環是一個「環」，幾乎填滿自己的
   外框；完成印記是「一筆打勾＋放射光」，同樣的框裡實際的墨色少一截，
   所以看起來會小一號。這裡只把框放大 8%（★不動版面結構★），
   兩種 viewport 都重新量過不會把下面的資訊擠出去。 */
.m3-result.win.m3-r-dreamberry .m3-remblem-wrap{
  width:min(55vw, 25.4vh, 254px); height:min(55vw, 25.4vh, 254px);
}
/* ★v0.61.29g：新的完成印記★自己就畫了暖光放射與星點★ ⇒ CSS 不再疊第二層光。
   舊版那層 drop-shadow(0 0 14px …) 是為了「沒有內建發光的花環」補的，
   現在留著只會把印記的光糊掉 —— 只保留讓它坐在場景上的接觸陰影。 */
.m3-result.win.m3-r-dreamberry .m3-remblem{
  animation:m3rEmblemDB .52s cubic-bezier(.22,.72,.3,1) .18s both;
  filter:drop-shadow(0 8px 16px rgba(18,8,12,.5));
}
/* 同理：擴散光環與持久星點都是舊素材缺光時的補強，現在會和印記自己的光打架。 */
.m3-result.win.m3-r-dreamberry .m3-rhalo,
.m3-result.win.m3-r-dreamberry .m3-rsparkles{ display:none; }
@keyframes m3rEmblemDB{
  from{ opacity:0; transform:scale(.92); }
  to  { opacity:1; transform:scale(1); }
}
/* 背後的光：★收斂成一層很淡的暖底★（印記自己已經有放射光），
   只負責讓主視覺與底下的深色場景之間有一點空氣，不再當第二個光源。 */
.m3-result.win.m3-r-dreamberry .m3-rglow{
  width:132%; height:150%;
  background:radial-gradient(closest-side, rgba(255,232,186,.20), rgba(255,232,186,0) 72%),
             radial-gradient(closest-side, rgba(146,196,132,.12), rgba(146,196,132,0) 64%);
}
.m3-result.win.m3-r-dreamberry .m3-rhalo{ border-color:rgba(246,223,168,.5); }
.m3-result.win.m3-r-dreamberry .m3-rsparkles i{
  background:#fff4da; box-shadow:0 0 6px 1px rgba(255,224,170,.85); opacity:.5;
}

/* ── ④「挑戰成功」：★暖奶油白為主★，莓金只當非常淡的點綴 ───────────────
   v0.61.29c 用 #f9e6b6（偏金黃），店長實機：「還有一點 Lv1–150 舊金色
   Victory 的味道。」⇒ 主色改成暖奶油白，金黃感降到只剩外圈的一層淡暖光，
   重量改由深莓描邊與陰影撐（★不是靠更黃★）。 */
.m3-result.win.m3-r-dreamberry .m3-rtitle{
  font-size:clamp(20px, 3.5vh, 28px);
  font-weight:900; letter-spacing:5px;
  color:#fdf4e6;
  text-shadow:
    0 2px 0 rgba(78,34,46,.55),
    0 3px 7px rgba(24,12,16,.52),
    0 0 14px rgba(250,222,196,.28);
  margin-top:clamp(2px, .8vh, 6px);
}

/* ── ⑤ Lv.151：★不是網頁 tag★ —— 小字＋兩側極細的花園裝飾線 ─────────── */
.m3-result.win.m3-r-dreamberry .m3-rlv{
  font:800 clamp(10px, 1.5vh, 12.5px)/1 -apple-system,"PingFang TC","Noto Sans TC",sans-serif;
  letter-spacing:2.6px;
  color:#e6d2a8; background:none; border:none; box-shadow:none;
  padding:0; border-radius:0;
  text-shadow:0 1px 3px rgba(20,10,14,.6);
}
.m3-result.win.m3-r-dreamberry .m3-rident{ gap:10px; margin-top:1px; }
.m3-result.win.m3-r-dreamberry .m3-rident::before,
.m3-result.win.m3-r-dreamberry .m3-rident::after{
  content:""; display:block; width:clamp(18px, 7vw, 34px); height:1px;
  background:linear-gradient(90deg, rgba(160,200,140,0), rgba(196,214,170,.6), rgba(160,200,140,0));
}

/* ── ⑥ 星級：★v0.61.29e 改用夢莓花園自己的星星素材★ ────────────────────
   店長交付一張 2-in-1 母圖（左＝已取得的暖橘→莓粉、右＝未取得的奶油灰），
   由 tools/gen-lv151-art-assets.py 分離成 dreamberry-star-on / -off.webp。
   ── 因此這裡★不再用 CSS 把彩色星硬轉灰★ ──
     舊版 .m3-star{ opacity:.26; filter:saturate(.2) } 是「沒有未取得素材」時的
     替代方案；現在未取得有自己的畫，兩顆都以原畫呈現、★同一個尺度★，
     只留一層很輕的接觸陰影讓它們坐在場景上（母圖本身沒有發光，不必再疊 glow）。 */
.m3-r-dreamberry .m3-rstars{
  gap:clamp(6px, 1.8vw, 11px);
  margin-bottom:clamp(2px, .6vh, 5px);
  margin-top:clamp(3px, .9vh, 7px);
}
.m3-r-dreamberry .m3-rstars .m3-star{
  width:clamp(30px, 4.9vh, 43px); height:clamp(30px, 4.9vh, 43px);
  /* ★兩種狀態一律原畫、原尺度★：不降透明度、不降飽和、不縮小 */
  opacity:1; transform:scale(1);
  filter:drop-shadow(0 2px 4px rgba(22,10,14,.45));
}
.m3-r-dreamberry .m3-rstars .m3-star.on,
.m3-r-dreamberry .m3-rstars .m3-star.old,
.m3-r-dreamberry .m3-rstars .m3-star.on.new{
  opacity:1; transform:scale(1);
  filter:drop-shadow(0 2px 4px rgba(22,10,14,.45));
}
/* ★真正把金光關掉的地方在這裡★（實機 computed 抓到）：
   星星的 filter 不是被上面那幾條 CSS 決定的 —— m3StarPop1/2/3 的 keyframe
   末格自己寫了 drop-shadow(... rgba(255,214,120,.85))，加上 animation-fill-mode:both，
   ⇒ 動畫跑完之後★關鍵影格的 filter 一直蓋著 CSS 的 filter★。
   所以只改 .m3-star{filter:…} 沒有用，必須換掉 keyframe 本身。
   這裡只換 animation-name（duration／delay／fill 沿用原本的 shorthand），
   ⇒ ★出場節奏與依序點亮的時序一個字沒改★，只是不再噴金光。 */
.m3-r-dreamberry .m3-rstars .m3-star.new[data-n="1"],
.m3-r-dreamberry .m3-rstars .m3-star.new[data-n="2"],
.m3-r-dreamberry .m3-rstars .m3-star.new[data-n="3"]{ animation-name:m3StarPopDB; }
@keyframes m3StarPopDB{
  0%  { opacity:0; transform:scale(.3) rotate(-14deg); filter:none; }
  55% { opacity:1; transform:scale(1.26) rotate(5deg);
        filter:drop-shadow(0 3px 8px rgba(22,10,14,.5)); }
  78% { transform:scale(.97) rotate(-1deg); }
  100%{ opacity:1; transform:scale(1);
        filter:drop-shadow(0 2px 4px rgba(22,10,14,.45)); }
}
/* 3★ 共鳴：原本 40% 會噴一層 rgba(255,238,180,.95) 的大金光 ⇒ 換成只放大一點點 */
.m3-r-dreamberry .m3-rstars.lv3{ animation-name:m3StarsResonateDB; }
@keyframes m3StarsResonateDB{
  0%  { transform:scale(1); }
  40% { transform:scale(1.06); }
  100%{ transform:scale(1); }
}

/* ── ⑦ 成績區：★沒有白色玻璃大卡★ —— 只有一層極淡的深莓底與細金髮絲線，
       層級靠字級／間距／陰影做出來。 */
.m3-result.win.m3-r-dreamberry .m3-rinfo{
  background:none; border:none; box-shadow:none;
  -webkit-backdrop-filter:none; backdrop-filter:none;
  padding:0; margin-top:clamp(2px, .7vh, 6px);
  gap:clamp(3px, .8vh, 7px);
}
/* ── COMBO 成績列（★店長第二次退回後重做★）─────────────────────────────
   舊版問題：黃色 COMBO 方塊太突兀、和上面的金色星星互搶；薄荷綠數字對比不足；
   26/26 過大；外框太厚太圓，像一顆可以點的大膠囊按鈕 ⇒ 功能型 App UI。
   現在改成「遊戲成績列」：
     COMBO（小、莓玫瑰色）　26 / 26（主資訊、奶油白）　✓（柔和葉綠、比數字小）
   底層是深莓棕半透明＋細緻暖金髮絲邊，圓角收斂，★不是白色玻璃卡、不是膠囊按鈕★。 */
.m3-result.win.m3-r-dreamberry .m3-rgoal{
  color:#f7ead0; font-size:clamp(15px, 2.2vh, 19px); gap:clamp(8px, 2.4vw, 12px);
  align-items:baseline;
  padding:clamp(5px, .9vh, 8px) clamp(13px, 3.6vw, 18px);
  border-radius:11px;
  background:
    linear-gradient(180deg, rgba(255,232,206,.06), rgba(0,0,0,0) 46%),
    linear-gradient(180deg, rgba(64,34,36,.62), rgba(42,20,26,.72));
  /* 外框再淡一階：店長實機「避免像按鈕即可」⇒ 邊線 .22 → .12，
     內高光也一起收，讓它讀起來是「成績底條」而不是可以點的元件。 */
  border:1px solid rgba(238,198,152,.12);
  box-shadow:0 2px 7px rgba(16,8,12,.30),
             inset 0 1px 0 rgba(255,232,200,.08);
}
/* 「COMBO」★不再是黃色方塊★：拿掉框、只留小字，當成成績的欄位名 */
.m3-result.win.m3-r-dreamberry .m3-rgoal .m3-goal-cbic{
  width:auto; height:auto; flex:0 0 auto; display:inline;
  background:none; border:none; box-shadow:none; border-radius:0;
  padding:0; overflow:visible;
  font-size:clamp(9px, 1.25vh, 11px); font-weight:900; letter-spacing:1.8px;
  line-height:1; color:#dda3b2;
  text-shadow:0 1px 2px rgba(20,10,14,.6);
}
/* 數字才是主角：奶油白，★不是螢光綠★ */
.m3-result.win.m3-r-dreamberry .m3-rinfo .m3-rgoal.ok > span:not(.m3-goal-cbic),
.m3-result.win.m3-r-dreamberry .m3-rgoal > span:not(.m3-goal-cbic){
  color:#f7ead0; font-weight:900; letter-spacing:.5px;
  text-shadow:0 1px 3px rgba(18,10,14,.55);
}
/* 打勾：柔和葉綠，而且★比數字小★，不搶視覺 */
.m3-result.win.m3-r-dreamberry .m3-rtick{
  color:#9cc98a; font-size:.76em; margin-left:-2px;
  text-shadow:0 1px 2px rgba(18,10,14,.5);
}
.m3-result.win.m3-r-dreamberry .m3-rmoves{
  color:#d2bd98; font-size:clamp(11px, 1.6vh, 13px); opacity:1;
  letter-spacing:.6px; text-shadow:0 1px 3px rgba(18,10,14,.6);
}
.m3-result.win.m3-r-dreamberry .m3-rupgrade{ color:#f0dcae; }

/* ── ⑧ 按鈕：遊戲按鈕的重量（實色底座陰影＋內高光＋細金邊），不是 App button ── */
.m3-result.win.m3-r-dreamberry .m3-rbtns{ margin-top:clamp(7px, 1.6vh, 14px); gap:clamp(8px,1.8vw,12px); }
.m3-result.win.m3-r-dreamberry .m3-rbtn{
  border-radius:14px; font-weight:900; letter-spacing:.6px;
}
.m3-result.win.m3-r-dreamberry .m3-rbtn.next{
  color:#fff4de;
  background:linear-gradient(180deg,#d1546b,#a62c47);
  border:1px solid rgba(246,223,168,.62);
  box-shadow:0 4px 0 rgba(92,22,40,.7), 0 9px 18px rgba(20,8,14,.5),
             inset 0 1px 0 rgba(255,222,196,.42);
  text-shadow:0 1px 2px rgba(72,14,30,.6);
}
.m3-result.win.m3-r-dreamberry .m3-rbtn.retry{
  color:#f2e2c0;
  background:linear-gradient(180deg, rgba(76,44,40,.80), rgba(50,26,28,.86));
  border:1px solid rgba(246,223,168,.32);
  box-shadow:0 3px 0 rgba(26,12,14,.6), 0 7px 14px rgba(16,8,12,.4),
             inset 0 1px 0 rgba(255,232,200,.16);
  text-shadow:0 1px 2px rgba(20,10,12,.6);
}
.m3-result.win.m3-r-dreamberry .m3-rbtn:active{ transform:translateY(2px); }
.m3-result.win.m3-r-dreamberry .m3-rbtn.exit{
  color:#e8d7b4; background:rgba(255,246,230,.07);
  border:1px solid rgba(246,223,168,.3); box-shadow:none;
}
.m3-result.win.m3-r-dreamberry .m3-rbtn.exit:hover{ background:rgba(255,246,230,.13); }
.m3-result.win.m3-r-dreamberry .m3-packend{ color:#e8d7b4; }

/* ── ⑨ 矮螢幕：先縮裝飾與間距，★最後才動主要文字★ ────────────────── */
@media (max-height:680px){
  /* 矮螢幕由 vh 那一項決定大小；實測 640px 高時 21.5vh＝138px 還是偏小，
     下面的資訊只用到 497/640 ⇒ 還有餘裕，拉到 23vh（147px）讓花環站得住。 */
  .m3-result.win.m3-r-dreamberry .m3-remblem-wrap{ width:min(50vw, 24.8vh, 216px); height:min(50vw, 24.8vh, 216px); }
  .m3-result.win.m3-r-dreamberry .m3-rtitle{ letter-spacing:4px; }
  .m3-r-dreamberry .m3-rstage{ gap:3px; }
  .m3-r-dreamberry .m3-rstars{ gap:clamp(6px, 1.8vw, 10px); }
}
@media (max-height:560px){
  .m3-result.win.m3-r-dreamberry .m3-remblem-wrap{ width:min(38vw, 19vh, 166px); height:min(38vw, 19vh, 166px); }
  /* 極矮時舊版直接 display:none 掉 .m3-rident；夢莓版留著（Lv.151 是這一區
     唯一的關卡身分，關卡名不輸出），只把裝飾線收掉。 */
  .m3-result.win.m3-r-dreamberry .m3-rident{ display:flex; }
  .m3-result.win.m3-r-dreamberry .m3-rident::before,
  .m3-result.win.m3-r-dreamberry .m3-rident::after{ display:none; }
}

/* ── ⑩ Reduced Motion：直接呈現完成狀態 ─────────────────────────────── */
.m3-reduced .m3-r-dreamberry .m3-remblem,
.m3-reduced .m3-r-dreamberry .m3-rtitle,
.m3-reduced .m3-r-dreamberry .m3-rident,
.m3-reduced .m3-r-dreamberry .m3-rinfo,
.m3-reduced .m3-r-dreamberry .m3-rbtns,
.m3-reduced .m3-r-dreamberry .m3-rbtns2{ animation:none !important; opacity:1 !important; transform:none !important; }
@media (prefers-reduced-motion: reduce){
  .m3-r-dreamberry .m3-remblem,
  .m3-r-dreamberry .m3-rtitle,
  .m3-r-dreamberry .m3-rident,
  .m3-r-dreamberry .m3-rinfo,
  .m3-r-dreamberry .m3-rbtns,
  .m3-r-dreamberry .m3-rbtns2{ animation:none !important; opacity:1 !important; transform:none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
   ✨👆 v0.61.30｜拖曳光跡 ＋ 教學手指（★只有拖珠關★）
   ──────────────────────────────────────────────────────────────────────────
   兩張素材早就壓進版控了（dreamberry-drag-trail.webp／dreamberry-tutor-hand.webp），
   這一段才是它們的動態接線。
   ★店長的兩條界線★
     ‧ 光跡要服務「玩家看得懂自己拖過哪條路」，★不是粒子秀★ ——
       一格只留一枚、低不透明度、0.5 秒淡掉、同時最多 12 枚，而且★在棋子底下★。
     ‧ 手指只在需要示範拖曳的那一步出現，不是每關反覆冒出來。
   Lv1–150 沒有 .m3-dgtrail／.m3-dgtut-hand 這兩個節點（JS 只在 isDrag() 才建）
   ⇒ 這一整段對它們永遠不會生效。
   ══════════════════════════════════════════════════════════════════════════ */
.m3-dgtrail{
  position:absolute; inset:0;
  /* ★實機截圖修正★：一開始放在 z-index:1（棋子底下），結果幾乎看不到 ——
     棋子是不透明的、又幾乎填滿整格，藏在底下的光跡只剩格線縫隙看得見，
     「玩家看得懂自己拖過哪條路」這件事等於沒做到。
     ⇒ 改成 z-index:5（棋子 2 之上、被抓住那顆 6 之下），
       但★把每一枚縮到格子的 46%★、不透明度壓低 ⇒ 看得見路徑，
       又不會蓋住棋子的樣子（店長：不要做成大量粒子特效遮住棋盤）。 */
  z-index:5;
  pointer-events:none;
  overflow:hidden;
}
.m3-dgt{
  position:absolute; display:block; pointer-events:none;
  background:url('match3/dreamberry-drag-trail.webp') center / 46% 46% no-repeat;
  opacity:0;
  animation:m3dgTrail .52s ease-out both;
  will-change:opacity, transform;
}
@keyframes m3dgTrail{
  0%  { opacity:.78; transform:scale(.62); }
  26% { opacity:.62; transform:scale(1); }
  100%{ opacity:0;   transform:scale(1.18); }
}

/* 👆 教學手指：從指定那一顆往旁邊示範一次「按住 → 拖過去」，循環到玩家真的抓起來。
   素材的指尖畫在畫布正中央 ⇒ 元素就放在格子上、用 translate 做手勢。 */
.m3-dgtut-hand{
  position:absolute; display:block; pointer-events:none;
  z-index:7;                 /* 在棋子之上、在教學卡片之下 */
  background:url('match3/dreamberry-tutor-hand.webp') center / contain no-repeat;
  filter:drop-shadow(0 3px 6px rgba(20,10,14,.45));
  transform:translate(0,0) scale(1);
  animation:m3dgHand 2.2s ease-in-out infinite;
}
@keyframes m3dgHand{
  0%   { opacity:0; transform:translate(0,0) scale(1.06); }
  12%  { opacity:1; transform:translate(0,0) scale(1); }
  26%  { opacity:1; transform:translate(0,0) scale(.92); }          /* 按下去 */
  62%  { opacity:1; transform:translate(var(--dgh-dx,28px),0) scale(.92); }  /* 拖過去 */
  80%  { opacity:1; transform:translate(var(--dgh-dx,28px),0) scale(.92); }
  100% { opacity:0; transform:translate(var(--dgh-dx,28px),0) scale(1); }
}
/* Reduced Motion／暫停：兩者都是純示範動畫 ⇒ 直接不顯示（JS 端也已經不建光跡）。
   教學的「哪一顆」資訊由亮框負責，不會因此丟失。 */
.m3-reduced .m3-dgt,
.m3-paused  .m3-dgt{ display:none; }
.m3-reduced .m3-dgtut-hand,
.m3-paused  .m3-dgtut-hand{ animation:none; opacity:.9; }
@media (prefers-reduced-motion:reduce){
  .m3-dgt{ display:none; }
  .m3-dgtut-hand{ animation:none; opacity:.9; }
}
