        /* 介面外殼配色 token（抽屜/彈窗/按鈕/頁首用）。
           2026-09-09 全面換成 Jeff 提供的「文青風」CIS 規格書色票，逐一對應：
             --ui-bg      ← 主色(Background)  #F9F8F6 燕麥白/米白
             --ui-surface ← 輔助色(Surface)    #FFFFFF 純淨白
             --ui-divider ← 點綴色(Accent)     #D3C5B4 亞麻灰棕/奶茶色（規格書標註
                            這色是「框線、次要按鈕、hover 狀態」，不是主要互動色）
             --ui-text       ← 主標題文字(Primary) #3E3A36 炭灰/墨色
             --ui-text-muted ← 內文與次要(Muted)   #7A7570 霧灰棕
             --ui-accent     ← 品牌標記(Brand)     #A4897E 乾燥栗子色/豆沙棕（規格書
                            標註這色是「促銷標籤、強調字、價格、CTA 按鈕」，對應我們
                            原本 --ui-accent 這個「主要互動色」的角色）
           規格書沒有涵蓋到的兩個 token，是我依規格書邏輯自己推算、不是文件裡寫的，
           跟 Jeff 確認過再定案：
             --ui-accent-hover：文件把 hover 狀態歸給 --ui-divider 那個較淺的
                            #D3C5B4，但我們的主要 CTA 是「深色底+白字」實心按鈕，
                            hover 變更淺會讓白字對比不足、看不清楚——這裡改成
                            --ui-accent 本身加深一階（#8b7267），維持白字可讀性；
                            #D3C5B4 仍照文件原意留給邊框／次要按鈕的 hover。
             --ui-alert：文件完全沒提供警示色，只說「強調字/價格避免用刺眼的純紅
                            色」——選了同色系但更深、更暖的赭石色（#b37b5e），跟
                            --ui-accent 有辨識度上的區隔（通知角標、錯誤訊息），
                            但不是文件明確指定的色碼。 */
        :root {
            --ui-bg: #F9F8F6;
            --ui-surface: #FFFFFF;
            --ui-divider: #D3C5B4;
            --ui-text: #3E3A36;
            --ui-text-muted: #7A7570;
            --ui-accent: #A4897E;
            --ui-accent-soft: rgba(164, 137, 126, 0.08);
            --ui-accent-hover: #8b7267;
            --ui-alert: #b37b5e;
        }
        /* 全局重置 */
        html,
        body {
            height: 100vh;
            width: 100vw;
            margin: 0;
            padding: 0;
            /* 2026-09-09 CIS 改版：底色從深色換成鵝白（--ui-bg 同色，這裡先寫死
               字面值，因為 :root 變數要等 <style> 解析完才生效，這條規則在最
               前面）——這是捲動快拍snap之間的縫隙、圖片還沒載入前會看到的底色，
               跟疊在照片上的文字/圖示是兩件事（那些因為要蓋在任意深淺照片上，
               維持白字+陰影不變，見 header/footer 與 renderReels() 的說明）。
               文字顏色改回深炭灰，跟底色配對。 */
            background-color: #F9F8F6;
            color: #3E3A36;
            overflow: hidden;
            /* Inter 排第一：拉丁字母/數字（價格、XS/S/M 尺寸標籤、按鈕英文）會自動
               吃到 Inter，中文字元 Inter 沒有對應字符，瀏覽器會自動 fallback 到
               Noto Serif TC——不用另外幫每個數字/價格元素加 class，字型堆疊本身
               就能同時滿足規格書「中文襯線＋英文數字用 Inter」這兩個要求。 */
            font-family: 'Inter', 'Noto Serif TC', serif;
        }
        /* #filtered-list-overlay 裡的 #filtered-reel-container 是同一套「一頁一個
           商品、上下滑動磁吸定格」的瀏覽畫面，本來應該跟 #reel-container 共用同一套
           捲動設定，但這條規則之前只寫給 #reel-container 自己，#filtered-reel-container
           完全沒有 scroll-snap-type 也沒有 -webkit-overflow-scrolling，導致這個畫面
           滑起來只是普通捲動、沒有定格感，在 WebKit 上這種「子層有 scroll-snap-align
           但父層沒設 scroll-snap-type」的不完整設定也是已知會造成捲動卡死/沒反應的
           情境之一。 */
        #reel-container, #filtered-reel-container {
            height: 100vh;
            width: 100vw;
            overflow-y: scroll;
            /* 修正真正的拖拽根因：只設定 overflow-y 沒設定 overflow-x 時，CSS 規範會把
               overflow-x 的計算值從 visible 自動變成 auto（兩者只要有一個不是 visible，
               另一個就不能維持 visible）。Task #19 給圖片加上 perspective +
               transform-style: preserve-3d 之後，3D 變形元素在部分瀏覽器引擎裡回報給
               祖先容器的「可捲動範圍」會比實際畫面顯示的範圍大，於是這個原本一直存在、
               但從來沒東西可以捲動的 overflow-x:auto，第一次真的變成「有東西可以左右拖」，
               這才是 Jeff 在改動前完全不會拖拽、改動後才出現的真正原因——不是圖片元素種類
               的問題，是這個容器一直沒有明確鎖死水平捲動。 */
            overflow-x: hidden;
            overscroll-behavior-x: none;
            scroll-snap-type: y mandatory;
            -webkit-overflow-scrolling: touch;
        }
        section {
            height: 100vh;
            width: 100vw;
            scroll-snap-align: start;
            /* 2026-09-20（Jeff 要求，手機版）：磁吸停止線整體上移 10px——
               scroll-margin-top 用負值讓瀏覽器對齊時，把每張卡片「往上多
               拉 10px」才停下來，效果是畫面實際停在比原本高 10px 的位置
               （最上緣被裁掉一小截，最下緣多露出下一張卡片一小截）。桌面
               版 scroll-snap-type 是 none（見 #reel-container 的桌面規則），
               這個屬性在那邊沒有磁吸可對齊，不會有任何效果，不用另外用
               media query 排除。 */
            scroll-margin-top: -10px;
            flex-shrink: 0;
            position: relative;
        }
        /* 2026-09-14（Jeff 要求，UX 診斷書桌面響應式項目）：手機版「一頁一商品、
           上下滑動磁吸定格」完全不動（<768px 沒有任何改變）；平板/桌面寬螢幕改成
           多欄格狀瀏覽，不要把手機版滿版卡片硬撐到整個寬螢幕（診斷書截圖抓到的
           版面失真問題——圖片、膠囊按鈕整個被拉爆比例）。原本只改 #reel-container
           （主頁商品列表），#filtered-reel-container（收藏/篩選清單那個次要畫面）
           刻意不動；2026-09-20（Jeff 明確要求「我的收藏」PC 版跟首頁一模一樣）
           跟進：下面所有規則的選擇器都加上
           `#filtered-reel-container.wishlist-home-grid`，只有「我的收藏」情境
           （renderReels() 裡的 wishlistHomeMode）會幫 #filtered-reel-container
           加上 `wishlist-home-grid` 這個 class 才會命中，AI 搜尋結果/訂單詳情
           商品瀏覽這兩個其他共用同一個容器的情境沒有這個 class，維持原本刻意
           保守的手機版單欄體驗，不受影響。斷點跟 Jeff 討論過：≥768px 2 欄、
           ≥1024px 4 欄。 */
        @media (min-width: 768px) {
            #reel-container,
            #filtered-reel-container.wishlist-home-grid {
                display: grid;
                grid-template-columns: repeat(2, 1fr);
                /* grid-auto-rows 預設值雖然是 auto，但這裡每張卡片（section）本身有
                   overflow:hidden（下面這條規則設的，圓角卡片需要）——CSS Grid 對
                   overflow 不是 visible 的格子，計算「auto 列軌」高度時是用內容的
                   自動最小尺寸（會被 overflow:hidden 拉成 0），不是實際內容高度，
                   實測會把每一列硬壓成不到 101px、圖片被裁到只剩一條窄縫（Jeff
                   2026-09-14 實測抓到）。明確指定 max-content 改用「內容的偏好
                   尺寸」去算列高，繞開這個 auto 列軌 + overflow:hidden 的組合坑。 */
                grid-auto-rows: max-content;
                gap: 16px;
                height: 100vh;
                width: 100vw;
                overflow-y: auto;
                /* 2026-09-14（Jeff 要求）：拿掉磁吸捲動——桌面格狀版面改成一般
                   自由捲動，不再強制對齊整排的頂端。手機版原本的
                   #reel-container/section 那組 scroll-snap-type/
                   scroll-snap-align 規則是寫在最外層、沒有包在手機專屬的
                   media query 裡（見檔案最前面），等於是全站預設值——這裡
                   一定要「明確設成 none」蓋掉那組預設值，不能只是把桌面
                   這裡原本加的 mandatory/start 刪掉就好，刪掉只是不再「疊加
                   覆寫」，底下全站預設的 mandatory/start 還是會生效，桌面
                   還是會磁吸（這就是第一版只刪不設 none 踩到的坑，實測
                   scrollTop 還是自動跳到磁吸點才發現）。scroll-padding-top
                   也一併拿掉——拿掉磁吸後，padding-top 不會再被 scroll-snap
                   的強制自動捲動「吃掉」（V97.0 那個坑是磁吸機制本身造成的，
                   磁吸拿掉了這個坑也一起不存在），單純的 padding-top 就能
                   正常負責 header 淨空。 */
                scroll-snap-type: none;
                align-content: start;
                /* 2026-09-19（Jeff 回報，4569.png 紅色虛線）：CSS Grid 預設
                   align-items:stretch，同一排卡片會被拉伸到跟最高的那張一樣
                   高——甲型選項少的商品卡片內容比較短，多出來的高度就變成卡片
                   自己白色方框裡的空白，不好看。改 align-items:start：每張卡片
                   只長到自己內容的實際高度，不會被同排最高的卡片撐開；grid
                   track（那一排的軌道高度）還是取最高值，但矮的卡片自己的白框
                   在到達自己內容高度就結束，下面留的是頁面底色而不是卡片內部
                   的空白。 */
                align-items: start;
                padding: calc(env(safe-area-inset-top, 0px) + var(--grid-header-clearance, 76px) + 16px) 16px 24px;
                box-sizing: border-box;
            }
            #reel-container > section,
            #filtered-reel-container.wishlist-home-grid > section {
                position: static;
                height: auto;
                width: auto;
                scroll-snap-align: none;
                flex-shrink: initial;
                border: 1px solid var(--ui-divider);
                border-radius: 16px;
                overflow: hidden;
            }
            /* 手機版拿來閃避固定 header 的每卡片留白，格狀版面改成整個容器只留一次
               （見上面 #reel-container 的 padding-top），每張卡片自己這塊要拿掉，
               不然每張卡片上方都會多一截用不到的空白。 */
            #reel-container > section > .reel-top-spacer,
            #filtered-reel-container.wishlist-home-grid > section > .reel-top-spacer {
                display: none;
            }
            /* 手機版圖片高度是「扣掉剩餘視窗高度」算出來的（卡片=整個螢幕才需要這樣
               算），格狀版面卡片沒有那個限制，交給 aspect-ratio 自己算比例即可。
               2026-09-19（Jeff 決定，跟 Gemini 討論過視覺設計）：桌面 4 欄並排
               4:6 圖片本身視覺效果更好（類似 Zara/Ssense 的 Gallery Grid），
               曾經改成跟手機版統一用 4:6。
               2026-09-20（Jeff 回報，附截圖 777.png，「PC 版跟手機版本來就不
               一樣」）：商品照片實際都是 1:1 正方形（來源就是這樣，見
               scripts/import_shopee_batch.py 的既有結論），contain 塞進 4:6
               這種比較瘦長的容器，會在上下留一截「容器比照片高」的空白
               （letterbox）。算法：容器寬 W，4:6 高度=1.5W，正方形照片用
               contain 只會撐到 W×W，上下留白共 0.5W（各 0.25W）。Jeff 要求
               「各縮短一半」——目標上下留白各剩 0.125W，總留白 0.25W，容器
               新高度＝W(照片)+0.25W(留白)=1.25W，換算比例剛好是 4:5（不是
               湊出來的數字，是照這個公式精確反推的）。這條規則只在桌面
               （≥768px）生效，手機版維持原本的 aspect-[4/6]（Tailwind class，
               寫在 HTML 本身）不受影響——Jeff 這次明確要求 PC 版跟手機版分開
               處理，不再套用 2026-09-19「兩邊統一」的決定。 */
            #reel-container .img-flip-container,
            #filtered-reel-container.wishlist-home-grid .img-flip-container {
                max-height: none;
                aspect-ratio: 4 / 5;
            }
            /* 手機版下方內容區留了一大截 pb-16 是為了閃避固定在螢幕底部的全域動作列
               （加入購物車/快速結帳/愛心/留言/分享），格狀版面改成每張卡片自己有一份
               動作列（見 renderReels() 的 .reel-card-actions），不需要那截留白，
               也不需要手機版那個全域動作列了（見下面 md:hidden）。 */
            #reel-container .reel-card-body,
            #filtered-reel-container.wishlist-home-grid .reel-card-body {
                padding-bottom: 16px;
            }
            /* 2026-09-14（Jeff 要求）：Jeff 實機照片抓到整排卡片還是塞不太下、
               不是切頭就是切尾，判斷是下面這整塊資訊區（價格/名稱/尺寸/甲型/
               數量/購物車按鈕/圖示）太佔空間。Jeff 要求往「壓扁」的方向設計——
               保留全部功能，不拿掉任何東西，改成整體再更緊湊，把版面讓給圖片。
               這裡把 card-body 的內距、字級、pills/按鈕的內距全部往下調一階，
               手機版完全不動（這些新增的 class 選擇器都用 #reel-container/
               #filtered-reel-container.wishlist-home-grid 開頭精準限定範圍，
               手機版沒有這兩個祖先所以不會命中）。 */
            #reel-container .reel-card-body,
            #filtered-reel-container.wishlist-home-grid .reel-card-body {
                padding-left: 16px;
                padding-right: 16px;
                padding-top: 10px;
                gap: 4px;
            }
            #reel-container .reel-card-body > * + *,
            #filtered-reel-container.wishlist-home-grid .reel-card-body > * + * {
                margin-top: 0 !important; /* 蓋掉 Tailwind space-y-1.5 的 margin，改用上面的 gap 統一控制間距 */
            }
            #reel-container .reel-card-price span,
            #filtered-reel-container.wishlist-home-grid .reel-card-price span {
                font-size: 13px;
                line-height: 1.3;
            }
            #reel-container .reel-card-name,
            #filtered-reel-container.wishlist-home-grid .reel-card-name {
                font-size: 14px;
                line-height: 1.3;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            #reel-container .reel-card-specs,
            #filtered-reel-container.wishlist-home-grid .reel-card-specs {
                padding-top: 0;
                display: flex;
                flex-direction: column;
                gap: 4px;
            }
            #reel-container .reel-card-specs > * + *,
            #filtered-reel-container.wishlist-home-grid .reel-card-specs > * + * {
                margin-top: 0 !important;
            }
            #reel-container .reel-card-pill,
            #filtered-reel-container.wishlist-home-grid .reel-card-pill {
                padding-top: 3px;
                padding-bottom: 3px;
                font-size: 12px;
            }
            #reel-container .reel-card-qty-box,
            #reel-container .reel-card-qty-btn,
            #filtered-reel-container.wishlist-home-grid .reel-card-qty-box,
            #filtered-reel-container.wishlist-home-grid .reel-card-qty-btn {
                height: 24px;
            }
            #reel-container .reel-card-qty-btn,
            #filtered-reel-container.wishlist-home-grid .reel-card-qty-btn {
                width: 24px;
            }
            #reel-container .reel-card-actions,
            #filtered-reel-container.wishlist-home-grid .reel-card-actions {
                padding-top: 4px;
            }
            #reel-container .reel-card-actions button,
            #filtered-reel-container.wishlist-home-grid .reel-card-actions button {
                padding-top: 4px;
                padding-bottom: 4px;
            }
            /* 2026-09-14（Jeff 要求，實機照片抓到按鈕文字擠到換行）：收藏/留言/
               分享圖示改成直向排在右側（跟手機版一樣），從原本跟購物車/結帳
               按鈕同一橫排搬到獨立的 .reel-card-icons 直欄，見上面 renderReels()
               的版面重排——讓兩顆按鈕獨占整行寬度，不會再被三個圖示擠到文字
               換行。這欄寬度只需要放得下圖示本身＋數字，用固定寬度、跟左欄
               （規格+按鈕）等高、三個圖示垂直平均分布（justify-around）。
               2026-09-20（Jeff 回報，附截圖 777.png，首頁+我的收藏都要改）：
               圖示放大 0.5 倍（16px→24px）、整欄往上移 50px（transform，不
               佔版面空間、不影響旁邊規格欄的高度計算）、圖示間距加大 20px
               （2px→22px）。寬度從 32px 放寬到 40px 配合放大的圖示（24px
               置中還有餘裕，不會貼邊）；文字（收藏/留言數字、分享文字）
               跟著等比例放大到 15px（10px×1.5），維持圖示+文字比例協調——
               Jeff 沒有明講文字要不要跟著放大，這裡判斷兩者要一起變、不然
               大圖示配小字會不協調。 */
            #reel-container .reel-card-icons,
            #filtered-reel-container.wishlist-home-grid .reel-card-icons {
                width: 40px;
                gap: 22px;
                transform: translateY(-50px);
            }
            #reel-container .reel-card-icons svg,
            #filtered-reel-container.wishlist-home-grid .reel-card-icons svg {
                width: 24px;
                height: 24px;
            }
            #reel-container .reel-card-icons span,
            #filtered-reel-container.wishlist-home-grid .reel-card-icons span {
                font-size: 15px;
            }
        }
        @media (min-width: 1024px) {
            #reel-container,
            #filtered-reel-container.wishlist-home-grid {
                /* 2026-09-14（Jeff 要求）：3 欄改 4 欄——欄數變多、每欄變窄，圖片
                   （寬度驅動 aspect-ratio 算高度）跟著變矮，整排總高度跟著降低，
                   從另一個角度緩解桌面格狀版面「整排太高」的問題，不用再犧牲
                   資料卡的完整性（尺寸/甲型/數量/購物車按鈕都保留，V97.0 剛復原
                   回來的）。 */
                grid-template-columns: repeat(4, 1fr);
                max-width: 1440px;
                margin: 0 auto;
            }
        }
        /* 2026-09-20：「我的收藏」桌面格狀版面（wishlist-home-grid）裡每張卡片
           自己就有加入購物車/收藏/留言/分享（跟主 feed 一樣，見 renderReels()
           的 wishlistHomeMode），#filtered-list-overlay 原本那條固定在螢幕底部
           的全域「加入購物車」footer 這時候會變成疊在整排卡片最下面、跟卡片
           自己的按鈕重複，桌面版藏起來。手機版不受影響（<768px 沒有這條
           規則）——手機版卡片自己的動作列本來就是 hidden md:flex（跟主 feed
           同一份 class），這個 footer 手機上還是唯一的加入購物車入口，跟
           AI 搜尋結果/訂單詳情商品瀏覽（沒有 wishlist-home-grid 這個 class，
           不受這條規則影響）行為一致。 */
        @media (min-width: 768px) {
            #filtered-reel-container.wishlist-home-grid ~ footer,
            #filtered-reel-container.wishlist-home-grid ~ #filtered-global-icons {
                display: none;
            }
        }
        /* 2026-09-20（Jeff 回報，PC 版「我的收藏」底色露出黑色）：
           #filtered-list-overlay 本身是 bg-black（不透明純黑，不是半透明
           的 backdrop）——這是手機版單欄「一次一張滿版照片」沉浸式瀏覽
           （AI 搜尋結果/訂單詳情商品瀏覽都還在用這個效果，故意的，跟
           IG/抖音同一種「黑底片框」美術風格）的既有設計。桌面格狀版面
           每排卡片之間有間距、商品數量不夠多時下面還會留一大片空白，
           這片黑底就會大面積露出來，跟全站其他頁面用的系統底色
           （var(--ui-bg)，米白）不一致，看起來像沒套上樣式。只有「我的
           收藏」（openWishlistBrowse() 開的）需要改成系統底色，AI 搜尋
           結果/訂單詳情商品瀏覽維持原本的黑底沉浸式效果不變——用
           wishlist-home-grid 這個 class（openWishlistBrowse()/
           closeFilteredProductList() 同時加在 #filtered-reel-container
           跟這裡的 #filtered-list-overlay 本身）精準區分。只在桌面
           （≥768px）生效，手機版「我的收藏」維持原本黑底的沉浸式瀏覽
           效果不變。 */
        @media (min-width: 768px) {
            #filtered-list-overlay.wishlist-home-grid {
                background-color: var(--ui-bg);
            }
        }
        /* 2026-09-20（Jeff 回報，PC 版，附截圖 777.png 才真正定案）：漢堡選單
           底下 9 個抽屜（我的折價券/我的訂單/通知中心/測量尺寸/黏貼教程/
           關於我們/隱私退貨服務政策等，見 V128.54）的標題／關閉 X 版面。
           截圖標出兩條線：「中心線」＝整個頁面水平中線（標題應該對齊這
           條線，真正置中，不是只在 66% 內容欄裡靠左）；「右邊邊線」＝
           66% 內容欄（跟下面內容區 md:max-w-[66%] md:mx-auto 同一個欄位）
           自己的右邊界，剛好對齊下面卡片內容的右緣——X 應該對齊這條線，
           不是離標題固定幾百 px（前兩輪 500px 的方向本身就不對，Jeff
           這輪直接用截圖劃線定案，取代之前的文字描述）。
           做法（第一版）：X 用 position:absolute 相對標題列自己
           （.drawer-close-btn-row 設 position:relative，right:0）。
           Jeff 實測回報：標題置中生效了，但 X 完全沒出現在畫面上任何
           地方（不是位置不對，是整個消失）——研判標題列這個 flex 容器
           當成 X 的 containing block 這條路徑有問題（原因不明，這個
           環境沒有瀏覽器可以實際除錯）。改成相對「面板本身」定位
           （#X-content 這幾個面板都已經有 Tailwind 的 transform class
           做開合動畫，V128.55 那一輪已經證實這條路徑真的會顯示出東西，
           只是當時位置抓在整個面板右上角、不是內容欄右邊界）：
           `right:17%`——內容欄寬度是面板的 66%，置中時左右各留
           (100%-66%)/2=17% 的空白，所以「面板右緣往內縮 17%」精確等於
           「66% 內容欄自己的右邊界」，效果跟目標一致，但用的是已經驗證
           可行的定位路徑，不是這次沒顯示東西的路徑。
           第二版：Jeff 附截圖標出 X 這次左右方向對了，但整顆「掉到下面
           去了」，明顯比標題矮一截。抓到自己的算錯：`top` 算式多加了
           1.5rem（誤以為 p-6 的 24px 上內距會跟 inline style 設的
           padding-top 疊加），但 inline style 設定的 padding-top
           會直接蓋掉 class 設的 padding-top（同一個屬性，inline 優先權
           higher，不是疊加），標題列的真正頂端就是
           `var(--grid-header-clearance,76px)`，不需要再加 1.5rem。改成
           `top: calc(var(--grid-header-clearance,76px) + 14px)`
           （14px＝標題 text-xl 那行文字行高的一半，讓 X 對齊文字垂直
           中心，不是對齊文字最頂端）+ `transform:translateY(-50%)`
           讓按鈕自己的高度也置中對齊這條線，不用再猜按鈕本身多高。只在
           桌面（≥768px）生效，手機版標題/X 維持原本同一行、左右排列的
           樣子不變。 */
        @media (min-width: 768px) {
            .drawer-close-btn-row {
                justify-content: center;
            }
            .drawer-close-btn {
                position: absolute;
                right: 17%;
                top: calc(var(--grid-header-clearance, 76px) + 14px);
                transform: translateY(-50%);
                z-index: 10;
            }
        }
        .no-scrollbar::-webkit-scrollbar {
            display: none;
        }
        .no-scrollbar {
            -ms-overflow-style: none;
            scrollbar-width: none;
        }
        /* 2026-09-09：Jeff 指定拿掉這個陰影（他稱之為「煙燻特效」），即使已經
           跟他說明過這是疊在照片上的白字唯一的可讀性安全網（深色商品照片上沒
           陰影會讀不清楚，這輪對話一開始的 A/B 對比截圖就是為了證實這件事）
           ——他聽完解釋後仍要求拿掉，這是他知情後的決定，不是誤解。拿掉後
           class 名稱在語意上有點失真（叫 text-outline 卻不再有描邊/陰影），
           先保留 class 名稱不動（15 處引用都在 renderReels()／header／footer／
           filtered-list-overlay，牽動範圍大，改名沒有實質好處），只清空這條
           規則本身。 */
        .text-outline {
            text-shadow: none;
        }
        /* 2026-09-09：讚按鈕的彈跳微互動（Jeff 的導購建議），bounceHeart() 觸發。 */
        @keyframes heartPop {
            0% { transform: scale(1); }
            35% { transform: scale(1.35); }
            100% { transform: scale(1); }
        }
        .anim-heart-pop {
            animation: heartPop 380ms cubic-bezier(0.34, 1.56, 0.64, 1);
        }
        /* 真正找到的當機根因：pointer-events:none 一般會正常繼承給所有子孫
           元素，但 .img-flip-container 因為設了 perspective（3D 變形容器），
           會被瀏覽器獨立提升成合成層，導致子孫元素的 pointer-events 沒有
           乖乖繼承外層 #filtered-list-overlay 關閉時設的 none——變成「看
           不見但摸得到」，會在畫面上 z-index 更低的東西（例如主畫面左上角
           的漢堡選單按鈕）上方無聲攔截點擊。AI 搜尋原本會回到 cs-modal
           （z-50，比 filtered-list-overlay 的 z-45 高，蓋得住），所以這個
           漏洞一直沒被踩到；訂單詳情／收藏夾這兩個新入口關到底會回到主畫面
           的 header（z-40，比 filtered-list-overlay 低），才第一次真的被
           踩中——點擊實際上被這層隱形但還在接收事件的圖層攔走了，不是
           JS 掛掉，是點擊根本沒送到漢堡選單按鈕上。這裡直接用更高優先度
           的規則強制蓋掉，不依賴繼承鏈。 */
        #filtered-list-overlay.pointer-events-none,
        #filtered-list-overlay.pointer-events-none * {
            pointer-events: none !important;
        }
        /* 商品圖片翻頁效果（Task #19）：左右滑動換圖時，舊圖以 3D 翻轉方式「像翻書頁」
           一樣掀開，新圖片一開始就墊在舊圖下方，翻轉過程中逐漸顯露。 */
        .img-flip-container {
            perspective: 1200px;
            -webkit-perspective: 1200px;
            /* Task #20：只把垂直方向的手勢交給瀏覽器原生處理（維持既有的上下滑動換商品），
               水平方向完全交給我們自己的 touchstart/touchend 判定，不讓瀏覽器對水平觸控
               做出任何自己的原生反應（例如把整個容器當成可拖動區域）。 */
            touch-action: pan-y;
        }
        .product-img,
        .product-img-incoming {
            position: absolute;
            inset: 0;
            /* Task #20 曾用 draggable=false / -webkit-user-drag:none 處理 <img> 原生拖曳，
               但 PO 實機測試回報行動瀏覽器上仍會出現拖曳跟隨。Task #21：改用 CSS
               background-image 的 <div> 取代 <img>——<div> 本身完全沒有「原生可拖曳圖片」
               這個行為，從機制上根除問題，不再只是用屬性/CSS 去抑制它。以下仍保留防禦性設定，
               雙重保險。 */
            -webkit-user-drag: none;
            user-drag: none;
            -webkit-touch-callout: none;
            user-select: none;
            -webkit-user-select: none;
            -webkit-tap-highlight-color: transparent;
            pointer-events: auto;
            touch-action: pan-y;
            width: 100%;
            height: 100%;
            /* 2026-09-19（Jeff 決定，第二輪：桌面格狀版面也統一改不裁切）：
               原本桌面 cover／手機 contain 分開處理，是因為桌面 4 欄需要
               每張卡片統一高度對齊——但裁不裁切跟「要不要對齊」無關，對齊
               靠的是外層 grid 的列高計算（aspect-ratio 固定寫死在
               .img-flip-container，跟這裡的 background-size 無關），改成
               contain 不會影響對齊。桌面/手機統一用 contain：素材比例不合
               時不裁切，留白處用下面的鵝白底色。 */
            background-size: contain;
            background-position: center;
            background-repeat: no-repeat;
            /* 2026-09-09 CIS 改版：圖片還沒載入完成前的底色，從深色換成鵝白。
               2026-09-19：曾經試過改成「虛化玻璃墊底」（V128.30/32），中間
               踩到兩輪 stacking context bug（見 engineering_logs V128.34/35）
               才修好，但 Jeff 實際看過修好後的效果，覺得純色底更乾淨，決定
               拿掉整個虛化玻璃功能，維持這裡最簡單的純色（V128.36）。 */
            background-color: #F9F8F6;
            backface-visibility: hidden;
            -webkit-backface-visibility: hidden;
        }
        /* 首圖是影片時（見 card-video-wrap，2026-09-19 新增）跟上面 .product-img
           同一個不裁切邏輯，只是影片用 object-fit 不是 background-size——
           原本直接寫 Tailwind 的 object-cover class，改成自己的 class 避免
           跟其他規則的特異度互相打架。桌面/手機統一 contain，不再分開。 */
        .card-video-media {
            object-fit: contain;
            background-color: #F9F8F6;
        }
        .product-img-incoming {
            z-index: 1;
        }
        .product-img {
            z-index: 2;
            transform-style: preserve-3d;
            -webkit-transform-style: preserve-3d;
            transform: rotateY(0deg);
            -webkit-transform: rotateY(0deg);
            transition: transform 350ms cubic-bezier(0.4, 0, 0.2, 1),
                        opacity 350ms ease,
                        box-shadow 350ms ease;
        }
        .product-img-flipping-out.flip-dir-next {
            transform-origin: left center;
            -webkit-transform-origin: left center;
            transform: rotateY(-95deg);
            -webkit-transform: rotateY(-95deg);
            opacity: 0;
            box-shadow: 8px 0 24px rgba(0, 0, 0, 0.5);
        }
        .product-img-flipping-out.flip-dir-prev {
            transform-origin: right center;
            -webkit-transform-origin: right center;
            transform: rotateY(95deg);
            -webkit-transform: rotateY(95deg);
            opacity: 0;
            box-shadow: -8px 0 24px rgba(0, 0, 0, 0.5);
        }
        @keyframes bounceSlow {
            0%,
            100% {
                transform: translateY(0);
            }
            50% {
                transform: translateY(-6px);
            }
        }
        .anim-bounce {
            animation: bounceSlow 3s ease-in-out infinite;
        }
        .double-strike-1pt {
            text-decoration: line-through double;
            text-decoration-thickness: 1px;
            text-decoration-skip-ink: none;
            text-underline-position: under;
        }
        /* 自定義滾動條（購物車等） */
        .custom-scroll::-webkit-scrollbar {
            width: 3px;
        }
        .custom-scroll::-webkit-scrollbar-track {
            background: transparent;
        }
        .custom-scroll::-webkit-scrollbar-thumb {
            background: var(--ui-divider);
            border-radius: 10px;
        }
        /* 2026-09-14（Jeff 要求）：結帳頁商品清單（直向）／優惠券票券帶（橫向）
           常駐提示拉桿——手機瀏覽器的原生捲軸通常只在滑動當下短暫浮現，平常
           看不到，客人容易沒發現「還可以繼續滑」。這條拉桿改成常駐顯示、由
           JS（updateCartScrollbar()/updateVoucherScrollbar()）依實際捲動位置
           更新粗細跟位置，純視覺提示，不能拖拉操作，不是真的捲軸。內容沒有
           溢出（例如只有 1、2 件商品／1 張券）時整條隱藏（靠 [hidden] 屬性）。*/
        .cart-scrollbar-track {
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            width: 4px;
            background: var(--ui-divider);
            border-radius: 999px;
        }
        .cart-scrollbar-thumb {
            position: absolute;
            left: 0;
            width: 100%;
            border-radius: 999px;
            background: var(--ui-accent);
        }
        .voucher-scrollbar-track {
            position: relative;
            height: 4px;
            margin-top: 6px;
            background: var(--ui-divider);
            border-radius: 999px;
        }
        .voucher-scrollbar-thumb {
            position: absolute;
            top: 0;
            height: 100%;
            border-radius: 999px;
            background: var(--ui-accent);
        }
        /* 「加入主畫面」全螢幕模式（viewport-fit=cover）底下，瀏海/動態島跟底部
           手勢列的安全區域在一般 Safari 分頁瀏覽時 env() 值是 0（Safari 自己的
           工具列已經佔掉那塊空間），只有真的用主畫面圖示開啟、沒有瀏覽器介面時
           env() 才會回傳非零值，所以這兩個 class 平常不影響現有版面，只在全螢幕
           模式下才會把 header/footer 往安全區域內推。 */
        .pt-safe {
            padding-top: calc(1rem + env(safe-area-inset-top, 0px));
        }
        .pb-safe {
            padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
        }
        /* 商品詳情頁(PDP)字體＋色碼別名——顏色本體已經升級成全站 :root 的 --ui-*
           token（見上面），這裡只是把 --pdp-* 這幾個舊變數名指到同一份顏色，
           pdp.js 裡原本寫的 --pdp-* class 才不用整批重寫。 */
        #pdp-overlay {
            --pdp-bg: var(--ui-bg);
            --pdp-surface: var(--ui-surface);
            --pdp-divider: var(--ui-divider);
            --pdp-text: var(--ui-text);
            --pdp-text-muted: var(--ui-text-muted);
            --pdp-accent: var(--ui-accent);
            --pdp-accent-soft: var(--ui-accent-soft);
            font-family: 'Inter', 'Noto Serif TC', serif;
            background: var(--pdp-bg);
            color: var(--pdp-text);
        }
        #pdp-overlay h1,
        #pdp-overlay h2,
        #pdp-overlay h3,
        #pdp-overlay .pdp-serif {
            font-family: 'Playfair Display', 'Noto Serif TC', serif;
        }
        /* 2026-09-15：height 原本寫在 #pdp-hero-wrap 的 inline style="" 屬性
           裡。開合動畫（pdp.js 的 openProductDetail/closeProductDetail）會
           暫時用 heroWrap.style.height 蓋掉這個值做共享元素轉場，動畫結束
           後再用 heroWrap.style.height='' 清掉——但這樣清除的是「JS 曾經
           設過的 inline 值」，不會神奇地變回原本 HTML attribute 寫的那個
           60vh，因為 JS 第一次賦值時就已經把 inline style 改寫掉了，原始
           字串沒有被保留在任何地方，清空後屬性會直接消失，圖片高度整個
           跑掉。改成寫在這裡的 CSS 規則（層疊優先權比 inline style 低，
           動畫期間的 JS inline 值可以正常覆蓋），JS 清除 inline style 後
           層疊會正確地「還給」這條規則接手，不會遺失原始數值。
           2026-09-20（V128.43 桌面兩欄改版）：淨空（margin-top）原本也寫
           在這裡，現在移到 #pdp-top-grid（見下方），讓桌面版左右兩欄從
           同一條起始線開始，不是只有左欄（圖片）被推下去、右欄（選購
           資訊）沒有——heroWrap 本身不再需要知道頭部淨空這件事，開合動畫
           也因此不用再暫時歸零 marginTop（見 pdp.js 對應清理）。 */
        /* 2026-09-19（Jeff 要求，第三輪定案）：不要用固定高度（60vh）或固定
           比例（aspect-ratio:4/6）的框再去 object-fit 塞圖片/影片——不管
           cover（會裁切）還是 contain（會留白空隙），本質上都是「先決定
           一個跟內容無關的框，再想辦法把內容塞進去」。改成框本身完全交給
           內容決定：img/video 用 width:100%、height:auto（見下方 img/video
           的 class），瀏覽器會自動用素材自己真實的長寬比例算出高度，寬度
           永遠貼滿容器兩側、不會留白也不會裁切；height:auto 的元素撐開
           relative 容器 #pdp-hero-wrap，容器高度=內容高度，不用另外設。
           副作用（已知、接受）：素材還沒載入完成前瀏覽器不知道比例，容器
           會短暫塌成 0 高度，直接背景色（下面 bg-[var(--pdp-surface)]）
           墊著，載入完成後跳到正確高度——比起裁切掉細節或留白太多，這個
           取捨 Jeff 這輪明確选了後者。 */
        /* 2026-09-20（Jeff 要求，附截圖 444.png，V128.43）：PC 版商品詳情頁
           改成仿照參考圖的兩欄式版面——左欄圖片/影片+縮圖列、右欄選購
           資訊+加入購物車/快速結帳（原本手機版那條固定在螢幕底部的按鈕列
           在桌面版隱藏，改成右欄裡的一般按鈕，見下面 #pdp-buybox-cta 與
           footer 的 md:hidden）。手機版（<768px）完全不受影響：grid 屬性
           只在 md: 生效，預設是普通的單欄由上往下排列，跟改版前一模一樣。
           右欄用 sticky 讓選購資訊/按鈕在往下捲動看商品說明時持續可見
           （常見電商 PDP 慣例，Jeff 這次沒有明確要求但也沒有反對，先這樣
           做，如果 Jeff 覺得不需要黏著可以再拿掉）。 */
        #pdp-top-grid {
            margin-top: calc(env(safe-area-inset-top, 0px) + var(--grid-header-clearance, 76px));
        }
        /* 2026-09-20（Jeff 回報，附截圖 111.png，V128.45）：圖片欄跟右欄
           （選購資訊）中間空隙太大。根因：V128.44 把 #pdp-media-col 這個
           「內容」限制成 md:max-w-[460px]，但它所在的 grid 軌道本身還是
           minmax(0,1fr)（可能撐到 600px+），圖片靠左對齊、軌道剩下的空間
           變成看不見的留白，疊加上明訂的 gap，兩欄視覺上就隔了一大段。
           改法：不是把軌道也寫死限制寬度（試過 min(1fr,460px)，結果整條
           grid-template-columns 因為 fr 不能當 min()/max()/calc() 的參數
           而判定成無效值，整條規則被瀏覽器忽略、退回單欄直式排版——Jeff
           2026-09-20 稍晚回報「整個變直式了」抓到，見 666.png，已改回這個
           版本），改成單純把「容器」的 max-width 從 1040px 收到 888px
           （460+340+2.5rem gap+左右各 1.5rem padding，剛好等於兩欄真正
           要吃到的總寬度）——維持軌道原本合法的 minmax(0,1fr)，讓 1fr
           在容器頂到 888px 上限時自然算出剛好 460px（不多不少，因為容器
           寬度本來就是照這個總和反推出來的），完全不用動用 min()，也就
           不會有 fr-in-min() 這個 CSS 陷阱；容器比 888px 窄時（768px 附近）
           1fr 一樣會自然縮小，不會溢出。#pdp-media-col 原本的
           md:max-w-[460px] class 已經在上一輪移除，維持不變即可。 */
        @media (min-width: 768px) {
            #pdp-top-grid {
                max-width: 888px;
                margin-left: auto;
                margin-right: auto;
                padding-left: 1.5rem;
                padding-right: 1.5rem;
                display: grid;
                grid-template-columns: minmax(0, 1fr) 340px;
                gap: 2.5rem;
                align-items: start;
            }
            #pdp-buybox-col {
                position: sticky;
                top: calc(env(safe-area-inset-top, 0px) + var(--grid-header-clearance, 76px) + 1rem);
            }
        }
        /* 2026-09-20（Jeff 回報，附截圖 555.png，V128.44；888px 數字於
           V128.45 隨 #pdp-top-grid 同步調整）：下面「商品說明/尺寸怎麼量/
           怎麼黏貼」置中欄要拉寬到「左：圖片左緣往內縮 100px／右：快速
           結帳按鈕右緣往內縮 100px」——圖片左緣＝#pdp-top-grid 的內容左
           邊界，快速結帳按鈕右緣（flex-1 撐滿右欄寬度）＝#pdp-top-grid
           的內容右邊界，兩者剛好等於 #pdp-top-grid 本身的
           padding-box（前提是兩欄軌道真的吃滿容器寬度，沒有留白——見上面
           V128.45 的軌道公式）。所以 #pdp-content 套用「跟 #pdp-top-grid
           完全一樣的 max-width + margin:auto 置中公式」，只是 padding
           多加 100px，數學上精確等於「頂部兩欄區塊的左右邊界，各往內縮
           100px」，不是憑感覺抓一個寬度數字。 */
        @media (min-width: 768px) {
            #pdp-content {
                max-width: 888px;
                margin-left: auto;
                margin-right: auto;
                padding-left: calc(1.5rem + 100px);
                padding-right: calc(1.5rem + 100px);
            }
        }
        /* 2026-09-15：Jeff 要求返回鍵直接壓在商品圖容器右上角，不拉標題列。
           #pdp-back-btn 是 position:fixed（不隨內容捲動），手機版圖片
           w-full 貼滿螢幕寬，right 直接貼螢幕右邊界即可。
           2026-09-20（V128.43，888px 數字於 V128.45 隨 #pdp-top-grid 同步
           調整）：桌面版改兩欄式之後，沒辦法再像舊版單欄那樣用固定寬度
           精算出圖片右緣算出精確對齊——退而求其次，對齊整個兩欄內容區
           （#pdp-top-grid，固定 max-width 置中）的右緣，位置落在右欄
           （選購資訊）右上角，一樣醒目好按，只是不再是「貼在圖片正
           上方」。用 max() 保底，避免 md 斷點剛過（768px，比容器寬度窄）
           時算出負值把按鈕推出螢幕外。 */
        #pdp-back-btn {
            right: 1.25rem;
        }
        /* 2026-09-19（Jeff 回報，附截圖）：主圖是影片時，手機原生播放器自己
           會在右上角疊一顆音量圖示（不同瀏覽器位置/樣式不受我們控制，是
           UA 原生 UI），跟這顆返回鍵疊在一起，兩顆幾乎沒辦法分開點。播影片
           時把返回鍵往下推，閃開原生控制列通常會出現圖示的那個高度區間；
           inline style 設的 top 優先權比一般 class 規則高，這裡要 !important
           才蓋得過去（見 pdpShowHeroMedia() 切換 .pdp-video-active）。 */
        #pdp-back-btn.pdp-video-active {
            top: calc(env(safe-area-inset-top, 0px) + var(--grid-header-clearance, 76px) + 0.75rem + 56px) !important;
        }
        @media (min-width: 768px) {
            #pdp-back-btn {
                right: max(1.5rem, calc((100vw - 888px) / 2 + 1.5rem));
            }
        }
        /* 2026-09-16：隱私權政策/服務條款/退貨政策三份法律文件內容較長，
           用一組共用 class 統一排版，不用每個段落都手動加 Tailwind class
           （三份文件加起來近三百行，逐行加 class 太雜）。*/
        .policy-doc-body {
            font-size: 0.95rem;
            line-height: 1.8;
            color: var(--ui-text);
        }
        .policy-doc-body .policy-meta {
            font-size: 0.8rem;
            color: var(--ui-text-muted);
            margin-bottom: 1.25rem;
        }
        .policy-doc-body h2 {
            font-size: 1.05rem;
            font-weight: 600;
            color: var(--ui-accent);
            margin-top: 1.75rem;
            margin-bottom: 0.6rem;
        }
        .policy-doc-body h2:first-of-type {
            margin-top: 0;
        }
        .policy-doc-body p {
            margin-bottom: 0.85rem;
        }
        .policy-doc-body ul,
        .policy-doc-body ol {
            margin-bottom: 0.85rem;
            padding-left: 1.35rem;
        }
        .policy-doc-body ul {
            list-style: disc;
        }
        .policy-doc-body ol {
            list-style: decimal;
        }
        .policy-doc-body li {
            margin-bottom: 0.4rem;
        }
        .policy-doc-body strong {
            color: var(--ui-text);
            font-weight: 700;
        }
        .policy-doc-body em {
            color: var(--ui-text-muted);
            font-style: normal;
        }
    

/* 2026-10-22 Toast 毛玻璃化（Jeff：彈窗不要一坨，做透明感、美化）：半透明白＋模糊＋細邊＋柔影，
   左側圖示、上標題下副標；點整個提示即關閉；由上方輕滑入。 */
.toast-glass {
  position: fixed; top: calc(env(safe-area-inset-top, 0px) + 72px); left: 50%;
  transform: translate(-50%, -10px); z-index: 60;
  display: flex; align-items: center; gap: 12px;
  width: max-content; max-width: min(92vw, 360px);
  padding: 12px 18px 12px 14px; border-radius: 18px;
  background: rgba(255, 255, 255, 0.82);
  -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6);
  border: 1px solid rgba(255, 255, 255, 0.65);
  box-shadow: 0 8px 28px rgba(60, 45, 40, 0.18), 0 1px 0 rgba(255, 255, 255, 0.5) inset;
  color: var(--ui-text); cursor: pointer;
  transition: opacity 0.28s ease, transform 0.28s ease;
}
.toast-glass:not(.opacity-0) { transform: translate(-50%, 0); }
.toast-icon {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 9999px; display: none;
  align-items: center; justify-content: center; font-size: 15px; line-height: 1; font-weight: 600;
  background: var(--ui-accent); color: #fff;
}
.toast-icon.show { display: inline-flex; }
.toast-icon.info { background: rgba(60, 45, 40, 0.55); }
.toast-body { min-width: 0; }
.toast-title { font-size: 16px; font-weight: 600; line-height: 1.35; letter-spacing: 0.01em; }
.toast-sub { font-size: 14px; line-height: 1.35; color: var(--ui-text-muted); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
/* 2026-10-29（Jeff 手機回報）：整站只有亮色主題；原本的深色模式覆寫只換了底色、沒換文字色，
   深色模式手機上變成「深底深字」整個看不見——移除，一律用亮色毛玻璃（底色再加深一點不透明度保可讀）。 */
