/* ============================================================
   VITRINE 主题 — 橱窗营销 · 摄影前置（近白 + 纯黑 CTA + 金色次强调，双模式）
   对标：bloomthis.co（马来西亚高端花店，同日达电商 / 高频礼赠 / 多场景选购）
   结构：三段式（契约见 themes/_shared/README.md §1）
     段1 token 覆盖 —— 换肤核心，功能页（商品/购物车/结账/付款/搜索/登录/会员中心）
         继承 florisse EJS + 基底 CSS，仅靠本段变量整体转黑白极简
     段2 nav / footer —— vt- 前缀（防污染，仿 florisse 的 fl- 惯例）；本次任务留空占位
     段3 首页 —— 大图 hero + 营销区块；本次任务留空占位
   视觉性格：大留白、近白页底、纯白卡片、发丝级分隔线、近黑 CTA、金色只做次要强调、
     圆角压到 2–4px（编辑风偏直角，胶囊 pill 仍保 980px 让徽章保持圆形）。

   ⚠️ vitrine 是「反转主题」（README §7 末条）——判断理由：
     cartToast 底色 --rose-ink 亮色是近黑 #111111、暗色必须翻成近白 #F2F0ED
     （暗色页底本身就是近黑，toast 若仍用近黑会与页底糊成一片、失去浮层层次），
     即 toast 底跨明暗发生了明度反转。故骑在它上面的 --on-accent（文字色）与
     --btn-close-filter（Bootstrap 关闭钮滤镜）必须成对、同块同步翻转：
     深底 → 白字 + 白钮 invert 滤镜；浅底 → 深字 + 黑钮 none。
     只在 :root 放一次的话，切暗色时关闭钮跌回亮色的 invert 白钮，压在浅色 toast 上
     对比度约 2:1 近乎不可见（手测最难发现的一类）。样板：zen。
   自引入色统一走 --vt-* 局部变量：
     · --vt-hairline（比 --line 更淡的发丝分隔线，段2/段3 消费）→ 随明暗变，四块各定义一次；
     · --vt-accent-hover（主按钮/链接 hover 底）→ 随明暗变，四块各定义一次；
     · --vt-hover-surface（列表项/幽灵钮/步进器的软底 hover 面）→ 随明暗变，四块各定义一次；
     · --vt-on-gold（金色 --rose 底上的字色）→ mode-invariant，只放 :root 一处：
       金色明暗虽不同(#C8A96A / #D4B87C)，但近黑字 #111111 骑在两种金上都够可读，故前景取单值。
   ============================================================ */

/* ========== 段1：token 覆盖（四个选择器块缺一不可，见 README §5） ========== */

/* —— ① 亮色默认 + mode-invariant token（近白页底 / 纯白卡 / 近黑 CTA / 金色次强调） —— */
:root {
  --content-max: 1240px; /* 内容区最大宽：比基底 1180 略放宽，给「大图摄影前置」的通栏视觉留呼吸 */
  --wrap-max: var(--content-max);
                         /* 让【继承来的】功能页外壳 .fl-wrap（搜索页、以及 index 和 product 补齐前继承的 florisse 版）
                            与 vt- 自绘的 nav 和 footer 同宽。不覆写就吃基底回退值 1120px，
                            比 vt-nav 的 1240px 窄 120px，滚到功能页时左右边缘会明显错台。
                            mode-invariant：--content-max 只在本 :root 定义一次，故此处也只需一处。 */
  --nav-bw: 1px;         /* .vt-nav 的 border-bottom 宽 —— **单一真源**。
                            这 1px 有三个消费方，写死在任一处都会与另两处失联（改一处不报错、只错位）：
                              ① .vt-nav 自己的 border-bottom-width（段2）
                              ② .vt-nav-inner 的 height: calc(var(--nav-h) - var(--nav-bw))（段2）
                              ③ .apple-mobile-menu 的 top: calc(100% + var(--nav-bw))（段2）——
                                 绝对定位的包含块是 .vt-nav 的 **padding box**，不含 border-bottom，
                                 所以 top:100% 会落在 nav 下沿**之上** 1px（重叠 1px）。
                            前科：上一轮只改了 --nav-h 没改 ②，nav 变 66px、错位从 1px 放大到 2px。
                            现在三处同读一个 token → 改边框宽只需改这一行。 */
  /* —— 顶栏两行高（Phase 4 Task 4.2）——
     对标站 bloomthis.co 实测：header **两层 108px**，第 1 行 logo 居中 + 账户/搜索/购物袋靠右，
     第 2 行 11 个主项（1440 下跨 138→1287）。原先我们是单层 65px。
     ⚠️ 拆成两个 token 而不是「把 --nav-h 直接写成 108」：第 2 行在手机端整个 display:none
        （导航折进汉堡菜单），那时 nav 真的变矮 —— 只有把两行分开，--nav-h 才能自动跟着变。
        写死 108 会让手机端所有读 --nav-h 的地方（手机菜单高度、sticky 偏移…）多让出 43px。 */
  --nav-row1-h: 64px;    /* 第 1 行：品牌 + 动作簇（= 改动前 .vt-nav-inner 的内容高，视觉不变） */
  --nav-row2-h: 44px;    /* 第 2 行：主导航。手机端下面的媒体查询把它归零 */
  /* 第 3 行：分类条。高度直接读**共享片段**在 :root 上给的 --ns-catbar-h
     （_shared/_category-bar 的 <style> 里，且只在那一行真渲染时才输出）——
     所以「没有分类条」时自动落回兜底 0，不再需要服务端往 <html> 上打 has-catbar 标记。
     ⚠️ 必须翻在 :root 上，不能翻在 nav 上：--nav-h 在 :root 处就把几个 var 代入算完了，
        在后代元素上改分行高，--nav-h 一动不动 —— 表现是「行画出来了、偏移全错」且不报错。
     ⚠️ 这条链是 hero 负边距的依据：--nav-h = 行1+行2+本行+边框，
        hero 靠 margin-top:-nav-h 顶到页面最上方。断了就是 hero 与顶栏之间空一条。 */
  --nav-row3-h: var(--ns-catbar-h, 0px);

  --nav-h: calc(var(--nav-row1-h) + var(--nav-row2-h) + var(--nav-row3-h) + var(--nav-bw));
                         /* 顶栏**实际占位高**（含 border-bottom）。
                            ⚠️ **刻意用 calc 而不是写一个数**：段1 上面 --nav-bw 那条注释记着的前科正是
                               「只改了 --nav-h 没改另一处，错位从 1px 放大到 2px」。
                               让它由两行高 + 边框宽算出来，就不存在「改了行高忘了改总高」这种漂移。
                            基底默认 64px 只算了内容高 → 正文 .fl-main 的 margin-top 与共享手机菜单
                            .apple-mobile-menu 的 top 都会比 nav 下沿少 1px，两者重叠 1px。
                            ⚠️ 本 token 的语义自 2026-08-25 起收窄为「**nav 本身**的占位高」，
                               **不含**它上方的通知条 —— 通知条走普通流、随页面滚走（段2 有完整推导），
                               所以它一个像素都不进这个 token。
                               加通知条 / 改通知条文案，本 token 一个字都不用碰。 */

  /* —— 11 基础色（语义对齐 florisse :root，按语义映射非按名映射）—— */
  --cream: #FAF9F7;      /* 页底：近白暖灰（大留白的底，不用纯白以便纯白卡片浮起来） */
  --card: #FFFFFF;       /* 卡片/浮层底：纯白（与近白页底形成极轻的层级差） */
  --rose: #C8A96A;       /* 次强调/描边：金（徽章、hover 描边、价格旁的小标记；黑白里唯一的彩） */
  --rose-deep: #111111;  /* 主按钮/链接：近黑 CTA（BloomThis 式黑底白字按钮） */
  --rose-ink: #111111;   /* 价格/品牌字/cartToast 底：近黑（编辑风把价格当标题排） */
  --sage: #7C8F79;       /* 绿点缀：压低饱和的灰绿（黑白主调里不能出鲜绿） */
  --sage-deep: #4F6B4A;  /* 深绿：成功态必须是绿（语义色，不可黑白化），取近白底上可读的深灰绿 */
  --ink: #1A1A1A;        /* 正文：近黑 */
  --muted: #6E6B67;      /* 次要字：中性暖灰。实测对比度（底 --cream #FAF9F7）：本值 5.04 ✓ AA。
                            原选的 #767370 只有 4.48，差 0.02 到小字 AA 线 4.5 —— 从 token 层修，
                            后续 index.ejs 直接写 var(--muted) 的小字不会再踩同一条线。
                            顺带核过更严的两个底：纯白卡 --card #FFFFFF 5.30 ✓ / 软块 --blush #F5F3F0 4.79 ✓。 */
  --blush: #F5F3F0;      /* 软背景块：暖白（黑白主题无腮红粉，改用比页底更暖一档的白） */
  --line: #E8E4DE;       /* 描边/分隔线：发丝级暖灰 */

  /* —— 三阴影 —— */
  --shadow: 0 24px 48px -32px rgba(17, 17, 17, .22); /* 卡片悬浮影：极淡、大扩散（编辑风靠留白不靠重影） */
  --apple-shadow: 0 1px 3px rgba(17, 17, 17, .05);   /* 组件浅影：mode-invariant——近黑半透明影压在暗色底上
                                                        本就几乎不可见，无需为暗色另配一套（真·两模式可共用） */
  --apple-shadow-hover: var(--shadow);               /* 别名跟随 --shadow，随明暗自动切换（var 在使用处解析） */
  --shadow-ink: #111111; /* 阴影着色基色（配 color-mix）：中性近黑，明暗恒定——与 --rose-ink 解耦，
                            免得暗色下 rose-ink 翻近白把浮层淡影染成白雾 */

  /* —— 遮罩 —— */
  --scrim: #0B0B0B;      /* 全屏遮罩基色（搜索层/FAB/分类面板背板）：明暗恒定的暗底 dimmer */

  /* —— 底部导航头像光晕 —— */
  --avatar-glow: rgba(17, 17, 17, 0.28);
                         /* 基底默认是玫瑰粉 rgba(194,94,119,.35)，罩在 vitrine 那颗「金→近黑→灰绿」
                            渐变头像外面会突兀（五个态全部命中，token 覆盖前够不着）。换成中性近黑：
                            不引入第二种彩色，保住黑白纪律；黑白主题里唯一的彩（金 --rose）已用在
                            徽章/描边/小图标上，头像外圈再来一圈金会让底部导航过重。
                            与 --apple-shadow 同理留在 :root 一处不进 ②③a③b：近黑半透明影压在暗色底上
                            本就几乎不可见，暗色下「没有光晕」正是想要的干净结果，无需另配一套暗值。 */

  /* —— 主色上字色（反转主题：随明暗翻转，四块都有）—— */
  --on-accent: #FAF9F7;  /* 近黑主按钮/toast 上的文字：近白（暗色翻为近黑，见 ②③a③b） */

  /* —— danger 双轨 —— */
  --danger: #B3261E;     /* 表单错误字：收敛的深红（近白底可读，不用刺眼的纯红破坏黑白调） */
  --danger-soft: #FBEEEC;/* 表单错误软底：极浅红白 */

  /* —— 字体 —— */
  --serif: Didot, "Bodoni MT", "Playfair Display", Georgia, "Times New Roman", "Songti SC", "Noto Serif SC", serif;
                         /* 衬线：高对比 Didone 编辑体（杂志刊头气质），中文落宋体；用于品牌字标/大标题/价格 */
  --apple-font: "Helvetica Neue", Helvetica, Inter, system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
                         /* 正文：中性无衬线（Helvetica 系的克制感是黑白极简的骨架） */

  /* —— 全部 radius：编辑风偏直角，除 pill 外全压到 2–4px —— */
  --radius: 4px;             /* 卡片 */
  --apple-radius: 3px;       /* 组件 */
  --apple-radius-lg: 4px;    /* 大组件 */
  --apple-radius-pill: 980px;/* 胶囊：保持 980px——购物袋角标/徽章仍要圆，压直角会变方块贴纸 */
  --radius-field: 3px;       /* 表单字段 */
  --radius-sm: 3px;          /* 小部件 */
  --radius-panel: 4px;       /* PDP 大面板 */

  --apple-transition: all 0.25s cubic-bezier(0.22, 0.61, 0.36, 1); /* 收尾更快的缓动，配合克制的交互动效 */

  /* —— 自引入色 --vt-*（本主题新引入的硬编码色，见 README §7 末二条）—— */
  --vt-hairline: #ECE8E2;      /* 发丝分隔线：比 --line 再淡一档，段2/段3 的 nav 底线与区块分隔用——随明暗变，四块各定义 */
  --vt-accent-hover: #333333;  /* 主按钮/链接 hover 底：近黑提亮一档——随明暗变，四块各定义 */
  --vt-hover-surface: #EDEAE4; /* 「软底 hover」专用面：列表项/幽灵钮/步进器 hover 时的填充色——随明暗变，四块各定义。
                                  为什么不能继续用 --blush：--blush #F5F3F0 与两种可能的底（页底 --cream #FAF9F7、
                                  卡底 --card #FFFFFF）实测只差 ΔL* 1.6 / 3.6（对比度 1.04:1 / 1.10:1），
                                  在 florisse 的粉调里靠 --rose-ink 变色兜住，vitrine 的 --rose-ink 与常态字色近乎同色
                                  → 整条 hover 归零。本值 ΔL* 对 --cream 4.7、对 --card 6.8，是「看得见但不打断留白」的量级；
                                  暖偏移(R−B=9)与 --blush(5) / --line(10) 同族，不会在黑白里冒出一块冷灰。 */
  --vt-on-gold: #111111;       /* 金底(--rose)上的字色：金色明暗不同(#C8A96A / #D4B87C)，但近黑字骑在两种金上
                                  都够可读，故前景取单值 mode-invariant，只放 :root、不进 ②③a③b */
  --vt-on-photo: #FAF9F7;      /* **照片上**的浅色前景（hero 标题/副标/实心钮底/圆点）——mode-invariant，只放 :root。
                                  ⚠️ 这里绝不能用 --on-accent：那个 token 跨明暗翻转（亮 #FAF9F7 / 暗 #111111），
                                  而照片**不会**跟着系统明暗翻转 —— 暗色下文字会翻成近黑压在压暗过的照片上，
                                  实测掉到 1.4:1 量级、几乎不可见。凡是骑在用户上传图片上的前景色，
                                  都必须是 mode-invariant 的单值（同 --vt-on-gold 的道理：底不随明暗变，前景就不该变）。 */
  --vt-photo-ink: #111111;     /* 骑在 --vt-on-photo 实心块上的深色字（hero 主 CTA）——同样 mode-invariant。
                                  #111111 骑在 #FAF9F7 上实测 17.95 ✓ AAA。 */

  /* —— --apple-* 兼容别名组（照 florisse 模式重定向到语义 token）——
     写成 var(...) 的别名在使用处才解析，故随 ②③a③b 里语义 token 的覆盖自动切换，只需 :root 一处；
     --apple-danger 在基底是字面量 #ff3b30，必须显式改并随明暗跟 --danger，否则暗色会出两种红。 */
  --apple-blue: var(--rose-deep);
  --apple-blue-hover: var(--vt-accent-hover);
  /* 基底新增的「主色按钮 hover 底」token（florisse-theme.css :root），默认值是 --rose-ink，
     而本主题 --rose-deep 与 --rose-ink 同值（亮 #111111 / 暗 #F2F0ED）→ 不接管就是 hover 零反馈。
     改这一行即一次性接管基底 + 功能页内联 <style> 里全部「底色型 hover」规则
     （购物车「前往结账」「继续购物」、结账「下一步」、确认「提交订单」，以及基底 5 条主钮）。
     写成 var(--vt-accent-hover) 的间接引用只需放 :root 一处：自定义属性在**使用处**才解析，
     ②③a③b 里 --vt-accent-hover 一翻，本 token 自动跟着翻。 */
  --accent-hover: var(--vt-accent-hover);
  /* 同上，基底「软底 hover 面」token 的接管（默认是 --blush，在本主题与页底/卡底只差 ΔL* 1.6/3.6）。
     一行同时管住基底 6 条 + 功能页内联 <style> 6 条（数量步进器、优惠券钮、继续购物、返回、修改）。 */
  --hover-surface: var(--vt-hover-surface);
  /* 灰底（--muted）上的字色。基底默认 #fff，在本主题**暗色**下只有 2.90:1（--muted 翻成浅灰 #9B9791）。
     指到 --on-accent 即可：亮色 #FAF9F7 骑在 #6E6B67 上 5.04 ✓、暗色 #111111 骑在 #9B9791 上 6.50 ✓，
     两档都过 AA。同为间接引用，只需 :root 一处。
     ⚠️ 别把这条当成「--on-muted 一律等于 --on-accent」的通例：实测 maison 亮色 3.49、zen 亮色 3.17，
     它们指过去反而比 #fff 更差（见本轮任务报告的 --muted 对照表）。这里成立是本主题的巧合，不是规律。 */
  --on-muted: var(--on-accent);
  --apple-dark: var(--ink);
  --apple-gray: var(--muted);
  --apple-light-gray: var(--blush);
  --apple-white: var(--card);
  --apple-border: var(--line);
  --apple-price: var(--rose-ink);
  --apple-success: var(--sage-deep);
  --apple-danger: #B3261E;

  /* cartToast 底 = --rose-ink（亮色近黑 #111111）→ 深底配白关闭钮（invert 滤镜）。
     ⚠️ 暗色下 --rose-ink 翻为近白 #F2F0ED，toast 变浅底 → 必须换黑钮(none)，故本 token 四块各定义。 */
  --btn-close-filter: invert(1) grayscale(100%) brightness(200%);
}

/* —— ② 系统暗色（同一套性格的暗版：近黑页底 · 稍亮卡片 · 金色提亮 · 近白 CTA） —— */
@media (prefers-color-scheme: dark) {
  :root {
    --cream: #0E0E0E; --card: #181817; --rose: #D4B87C; --rose-deep: #F2F0ED;
    --rose-ink: #F2F0ED; --sage: #8FA189; --sage-deep: #B2C4AB; --ink: #F0EEEB;
    --muted: #9B9791; --blush: #1E1E1D; --line: #2C2B29;
    /* --muted 暗值已核过对比度，无需跟着亮侧调：底 --cream #0E0E0E 6.65 ✓ / --card #181817 6.12 ✓
       / --blush #1E1E1D 5.74 ✓，三个底全过 AA（暗色侧本来就比亮侧宽裕，故 ②③a 保持原值）。 */
    --shadow: 0 24px 48px -32px rgba(0, 0, 0, .72); /* 暗底上近黑影看不见，改用纯黑加重 */
    --on-accent: #111111;   /* 主按钮/toast 底翻近白 → 其上文字翻近黑（与 btn-close-filter 成对反转） */
    --danger: #EFA79F;      /* 暗底错误字：提亮的红 */
    --danger-soft: #3A211E; /* 暗底错误软底：基底浅色在暗底刺眼，必改深色 */
    --apple-danger: var(--danger);
    --vt-hairline: #262523;     /* 暗色发丝线：比 --line 再暗一档 */
    --vt-accent-hover: #D8D4CE; /* 近白按钮 hover：压暗一档（亮色是提亮，暗色反向） */
    --vt-hover-surface: #282826;/* 暗色软底 hover：亮色是「压暗一档」，暗色反向「提亮一档」。
                                   实测 ΔL* 对暗 --card #181817 是 8.0、对暗 --blush #1E1E1D 是 4.9 —— 与亮侧同量级。 */
    --btn-close-filter: none;   /* toast 底翻浅 → 恢复黑关闭钮（成对反转的另一半） */
  }
}
/* —— ③a 用户手动锁暗（specificity 高于裸 :root，漏写=手动切暗时跌回亮值/基底粉；值 = ② 同暗值） —— */
:root[data-theme="dark"] {
  --cream: #0E0E0E; --card: #181817; --rose: #D4B87C; --rose-deep: #F2F0ED;
  --rose-ink: #F2F0ED; --sage: #8FA189; --sage-deep: #B2C4AB; --ink: #F0EEEB;
  --muted: #9B9791; --blush: #1E1E1D; --line: #2C2B29;
  --shadow: 0 24px 48px -32px rgba(0, 0, 0, .72);
  --on-accent: #111111;
  --danger: #EFA79F;
  --danger-soft: #3A211E;
  --apple-danger: var(--danger);
  --vt-hairline: #262523;
  --vt-accent-hover: #D8D4CE;
  --vt-hover-surface: #282826;
  --btn-close-filter: none;
}
/* —— ③b 用户手动锁亮（须压过系统暗色偏好；值 = ① 同亮值） —— */
:root[data-theme="light"] {
  --cream: #FAF9F7; --card: #FFFFFF; --rose: #C8A96A; --rose-deep: #111111;
  --rose-ink: #111111; --sage: #7C8F79; --sage-deep: #4F6B4A; --ink: #1A1A1A;
  --muted: #6E6B67; --blush: #F5F3F0; --line: #E8E4DE; /* --muted 同 ① 的 AA 修值（5.04），③b 必须跟改 */
  --shadow: 0 24px 48px -32px rgba(17, 17, 17, .22);
  --on-accent: #FAF9F7;
  --danger: #B3261E;
  --danger-soft: #FBEEEC;
  --apple-danger: #B3261E;
  --vt-hairline: #ECE8E2;
  --vt-accent-hover: #333333;
  --vt-hover-surface: #EDEAE4;
  --btn-close-filter: invert(1) grayscale(100%) brightness(200%);
}

/* —— 基底逃逸修补（这两处基底写死/直引色值，token 覆盖管不到，见 README §7）—— */

/* PDP 无图占位：基底 .fl-pdp-img 的粉紫渐变是写死的装饰色（不走 token），
   换成近白 + 极淡金晕，保持黑白极简里唯一一抹金的用色纪律。 */
.fl-pdp-img {
  background:
    radial-gradient(360px 250px at 64% 38%, var(--rose) 0%, transparent 56%),
    radial-gradient(260px 210px at 32% 68%, var(--blush) 0%, transparent 62%),
    var(--blush);
}

/* 8px 圆角残留（README §7 首条：14px / 8px 不在 radius token 内，直角主题需**按类覆写**）。
   两处都归 --radius-sm(3px)、而不是 --radius(4px)：
     · .apple-modal-tab 是 .apple-modal-tabs（该容器吃 var(--radius-sm)）的内嵌页签 ——
       基底原值 8px 就是「外容器 10px 减内 padding 视觉配比」推出来的，内圆角不能大过外容器，
       容器已被压到 3px，页签再用 4px 会反过来鼓出来；
     · .apple-mini-cart-item img 是 44px 见方的小缩略图，语义就是「小部件」= --radius-sm。
   （PWP 卡 .fl-addon 与批量块的 14px 已在段4 各自覆写过，此处不重复。） */
.apple-modal-tab { border-radius: var(--radius-sm); }
.apple-mini-cart-item img { border-radius: var(--radius-sm); }

/* 主按钮逃逸阴影抹平：基底 .fl-btn-primary 的 box-shadow 直接用 var(--rose-deep)，
   而 vitrine 的 --rose-deep 是近黑 #111111，会在每个主按钮下投一坨近黑重影，
   与「平面编辑风」相悖（--shadow 归淡管不到这条直引 --rose-deep 的投影）。 */
.fl-btn-primary, .apple-btn-primary { box-shadow: none; }

/* ========== 段2：nav / footer（vt- 前缀，配 themes/vitrine/layout.ejs） ==========
   设计口径：黑白极简 · 发丝线分隔 · 大留白 · 编辑感排版（衬线字标 + 全大写小字号大字距菜单）。
   nav 高度：本块只读 var(--nav-h) —— 基底已把顶栏高抽成 token（旧写法是三处写死 64px、改高就错位且不报错）。
   要改高 nav 请去段1 改 --nav-h 那一行，别在这里改 .vt-nav-inner 的 height。
   ⚠️ vitrine 的 --nav-h 含 border-bottom（= 内容高 + --nav-bw），故 .vt-nav-inner 写的是
      calc(--nav-h - --nav-bw)。通知条**不进** --nav-h（它走普通流，见下方「顶部区机制」大段注释）。 */

/* ============================================================================
   hover 反馈修复（本主题独有的病）—— 基底 florisse-theme.css 里用 var(--rose-ink)
   做 hover 的规则共 15 条。根因一句话：基底把 --rose-ink 当成「--rose-deep 的 hover 变体」在用，
   florisse 下两者是 #C25E77 / #8E3F55（差得开），vitrine 下两者同为近黑 #111111（暗色同为近白 #F2F0ED）
   → 凡是「常态 --rose-deep / --ink，hover --rose-ink」的规则，一律视觉归零。
   本块按三类分别收口（全部只改主题层，不动基底——这是主题适配，不是基底缺陷）：

     (a) 底色型（background: --rose-ink，常态底也是 --rose-deep）→ 改指 --vt-accent-hover。
     (b) 文字色型（color: --rose-ink，常态字是 --rose-deep/--ink）→ 近黑到近黑肉眼无差，
         **不能靠换颜色救**：改为「金色下划线 + 提亮一档字色」双线索。
         金 #C8A96A 配浅底只有 2.14:1，做正文字色不合规，但**做下划线/描边**是本主题既定纪律
         （.vt-navlink 的 ::after、.vt-footer-col a:hover 的 border-bottom 都是这套语言），
         且「凭空多出一条线」是形状变化不是纯颜色变化 —— 不违反 WCAG 1.4.1「不得只靠颜色传达」。
     (c) 软底型（background: --blush）→ 改指 --vt-hover-surface（理由见段1 该 token 的注释）。

   实测口径：Playwright 真实 hover（locator.hover()，不用程序化 click/dispatchEvent），
   量 hover 前后 computed 值。逐条数据见本次任务报告。
   ============================================================================ */

/* —— (a) 底色型：已收进段1 的一行 `--accent-hover: var(--vt-accent-hover);`，本处按类覆写**已删除** ——
   原先这里列了 .fl-btn-primary / .apple-btn-primary / .apple-auth-submit / .apple-scroll-top / .fl-nav-btn
   五条，用同名选择器 + 源序靠后压过基底。基底补上 --accent-hover token 后这五条逐条冗余：
   基底规则本身已是 `background: var(--accent-hover, var(--rose-ink))`，
   五个选择器的常态规则也都已经写着 `color: var(--on-accent, #fff)`（hover 不改字色），
   本块的 color 声明同样是空转。留着两套机制做同一件事，后人会不知道该改哪个 → 删。
   顺带：token 化后连功能页内联 <style> 里的四条（购物车「前往结账」「继续购物」、结账「下一步」、
   确认「提交订单」）也一并被接管 —— 那四条**按类覆写永远盖不到**（内联 <style> 在 <body> 里，
   源序晚于本文件，同 specificity 必输），这正是必须回基底补 token 的原因。
   实测（改后）：亮色 #333333 底 + #FAF9F7 字 = 12.01 ✓ AAA；暗色 #D8D4CE 底 + #111111 字 = 12.76 ✓。 */

/* —— (b) 文字色型：#1A1A1A/#111111 → #111111，肉眼几乎无差 ——
   全局 a:hover（基底 130）保持「只提亮字色」不加下划线：它命中全站每个 <a>，
   加下划线会在品牌字标、商品卡整块链接、footer 链接底下冒出多余的线
   （基底并非每条 :hover 都写了 text-decoration:none，压不住）；
   这些组件各自都已有自己的 hover 交代（.vt-navlink 金下划线 / .vt-brand / .fl-card 抬升）。 */
a:hover { color: var(--vt-accent-hover); }
/* 纯文字链接（基底 596 小节「查看全部」/ 794 面包屑）才加下划线 —— 它们没有边框没有底色，
   除了字色本身没有第二个可变的视觉属性，不给形状变化就真的什么都不剩。
   2px 而非发丝 1px：这条线是**功能反馈**不是装饰，要在 2.14:1 的金色下仍读得出来。 */
.fl-sec-link:hover,
.fl-crumb a:hover {
  color: var(--vt-accent-hover);
  text-decoration: underline; text-decoration-color: var(--rose);
  text-decoration-thickness: 2px; text-underline-offset: 4px;
}

/* —— (c) 软底型：已收进段1 的一行 `--hover-surface: var(--vt-hover-surface);`，本处按类覆写**已删除** ——
   原先这里列了 .fl-btn-ghost / 账户下拉项 / 分类 sheet 项 / 会员侧栏项 / 首页分类侧栏项 / PDP 数量步进器
   六条。基底改成 `background: var(--hover-surface, var(--blush))` 后逐条冗余（同 (a) 的理由）。
   基底同规则里的 color/text-decoration 本来就没被覆写，原样留用（近黑字 #111111 骑在 #EDEAE4 上 15.9 ✓）。
   同一行还顺带管住了功能页内联 <style> 的另外六条同型 hover（数量步进器、优惠券钮、
   继续购物、结账返回、确认返回、修改地址）—— 那六条按类覆写同样盖不到（源序在本文件之后）。 */

/* ════════════════════════════════════════════════════════════════════════════
   顶部区（通知条 + nav）的整体机制 —— 2026-08-25 由「nav 固定 / 通知条在 nav 之下」改成
   「通知条在 nav **之上**、走普通流；nav 改 position: sticky」。

   为什么改：实测对标站 bloomthis.co 的 DOM 顺序就是「通知条 → header」，两者都 sticky top:0，
   滚动后 header 上来盖住通知条 → 净效果「通知条消失、nav 吸顶」。我们此前的滚动态与它一致，
   差的只有页面顶部那一眼（我们把通知条放在了 nav 之下）。用户对本项目的要求是「必须跟它一样」，
   而这是第一眼就看得见的差异 → 对齐。

   为什么是 sticky 而不是「把通知条并进固定容器、滚动时收起」（曾评估过的方案 b）：
     · 通知条必须**占真实流空间**，否则滚动时收起 = 文档高度突变 = 正文整体跳一下。
       要既占流空间又能滚走，只有「普通流 + sticky nav」这一条路。
     · 方案 b 还要求 --nav-h 按断点给两个值（1440/768 是 45.6px、390 折两行是 53.6px），
       而 Phase 5 文案交给 site_notices 表后长度由店主定 → 折行数不可能提前知道。
       sticky 方案里**没有任何地方需要知道条高**，这才是它真正的好处，不是少写几行。

   四处 --nav-h 消费方的处置（改前 grep 确认过，一个不漏）：
     ① .fl-main 的 margin-top —— 本文件下方覆写为 0。nav 不再脱离文档流，正文无需再给它让位；
        再留 65px 就是**每页 nav 下一条 65px 的白洞**。这也是「任何页面都不许有白洞」的结构性保证：
        改后正文位置**不依赖任何顶部元素的高度**，通知条有没有、几行高，都影响不到它。
        （实测：五个功能页 .fl-main 的首个子元素 margin-top 全是 0，只有会员中心 cover 是 -48px
         的**刻意**上提，margin 折叠后行为与改前逐像素一致 —— 数据见任务报告。）
     ② .apple-mobile-menu 的 top —— 见下方覆写：改成挂在 .vt-nav 里的绝对定位，永远贴住 nav 下沿。
     ③ .vt-nav-inner 的 calc —— 语义未变（nav 内容高 = 总高 - 边框），只把写死的 1px 换成 --nav-bw。
     ④ auth/login-florisse.ejs 的 min-height: calc(100vh - var(--nav-h)) —— **不动**。
        它是「视口减顶栏」的垂直居中辅助，nav 高度没变，语义仍成立；
        通知条在页面顶部时该页会比视口高出一个条高（≈46px 的滚动），不是白洞、不遮内容。
   ════════════════════════════════════════════════════════════════════════════ */

/* —— 顶部通知条（配 layout.ejs，**在 nav 之上**，全站每页）——
   位置：它是 <body> 的第一个块级子元素，无 max-width 无横向 padding → 天生通栏，
   内层再收到 --content-max。走**普通流**：滚动时自然滚出视口，nav 随即吸顶（见上方机制说明）。

   ⚠️ 它不是「店主公告」—— 这是平台/店家的**配送截单**促销语（硬编码，Phase 5 交给 site_notices 表）。
      店主自己写的公告是 .vt-storenote（段3，读 bloggers.status_content）。
      两者语义不同、来源不同，**不能合并也不能互相顶替**：一个是每天都一样的规则，
      一个是店主今天想说的话。合并的表现是店主一改公告，全站的截单时间就没了。

   配色：--rose-deep 底 + --on-accent 字 —— 这对 token 亮色是「近黑底 + 近白字」(17.95 ✓)、
   暗色自动翻成「近白底 + 近黑字」(约 17.4 ✓)，两档都远超 AA。绝不写死 #fff（零容忍护栏）。 */
.vt-notice {
  background: var(--rose-deep);
  color: var(--on-accent, #fff);
}
.vt-notice-inner {
  max-width: var(--content-max); margin: 0 auto; padding: 12px 40px;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-size: .76rem; line-height: 1.5; letter-spacing: .05em; text-align: center;
}
/* 图标走 currentColor（= --on-accent），跟着底色一起翻，不会在暗色下变成一枚看不见的金点 */
.vt-notice-inner i { flex: 0 0 auto; font-size: .9rem; opacity: .82; }

/* —— 顶栏：实底近白 + 发丝底线；滚动后（JS 加 .scrolled）只加深底线，不加阴影（保持平面编辑感） —— */
.vt-nav {
  /* z-index 1030 同基底 .fl-nav 理由：须盖过页面 sticky 摘要(1020)，低于搜索遮罩(2000)。
     sticky（不是 fixed）：nav 之上还有一条走普通流的通知条，nav 必须先跟着它往下排、
     再在滚到视口顶时钉住 —— fixed 做不到「先随流、后钉住」。
     ⚠️ 不写 left/right：sticky 的 left/right 会在有横向滚动容器时参与横向吸附；
        块级元素本来就铺满父宽，写了只会多一种出错方式。 */
  position: sticky; top: 0; z-index: 1030;
  background: var(--cream);
  border-bottom: var(--nav-bw, 1px) solid var(--vt-hairline);
  transition: border-color .25s;
}
.vt-nav.scrolled { border-bottom-color: var(--ink); }

/* ══════════════════════════════════════════════════════════════════════
   顶栏与 hero 融合（Task 4.8）—— 页面在最顶端时，nav 透明压在 hero 图上。
   对标 bloomthis.co：它的 header 在首屏是**透明浮在大图之上**的，滚动后才变实底。

   ── 🔴 为什么是「JS 加上才透明」而不是「JS 没加就透明」──────────────
   透明态由 body.vt-nav-over-hero 开启（JS 加）。反过来写（`.vt-nav:not(.scrolled)`）
   看着更省事，但**失败方向是错的**：JS 一旦没跑（报错 / 被拦 / 老浏览器），
   nav 会永久停在透明态 —— 滚到正文区就是白字压白底，整条导航看不见。
   现在这个方向失败时退回实底 nav = 当前外观，只是少了个效果。
   ⚠️ 2026-08-26 实测：本机 headless 里 `scroll` 事件根本不派发
   （空白对照页同样复现，是环境问题不是站点 bug）→ 更不该把可读性押在 JS 必然生效上。

   ── hero 上移是**静态**的，不跟着滚动切 ────────────────────────────
   margin-top 的负值恒定，只有 nav 的外观在切。若把上移也做成「仅顶部时」，
   类一摘除页面就会**跳一下**（109px）—— 那比没有这个效果还糟。
   JS 没跑时的退化：实底 nav 盖住 hero 顶部约一个 nav 高，hero 的文案在下半部、不受影响。 */
.vt-hero, .vt-cathero { margin-top: calc(-1 * var(--nav-h)); }

body.vt-nav-over-hero .vt-nav {
  background: transparent;
  border-bottom-color: transparent;
}
/* 压暗带：hero 自己的 .vt-hero-scrim 是**底重顶轻**的（顶端只有 .10），
   照片上半部基本是原色 —— 浅色照片（本库当前那张绣球就是）上白字直接读不了。

   常数 .58 是算出来的，不是调出来的（口径同 .vt-hero-scrim 那段注释）：
   最坏情况 = 纯白照片。黑色 .58 叠上去 → 合成 sRGB ≈ 0.42 → 相对亮度 ≈ 0.147，
   与 --vt-on-photo (#FAF9F7, 亮度 0.948) 的对比度 = (0.948+0.05)/(0.147+0.05) ≈ **5.05:1** ✓ AA。
   照片越暗只会更高，所以 5.05 是**下界**不是典型值。
   **2026-08-27 真跑实测**（藏掉 nav 文字后截 nav 那条带子、取最亮像素）：当前这张绣球照下
   最坏对比度 **5.10:1**，与算出来的下界吻合。阳性对照（临时关掉本压暗带）掉到 **1.20:1** ——
   那条对照是必要的：不做的话，量到的可能根本不是这条带子。
   ⚠️ 采样宽度要避开右侧滚动条：把 x 取到视口满宽会把滚动条那条白边当成底色，
      最亮值被顶成 0.948（恰好等于前景色亮度），算出来的对比度是假的 1.00:1。
   往下迅速淡出，避免在 hero 顶上压出一条明显的横带。
   ⚠️ 改 nav 高度或改 --vt-on-photo 都要重算这个数。 */
/* 🔴 2026-09-09 业主报障：上面那条**整条压暗带已删除**。
   现场：店主传什么 hero 图，顶部都被压成一片黑 —— 于是「hero 图必须自带黑底」
   变成了事实上的设计约束。而且它是 `--nav-h * 1.5`，同日加了第 3 行分类条之后
   nav 从 109px 长到 150px，这条带子跟着从 163px 变成 **225px**，问题被放大了。

   ── 换成「只在文字背后局部压暗」（业主 2026-09-09 在三个方案里选的）──────────
   照片大面积保持原色，只有 logo / 导航项 / 分类项 / 图标各自背后有一小块暗底。
   🔴 **透明度沿用同一个 .58**，不是重新调的数 —— 文字**正下方**的合成底色因此与旧版
      逐字节相同，上面那段算出来的「最坏情况（纯白照片）5.05:1 ✓ AA」原封不动仍然成立。
      改的只是「这块暗底铺多大」，不是「暗到什么程度」。改这个 .58 要重算那笔账。
   ⚠️ 用 box-shadow 的 spread 把暗底**向外扩**，不用 padding —— padding 会改行高与间距，
      透明态/实底态之间一切换整条 nav 就会跳一下。box-shadow 不占布局。
   ⚠️ 文字阴影是第二道保险（照片有高频细节时描边比色块更救得回来），不计入上面那笔账。 */
/* 🔴 终稿（业主 2026-09-09，三次裁定）：**hero 一个像素都不压暗**，nav 只靠投影。
   原话：「hero 图不必压黑，这样管理员才能自己决定 hero 图配色」。
   走过并被否掉的两版记在这里，免得有人"优化"回去：
     · 整条 225px 压暗带（原始实现）→ 店主传什么图，顶部都是黑的 —— 最初的报障；
     · 逐项半透明胶囊 → 分类那行十几个胶囊连成一片、等于黑带边缘变毛，
       logo / 链接上的圆底也被明确否掉（「不需要用圈围起它们」）。
   现在：文字与图标各带一层紧贴的深色投影 + 一层扩散的柔光。

   ⚠️ **这是明确的取舍，不是免费的**：投影保不住「纯白照片」那种极端情况的 AA 对比度
      （那正是原来 .58 压暗带算出来的 5.05:1 下界）。业主选了「把配色权交给管理员」——
      判据因此从代码转到内容：**hero 图顶部别用大面积高亮白**，否则导航文字会发灰。
      将来若要退回可读性优先，压暗带那段在 git 历史里（本文件 2026-09-09 之前的版本）。 */
body.vt-nav-over-hero .vt-nav .vt-brand,
body.vt-nav-over-hero .vt-nav .vt-navlink,
body.vt-nav-over-hero .vt-nav .ns-catlink,
body.vt-nav-over-hero .vt-nav .vt-icon-btn {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .70), 0 0 14px rgba(0, 0, 0, .50);
}
/* 「更多」那个 › 圆钮在照片上：描边换成半透明白，原来那条 --line 在深色照片上看不见 */
body.vt-nav-over-hero .vt-nav .ns-catmore-btn { border-color: rgba(250, 249, 247, .45); }
/* 图标与 logo 是图形不是文字，text-shadow 对它们无效 —— 必须走 drop-shadow */
body.vt-nav-over-hero .vt-nav .vt-icon-btn i,
body.vt-nav-over-hero .vt-nav .vt-icon-btn svg,
body.vt-nav-over-hero .vt-nav .vt-brand img {
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .70)) drop-shadow(0 0 10px rgba(0, 0, 0, .45));
}
/* 分类条（第 3 行）在 hero 上**照常显示**（业主 2026-09-09 终裁：「顶端自定义链接下的分类必须保留」）。
   它和上面两行一样只靠投影，不加任何底 —— 见上方那段「不压暗」的取舍说明。 */
/* 逐个元素改色，**不改 --ink 令牌**：--ink 一改，同在 nav 里的下拉/mega 面板
   （白底）文字也会跟着变白 → 白底白字。列在这里的都是「浮在照片上」的那几个。 */
body.vt-nav-over-hero .vt-nav .vt-mark,
body.vt-nav-over-hero .vt-nav .vt-sub,
body.vt-nav-over-hero .vt-nav .vt-brand,
body.vt-nav-over-hero .vt-nav .vt-navlink,
body.vt-nav-over-hero .vt-nav .ns-catlink,
body.vt-nav-over-hero .vt-nav .vt-icon-btn {
  color: var(--vt-on-photo);
  /* 分类条（第 3 行，2026-09-09 新增）也在这份名单里：漏了它就是**深色字压在深色暗底上**，
     整行读不了。名单漏项这类错没有任何自动信号 —— 加新的 nav 元素时记得回这里补一行。 */
  opacity: 1;
}
/* 分类条那条上边框在照片上会变成一道横贯全屏的亮线（它本来是给实底 nav 分隔用的）→ 撤掉 */
body.vt-nav-over-hero .ns-catbar { border-top-color: transparent; }
/* 图标钮的 hover 底色在照片上要换成半透明白，原来那个软底色块在深色照片上会糊成一团 */
body.vt-nav-over-hero .vt-nav .vt-icon-btn:hover {
  background: rgba(250, 249, 247, .16);
}
/* 鼠标一进顶栏**整片区域**就变实底 —— 不是「指到某个链接才变」。
   🔴 业主 2026-09-09：原来的判据是 `:has(.vt-navitem:hover, .ns-catitem:hover)`，
      于是鼠标在链接之间的空隙上划过时，实底态被反复开关 —— **一闪一闪**。
      改成 `.vt-nav:hover`：只要指针在顶栏范围内（含展开的下拉面板，它是 nav 的子节点）
      就一直是实底，横向移动全程稳定。
   ⚠️ 用 :where() 包住是为了**保持零特异性**：这几条实底规则与上面的透明态规则
      特异性必须相等、靠源序在后取胜。写成 `:hover` 直接挂上去会把特异性抬高一级，
      看着也能用，但后面任何人想再覆盖就得跟着加权重 —— 那是一条会滚雪球的路。
   ⚠️ 保留 :focus-within 那一支：键盘 Tab 进来的人没有 hover，
      漏了它就是「键盘用户看到的 nav 永远是透明的」。
   ⚠️ :has() 不支持的老浏览器只是保持透明（面板本身照常可读），不是坏掉。 */
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) {
  background: var(--cream);
  border-bottom-color: var(--vt-hairline);
}
/* nav 变回实底时：投影撤掉、颜色翻回 --ink。
   🔴 **这份名单必须与上面「透明态」那份逐字对应**（业主 2026-09-09 报障：
      分类那行 hover 后仍是浅色字压在浅底 nav 上，几乎看不见）——
      根因就是这里漏了 .ns-catlink：透明态给它上了白字 + 投影，实底态没翻回来。
      业主要求：「顶部分类 hover 时用跟顶部自定义链接一样的字体颜色」= 都走 --ink。
   ⚠️ 这是一份典型的「会漂的清单」（gotcha #60）：nav 里每加一个浮在照片上的元素，
      **透明态 1 处 + 实底态 3 处**都要补，漏哪一处都没有任何报错，
      只是那个元素在某个状态下看不见。改这里时四处一起看。 */
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-brand,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-navlink,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .ns-catlink,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-icon-btn {
  text-shadow: none;
}
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-icon-btn i,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-icon-btn svg,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-brand img {
  filter: none;
}
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-mark,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-sub,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-brand,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-navlink,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .ns-catlink,
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .vt-icon-btn {
  color: var(--ink);
}
/* 「更多」那个 › 圆钮的描边也要翻回来（透明态换成了半透明白） */
body.vt-nav-over-hero .vt-nav:where(:hover, :has(.vt-navitem:focus-within, .ns-catitem:focus-within)) .ns-catmore-btn { border-color: var(--line); }
/* 切换要有过渡，否则从透明跳到实底会「啪」一下（原规则只过渡了 border-color） */
.vt-nav { transition: background-color .25s ease, border-color .25s; }
.vt-nav .vt-mark, .vt-nav .vt-sub, .vt-nav .vt-navlink, .vt-nav .vt-icon-btn {
  transition: color .25s ease;
}
/* —— 第 1 行：品牌居中 + 动作簇靠右 ——
   对标站实测（1440）：logo @653–773（中心 713 ≈ 视口中心 720）、账户/搜索/购物袋 @1181–1377。
   ⚠️ 三栏 grid 而不是 flex + margin:auto：要让 logo 对齐**视口中心**而不是「剩余空间的中心」，
      两侧必须各占一个等宽轨（1fr）。flex 方案里 logo 会被右侧动作簇的宽度推着偏左，
      且偏多少随语言（购物袋数字位数）变 —— 那种「大部分时候看着差不多」的偏差最难查。
   ⚠️ 手机端回退成两栏（logo 左 + 动作右）：见下方媒体查询。 */
.vt-nav-inner {
  max-width: var(--content-max); margin: 0 auto; height: var(--nav-row1-h, 64px);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 40px;
}
.vt-nav-inner > .vt-brand { grid-column: 2; justify-self: center; margin-right: 0; }
.vt-nav-inner > .vt-nav-actions { grid-column: 3; justify-self: end; }

/* —— 第 2 行：主导航 ——
   高度单独成一个 token（--nav-row2-h），手机端归零后 --nav-h 自动缩回单行高度。
   居中 + 固定 gap，不用 space-between：对标站 11 项时两者几乎等效，
   但我们现在只有 4 项 —— space-between 会把 4 项拉到左右两端、中间空一大片。
   居中方案在项少时聚拢、项多时自然铺开，两端都体面。 */
.vt-navbar { display: block; }
.vt-navbar-inner {
  /* ⚠️ 这里**不减** --nav-bw：边框已经作为独立一项算进了 --nav-h 的 calc
     （row1 + row2 + bw）。再减一次就是把同一个 1px 扣两遍 ——
     旧的单行写法要减，是因为那时 --nav-h 写死 65 且**含**边框。 */
  max-width: var(--content-max); margin: 0 auto; height: var(--nav-row2-h, 44px);
  display: flex; align-items: center; justify-content: center; padding: 0 40px;
}

/* —— 手机端：第 2 行整个收进汉堡菜单 ——
   ⚠️ **display:none 与 --nav-row2-h: 0 必须成对**，只做一半就是「看不见但仍占高」
   或「不占高却还画着」。归零后 --nav-h 自动缩回 65px（= 64 + 0 + 1），
   手机菜单高度 / sticky 偏移等 7 处消费方全部跟着对，一处都不用单独改。 */
@media (max-width: 767.98px) {
  /* ⚠️ 这里必须把 .has-catbar 一起写上：它是 :root.has-catbar（0,1,0），
     比裸 :root（0,0,1）权重高，只写 `:root {}` 归不掉第 3 行的高度 ——
     表现是手机端顶栏白白高出一截，而那一行 display:none 根本看不见。 */
  :root { --nav-row2-h: 0px; --nav-row3-h: 0px; }
  .vt-navbar { display: none; }   /* 分类条自己在片段里 767.98px 以下 display:none */
  /* 手机端品牌回到左侧（logo 居中会和右侧汉堡挤在一起，且对标站手机端也是左对齐） */
  .vt-nav-inner { grid-template-columns: auto 1fr; padding: 0 16px; }
  .vt-nav-inner > .vt-brand { grid-column: 1; justify-self: start; }
  .vt-nav-inner > .vt-nav-actions { grid-column: 2; justify-self: end; }
}

/* —— 第 3 行：分类条 ——
   样式全部在共享片段 _shared/_category-bar.ejs 里（那一份要在 13 套主题上跑，
   所以刻意不引用任何主题 token，只用 inherit / 中性灰 / 系统色）。
   本主题只保留两件事：① 上面 --nav-row3-h 读片段给的高度；② 下面 hero 透明态的前景处理。
   要精细化本主题的分类条外观，在这里写 `.vt-nav .ns-catbar{…}` 覆写，别再复制一份结构。 */

/* —— 正文：nav 已回到文档流，撤掉基底给固定顶栏留的让位 ——
   基底 .fl-main { margin-top: var(--nav-h) } 的前提是「nav 是 fixed、不占流空间」。
   vitrine 的 nav 现在是 sticky（占流空间），再留这条 margin 就是 nav 下沿与正文之间
   一条 65px 的空隙 —— 且**不报错**，只是每一页顶上多一条白洞。
   ⚠️ 这是本主题唯一一处按类去盖基底 .fl-main 的地方，因为改的是**定位机制的前提**，
      不是「调一个数」。要调 nav 高度仍然只改段1 的 --nav-h。
   为什么不担心 margin 折叠：.fl-main 无 border/padding，首个子元素的 top margin 会与它折叠。
   改前实测五个功能页 + 首页 + collection 页的首子元素 margin-top 全是 0px，
   唯一的例外是会员中心 .mb-cover-wrap 的 -48px（刻意让 cover 钻到透明 nav 底下），
   负 margin 在两种写法下都照原样生效 → 无一处渲染位移。 */
.fl-main { margin-top: 0; }

/* —— 共享手机菜单：改成挂在 .vt-nav 里的绝对定位浮层（layout.ejs 已把 include 移进 <nav>）——
   基底写的是 position: fixed; top: var(--nav-h)，前提同样是「nav 恒在视口 0..--nav-h」。
   sticky nav 在页面顶部时下沿在「条高 + --nav-h」处，固定浮层的 top 是个静态数追不上
   —— 那就会在通知条可见时把 nav 盖掉半截（重叠），而条高又随折行数变（390 折两行 53.6px）、
   Phase 5 还要交给店主写 → 静态数在这里**永远**是错的。
   改法：让浮层自己挂在 nav 上（top: 100%），它就不再需要知道任何高度：
   nav 在哪儿，菜单顶沿就在哪儿 —— 任意滚动位置、任意条高、任意断点，恒为 0px 缝 0px 重叠。
   ⚠️ + var(--nav-bw)：绝对定位的包含块是 .vt-nav 的 **padding box**，不含 border-bottom，
      少了这一项菜单会压在发丝线上（重叠 1px，正是段1 前科那一类）。
   高度：滚定后 nav 下沿恰在 --nav-h，故 calc(100dvh - var(--nav-h)) 正好铺到视口底；
   页面顶部（通知条可见）时 nav 下沿更低，浮层于是多伸出一个条高到视口之外 ——
   **宁可多伸也不能少伸**：多出去的部分看不见，少了就是菜单底下露出一条页面。
   100vh 那行是 dvh 不支持时（Chrome<108 / Safari<15.4）的兜底，源序在前被覆盖。
   实测（改后，390×844）：首页/PDP/搜索/登录，滚动前后菜单顶沿 - nav 下沿恒为 **0.00px**。
   已知的唯一例外（**改前就有、量级相同**）：会员中心 dashboard 手机端，共享脚本给 nav 加
   .mb-cover-nav-transparent（其中 border-bottom: none !important），nav 少了 --nav-bw 那 1px
   而本规则仍在加它 → 菜单低 1px。那 1px 落在 cover 图上、无内容丢失；
   要收口得让「透明 nav」不去动边框宽（属 _main-content.ejs 那段共享脚本，不在本主题范围）。 */
.apple-mobile-menu {
  position: absolute; top: calc(100% + var(--nav-bw, 1px)); left: 0; right: 0; bottom: auto;
  height: calc(100vh - var(--nav-h, 65px));
  height: calc(100dvh - var(--nav-h, 65px));
}

/* 品牌字标：Didone 衬线大写 + 字距（刊头气质）；「花事」作副标小字 */
.vt-brand {
  display: flex; align-items: baseline; gap: 10px; margin-right: auto;
  text-decoration: none; color: var(--ink);
}
.vt-brand:hover { color: var(--ink); text-decoration: none; }
.vt-brand .vt-mark {
  font-family: var(--serif, Georgia, serif); font-size: 1.32rem; font-weight: 500;
  letter-spacing: .22em; text-transform: uppercase; color: var(--rose-ink); white-space: nowrap;
}
.vt-brand .vt-sub {
  font-size: .64rem; letter-spacing: .34em; text-transform: uppercase;
  /* 副标用 --ink + 透明度而非 --muted：这是排版取舍（副标要读成品牌字标的淡影，跟字标同色系），
     不是无障碍绕道 —— --muted 已在段1 修到 5.04 ✓ AA，两种写法现在都合规。
     实测本行：亮 4.78 ✓ / 暗 6.78 ✓（--ink 暗色是近白，同一条规则两模式通吃）。 */
  color: var(--ink); opacity: .62;
}
.vt-brand-logo { height: 30px; width: auto; border-radius: var(--radius-sm); }

/* 菜单：小字号全大写 + 大字距；hover 从左展开一道金色发丝下划线（金只做描边细节，不承字） */
.vt-navlinks { display: flex; gap: 30px; }
.vt-navlink {
  position: relative; color: var(--ink); text-decoration: none;
  font-size: .74rem; font-weight: 500; letter-spacing: .16em; text-transform: uppercase;
  padding-bottom: 4px; opacity: .78; transition: opacity .18s;
}
.vt-navlink::after {
  content: ''; position: absolute; left: 0; bottom: 0; width: 0; height: 1px;
  background: var(--rose); transition: width .22s ease;
}
.vt-navlink:hover { color: var(--ink); opacity: 1; text-decoration: none; }
.vt-navlink:hover::after { width: 100%; }

/* —— 二级下拉（Phase 4 Task 4.2）——
   对标站的形态：11 个主项里 6 个挂下拉、5 个是纯链接 —— 判据就是**有没有子项**，
   没有额外的「是否下拉」开关（migration 143 因此也没建那个字段）。

   ⚠️ 纯 CSS，不接 JS：hover 展开 + :focus-within 让键盘用户也能进。
      接 JS 的话，「点开/点关/点外面关/ESC 关/换页清状态」五件事都得自己维护，
      而它们每一件失效都是静默的。
   ⚠️ 用 visibility 而不是 display 切换：display 变化不参与过渡，会「啪」地闪出来；
      而且 display:none 的子树拿不到焦点，:focus-within 永远不会命中。 */
.vt-navitem { position: relative; display: flex; align-items: center; }
/* 悬停区要连到下拉上沿，否则鼠标从主项往下移的途中会经过一段空隙 → 下拉闪退。
   ::after 是个透明的桥，只在展开时存在（不展开时它会白白吃掉下方元素的点击）。 */
.vt-navitem.has-kids::after {
  content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 12px;
  display: none;
}
.vt-navitem.has-kids:hover::after,
.vt-navitem.has-kids:focus-within::after { display: block; }

.vt-navcaret { font-size: .55rem; margin-left: 5px; opacity: .6; transition: transform .2s; }
.vt-navitem:hover .vt-navcaret,
.vt-navitem:focus-within .vt-navcaret { transform: rotate(180deg); }

.vt-dropdown {
  position: absolute; top: calc(100% + 12px); left: 50%; transform: translateX(-50%) translateY(-4px);
  min-width: 190px; padding: 10px 0;
  background: var(--card); border: 1px solid var(--vt-hairline);
  /* 下拉要盖住页面内容。它在 .vt-nav（z-index:1030）的层叠上下文里，
     这个值只用于压住同层的兄弟节点。 */
  z-index: 5;
  opacity: 0; visibility: hidden; transition: opacity .18s ease, transform .18s ease, visibility .18s;
}
.vt-navitem:hover > .vt-dropdown,
.vt-navitem:focus-within > .vt-dropdown {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
}
.vt-dropitem {
  display: block; padding: 7px 20px; white-space: nowrap;
  color: var(--ink); text-decoration: none; opacity: .8;
  font-size: .72rem; letter-spacing: .1em; text-transform: uppercase;
  transition: opacity .16s, background .16s;
}
.vt-dropitem:hover, .vt-dropitem:focus {
  opacity: 1; color: var(--ink); text-decoration: none; background: var(--cream);
}

/* ── mega-menu（Task 4.7 / mig146）── 对标 bloomthis.co 的 Occasion 面板：
      **通栏**，左侧若干分组列（列标题 + 链接），右侧若干促销图卡。

   ⚠️ 只有「子项还有子项」或「子项带 image」时才会渲染成这个（判定在 layout.ejs）。
      纯两级文字导航仍走上面的 .vt-dropdown —— 本主题要上架成通用可选主题，
      没配 mega 的店铺必须与 Task 4.2 那版逐像素一致。

   ── 定位（这里栽过一次，把推导写下来）──────────────────────────
   面板要**通栏**，而它的祖先 .vt-navitem 是个 position:relative 的窄条，
   absolute 会被收进那个窄条里。第一版因此用了 position:fixed + top:var(--nav-h)，
   **是错的**：fixed 的 top 相对**视口**，而 nav 上面还有一条 .vt-notice 通知条
   把 nav 整个推下去约 45px → 面板正好盖住导航第二行（那一行整排链接凭空消失，
   而且因为面板是悬停才出现的，看起来就像「一悬停导航就没了」）。
   ⚠️ 任何「按 nav 高度算偏移」的写法都会重蹈覆辙 —— 通知条本身可由店主配、
      可以折行，高度不是常量。

   正解：让面板相对**导航条**定位，高度由 DOM 决定而不是算出来：
     · .vt-navbar 设 position:relative → 成为包含块
     · .vt-navitem.has-mega 设 position:static → 让面板跳过窄条祖先，
       去认 .vt-navbar（其余 navitem 保持 relative，单列下拉照旧居中对齐自己）
     · 面板 top:100% → 紧贴导航条下沿，天然没有缝隙（因此也不需要那条 ::after 悬停桥）
   内容再靠 .vt-mega-inner 收进 --wrap-max 宽度里居中，否则宽屏上会顶到屏幕边缘。 */
.vt-navbar { position: relative; }
.vt-navitem.has-mega { position: static; }
/* 悬停桥对 has-mega 既没用也有害：position:static 之后它会相对 .vt-navbar 铺满整行宽 */
.vt-navitem.has-mega::after { display: none !important; }
.vt-mega {
  position: absolute; left: 0; right: 0; top: 100%;
  background: var(--card); border-top: 1px solid var(--vt-hairline);
  border-bottom: 1px solid var(--vt-hairline);
  box-shadow: 0 18px 40px rgba(0, 0, 0, .07);
  z-index: 5;
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity .18s ease, transform .18s ease, visibility .18s;
}
.vt-navitem:hover > .vt-mega,
.vt-navitem:focus-within > .vt-mega {
  opacity: 1; visibility: visible; transform: translateY(0);
}
.vt-mega-inner {
  max-width: var(--wrap-max, 1240px); margin: 0 auto;
  padding: 34px 24px 40px;
  display: flex; gap: 48px; align-items: flex-start;
}
/* 分组列：auto-fit 让 2 组和 5 组都排得住，不必按组数写死列数 */
.vt-mega-cols {
  flex: 1 1 auto; display: grid; gap: 10px 44px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.vt-mega-col { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.vt-mega-coltitle {
  font-size: .72rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); font-weight: 600; margin-bottom: 12px;
}
/* 散项列没有标题，用一段等高的上边距对齐其他列的第一条链接 —— 
   不补这一下，没标题的那列会整体上浮一行，看起来像排版坏了。 */
.vt-mega-col-plain { margin-top: 30px; }
.vt-mega-link {
  display: block; padding: 6px 0; color: var(--ink); text-decoration: none;
  opacity: .72; font-size: .78rem; letter-spacing: .02em;
  transition: opacity .16s, transform .16s;
}
.vt-mega-link:hover, .vt-mega-link:focus {
  opacity: 1; color: var(--ink); text-decoration: none; transform: translateX(3px);
}
/* 促销卡：右侧固定宽度，不随分组列数变化 —— 图卡宽度一跳，整个面板就显得晃 */
.vt-mega-promos { flex: 0 0 auto; display: flex; gap: 20px; }
.vt-mega-promo {
  display: flex; flex-direction: column; width: 200px;
  color: var(--ink); text-decoration: none;
}
.vt-mega-promo-media {
  display: block; overflow: hidden; background: var(--cream);
  aspect-ratio: 200 / 256;   /* 与 layout.ejs 里 slotAspect:0.78 同一口径，改一处要改两处 */
}
.vt-mega-promo-img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .5s cubic-bezier(.2, .7, .3, 1);
}
.vt-mega-promo:hover .vt-mega-promo-img { transform: scale(1.04); }
.vt-mega-promo-title {
  margin-top: 12px; font-size: .78rem; font-weight: 600; letter-spacing: .04em;
}
.vt-mega-promo-sub {
  margin-top: 3px; font-size: .68rem; letter-spacing: .08em;
  text-transform: uppercase; opacity: .55;
}
/* 窄一些的桌面：促销卡先让位（分组链接比图卡重要），再窄就整块进汉堡菜单 */
@media (max-width: 1199.98px) {
  .vt-mega-inner { gap: 28px; padding: 26px 20px 30px; }
  .vt-mega-promos { display: none; }
}

/* 右侧图标钮：恒近黑（金色在近白底上只有 2.25:1，做图标会掉到可辨识线下），hover 换软底色块 */
.vt-nav-actions { display: flex; align-items: center; gap: 10px; }
.vt-icon-btn {
  background: none; border: none; color: var(--ink); cursor: pointer;
  padding: 7px; display: inline-flex; align-items: center; justify-content: center;
  position: relative; border-radius: var(--radius-sm); transition: background .18s, color .18s;
}
/* hover 底走 --vt-hover-surface 不走 --blush：本块（本主题自绘）也踩了上面 (c) 那条同根病 ——
   --blush #F5F3F0 骑在 nav 的 --cream #FAF9F7 上 ΔL* 只有 1.6，而 color 从 --ink #1A1A1A
   变到 --rose-ink #111111 肉眼无差 → 搜索/购物袋/账户三个钮 hover 整体归零。 */
.vt-icon-btn:hover { color: var(--rose-ink); background: var(--vt-hover-surface); }
.vt-icon-btn svg, .vt-icon { width: 19px; height: 19px; }
/* 购物袋角标：金底 + --vt-on-gold 近黑字（8.40:1）——全站唯一的一块金色底，pill 保持圆形 */
.vt-cart-badge {
  position: absolute; top: 0; right: 0;
  background: var(--rose); color: var(--vt-on-gold);
  font-family: var(--apple-font); font-size: .58rem; font-weight: 700;
  min-width: 17px; height: 17px; border-radius: var(--apple-radius-pill); padding: 0 4px;
  display: flex; align-items: center; justify-content: center;
}

/* —— footer：两段式（双栏链接 / 版权行），段与段之间用发丝线断开 ——
   ⚠️ 2026-08-24 删掉了这里的第一段「服务承诺条」（.vt-footer-usp / .vt-usp）：
      实测对标站 footer 没有这一条，且它与正文 USP 黑条（.vt-sp-）文案几乎同义。
      完整理由写在 layout.ejs 的 footer 注释里。**规则一并删除，不留死规则** ——
      留着的 CSS 没有任何选择器命中，改样式的人却会以为它还在生效。 */
.vt-footer { border-top: 1px solid var(--vt-hairline); background: var(--cream); margin-top: 90px; }
.vt-footer-inner { max-width: var(--content-max); margin: 0 auto; padding: 0 40px; }

/* ① 双栏：左品牌 + 一句话，右两组链接 */
.vt-footer-cols {
  display: flex; justify-content: space-between; gap: 40px 60px; flex-wrap: wrap;
  padding: 46px 0 40px;
}
.vt-footer-brandcol { max-width: 340px; }
.vt-footer-brand { display: flex; align-items: baseline; gap: 10px; }
.vt-footer-logo { height: 28px; width: auto; border-radius: var(--radius-sm); }
.vt-footer-brandname {
  font-family: var(--serif, Georgia, serif); font-size: 1.2rem; font-weight: 500;
  letter-spacing: .2em; text-transform: uppercase; color: var(--rose-ink);
}
/* 同 nav 副标：--ink + 透明度是排版取舍（跟字标同色系），实测亮 4.78 ✓ / 暗 6.78 ✓ */
.vt-footer-sub { font-size: .62rem; letter-spacing: .32em; text-transform: uppercase; color: var(--ink); opacity: .62; }
.vt-footer-tag {
  margin: 16px 0 0; font-size: .84rem; line-height: 1.9;
  color: var(--ink); opacity: .68; /* ink+透明度，实测亮 5.87 ✓ / 暗 7.96 ✓（--muted 现也已达标 5.04，两者皆可） */
}
.vt-footer-links { display: flex; gap: 70px; flex-wrap: wrap; }
.vt-footer-col { display: flex; flex-direction: column; gap: 11px; }
.vt-footer-col h4 {
  margin: 0 0 5px; font-size: .68rem; font-weight: 600;
  letter-spacing: .24em; text-transform: uppercase; color: var(--rose-ink);
}
.vt-footer-col a {
  font-size: .84rem; color: var(--ink); opacity: .72; text-decoration: none;
  transition: opacity .18s; align-self: flex-start;
  border-bottom: 1px solid transparent;
}
.vt-footer-col a:hover { color: var(--ink); opacity: 1; border-bottom-color: var(--rose); text-decoration: none; }

/* ② 社媒 + 支付行（layout 的 .vt-footer-meta：左 ul.vt-footer-social、右 ul.vt-footer-pay）。
   2026-08-28 补：这一段标记加进 layout 时 CSS 没跟上，十套皮肤的页脚都渲染成裸 <ul> 圆点列表。
   社媒 = 圆形描边图标钮；支付 = 「图标 + 文字」小徽章（通用图标不是品牌 logo，见 layout 注释），
   全走 token，皮肤（peony 的炭黑页脚等）只需覆写颜色。 */
.vt-footer-meta {
  border-top: 1px solid var(--vt-hairline);
  padding: 18px 0;
  display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap;
}
.vt-footer-social, .vt-footer-pay {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.vt-footer-social a {
  display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center;
  border: 1px solid var(--vt-hairline); border-radius: var(--apple-radius-pill);
  color: var(--ink); text-decoration: none; transition: border-color .18s, color .18s;
}
.vt-footer-social a:hover { border-color: var(--ink); color: var(--ink); text-decoration: none; }
.vt-footer-social i { font-size: .95rem; line-height: 1; }
.vt-footer-pay li {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px;
  border: 1px solid var(--vt-hairline); border-radius: var(--radius-sm);
  font-size: .72rem; letter-spacing: .04em; color: var(--muted); white-space: nowrap;
}
.vt-footer-pay i { font-size: .95rem; line-height: 1; color: var(--ink); }
@media (max-width: 767.98px) { .vt-footer-meta { justify-content: center; } }

/* ③ 版权行：发丝上缘线 + 两端对齐小字 */
.vt-footer-bottom {
  border-top: 1px solid var(--vt-hairline);
  padding: 20px 0 28px;
  display: flex; justify-content: space-between; align-items: center; gap: 10px 24px; flex-wrap: wrap;
  font-size: .74rem; letter-spacing: .06em; color: var(--ink);
}
.vt-footer-copy { opacity: .62; }
.vt-footer-lang { display: flex; align-items: center; gap: 8px; }
.vt-footer-lang a { color: var(--ink); opacity: .62; text-decoration: none; transition: opacity .18s; }
.vt-footer-lang a:hover, .vt-footer-lang a.active { opacity: 1; color: var(--ink); }
.vt-footer-lang-sep { color: var(--line); }

/* —— 手机端：nav 收窄留白、footer 双栏改竖排 —— */
@media (max-width: 767.98px) {
  .vt-nav-inner { padding: 0 18px; gap: 14px; }
  /* 通知条：那句截单文案在 390px 下会折成两行。因为它走普通流、随页面滚走，
     多占的高度只是一次性的，不会永久吃首屏 —— 故这里只收字号与留白，不缩文案。
     改成左对齐 + 图标顶对齐：多行居中时图标会浮在中间那行边上，读起来像插图不像前缀。 */
  .vt-notice-inner {
    padding: 10px 18px; gap: 8px; font-size: .7rem; letter-spacing: .03em;
    align-items: flex-start; justify-content: flex-start; text-align: left;
  }
  .vt-notice-inner i { margin-top: 2px; font-size: .82rem; }

  .vt-brand .vt-mark { font-size: 1.12rem; letter-spacing: .16em; }
  .vt-footer-inner { padding: 0 18px; }
  .vt-footer-cols { flex-direction: column; gap: 30px; padding: 32px 0 28px; }
  .vt-footer-links { gap: 44px; }
  /* 底部有共享 _bottom-nav 悬浮条，footer 末端留出让位空间 */
  /* 页脚底部留白改由 _shared/_bottom-nav.ejs 统一给 body 垫（所有带底栏的主题同一处）*/
}

/* ========== 段3：首页（大图 hero + 营销区块）==========
   已落地：
     · .vt-storenote- 系列 —— 店主公告（读 bloggers.status_content），配 index.ejs
       ⚠️ 顶部通知条 .vt-notice 已于 2026-08-25 搬到**段2**（它现在在 nav 之上、属 layout 不属首页）。
          .vt-notice 是硬编码的配送截单规则，.vt-storenote 是店主自己写的话 —— 别把两者当成一件事。
     · .vt-occ-count —— 在售款数（totalItems），落在区块 #6 的区块头下
     · .vt-hero-   系列 —— 区块 #5 Hero（大图 + 双 CTA + 品牌兜底），配 index.ejs
     · .vt-occ-    系列 —— 区块 #6 快捷分类网格（方形卡 + 压图标签），配 index.ejs
     · .vt-sp-     系列 —— 区块 #7 USP 卖点条（通栏近黑条 · 四格），配 index.ejs
     · .vt-wrap / .vt-sec- 系列 —— 首页各营销区块**共用**的内容外壳与区块头
     · .vt-card-   系列 —— 通用产品卡，配 partials/_product-card.ejs（首页 3 个区块共用）
     · .vt-carousel- 系列 —— 通用轮播，配 partials/_carousel.ejs（首页 3 处轮播共用）
     · .vt-tabs / .vt-tablist / .vt-tab / .vt-tabpanel —— 轮播的 tab 条（_carousel 的 tabs 参数）
     · .vt-coll8 / .vt-catprod / .vt-best —— 中段三块轮播区（#8 当季系列 / #9 带 tab 的
       系列商品 / #10 店主精选），配 index.ejs
     · .vt-cc-    系列 —— 系列卡，配 partials/_collection-card.ejs（区块 #8 用）
     · .vt-retail- 系列 —— 区块 #11 到店（通栏照片带 + 压图文案 + 致电 CTA），配 index.ejs
     · .vt-pitch-  系列 —— 区块 #13 配送承诺文字块（软底通栏带 · 0 图），配 index.ejs
     · .vt-story-  系列 —— 区块 #14 品牌故事五块（通栏两栏图文交替 · 序号占位），配 index.ejs
     · .vt-reads- / .vt-read- 系列 —— 区块 #15/#16 文章区 ×2（眉题 + 标题 + 3 篇卡片），配 index.ejs
     · .vt-contact- 系列 —— 区块 #17 联系方式（1–3 张卡，**有值才出**），配 index.ejs
     · .vt-cathero- / .vt-coll- / .vt-filters- / .vt-filter- / .vt-pager- 系列 ——
       **collection 形态**（?cat=<id>）的分类 hero + 工具条 + 筛选面板 + 网格 + 分页，
       配 index.ejs 的 _isColl 分支。营销区块在该形态下整块不渲染。
   其余营销区块（#8/#10–#17）留待 Task 2.5–2.7。
   ⚠️ 区块 #7 叫 .vt-sp- 而不是 .vt-usp 是**历史原因**（段2 的 footer 曾有一条同名的服务承诺条，
      2026-08-24 已删）。名字保持不动：.vt-sp- = selling points = Phase 5 那张 selling_points 表，
      改名只会让 git blame 与 Phase 5 的接表对不上，换不来任何东西。
   段4（PDP）在本段之后，**别把首页的规则写到那边去**（惯例：段3 恒为首页、段4 恒为 PDP）。

   金色纪律（同段4）：--rose 亮 #C8A96A / 暗 #D4B87C，配浅字实测 2.25 ✗、配近黑字 8.40 ✓。
   所以金在本段只出现在两处 —— 「热销」徽章底（字色一律 var(--vt-on-gold)）与无图占位的淡晕。
   徽章/条状底色一律 token 对（--rose-deep 配 --on-accent、--rose 配 --vt-on-gold），
   绝不写死 #fff（零容忍护栏 token-bg-hardcoded-fg.test.js）。

   ⚠️ 改本段必须 bump layout.ejs 里 vitrine-theme.css 的 ?v= ，否则浏览器吃旧缓存、
      服务器返回的文本却是新的 —— 这类「看起来没生效」最费时间。 */

/* —— 区块 #5：Hero（配 index.ejs 的 .vt-hero）——
   两种形态共用一套排版类（.vt-hero-title / .vt-hero-sub / .vt-hero-btn），只有**配色**按上下文分叉：
     · .vt-hero-slide 内 = 骑在用户上传的照片上 → 一律 --vt-on-photo / --vt-photo-ink（mode-invariant）
     · .vt-hero-brand 内 = 无照片、骑在页面底色上 → 走常规 token（--ink / --rose-deep / --on-accent）
   分叉的原因见段1 --vt-on-photo 的注释：底不随明暗翻转，前景就不该翻转。 */
.vt-hero { position: relative; width: 100%; }

/* 高度用 clamp 而不是 aspect-ratio：hero 要在超宽屏上保持「一条横幅」而不是越拉越高，
   同时在矮窗口里也不能把整个首屏吃光。下限 420 保证文案块放得下，上限 620 留出下一区块的钩子。 */
.vt-hero-slide {
  position: relative; overflow: hidden;
  height: clamp(420px, 62vh, 620px);
  background: var(--blush); /* 图片加载完成前的底：不留纯白闪一下 */
}
.vt-hero-linkwrap { position: absolute; inset: 0; display: block; }
/* 图片与视频同一套铺满规则（object-fit 对 video 同样生效） */
.vt-hero-photo {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; object-fit: cover;
}
/* banner 记录存在却没有媒体文件：淡金晕占位（同 .vt-card-ph 的语言，只是放大到通栏尺度） */
.vt-hero-ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: radial-gradient(60% 70% at 50% 46%, var(--blush) 0%, transparent 66%), var(--cream);
}
.vt-hero-ph svg { width: 96px; height: 96px; color: var(--rose); opacity: .34; }

/* 压暗层 —— 本主题最容易翻车的一处，数值是**算出来的**不是调出来的。
   前提：照片由店主上传，亮暗完全不可控，所以对比度不能指望「照片本身够暗」。
   做法：底部一段做成**常数平台**（0% → 58% 都是 .74），文案块整体落在平台内，
   于是文字带的合成底色与照片内容无关、可精确计算：
     最坏情况 = 纯白照片 → 合成 = .74×17 + .26×255 = 78.9 ≈ #4F4F4F
     与 --vt-on-photo #FAF9F7 的对比度 = 7.80:1 ✓ AAA（AA 只要 4.5）
   照片越暗结果只会更高，所以 7.80 是**下界**不是典型值。
   58% 这个拐点是按文案块实测高度定的（桌面 620px 高时文案块约 292px = 47%，
   390px 下约 200px = 48%），两档都留了约 10 个百分点的余量；
   改大标题字号或多加一行副标时**必须重新核这个余量**，否则标题顶端会飘到渐变段里。
   平台之上（58% → 100%）迅速淡出，让照片上半部保持通透 —— 整张压 .74 会把好照片压成一块灰。 */
.vt-hero-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top,
    rgba(17, 17, 17, .74) 0%,
    rgba(17, 17, 17, .74) 58%,
    rgba(17, 17, 17, .34) 84%,
    rgba(17, 17, 17, .10) 100%);
}

.vt-hero-copy {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 0;
  width: 100%; max-width: var(--content-max); padding: 0 40px 62px;
  color: var(--vt-on-photo);
}
/* 文字阴影是**第二道**保险，不计入上面那个 7.80 的账：压暗层保证的是平均底色，
   而照片局部可能有一块高光恰好落在某个字后面 —— 阴影管的是那种局部情况。 */
.vt-hero-slide .vt-hero-title,
.vt-hero-slide .vt-hero-sub { text-shadow: 0 1px 3px rgba(17, 17, 17, .45); }

.vt-hero-title {
  margin: 0; font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.9rem, 4.4vw, 3.3rem); line-height: 1.14; letter-spacing: .01em;
  max-width: 20ch; /* 刊头式换行：一行不超过约 20 字，超长标题不会拉成一条横线 */
}
.vt-hero-sub {
  margin: 14px 0 0; font-size: .95rem; line-height: 1.7; letter-spacing: .04em;
  max-width: 46ch;
}
.vt-hero-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }

/* 按钮的**形状**在这里定死，颜色交给下面按上下文分叉的两组规则。
   直角（--apple-radius 3px）+ 小字号大字距全大写 = 与 nav 菜单/徽章同一套排版语言。 */
.vt-hero-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 46px; padding: 0 26px;
  border: 1px solid transparent; border-radius: var(--apple-radius);
  font-family: var(--apple-font); font-size: .74rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; text-decoration: none;
  transition: var(--apple-transition);
}
/* 🔴 display 单独再钉一次、用更高特异度（2026-08-28 实栽，四套皮肤同时中招）：
   基底 public/css/style.css:848 有一条 `.carousel-item a { display: block }`，特异度 (0,1,1)
   压过上面的 `.vt-hero-btn` (0,1,0) —— 源序在后也没用，按钮被打回 block，align-items 失效，
   文字贴在按钮顶部（Roboto / Crimson 这类字体下最明显）。
   只钉 display、不把整条规则升特异度：皮肤（peony / meadow / petal / muse）用 `.vt-hero-btn` (0,1,0)
   覆写圆角 / 字号 / 内边距，整条升级会把它们全部盖掉。 */
.vt-hero .vt-hero-btn { display: inline-flex; }
/* ⚠️ 下面每条 :hover 都要显式写 text-decoration:none 与 color ——
   基底有一条全局 a:hover { color: var(--vt-accent-hover) }（段2 (b)），
   不写死就会在 hover 时把按钮文字染成「近黑/近白」，在照片上直接消失。
   specificity：本处两个类 (0,2,0) > a:hover (0,1,1)，压得住。 */

/* (1) 照片上的双 CTA：主钮实心浅底深字，次钮描边 —— hover 时两者互换填充，
       形状不变、只有明暗对调，在任何照片上都读得出「状态变了」。 */
.vt-hero-slide .vt-hero-btn-solid {
  background: var(--vt-on-photo); color: var(--vt-photo-ink); border-color: var(--vt-on-photo);
}
.vt-hero-slide .vt-hero-btn-solid:hover {
  background: var(--vt-photo-ink); color: var(--vt-on-photo); border-color: var(--vt-on-photo);
  text-decoration: none;
}
.vt-hero-slide .vt-hero-btn-ghost {
  background: transparent; color: var(--vt-on-photo); border-color: var(--vt-on-photo);
}
.vt-hero-slide .vt-hero-btn-ghost:hover {
  background: var(--vt-on-photo); color: var(--vt-photo-ink); border-color: var(--vt-on-photo);
  text-decoration: none;
}
.vt-hero-btn:focus-visible { outline: 2px solid var(--vt-on-photo); outline-offset: 3px; }

/* 圆点：与 .vt-carousel-dots 同一套发丝短横语言，只是改成压在照片上（故用 --vt-on-photo）。
   透明边撑点击热区，归零后热区只剩 2px 高、手指点不中（同段3 通用轮播的注释）。 */
.vt-hero-dots { position: absolute; left: 0; right: 0; bottom: 22px; margin: 0; padding: 0; z-index: 2; }
.vt-hero-dots [data-bs-target] {
  width: 26px; height: 2px; margin: 0 4px; border-radius: 0; opacity: 1;
  background-color: var(--vt-on-photo);
  border-top: 8px solid transparent; border-bottom: 8px solid transparent;
  transition: opacity .2s;
}
.vt-hero-dots [data-bs-target]:not(.active) { opacity: .45; }
.vt-hero-dots [data-bs-target]:focus-visible { outline: 2px solid var(--vt-on-photo); outline-offset: 3px; }

/* (2) 空态：品牌 hero（无 banner 时的兜底，不留白洞）——
       没有照片就没有压暗层，文字直接骑在页面底色上：--ink 对 --cream 实测亮 15.4 ✓ / 暗 14.9 ✓。 */
.vt-hero-brand {
  position: relative; overflow: hidden;
  background: var(--cream); border-bottom: 1px solid var(--vt-hairline);
  padding: clamp(64px, 11vh, 118px) 0 clamp(72px, 12vh, 128px);
}
.vt-hero-brand-inner { position: relative; z-index: 1; max-width: var(--content-max); margin: 0 auto; padding: 0 40px; }
.vt-hero-eyebrow {
  display: block; margin-bottom: 20px;
  font-size: .68rem; font-weight: 600; letter-spacing: .26em; text-transform: uppercase;
  color: var(--muted); /* 段1 已把 --muted 修到 5.04 ✓ AA，小字眉题可以放心用 */
}
.vt-hero-brand .vt-hero-title { color: var(--rose-ink); }
.vt-hero-brand .vt-hero-sub { color: var(--ink); opacity: .74; } /* 实测 7.18 ✓（同 footer 那套 ink+透明度的排版取舍） */
.vt-hero-brand .vt-hero-btn-solid { background: var(--rose-deep); color: var(--on-accent, #fff); border-color: var(--rose-deep); }
.vt-hero-brand .vt-hero-btn-solid:hover {
  background: var(--vt-accent-hover); color: var(--on-accent, #fff); border-color: var(--vt-accent-hover);
  text-decoration: none;
}
.vt-hero-brand .vt-hero-btn-ghost { background: transparent; color: var(--ink); border-color: var(--ink); }
.vt-hero-brand .vt-hero-btn-ghost:hover {
  background: var(--vt-hover-surface); color: var(--ink); border-color: var(--ink); text-decoration: none;
}
.vt-hero-brand .vt-hero-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
/* 手绘花：纯装饰，金色低透明度（金在本主题只做描边/细节，不承载文字）。
   overflow:hidden 在 .vt-hero-brand 上，所以负偏移只是让它「出画」，不会撑出横向滚动条。 */
.vt-hero-bloom {
  position: absolute; right: -40px; bottom: -70px;
  width: min(46vw, 420px); height: min(46vw, 420px);
  color: var(--rose); opacity: .16; pointer-events: none;
}

/* —— 首页通用件：内容外壳 + 区块头 ——
   .vt-wrap 与 .vt-sec-head 是**首页各营销区块共用**的，不属于任何单个区块。
   先在这里立好，Task 2.5–2.7 的区块直接用，免得五个区块各写一遍
   「max-width + 居中 + 左右 40」和「眉题 + 标题」——各写一遍的结果是宽度对不齐，
   而且不报错，只有把两个区块并排截图才看得出错台。
   宽度基准与 vt-nav / vt-footer 同为 --content-max（1240），左右 40 与 .vt-hero-copy 一致。 */
.vt-wrap { max-width: var(--content-max); margin: 0 auto; padding: 0 40px; }

/* 区块头居中：本主题是「刊头式」编辑风，居中标题配下方居中收缩的网格才成立
   （少态时网格会收窄居中，标题若靠左会与网格错开一大截）。 */
.vt-sec-head { text-align: center; margin-bottom: clamp(24px, 3.2vw, 38px); }
.vt-sec-eyebrow {
  display: block; margin-bottom: 12px;
  font-family: var(--apple-font); font-size: .66rem; font-weight: 600;
  letter-spacing: .26em; text-transform: uppercase;
  color: var(--muted); /* 段1 已把 --muted 修到 5.04 ✓ AA，小字眉题可以放心用 */
}
.vt-sec-title {
  margin: 0; font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.5rem, 3vw, 2.15rem); line-height: 1.2; letter-spacing: .01em;
  color: var(--rose-ink);
}

/* —— 店主公告（配 index.ejs 的 .vt-storenote）——
   来源：bloggers.status_content（后台富文本）。**不是**顶部通知条（那条在段2，是硬编码的
   配送截单规则）—— 两者语义不同、来源不同，绝不能互相顶替，详见 index.ejs 那一段的注释。

   形态：页面底色 + 一条金色短横 + 眉题 + 一段衬线正文，下缘一道发丝线收边。
   ⚠️ **刻意不做成第三条通栏实底带**：本页已有 .vt-sp（近黑）与 .vt-pitch（软底）两条，
      再来一条实底，三块会连成一片、编辑风的节奏全失。这一块要读起来像刊物里的一段引言，
      不像又一个促销条。
   ⚠️ 金只做那条 24px 的短横（描边/细节）—— 金 #C8A96A 配浅字实测 2.25 ✗、配近黑字 8.40 ✓，
      本主题的既定纪律是金不承载文字。眉题走 --muted（段1 已修到 5.04 ✓ AA）。

   正文用 --ink 而不是 --muted：这是店主亲口说的话，不是辅助说明，
   该按正文分量排（亮 15.4 ✓ / 暗 14.9 ✓，两态同一条规则通吃）。 */
.vt-storenote {
  padding: clamp(30px, 3.8vw, 46px) 0;
  border-bottom: 1px solid var(--vt-hairline);
  text-align: center;
}
.vt-storenote-rule {
  display: block; width: 24px; height: 1px; margin: 0 auto 14px;
  background: var(--rose);
}
.vt-storenote-eyebrow {
  display: block; margin-bottom: 10px;
  font-family: var(--apple-font); font-size: .66rem; font-weight: 600;
  letter-spacing: .26em; text-transform: uppercase; color: var(--muted);
}
/* 宽度上界用 px 不用 ch —— 与 .vt-pitch-title 同一条教训：ch 按拉丁字宽算，对 CJK 偏窄一倍，
   中英共用的宽度上界一律别用它。640px 下中文约 26 字/行、英文约 78 字符/行，都在舒适区。 */
.vt-storenote-body {
  margin: 0 auto; max-width: 640px;
  font-family: var(--serif, Georgia, serif);
  font-size: 1.02rem; line-height: 1.8; color: var(--ink);
}
/* 富文本存的是段落标签，浏览器默认给它上下各 1em 外边距 —— 第一段/最后一段会把本块
   撑出两截不对称的留白。收掉首尾、段间留 .55em。
   加粗/链接的样式跟随全局（段2 的 a:hover 等），这里不另立一套。 */
.vt-storenote-body > :first-child { margin-top: 0; }
.vt-storenote-body > :last-child { margin-bottom: 0; }
.vt-storenote-body p { margin: 0 0 .55em; }

/* —— 区块 #6：快捷分类网格（配 index.ejs 的 .vt-occ）——
   形态：正方形卡片，图铺满，分类名**压在图上**（不是排在图下）。
   为什么压图上而不是「图 + 图下文字」：一排 5 个正方形，文字排在框外时每个格子会变成
   「一张图 + 一行标题」的双层结构，5 个并排就是 10 条水平线，编辑风的整洁感全失；
   压在图上时每格只有一个方块轮廓，整排读起来是一条马赛克带 —— 这也是对标站的做法。
   代价是要认真处理对比度，见下方 .vt-occ-scrim。 */
.vt-occ { padding: clamp(56px, 7vw, 92px) 0 0; }

/* 在售款数（区块头下的一行）—— florisse 那条店铺信息条右端 `N 款花束` 的落点。
   小字 + --muted：它是一个规模信号，不是标题的一部分，不该跟衬线大标题抢分量。
   居中：.vt-sec-head 整体就是居中的刊头式（见上方注释），靠左会与标题错开。
   ⚠️ 这里**不出货币也不出价格**，纯计数 —— 别顺手加「起价…」之类的东西，
      那需要 formatPrice()，而本行是店铺规模不是商品信息。 */
.vt-occ-count {
  margin: 12px 0 0;
  font-size: .78rem; letter-spacing: .06em; line-height: 1.6;
  color: var(--muted);   /* 段1 已把 --muted 修到 5.04 ✓ AA */
}

/* 列数交给 auto-fit（自然响应，不写死断点）；**少态的收缩**交给 max-width。
   为什么必须两者配合（三种单一写法都不行，实测过）：
     · auto-fill              → 保留空轨道：2 个分类后面跟着 3 个看得见的空位；
     · auto-fit 不限宽        → 塌掉空轨道后 1fr 把 2 个格子各拉成约 580px 的巨幅方块；
     · repeat(min(var(),5),…) → 不合法，repeat() 的次数必须是字面整数，不吃 min()。
   max-width 里的 216px 是**桌面满列时的实际格宽**（容器内宽 1160 = 5 × 216 + 4 × 20），
   所以 --vt-occ-cols 取到 5（即分类数 ≥ 5）时算出来正好 1160 = 不设限；
   取 2 时算出 452px，网格收成两格居中，右边不留空位。
   --vt-occ-cols 由 index.ejs 下发 = min(分类数, 5)。 */
.vt-occ-grid {
  display: grid;
  gap: 22px 20px;
  grid-template-columns: repeat(auto-fit, minmax(var(--vt-occ-min, 180px), 1fr));
  max-width: calc(var(--vt-occ-cols, 5) * 216px + (var(--vt-occ-cols, 5) - 1) * 20px);
  margin-inline: auto;
}

.vt-occ-card { display: block; min-width: 0; text-decoration: none; }
.vt-occ-card:hover { text-decoration: none; }
.vt-occ-card:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* aspect-ratio 1:1 —— 方形是本区的识别特征，同时把版面高度提前占住，图未到也不 CLS。
   发丝描边 + hover 转近黑：与 .vt-card-media 同一套反馈语言（不加投影、不改形状）。 */
.vt-occ-media {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 1 / 1;
  background: var(--cream);
  border: 1px solid var(--vt-hairline); border-radius: var(--radius);
  transition: border-color .22s;
}
.vt-occ-card:hover .vt-occ-media { border-color: var(--ink); }
.vt-occ-photo {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.vt-occ-card:hover .vt-occ-photo { transform: scale(1.05); }

/* 无图占位：**近黑排版方块** —— 暗底 + 一道金色发丝内框 + 分类首字（Didone）。
   为什么不用 #bloom 手绘花（hero 与产品卡的无图占位都用它）：那两处一屏最多出现一两个，
   而这里一排 5 个格子若都没配图，就会变成 5 朵一模一样的花 —— 分不出彼此、也读不出分类。
   首字至少携带信息。
   ⚠️ 为什么底色是**暗**的（第一版做成近白底，实测推翻）：本区的标签一律压在压暗层之上、
      用 mode-invariant 的近白字（理由见 .vt-occ-scrim）。近白占位 + 那层 .74 压暗 =
      整格变成一块自上而下的灰渐变，与旁边的照片格摆在一起像「图没加载完」。
      直接把占位做成暗底后，压暗层压在它上面几乎无形，格子读起来是一枚**有意为之的**
      黑底金框排版贴片，而不是一张坏掉的图。
   ⚠️ 底色用 --scrim（#0B0B0B，段1 里唯一标注「明暗恒定」的暗底）而不是 --rose-ink：
      --rose-ink 跨明暗翻转（暗色下变近白），一翻标签就没了底 —— 与 --vt-on-photo
      那条注释是同一个道理：底不随明暗变，前景才可以是单值。
   金只做那道内框（描边/细节），不承载任何文字 —— 全主题的金色纪律。
   padding-bottom 把首字整体抬高：下方 40% 是压暗层的常数平台，字落进去会被压暗一档。
   ph1..ph3 只错开内框留白与字号，避免一排全无图时像三张同款贴纸。 */
.vt-occ-ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  padding-bottom: 22%;
  background: var(--scrim);
}
.vt-occ-ph::before {
  content: ''; position: absolute; inset: 14px;
  border: 1px solid var(--rose); opacity: .55;
}
.vt-occ-ph2::before { inset: 20px; }
.vt-occ-ph3::before { inset: 11px; }
/* 首字用 --vt-on-photo（同标签，mode-invariant 的近白）压低透明度：
   #FAF9F7 @ .34 合成在 #0B0B0B 上约 #545453，比标签暗一档 —— 装饰不该抢标签的重量。 */
.vt-occ-ph-letter {
  font-family: var(--serif, Georgia, serif); font-weight: 400; line-height: 1;
  font-size: clamp(1.9rem, 4.6vw, 2.9rem); letter-spacing: .02em;
  color: var(--vt-on-photo); opacity: .34;
}
.vt-occ-ph2 .vt-occ-ph-letter { font-size: clamp(1.7rem, 4.1vw, 2.5rem); }
.vt-occ-ph3 .vt-occ-ph-letter { font-size: clamp(2.1rem, 5vw, 3.2rem); }

/* 压暗层 —— 与 .vt-hero-scrim 同一套「常数平台」做法，**常数与结论刻意取同一个数**，
   这样全主题「压在照片上的文字」只有一个对比度要记。
   前提：分类图由店主上传，亮暗不可控；且无图时底是近白占位 —— 两者都得由这一层兜。
     底部 0% → 40% 恒为 .74（常数平台，不是渐变的一段）→ 标签带的合成底色与底下是什么无关：
       最坏情况（纯白图）合成 = .74 × 17 + .26 × 255 = 78.9 ≈ #4F4F4F
       与 --vt-on-photo #FAF9F7 = 7.80:1 ✓ AAA（AA 只要 4.5）
     浏览器实测（canvas 按 computed 的 background-image 复刻这条渐变、底铺纯白、
     在标签带的上下沿取样）：合成 #4F4F4F–#4E4E4E → 7.78 / 7.91，与上式一致。
     图越暗结果只会更高，所以 7.78 是**下界**不是典型值。
     无图占位底是 --cream #FAF9F7（比纯白还暗一点点）→ 同样在下界之上。
   40% 这个拐点按标签实测高度定：标签最多两行，.78rem × 1.35 + 下留白 12 ≈ 40px。
   实测标签带（含两行时的上界）距格底：桌面 216px 格 13.8%、390px 下 165–170px 格约 15.5%
   —— 两档距平台上沿都留了 24 个百分点以上余量。
   ⚠️ 改大标签字号或放开行数时必须重核这个余量，否则第二行会飘进渐变段里。
   平台之上迅速淡出，图片上半部保持通透 —— 整张压 .74 会把好照片压成一块灰。
   ⚠️ 前景**不能**用 --on-accent：它跨明暗翻转，而照片不会跟着系统明暗翻转
      （详见段1 --vt-on-photo 的注释）。 */
.vt-occ-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top,
    rgba(17, 17, 17, .74) 0%,
    rgba(17, 17, 17, .74) 40%,
    rgba(17, 17, 17, .30) 70%,
    rgba(17, 17, 17, 0) 100%);
}

/* 标签：小字号大字距全大写 —— 与 nav 菜单 / 徽章 / 按钮同一套排版语言。
   两行截断 + overflow-wrap:anywhere：超长分类名不会顶穿平台、也不会撑破格子。
   text-shadow 是**第二道**保险（不计入上面 7.80 的账）：平台保证的是平均底色，
   而图片局部可能有一块高光恰好落在某个字后面，阴影管的就是那种局部情况。
   color 显式写在标签自己身上（不靠从 <a> 继承）：基底有一条全局 a:hover 会改 <a> 的 color，
   继承的话 hover 时标签会被染成近黑/近白，在压暗过的图上直接消失。 */
.vt-occ-label {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 0 12px 12px;
  color: var(--vt-on-photo);
  font-family: var(--apple-font); font-size: .78rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase; line-height: 1.35;
  text-align: center; overflow-wrap: anywhere;
  text-shadow: 0 1px 3px rgba(17, 17, 17, .45);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* —— 区块 #7：USP 卖点区（配 index.ejs 的 .vt-sp）——
   前缀是 .vt-sp-（selling points，= Phase 5 那张 selling_points 表的名字）。
   ⚠️ 曾经 .vt-usp 被段2 的 footer 服务承诺条占着，所以这里避开了那个名字；那条 2026-08-24
      已删，但**名字不改回去** —— .vt-sp- 与 Phase 5 的表名对得上，改名只赚一次手感、
      赔掉 git blame 与接表时的可检索性。

   形态：通栏近黑条（本主题唯一的大色块），四格等分、图标在上文字在下、整体居中。
   为什么不做成「页面底色 + 四格发丝竖线」：那正是被删掉的 footer 卖点条的样子 ——
   一页上出现两条长得一样的四格条，读者会以为已经翻到页脚（这也是删那条的原因之一）。
   通栏实色条同时把首页上半段（hero + 分类网格）与下半段（后续产品区块）断开，
   是这一区在版面上的第二个作用。

   配色纪律：底走 token（--rose-deep：亮 #111111 / 暗 #F2F0ED），前景一律
   var(--on-accent, #fff) —— 底色走 token 就绝不写死白字（零容忍护栏 token-bg-hardcoded-fg）。
   实测：亮 #FAF9F7 骑在 #111111 上 17.95 ✓ AAA；暗 #111111 骑在 #F2F0ED 上 16.60 ✓ AAA（两个模式的图标同色同值）。
   ⚠️ 图标同样走 --on-accent，**不能**用金（--rose）：金在亮色的近黑底上是 8.40 ✓，
      但暗色下底翻成近白，金配近白只有 2.25 ✗ —— 图标会几乎消失。金在本主题只做
      描边/细节，且只在**不随明暗翻转**的底上（如 .vt-occ-ph 的近黑占位内框）。

   margin-top 是本区与上一区（分类网格）之间的唯一间距 —— .vt-occ 只有 padding-top、
   没有 padding-bottom，正是为了让「谁在下面谁负责间距」这条规则在后续区块里继续成立。 */
.vt-sp {
  margin-top: clamp(56px, 7vw, 92px);
  padding: clamp(30px, 3.6vw, 44px) 0;
  background: var(--rose-deep);
  color: var(--on-accent, #fff);
}
.vt-sp-row {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 26px 20px;
}
/* min-width:0 —— grid 项默认 min-width:auto，超长英文卖点会把格子撑破、整条溢出 */
.vt-sp-item {
  min-width: 0;
  display: flex; flex-direction: column; align-items: center;
  gap: 12px; text-align: center;
}
.vt-sp-icon { font-size: 1.45rem; line-height: 1; color: var(--on-accent, #fff); }
/* 小字号大字距全大写：与 nav 菜单 / 徽章 / 按钮 / 分类标签同一套排版语言。
   中文没有大小写，text-transform 对它是空操作，字距则同样成立（编辑风的疏排）。
   max-width 26ch 让四条各自换行的位置相近：实测英文四条在 26ch 下**都是整两行**（22ch 时
   「Free personalised card + photo」会掉成三行、四格高度参差；不设上限则四条都挤成一行、
   最长那条离邻格只剩 20px）。中文四条在同一上限下都是一行。 */
.vt-sp-text {
  max-width: 26ch;
  font-family: var(--apple-font); font-size: .74rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; line-height: 1.65;
  color: var(--on-accent, #fff);
  overflow-wrap: anywhere;
}

/* —— 中段三块轮播区（#8 当季系列 / #9 带 tab 的系列商品 / #10 店主精选）——
   为什么是三块而不是对标站的四块：完整理由在 index.ejs「中段轮播区的数据准备」那段注释里
   （一句话版：分类表里没有任何「最新 / 最热」的信号，把同一批系列切成两块再各起一个标题
   等于编造，与「不许拿会员价差冒充折扣率」是同一类错误）。

   间距沿用本段既定的「谁在下面谁负责间距」：三块都只有 padding-top、没有 padding-bottom。
   最后一块与页脚之间由 .vt-footer 自己的 margin-top:90px 负责，所以这里一条 bottom 都不用写
   —— 而且这样一来「哪一块是最后一块」（随空态变）不影响版面。

   --vt-arrow-top：把箭头从「整张卡的中线」上提到**图片的中线**。
   卡片是「图 + 图下文字」的双层结构，图占约八成高度，箭头落在 50% 时会压在文字行上、
   看起来像在指标题而不是指图。40% 由两块的实测尺寸取交集：
     产品卡 273px 宽 → 图 341 + 文字 66 = 407，图心 170 = 41.8%
     系列卡 372px 宽 → 图 465 + 文字 110 = 575，图心 232 = 40.3%  */
.vt-coll8, .vt-catprod, .vt-best, .vt-collbest { padding: clamp(56px, 7vw, 92px) 0 0; }
.vt-coll8 .vt-carousel-arrow,
.vt-catprod .vt-carousel-arrow,
.vt-best .vt-carousel-arrow { --vt-arrow-top: 40%; }

/* —— #8b 热销系列：3 列**静态网格**（Phase 4 Task 4.4）——
   2026-08-26 实测对标站（1440）：它首页两块系列区都是 3 列网格、卡片 423×576，
   不是轮播（设计文档 §3 #8 写的「轮播，箭头」与实测不符）。
   本块按实测做网格；#8 维持轮播不动 —— 那是 Phase 2 已验收的形态。

   ⚠️ 用 auto-fit + minmax 而不是写死 `repeat(3, 1fr)`：卡片数**由店主勾多少决定**，
      写死 3 列时若只勾了 1 个，那张卡会被拉成整行宽的怪物（.vt-cc 是竖幅卡，
      宽高比一崩就很难看）。auto-fit 让 1–2 张时也保持正常卡宽、靠左排。
   ⚠️ minmax 的下限 260px 是 .vt-cc 竖幅卡还立得住的最小宽度；上限 1fr 让它在
      3 列排不满时把剩余宽度均分，不留右侧空洞。
   两块的卡片长相由同一个 .vt-cc 规则供给，所以网格化不会让两块的卡片漂开。 */
.vt-collbest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(20px, 2.2vw, 34px);
  align-items: stretch;   /* 配 .vt-cc 的 height:100%，让一行卡的底行对齐 */
}
@media (max-width: 767.98px) {
  /* 手机端两列 —— 单列会让这一块在 12 张卡时长得没有尽头。
     对标站手机端同样是两列。 */
  .vt-collbest-grid { grid-template-columns: repeat(2, 1fr); gap: 14px; }
}

/* —— 系列卡（配 partials/_collection-card.ejs）——
   与区块 #6 的 .vt-occ-card 是**两张不同的卡**，不是一张卡的两个尺寸：
   #6 读 categories.image + name（导航用，方形、名字压图上），
   本卡读 homepage_image / homepage_title_* / homepage_description_*（编辑用，竖幅、文字排在图下）。
   那句描述在全站没有第二个消费方，也压不进图里 —— 这就是本卡存在的理由。

   ⚠️ 文字排在图**下方**（骑在页面底色上）而不是压在图上，于是本卡**不需要压暗层**，
      也就没有 .vt-occ-scrim 那笔「店主上传的图亮暗不可控」的对比度账。
      标题走 --rose-ink、描述走 --muted，两者都是段1 已核过的正文级 token。 */
.vt-cc {
  /* 竖向 flex + height:100%：同一屏里的卡描述行数可能不同（有的分类没配描述），
     靠 .vt-cc-foot 的 margin-top:auto 把「件数 + 箭头」那行压到底部，
     一排卡的底行才对得齐。不这么写的话每张卡的箭头高度各不相同，一眼就散。 */
  display: flex; flex-direction: column; height: 100%;
  min-width: 0; text-decoration: none; color: var(--ink);
}
.vt-cc:hover { text-decoration: none; color: var(--ink); }
.vt-cc:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.vt-cc-media {
  position: relative; overflow: hidden;
  background: var(--card);
  border: 1px solid var(--vt-hairline); border-radius: var(--radius);
  transition: border-color .22s;
}
.vt-cc:hover .vt-cc-media { border-color: var(--ink); }
/* 4:5 竖幅：与产品卡 / PDP 主图同比例，整页的图形节奏只有一种。
   aspect-ratio 同时把版面高度提前占住，图未加载完也不会 CLS。 */
.vt-cc-photo {
  display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover;
  transition: transform .5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.vt-cc:hover .vt-cc-photo { transform: scale(1.04); }

/* 无图占位：近黑排版贴片 + 金色发丝内框 + 系列首字 —— 与 .vt-occ-ph 同一套语言，
   连「为什么底是暗的」的理由都一样（见该处）：本卡虽然文字不压图，但一排三张竖幅
   若都做成近白占位，会读成三张没加载完的图；做成有意为之的黑底金框贴片就不会。
   底色用 --scrim（明暗恒定的近黑）而不是 --rose-ink：--rose-ink 暗色下翻近白，
   一翻首字（mode-invariant 的近白）就没了底。
   金只做那道内框，不承载文字 —— 全主题的金色纪律。 */
.vt-cc-ph {
  position: relative; aspect-ratio: 4 / 5;
  display: grid; place-items: center;
  background: var(--scrim);
}
.vt-cc-ph::before {
  content: ''; position: absolute; inset: 16px;
  border: 1px solid var(--rose); opacity: .55;
}
.vt-cc-ph2::before { inset: 23px; }
.vt-cc-ph3::before { inset: 12px; }
/* #FAF9F7 @ .34 合成在 #0B0B0B 上约 #545453 —— 装饰不该有正文的重量（同 .vt-occ-ph-letter） */
.vt-cc-ph-letter {
  font-family: var(--serif, Georgia, serif); font-weight: 400; line-height: 1;
  font-size: clamp(2.2rem, 5vw, 3.4rem); letter-spacing: .02em;
  color: var(--vt-on-photo); opacity: .34;
}
.vt-cc-ph2 .vt-cc-ph-letter { font-size: clamp(2rem, 4.5vw, 3rem); }
.vt-cc-ph3 .vt-cc-ph-letter { font-size: clamp(2.4rem, 5.4vw, 3.8rem); }

.vt-cc-body { flex: 1; display: flex; flex-direction: column; padding-top: 16px; }
/* 标题在 hover 时**不**跟着全局 a:hover 变色（同 .vt-card-title 的理由）：
   --vt-accent-hover 在暗色是压暗一档，标题跟着变暗会读成「失效」而不是「可点」。 */
.vt-cc-title {
  margin: 0; font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.02rem, 1.5vw, 1.24rem); line-height: 1.32; letter-spacing: .01em;
  color: var(--rose-ink);
}
.vt-cc:hover .vt-cc-title { color: var(--rose-ink); }
/* 两行截断：描述由店主自由填写，长度完全不可控 —— 不截的话一张卡能把整排撑高一倍。
   min-height 锁两行的高度，让「有描述」与「只有一行描述」的卡片底行仍然齐平
   （完全没有描述的卡不渲染这个元素，由上面 .vt-cc-foot 的 margin-top:auto 兜）。 */
.vt-cc-desc {
  margin: 9px 0 0; font-size: .82rem; line-height: 1.65; color: var(--muted);
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  min-height: calc(.82rem * 1.65 * 2);
}
/* margin-top:auto 把本行压到卡片底部（理由见 .vt-cc 的注释） */
.vt-cc-foot { margin-top: auto; padding-top: 14px; display: flex; align-items: center; gap: 12px; }
.vt-cc-count {
  font-family: var(--apple-font); font-size: .64rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--muted);
}
/* 箭头恒定靠右（margin-left:auto）：没有件数可显示时它也不会跳到左边。
   纯装饰（EJS 里带 aria-hidden）——整张卡本身就是链接，再给箭头一个可读文本，
   读屏器会把同一个链接念两遍。 */
.vt-cc-go {
  margin-left: auto; display: inline-flex; color: var(--ink);
  transition: transform .22s;
}
.vt-cc-go svg { width: 20px; height: 20px; }
.vt-cc:hover .vt-cc-go { transform: translateX(4px); }

/* —— 通用产品卡 ——
   外观取舍：卡片本体**不描边不投影**，只给图框一道发丝线。理由是本主题的性格靠留白与
   发丝线撑，给每张卡再套一个框会在一排四张时变成「四个盒子」，编辑感全失。 */
.vt-card {
  display: block; text-decoration: none; color: var(--ink);
  /* min-width:0 ——卡片常作 grid/flex 子项，不写这行时超长标题会把格子撑破（grid 项默认 min-width:auto） */
  min-width: 0;
}
.vt-card:hover { text-decoration: none; color: var(--ink); }
/* 标题在 hover 时**不**跟着全局 a:hover 变色：--vt-accent-hover 在暗色是压暗一档，
   卡片标题跟着变暗会读成「失效」而不是「可点」。hover 的真反馈交给图框与图片缩放（见下）。 */
.vt-card:hover .vt-card-title { color: var(--ink); }

.vt-card-media {
  position: relative; overflow: hidden;
  background: var(--card);
  border: 1px solid var(--vt-hairline); border-radius: var(--radius);
  transition: border-color .22s;
}
.vt-card:hover .vt-card-media { border-color: var(--ink); }

/* 4:5 竖幅：与 PDP 主图（.vt-pdp-hero-img）同比例，翻到内页时构图不跳。
   aspect-ratio 同时把版面高度提前占住，图未加载完也不会 CLS。 */
.vt-card-photo {
  display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover;
  transition: transform .5s cubic-bezier(0.22, 0.61, 0.36, 1), opacity .35s;
}
.vt-card:hover .vt-card-photo { transform: scale(1.04); }

/* 双图 hover：第二层叠在第一层上，常态 opacity:0。纯 CSS，不需要任何脚本，
   也就不需要给图片挂事件属性（imgTag 会拒绝 on 开头的属性）。
   没传 hoverImage 时这一层根本不渲染 —— 「有就切、没有就不切」。 */
.vt-card-photo-hover { position: absolute; inset: 0; height: 100%; opacity: 0; }
.vt-card:hover .vt-card-photo-hover { opacity: 1; }

/* 无图占位：近白 + 一枝淡花影（黑白极简不留粉，同 .vt-pdp-hero-ph 的语言）。
   ph1..ph3 只错开淡晕位置，避免一排都没图时像三张同款贴纸。 */
.vt-card-ph {
  aspect-ratio: 4 / 5; display: grid; place-items: center;
  background: radial-gradient(200px 170px at 50% 44%, var(--blush) 0%, transparent 64%), var(--cream);
}
.vt-card-ph2 { background: radial-gradient(190px 180px at 36% 56%, var(--blush) 0%, transparent 62%), var(--cream); }
.vt-card-ph3 { background: radial-gradient(210px 160px at 64% 38%, var(--blush) 0%, transparent 66%), var(--cream); }
.vt-card-ph svg { width: 54px; height: 54px; color: var(--rose); opacity: .36; }

/* 徽章簇：左上角竖排。字号极小 + 大字距，与 nav 菜单/眉题同一套排版语言。 */
.vt-card-badges {
  position: absolute; top: 10px; left: 10px; z-index: 1;
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
}
.vt-card-badge {
  font-family: var(--apple-font); font-size: .56rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; line-height: 1;
  padding: 5px 8px; border-radius: var(--radius-sm); white-space: nowrap;
}
/* 新品：描边式（本主题的「次要信息」语言就是描边不填色），底走卡底 token、字走 --ink */
.vt-card-badge-new { background: var(--card); color: var(--ink); border: 1px solid var(--ink); }
/* 热销：全站第三块金底。字色必须 --vt-on-gold（近黑 8.40 ✓）；白字只有 2.25 ✗ */
.vt-card-badge-best { background: var(--rose); color: var(--vt-on-gold); }

/* 售罄：图片压暗 + 底部通栏近黑条。用 --rose-deep 配 --on-accent 这对 token，
   暗色下自动翻成「近白底 + 近黑字」，两模式都是 12+ 对比度。 */
.vt-card.is-out .vt-card-photo,
.vt-card.is-out .vt-card-ph { opacity: .48; }
.vt-card.is-out:hover .vt-card-photo { transform: none; } /* 售罄不给「可买」的暗示 */
.vt-card-oos {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  background: var(--rose-deep); color: var(--on-accent);
  text-align: center; padding: 8px 6px;
  font-family: var(--apple-font); font-size: .58rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
}

.vt-card-body { padding: 14px 2px 0; }
/* 两行截断 + 最小高度：同一排卡片的价格行才对得齐（标题一行一张、两行一张就会参差）。
   overflow-wrap:anywhere 兜住「一长串没有空格的英文名」把格子撑破的情况。 */
.vt-card-title {
  margin: 0 0 7px; font-family: var(--apple-font);
  font-size: .86rem; font-weight: 500; line-height: 1.55; color: var(--ink);
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  /* 2 行 × line-height 1.55 = 3.1em。⚠️ em 相对**本元素自己的 font-size**，
     所以这里必须是 2 × 1.55 = 3.1，不是 2 × 1.33。写小了的表现是：
     单行标题的卡片价格行比双行标题的高几个像素，一排卡片的价格纵向对不齐 —— 实测踩过。 */
  overflow: hidden; min-height: 3.1em;
}
.vt-card-priceline { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
/* 价格当标题排（同 PDP）：Didone 衬线 + 表格数字，一排卡片的价格纵向对得齐 */
.vt-card-price {
  font-family: var(--serif, Georgia, serif); font-size: 1.02rem; font-weight: 400;
  letter-spacing: .02em; color: var(--rose-ink); font-variant-numeric: tabular-nums;
}
/* 「起 / From」：小字中性色，不能抢价格本身的视觉重量 */
.vt-card-from {
  font-family: var(--apple-font); font-size: .62rem; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted);
}
/* 无价空态：不显 0.00，给一句中性文案（--muted 段1 已修到 5.04 ✓ AA） */
.vt-card-ask { font-size: .74rem; letter-spacing: .08em; color: var(--muted); }

/* 可选的卡片网格容器（区块自己也可以另写布局，这只是省得五个区块各写一遍 auto-fill）。
   --vt-card-min 决定塌列时机：默认 208px，1240 容器下桌面 4 列、平板 3 列、390px 手机 2 列。 */
.vt-card-grid {
  display: grid; gap: 26px 22px;
  grid-template-columns: repeat(auto-fill, minmax(var(--vt-card-min, 208px), 1fr));
}

/* —— 通用轮播（基于 Bootstrap 5 carousel）——
   DOM 见 partials/_carousel.ejs：.vt-carousel  外壳（同时是 Bootstrap 的 .carousel）
                                 └ .vt-carousel-inner  (.carousel-inner)
                                    └ .vt-carousel-slide  (.carousel-item)  = 桌面一屏
                                       └ .vt-carousel-row  = 一屏内的 N 格
                                          └ .vt-carousel-cell  = 一格
   每屏几格由 EJS 下发的自定义属性 --vt-per-view 决定（自定义属性会继承，故写在最外层一处）。 */
.vt-carousel { position: relative; }
/* 箭头骑在容器外沿 -18px，故左右各留出让位；不留的话箭头会被 .carousel-inner 的
   overflow:hidden 切掉一半（Bootstrap 的 overflow 挂在 inner 上、不在外壳上，所以不会切） */
.vt-carousel.has-arrows { padding: 0 2px; }

.vt-carousel-row {
  display: grid;
  grid-template-columns: repeat(var(--vt-per-view, 4), minmax(0, 1fr));
  gap: 22px;
}
.vt-carousel-cell { min-width: 0; }

/* 箭头：直角主题里唯一保留圆形的元件之一（同购物袋角标——控件要圆才读得出「可点」）。
   --vt-arrow-top 给区块留一个覆写口：产品卡的视觉重心在上半部的图上，
   区块可以写 .xx .vt-carousel-arrow { --vt-arrow-top: 38% } 把箭头对到图片中线。 */
.vt-carousel-arrow {
  position: absolute; top: var(--vt-arrow-top, 50%); transform: translateY(-50%);
  z-index: 2; width: 40px; height: 40px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--apple-radius-pill);
  cursor: pointer; transition: background .18s, border-color .18s, color .18s;
}
/* hover 底走 --vt-hover-surface 而不是 --blush：理由同段2 的 (c) 那一类
   （--blush 骑在卡底 --card 上 ΔL* 只有 3.6，本主题里等于没有反馈） */
.vt-carousel-arrow:hover { background: var(--vt-hover-surface); border-color: var(--ink); }
.vt-carousel-arrow:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.vt-carousel-arrow svg { width: 17px; height: 17px; }
.vt-carousel-prev { left: -18px; }
.vt-carousel-next { right: -18px; }

/* 圆点：Bootstrap 默认是绝对定位压在内容上的白色小条，这里改成静态排在下方的发丝短横。
   border-top/bottom 的透明边是 Bootstrap 用来撑点击热区的，保留（缩到 8px），别归零 ——
   归零后热区只剩 2px 高，手指根本点不中。 */
.vt-carousel-dots { position: static; margin: 22px 0 0; }
.vt-carousel-dots [data-bs-target] {
  width: 26px; height: 2px; margin: 0 4px; opacity: 1; border-radius: 0;
  background-color: var(--line);
  border-top: 8px solid transparent; border-bottom: 8px solid transparent;
  transition: background-color .2s;
}
.vt-carousel-dots [data-bs-target].active { background-color: var(--ink); }
.vt-carousel-dots [data-bs-target]:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* —— 轮播的 tab 条（配 partials/_carousel.ejs 的 tabs 参数）——
   DOM：.vt-tabs 外壳 └ .vt-tablist[role=tablist] └ .vt-tab[role=tab] ×N
                      └ .vt-tabpanel[role=tabpanel] ×N （每个面板里是一个完整的 .vt-carousel）
   为什么 tab 长在轮播组件里而不是另做一件：见 _carousel.ejs 的文件头。

   外观走「刊头式」的下划线 tab（不是胶囊钮）：本主题的排版语言是小字号 + 大字距 + 全大写 +
   发丝线，胶囊钮会在一屏黑白照片中间凭空多出几块填色。
   当前项 = 近黑字 + 近黑下划线（压住 tablist 的发丝底线，靠 margin-bottom:-1px 对齐），
   非当前项 = --muted（段1 已修到 5.04 ✓ AA，小字可以放心用）。
   ⚠️ 金**不**参与：金配近白 2.25 ✗，暗色模式下页面底色翻近黑虽然 8.40 ✓，
      但亮色下就没了 —— 一个跨明暗只有一半成立的强调色不能拿来做状态指示。
   实测（Playwright 取 computed 值算对比度，两态各测一次）：
     亮：当前项 16.54 ✓ AAA ｜ 非当前项 5.04 ✓ AA
     暗：当前项 16.67 ✓ AAA ｜ 非当前项 6.65 ✓ AA
   ⚠️ 量的时候要等 transition 跑完再取 computed —— 本规则有 color .18s 的过渡，
      切完主题立刻读会读到**插值中的中间色**（实测读到 1.10，差点当成不合格）。 */
.vt-tabs { position: relative; }

/* 居中：区块头（.vt-sec-head）是居中的刊头式，tab 条靠左会与标题错开一大截。
   flex-wrap:nowrap + 窄屏横滚，不折行 —— 折行时行数随文案长度变，
   区块高度会跟着店主改分类名而跳动（而且改完不报错，只是版面忽高忽低）。 */
.vt-tablist {
  display: flex; flex-wrap: nowrap; justify-content: center;
  gap: clamp(20px, 3.2vw, 44px);
  margin: 0 0 clamp(22px, 2.8vw, 34px);
  border-bottom: 1px solid var(--vt-hairline);
}
.vt-tab {
  flex: 0 0 auto; margin-bottom: -1px; padding: 0 2px 12px;
  background: none; border: 0; border-bottom: 1px solid transparent;
  font-family: var(--apple-font); font-size: .7rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; line-height: 1.4;
  color: var(--muted); white-space: nowrap; cursor: pointer;
  transition: color .18s, border-color .18s;
}
.vt-tab:hover { color: var(--ink); }
.vt-tab.is-on { color: var(--ink); border-bottom-color: var(--ink); }
.vt-tab:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* 隐藏面板：EJS 输出的是 hidden 属性（对读屏器与页内查找一并生效）。
   这条规则是**保险**不是主力 —— 基底 CSS 里若有哪条规则给面板设了 display，
   会盖掉 UA 样式表对 [hidden] 的 display:none，表现是三个 tab 的内容同时铺出来。 */
.vt-tabpanel[hidden] { display: none; }

/* —— 区块 #11：到店（配 index.ejs 的 .vt-retail）——
   对标 bloomthis.co 的 Retail 块（实测 top=7163 / 高 489 / 1 图 1 链接）。
   形态：通栏照片带 + 压在图上的文案（靠左）+ 一个 CTA。

   高度用 clamp 而不是 aspect-ratio：与 hero 同一条理由 —— 超宽屏上它该保持「一条横幅」，
   矮窗口里也不该把一整屏吃掉。下限 340 保证四行文案（眉题/标题/正文/钮）放得下。

   margin-top 而不是 padding-top：本块是**通栏实底**，用 padding 的话上边那段留白会跟着
   染成近黑，与「谁在下面谁负责间距」这条规则打架（那条规则假定区块背景是页面底色）。
   同 .vt-sp 的做法。 */
.vt-retail {
  position: relative; overflow: hidden;
  margin-top: clamp(56px, 7vw, 92px);
  height: clamp(340px, 42vh, 489px);
  display: flex; align-items: center;
}
.vt-retail-photo {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; object-fit: cover;
}
/* 压暗层：与 .vt-hero-scrim / .vt-occ-scrim / .vt-cathero-scrim **同一个常数 .74** ——
   全主题「压在照片上的文字」因此只有一个对比度要记：
     最坏情况（纯白照片）合成 = .74 x 17 + .26 x 255 = 78.9 ≈ #4F4F4F
     与 --vt-on-photo #FAF9F7 = 7.80:1 ✓ AAA（AA 只要 4.5）
   平台取到 100%（整块恒定，不做渐变）：本块文案靠左**竖向居中**，不像 hero 贴底，
   渐变的哪一段都可能落在文字上，常数平台才算得准。
   代价是照片整体被压暗一档 —— 这一块本来就是「文字为主、照片作底」的带子，可以接受。 */
.vt-retail-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: rgba(17, 17, 17, .74);
}
/* 无图（本期恒是这一支）：近黑底 + 金色发丝内框，**不叠压暗层**（底本来就是暗的）。
   底色用 --scrim #0B0B0B 而不是 --rose-ink：--rose-ink 暗色模式下翻近白，
   一翻文字（mode-invariant 的近白 --vt-on-photo）就没了底。
   为什么不是「近白底 + 压暗层」：前面的任务实测过 —— 那会被读成「图没加载完」。 */
/* ⚠️ 再加一道发丝描边：**暗色模式下**页面底色 --cream 翻成 #0E0E0E，与占位的 --scrim
   #0B0B0B 实测只差 1.03:1 —— 整条带子会与页面糊成一片，只剩里面那道金框孤零零地浮着。
   --vt-hairline 是随明暗变的：暗色 #262523 在两种近黑之间正好画得出边界；
   亮色 #ECE8E2 与页面近白同色，等于不存在（不会在亮色下多出一条线）。
   用 outline + 负 offset 而不是 border：不进盒模型，不用回头调 inset 的数值。 */
.vt-retail.no-photo {
  background: var(--scrim);
  outline: 1px solid var(--vt-hairline); outline-offset: -1px;
}
.vt-retail.no-photo::before {
  content: ''; position: absolute; inset: clamp(16px, 2.4vw, 30px);
  border: 1px solid var(--rose); opacity: .42; pointer-events: none;
}
.vt-retail-inner {
  position: relative; z-index: 1;
  max-width: var(--content-max); width: 100%; margin: 0 auto; padding: 0 40px;
}
/* 文案宽度设上界：通栏带上一行拉满 1160px 的正文没法读（一行超过 120 字符）。
   46ch 是编辑排版的常用上界，与 .vt-story-text 同源。 */
.vt-retail-eyebrow, .vt-retail-title, .vt-retail-body { color: var(--vt-on-photo); }
.vt-retail-eyebrow {
  display: block; margin-bottom: 14px;
  font-family: var(--apple-font); font-size: .66rem; font-weight: 600;
  letter-spacing: .26em; text-transform: uppercase; opacity: .84;
}
.vt-retail-title {
  margin: 0; max-width: 20ch;
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.7rem, 3.6vw, 2.6rem); line-height: 1.18; letter-spacing: .01em;
}
.vt-retail-body {
  margin: 14px 0 0; max-width: 46ch;
  font-size: .92rem; line-height: 1.75; opacity: .88;
}
/* CTA：照片上的实心钮走 --vt-on-photo 底 + --vt-photo-ink 字（两者都 mode-invariant，
   实测 17.95 ✓ AAA）—— 与 .vt-hero-btn-solid 同一对，别在这里另起一套。
   ⚠️ 不用金做底：金配近黑字虽是 8.40 ✓，但金在本主题只做描边/细节，
      正文级按钮一上金，整页唯一的彩就压不住了。 */
.vt-retail-cta {
  display: inline-flex; align-items: center; gap: 10px;
  margin-top: clamp(20px, 2.6vw, 30px); min-height: 46px; padding: 0 22px;
  background: var(--vt-on-photo); color: var(--vt-photo-ink);
  border: 1px solid var(--vt-on-photo); border-radius: var(--apple-radius);
  font-family: var(--apple-font); font-size: .72rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; text-decoration: none;
  transition: opacity .2s;
}
.vt-retail-cta:hover { opacity: .86; color: var(--vt-photo-ink); text-decoration: none; }
.vt-retail-cta:focus-visible { outline: 2px solid var(--vt-on-photo); outline-offset: 3px; }
.vt-retail-cta i { font-size: .95rem; line-height: 1; }
/* 号码用等宽数字：不同位数的号码在钮里不会左右跳（本站号码由后台配，长度不定） */
.vt-retail-cta-num { letter-spacing: .08em; font-variant-numeric: tabular-nums; opacity: .78; }

/* —— 区块 #13：配送承诺文字块（配 index.ejs 的 .vt-pitch）——
   对标 bloomthis.co 的 Free same-day delivery 块（实测 top=7766 / 高 211 / **0 图** 1 链接）。

   形态：软底通栏带（--blush）+ 上下发丝线 + 一句大号衬线主张 + 一行说明 + 一个文字链接。
   ⚠️ **刻意不做成第三条近黑带**：本页已经有两条（.vt-sp 卖点条、.vt-retail 到店），
      再来一条黑的，三块会连成一片、失去节奏。软底恰好把上面的近黑带与下面的品牌故事分开。

   ⚠️ 这一块**不**用 --scrim / --vt-on-photo 那一套：它没有照片，文字骑在页面级的软底上，
      该走随明暗翻转的常规 token（--blush / --rose-ink / --muted）。
      把「照片上的前景色」用到没有照片的地方，暗色模式下就是近白字骑在近白底上。

   margin-top 而不是 padding-top：同 .vt-sp / .vt-retail —— 本块是通栏实底，
   用 padding 的话上边那段留白会跟着染色。 */
.vt-pitch {
  margin-top: clamp(56px, 7vw, 92px);
  padding: clamp(34px, 4.4vw, 52px) 0;
  background: var(--blush);
  border-top: 1px solid var(--vt-hairline);
  border-bottom: 1px solid var(--vt-hairline);
  text-align: center;
}
/* 用 <p> 而不是 <h2>：这一块是**主张**不是章节标题，页面的标题层级不该多出一级
   （读屏器按标题跳转时，跳到一句促销语上是噪音）。视觉上仍按大标题排。

   ⚠️ 宽度上界用 **px 不用 ch** —— 第一版写的是 26ch，1440 实测把中文那句
      「下午 5 点前下单，今天就到。」折成了「…今天就 / 到。」，第二行只剩两个字。
      成因：ch = 字符「0」的宽度，在本主题的 Didone 衬线里约 0.5em，于是 26ch ≈ 13em；
      而**中文一个字就是 1em**，26ch 实际只装得下 13 个汉字，那句正好 14 个。
      ch 这个单位天生按拉丁字宽算，对 CJK 一律偏窄一倍 —— 凡是中英共用的宽度上界都别用它。
      560px 是两边一起量出来的：中文那句 459px < 560 → 一行；英文 67 字符约 1100px → 正好两行。 */
.vt-pitch-title {
  margin: 0 auto; max-width: 560px;
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.45rem, 2.9vw, 2.05rem); line-height: 1.26; letter-spacing: .01em;
  color: var(--rose-ink);
}
.vt-pitch-body {
  margin: 14px auto 0; max-width: 52ch;
  font-size: .88rem; line-height: 1.75; color: var(--muted); /* 段1 已把 --muted 修到 5.04 ✓ AA */
}
/* 文字链接而不是实心钮：上一块（.vt-retail）刚出过一个实心钮，紧接着再来一个
   会让两块看起来是同一个组件的两次重复。下划线用 border-bottom 而不是 text-decoration，
   这样 hover 时能单独把线加粗/换色，字本身不动。 */
.vt-pitch-link {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: clamp(18px, 2.2vw, 26px); padding-bottom: 3px;
  border-bottom: 1px solid var(--ink);
  font-family: var(--apple-font); font-size: .72rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink); text-decoration: none;
  transition: border-color .2s, gap .2s;
}
.vt-pitch-link:hover { color: var(--ink); text-decoration: none; border-bottom-color: var(--rose); gap: 12px; }
.vt-pitch-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
/* 箭头随 hover 往右挪一点（靠上面那条 gap 过渡）—— 唯一的动效，与本主题的克制一致 */
.vt-pitch-arrow { font-size: .9rem; line-height: 1; }

/* —— 区块 #14：品牌故事五块（配 index.ejs 的 .vt-story）——
   对标 bloomthis.co（实测 top=8032–10718 / **5 × 499** / 每块 1 图 0 链接 / 全宽图文交替）。

   形态：通栏两栏，图占一半并**通到屏幕边**，文字栏收在 --content-max 的边界上。
   为什么图要通到屏幕边而不是像别的区块那样收进 .vt-wrap：这五块是全页唯一一处
   「大图叙事」，收进 1240 的框里就变成五张并排的卡片 —— 与「大图摄影前置」的主张相反。
   代价是这一区不能用 .vt-wrap，两栏的对齐要自己算（见下方 justify-self / max-width）。

   ⚠️ 本区**没有 .vt-sec-head**（眉题 + 标题）—— 对标站也没有。五块本身就是叙事，
      再加一个「关于我们」的章节头，读者会以为下面是一整块折叠内容。

   间距沿用本段既定的「谁在下面谁负责间距」：只有 padding-top，没有 padding-bottom。 */
.vt-story { padding: clamp(56px, 7vw, 92px) 0 0; }

/* 两栏等分 + 竖向居中。align-items:center 让文字短的那几块也在图的中线上，
   不加的话文字会顶到图的上沿，五块并排时高低不齐。 */
.vt-story-row {
  display: grid; grid-template-columns: 1fr 1fr; align-items: center;
}
/* 交替：第 2、4 块把图挪到右边。用 order 而不是 direction:rtl ——
   rtl 会把标点与数字的方向一起翻掉（「5 PM」会变成「PM 5」），是个很难查的坑。 */
.vt-story-row.is-rev .vt-story-media { order: 2; }

/* 高度 499 直接照实测取，用 clamp 收窄屏那一档。
   给高度而不是给 aspect-ratio：两栏是等分的，图的宽度随视口变，
   用 aspect-ratio 的话超宽屏上这一块会长到 700 多。 */
.vt-story-media {
  position: relative; overflow: hidden;
  height: clamp(300px, 34vw, 499px);
  background: var(--blush); /* 图加载完成前的底：不留纯白闪一下 */
}
.vt-story-photo { display: block; width: 100%; height: 100%; object-fit: cover; }

/* 无图占位（本期恒是这一支）：近黑底 + 金色发丝内框 + 大号 Didone 序号。
   底色用 --scrim（明暗恒定的近黑）而不是 --rose-ink：--rose-ink 暗色下翻近白，
   一翻序号（mode-invariant 的近白）就没了底 —— 与 .vt-cc-ph 同一条理由。
   ph1..ph3 只错开内框留白与字号，避免五块看起来是同一张贴纸印了五遍。
   为什么不是「近白底 + 压暗层」：前面的任务实测过，那会被读成「图没加载完」。 */
.vt-story-ph {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: var(--scrim);
  /* 发丝描边的理由同 .vt-retail.no-photo：暗色下近黑占位与近黑页底只差 1.03:1，
     不画边界的话这五块的「图」那一半会整个融进页面。 */
  outline: 1px solid var(--vt-hairline); outline-offset: -1px;
}
.vt-story-ph::before {
  content: ''; position: absolute; inset: 26px;
  border: 1px solid var(--rose); opacity: .40;
}
.vt-story-ph2::before { inset: 34px; }
.vt-story-ph3::before { inset: 18px; }
/* #FAF9F7 @ .30 合成在 #0B0B0B 上约 #4D4D4D —— 装饰不该有正文的重量
   （同 .vt-cc-ph-letter / .vt-occ-ph-letter 的取舍：它不承载信息，真正的序号在文字栏里） */
.vt-story-ph-num {
  position: relative;
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(3.4rem, 7vw, 6rem); line-height: 1;
  color: var(--vt-on-photo); opacity: .30;
  font-variant-numeric: tabular-nums;
}
.vt-story-ph2 .vt-story-ph-num { font-size: clamp(3rem, 6vw, 5rem); }
.vt-story-ph3 .vt-story-ph-num { font-size: clamp(4rem, 8.4vw, 7rem); }

/* 文字栏：靠着内容边界排，不贴屏幕边。
   左块（图在左）的文字 justify-self:start，右块反过来 —— 于是文字始终朝版心一侧靠，
   两块之间形成一条对称的中缝。max-width 保证正文一行不超过约 46ch。 */
.vt-story-body {
  padding: clamp(28px, 4vw, 64px) clamp(24px, 5vw, 88px);
  max-width: 560px;
}
.vt-story-row .vt-story-body { justify-self: start; }
.vt-story-row.is-rev .vt-story-body { justify-self: end; }

/* 序号在文字栏里也出一次 —— 它是「第几块」的唯一可靠指示：
   Phase 5 配上真图之后，占位里的那个大序号就没了，只剩这一个。
   ⚠️ 这里**刻意不用金**（第一版写的是 var(--rose)，实测当场毙掉）：
      金 #C8A96A 骑在页面底色 --cream 上实测只有 **2.14 ✗**，连大字号的 AA 线 3.0 都够不到。
      它虽然 aria-hidden，但**看得见的数字对明眼人就是文字** —— 拿 aria-hidden 当
      对比度的豁免证是自欺欺人（读屏器用户没事，看得见的人反而读不清）。
      本主题的金色纪律原文就是「金只做描边/细节/小徽章」，一个序号不在其列。
      改走 --muted（段1 已修到 5.04 ✓ AA）。金在本区只剩占位里那道发丝内框。 */
.vt-story-index {
  display: block; margin-bottom: 14px;
  font-family: var(--serif, Georgia, serif); font-size: .95rem; line-height: 1;
  letter-spacing: .18em; color: var(--muted); font-variant-numeric: tabular-nums;
}
.vt-story-title {
  margin: 0;
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.35rem, 2.5vw, 1.85rem); line-height: 1.25; letter-spacing: .01em;
  color: var(--rose-ink);
}
.vt-story-text {
  margin: 16px 0 0; max-width: 46ch;
  font-size: .9rem; line-height: 1.8; color: var(--ink); opacity: .82;
}

/* —— 区块 #15 / #16：文章区 ×2（配 index.ejs 的 .vt-reads）——
   对标 bloomthis.co 的 Latest guides（top=11162 / 高 644）与 Flower facts（top=11919 / 高 669），
   各「眉题 + 标题 + 3 篇卡片」。两块共用本段 —— markup 也只有一份（VT_READS 是两元素数组）。

   ⚠️ 本期的卡片**没有链接**（没有文章详情页，理由见 index.ejs 本区块顶部）。
      所以下面所有交互态（hover 抬边框、放大图、显示箭头）都收在 `.vt-read.is-link` 下：
      不能点的卡片给出 hover 反馈，是又一种假的可供性。
      Phase 5 接 articles 表、卡片带上 href 之后，这些规则**自动**生效，不用再改 CSS。

   间距沿用「谁在下面谁负责间距」：只有 padding-top。
   最后一块与页脚之间由 .vt-footer 自己的 margin-top 负责 —— 于是「哪一块是最后一块」
   （随空态变）不影响版面。 */
/* —— #12 媒体报道（Phase 5 / mig145）——
   ⚠️ 这一块**只在 press_mentions 表里真有内容时才存在**（没有硬编码兜底，
      理由见 index.ejs 区块 #12 的注释）。所以这段 CSS 平时一条规则都不生效。

   对标站实测：logo 宽统一 140、高 44–85 不等（各刊头长宽比不同）、**无灰度滤镜**。
   ⚠️ 因此这里也**不加** `filter: grayscale()`：那是「让 logo 看起来统一」的常见做法，
      但媒体刊头的颜色往往是品牌资产的一部分，擅自去色在授权上是另一回事。
      对齐实测＝不动它。 */
.vt-press { padding: clamp(56px, 7vw, 92px) 0 0; }
.vt-press-row {
  display: grid;
  /* 条数由业主决定 —— auto-fit 让 1 条时不被拉成整行、5 条时自然铺开 */
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: clamp(24px, 3vw, 48px);
  list-style: none; margin: 0; padding: 0;
}
.vt-press-item { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 14px; }
.vt-press-logo {
  /* 宽度封顶、高度自适应：各家刊头长宽比不同，锁死高度会把宽的压扁 */
  max-width: 140px; width: 100%; height: auto; object-fit: contain;
}
/* 没有 logo 时用媒体名排成小字标 —— 与 logo 同一个视觉位，不留空洞 */
.vt-press-outlet {
  font-size: .82rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink); opacity: .7;
}
.vt-press-quote {
  margin: 0; font-size: .92rem; line-height: 1.7; color: var(--muted);
  font-style: italic;
}
.vt-press-cite {
  display: block; margin-top: 8px; font-style: normal;
  font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; opacity: .7;
}
.vt-press-link {
  font-size: .72rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--rose);
  padding-bottom: 2px; transition: opacity .18s;
}
.vt-press-link:hover { color: var(--ink); opacity: .7; text-decoration: none; }

.vt-reads { padding: clamp(56px, 7vw, 92px) 0 0; }

/* 三列等分。用固定的 3 列而不是 auto-fit：本区恒定 3 篇（对标站也是 3 篇），
   auto-fit 在只剩 1–2 篇时会把卡片拉成半屏宽的巨幅 —— 那是 .vt-occ-grid 踩过的坑，
   但那一区的条数由店主定、这一区不是，所以这里用更简单的写法 + 窄屏塌单列即可。
   少态（Phase 5 表里只有 1–2 篇）时右侧留空位是**对的**：三列的节奏比「两张被拉大」好看。 */
.vt-reads-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 28px;
}

/* 卡片：竖向 flex + height:100%，让摘要行数不同的三张卡底边对齐（同 .vt-cc 的做法）。
   ⚠️ 卡片可能是 <a> 也可能是 <div>（有没有 href），所以 color / text-decoration
      两种标签都要写死一遍 —— <div> 不继承 a 的样式，但 <a> 会吃到基底的全局链接色。 */
.vt-read {
  display: flex; flex-direction: column; height: 100%;
  min-width: 0; color: var(--ink); text-decoration: none;
}
.vt-read.is-link:hover { text-decoration: none; color: var(--ink); }
.vt-read.is-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.vt-read-media {
  position: relative; overflow: hidden;
  background: var(--card);
  border: 1px solid var(--vt-hairline); border-radius: var(--radius);
  transition: border-color .22s;
}
.vt-read.is-link:hover .vt-read-media { border-color: var(--ink); }
/* 3:2 横幅：与竖幅的产品卡 / 系列卡刻意不同 —— 文章不是商品，版面上要一眼分得开。
   aspect-ratio 同时把高度提前占住，图未加载完也不会 CLS。 */
.vt-read-photo {
  display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
  transition: transform .5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.vt-read.is-link:hover .vt-read-photo { transform: scale(1.04); }

/* 无图占位（本期恒是这一支）：近黑底 + 金色发丝内框 + 标题首字 —— 同 .vt-cc-ph / .vt-story-ph
   的语言，连「为什么底是暗的」的理由都一样：一排三张若都做成近白占位，会读成三张没加载完的图。
   底色用 --scrim（明暗恒定的近黑）而不是 --rose-ink：后者暗色下翻近白，一翻首字就没了底。 */
.vt-read-ph {
  display: grid; place-items: center;
  width: 100%; aspect-ratio: 3 / 2;
  background: var(--scrim); position: relative;
}
.vt-read-ph::before {
  content: ''; position: absolute; inset: 16px;
  border: 1px solid var(--rose); opacity: .40;
}
.vt-read-ph2::before { inset: 22px; }
.vt-read-ph3::before { inset: 11px; }
/* #FAF9F7 @ .34 合成在 #0B0B0B 上约 #545453 —— 装饰不该有正文的重量（同 .vt-cc-ph-letter） */
.vt-read-ph-letter {
  position: relative;
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(2.2rem, 4.6vw, 3.2rem); line-height: 1;
  color: var(--vt-on-photo); opacity: .34;
}
.vt-read-ph2 .vt-read-ph-letter { font-size: clamp(1.9rem, 4vw, 2.7rem); }
.vt-read-ph3 .vt-read-ph-letter { font-size: clamp(2.6rem, 5.4vw, 3.8rem); }

.vt-read-body { padding-top: 16px; display: flex; flex-direction: column; flex: 1 1 auto; }
/* 两行截断 + 锁高：三张卡的摘要起始行因此对齐，标题一行与两行的卡不会参差。
   min-height 用 calc(字号 x 行高 x 2) 而不是写死 px —— 改字号时不用记得同步改这里。 */
.vt-read-title {
  margin: 0; min-height: calc(1.02rem * 1.4 * 2);
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: 1.02rem; line-height: 1.4; color: var(--rose-ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* 摘要**不截断**：本期它就是全部内容（没有详情页可跳），截掉等于把话说一半。
   Phase 5 接表、卡片变成链接之后再考虑要不要 clamp —— 那时它才是「预告」。 */
.vt-read-excerpt {
  margin: 10px 0 0;
  font-size: .84rem; line-height: 1.8; color: var(--ink); opacity: .80;
}
/* margin-top:auto 把「阅读全文」压到卡底，三张卡的这一行才对得齐 */
.vt-read-more {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: auto; padding-top: 14px;
  font-family: var(--apple-font); font-size: .68rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink);
  transition: gap .2s;
}
.vt-read.is-link:hover .vt-read-more { gap: 11px; }
.vt-read-arrow { font-size: .85rem; line-height: 1; }

/* —— 区块 #17：联系方式（配 index.ejs 的 .vt-contact）——
   对标 bloomthis.co 正文里的 Email / WhatsApp 双列卡片。本区在**整页最后一块**
   （footer 之上），所以只写 padding-top，下缘间距仍由 .vt-footer 的 margin-top 负责 ——
   与本页其余各块「谁在下面谁负责间距」的口径一致，于是「哪一块是最后一块」（随空态变）
   不影响版面。

   ⚠️ 卡片张数是**动态的 1–3 张**（有值才出，判据见 utils/public-contact.js），
      所以这里用 auto-fit + minmax 而不是固定 3 列：只剩 1 张时固定 3 列会在右边留两个
      空格子，读起来像「另外两种联系方式加载失败了」。
      minmax 下限 240px 是实测值：一张卡里最长的一行是邮箱（约 22 个西文字符），
      240px 下 .92rem 刚好一行放得下不折。
      ⚠️ 上限给 `1fr` 而不是 `auto`：只剩 1 张时它会撑满整行 —— 这正是想要的，
         一张孤零零的窄卡片贴在左边会像没排完版。

   金色纪律（同全段）：金只做描边与图标点缀，绝不承载文字
   （--rose 配浅字实测 2.25 ✗）。卡片文字一律 --ink / --muted。 */
.vt-contact { padding: clamp(56px, 7vw, 92px) 0 0; }

/* 区块头下的一句话：与 .vt-sec-head 里的标题同列左对齐，宽度锁住不让它拉成通栏长行 */
.vt-contact-intro {
  margin: 14px 0 0; max-width: 46em;
  font-size: .88rem; line-height: 1.85;
  color: var(--muted);   /* 段1 已把 --muted 修到 5.04 ✓ AA */
}

.vt-contact-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px; margin-top: 34px;
}

/* 卡片：发丝边框 + 大留白（本主题的卡片语言，同 .vt-cc）。
   整张卡是一个 <a> —— 点击热区是整块而不只是那行小字（手指点得中，读屏器也只念一个链接）。 */
.vt-contact-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 30px 28px;
  border: 1px solid var(--vt-hairline);
  border-radius: var(--radius-sm);
  color: var(--ink); text-decoration: none;
  transition: border-color .22s, transform .22s;
}
/* hover：边框转金 + 极轻上浮。不换底色 —— 本主题除 #7 那条黑带外不做实色块。 */
.vt-contact-card:hover {
  border-color: var(--rose); text-decoration: none;
  color: var(--ink); transform: translateY(-2px);
}
.vt-contact-icon { font-size: 1.15rem; line-height: 1; color: var(--rose); margin-bottom: 8px; }
.vt-contact-label {
  font-family: var(--apple-font); font-size: .66rem; font-weight: 600;
  letter-spacing: .24em; text-transform: uppercase; color: var(--muted);
}
/* 值本身是这张卡的主角：字号最大、可读性优先。
   ⚠️ overflow-wrap 必须给 —— 长邮箱在 240px 的格子里会顶破卡片右边框（无空格可断）。 */
.vt-contact-value {
  font-size: .98rem; line-height: 1.5; color: var(--ink);
  overflow-wrap: anywhere;
}

/* —— 窄屏（<= 991.98px）：轮播整体降级为横向滚动 ——
   触屏上「滑」比点小箭头自然，且半张卡露在右边自带「还有更多」的可供性。
   断点取 991.98 与 PDP 塌单列一致：桌面 4 格塞进 768–991 只有 ~160px/格，比横滚更难用。
   做法是把 Bootstrap 给 .carousel-item 的三件套还原（display:none / float / margin-right:-100%），
   让所有组一字排开，容器自己 overflow-x:auto。
   选择器全部用「两个类」形态并靠源序压过 bootstrap.min.css（它在 _head-block 里先加载）。 */
@media (max-width: 991.98px) {
  .vt-carousel .vt-carousel-inner {
    display: flex; gap: 14px;
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; padding-bottom: 2px;
  }
  .vt-carousel .vt-carousel-inner::-webkit-scrollbar { display: none; }
  .vt-carousel .vt-carousel-slide {
    display: block; float: none; width: auto; margin-right: 0;
    flex: 0 0 auto; transform: none; transition: none;
  }
  .vt-carousel .vt-carousel-row {
    display: flex; grid-template-columns: none; gap: 14px;
  }
  /* 每格定宽（默认 62vw、上限 280px）：一屏露出「一张半」，右边那半张就是「可以滑」的提示。
     区块可用 _carousel 的 cellWidth 参数改这个值。 */
  .vt-carousel .vt-carousel-cell {
    flex: 0 0 var(--vt-cell-w, 62vw); max-width: 280px; scroll-snap-align: start;
  }
  /* 横滚模式下箭头与圆点都没有意义（它们驱动的是 Bootstrap 的翻页，与滚动位置不同步） */
  .vt-carousel .vt-carousel-arrow,
  .vt-carousel .vt-carousel-dots { display: none; }
  .vt-carousel.has-arrows { padding: 0; }
}

@media (max-width: 767.98px) {
  /* hero：窗口更矮更窄，改用 vh 占比略高的一档；文案块留白同步收，双 CTA 在 390px 下
     仍并排放得下（实测两钮合计约 272px < 可用 354px），放不下时 flex-wrap 自然堆叠。 */
  /* 满屏满高（业主 2026-09-07「手机上都改成这种模式」）；svh 不跟地址栏伸缩抖动 */
  .vt-hero-slide { height: 100vh; height: 100svh; }
  .vt-hero-copy { padding: 0 18px 48px; }
  .vt-hero-sub { margin-top: 10px; font-size: .86rem; line-height: 1.6; }
  .vt-hero-cta { gap: 10px; margin-top: 20px; }
  .vt-hero-btn { min-height: 44px; padding: 0 18px; font-size: .68rem; letter-spacing: .14em; }
  .vt-hero-dots { bottom: 14px; }
  .vt-hero-brand-inner { padding: 0 18px; }
  .vt-hero-bloom { right: -70px; bottom: -90px; opacity: .13; }

  /* 首页通用外壳与区块头：左右留白与 .vt-hero-brand-inner 对齐（18px），标题收一档 */
  .vt-wrap { padding: 0 18px; }
  .vt-sec-eyebrow { font-size: .62rem; letter-spacing: .22em; margin-bottom: 9px; }

  /* 分类网格：格宽下限收到 148 + 间距收到 14/16，实测 390px 内宽 354 → 2 列（每格 170px）、
     767px 内宽 731 → 4 列。少态的 max-width 公式沿用桌面的 216px 基准 —— 手机上它只在
     「只有 1 个分类」时才咬得住（216 < 354），正是想要的：一个分类不该被拉成整屏宽。 */
  .vt-occ { padding: clamp(40px, 9vw, 56px) 0 0; }
  .vt-occ-grid { gap: 16px 14px; --vt-occ-min: 148px; }
  /* 标签同步收一档：390px 下 170px 的格子里，.78rem + .1em 字距会让四字分类名折成两行 */
  .vt-occ-label { padding: 0 8px 10px; font-size: .72rem; letter-spacing: .06em; }
  .vt-occ-ph::before { inset: 10px; }
  .vt-occ-ph2::before { inset: 15px; }
  .vt-occ-ph3::before { inset: 8px; }

  /* 卖点条：四格折成 2×2。为什么不是一列四行：390px 下四行竖排要吃掉约 320px 高，
     而这四句是「扫一眼确认」的信息，不值这个首屏预算；2×2 实测约 170px。
     为什么不是横滚：横滚会把后两条藏起来 —— 承诺类信息一条都不该要人去划。 */
  .vt-sp { margin-top: clamp(40px, 9vw, 56px); padding: 26px 0; }
  .vt-sp-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 14px; }
  .vt-sp-item { gap: 9px; }
  .vt-sp-icon { font-size: 1.25rem; }
  /* 不再收 max-width：ch 跟着字号一起缩，26ch 在 390px 的 165px 格里恰好让英文四条都是整两行；
     再收到 18ch 会掉成三行、四格高度参差（实测 52px vs 35px）。 */
  .vt-sp-text { font-size: .68rem; letter-spacing: .08em; line-height: 1.6; }

  /* 手机上卡片文字整体收一档，两列并排时标题才不至于每张都撑满两行 */
  .vt-card-title { font-size: .8rem; }
  .vt-card-price { font-size: .94rem; }
  .vt-card-grid { gap: 20px 14px; --vt-card-min: 150px; }

  /* 中段三块轮播：与 .vt-occ 同一档收紧 */
  .vt-coll8, .vt-catprod, .vt-best { padding: clamp(40px, 9vw, 56px) 0 0; }

  /* —— 区块 #11 到店：390px 下不塌结构，只收高度与字号 ——
     为什么不改成「图上文下」两段：本块只有一张底图 + 四行文案，拆两段后手机上要吃掉
     约 560px 高，而这四行是「顺手看一眼」的信息，不值这个预算。
     左右留白与 .vt-wrap 在本断点下一致（18px），否则文案会与上下区块错台。 */
  .vt-retail {
    margin-top: clamp(40px, 9vw, 56px);
    height: clamp(300px, 46vh, 380px);
  }
  .vt-retail-inner { padding: 0 18px; }
  .vt-retail.no-photo::before { inset: 12px; }
  .vt-retail-eyebrow { font-size: .62rem; letter-spacing: .22em; margin-bottom: 10px; }
  .vt-retail-body { font-size: .84rem; line-height: 1.7; margin-top: 11px; }
  /* CTA 在 390px 下要放得下「致电花房 + 号码」：实测 zh 约 246px < 可用 354px；
     英文 "Call the studio" 更长（约 292px），仍在内。放不下时 flex-wrap 自然堆叠。 */
  .vt-retail-cta {
    flex-wrap: wrap; min-height: 44px; padding: 0 16px; gap: 8px;
    font-size: .68rem; letter-spacing: .12em;
  }

  /* —— 区块 #13 配送承诺文字块：只收留白与字号，不改结构 ——
     26ch/52ch 两个上界不必收：ch 跟着字号一起缩，在 354px 的可用宽里两者都已大于容器，
     实际折行由容器决定。再往下压反而会让主张折成四五行。 */
  .vt-pitch { margin-top: clamp(40px, 9vw, 56px); padding: 30px 0; }
  .vt-pitch-body { font-size: .82rem; margin-top: 11px; }
  .vt-pitch-link { font-size: .68rem; letter-spacing: .12em; margin-top: 18px; }

  /* —— 区块 #14 品牌故事：两栏塌成单列（图在上、文字在下）——
     为什么必须塌：390px 下两栏各 195px，图会变成一条 195×300 的窄缝、文字每行只有 6 个字。
     ⚠️ 塌了之后**交替就没有意义了** —— 单列里「图在左 / 图在右」都长一个样，
        但 .is-rev 的 order:2 会把图挪到文字**下面**，五块就变成「图文 / 文图 / 图文…」，
        读起来像排错了。所以这里把 order 归位，单列一律「图在上、文字在下」。
     justify-self 同理归位：单列下 start/end 会让文字栏贴左或贴右，两块之间左右横跳。 */
  .vt-story { padding: clamp(40px, 9vw, 56px) 0 0; }
  .vt-story-row { grid-template-columns: 1fr; }
  .vt-story-row.is-rev .vt-story-media { order: 0; }
  .vt-story-row .vt-story-body,
  .vt-story-row.is-rev .vt-story-body { justify-self: stretch; max-width: none; }
  /* 图收到 240：五块连着排，单列下每块「图 + 文」约 420px，五块合计约 2100px —— 
     再高手机上要划四五屏才走得完这一段。 */
  .vt-story-media { height: 240px; }
  .vt-story-ph::before { inset: 16px; }
  .vt-story-ph2::before { inset: 22px; }
  .vt-story-ph3::before { inset: 11px; }
  .vt-story-body { padding: 22px 18px 0; }
  .vt-story-index { margin-bottom: 10px; font-size: .85rem; }
  .vt-story-text { font-size: .84rem; line-height: 1.75; margin-top: 12px; }

  /* —— 区块 #15/#16 文章区：三列塌成单列 ——
     为什么不是横滚（中段那几个轮播的做法）：那几处是**商品**，横滚的代价只是少看几件；
     这三张是知识卡片，摘要本身就是全部内容（本期没有详情页），藏起来等于把话说没了。
     为什么不是两列：390px 下两列每格 170px，3:2 的图只有 113px 高、摘要每行 5 个字。
     单列 3 张实测约 1090px —— 在整页最下方，愿意读到这里的人不介意再划一屏。 */
  .vt-reads { padding: clamp(40px, 9vw, 56px) 0 0; }
  .vt-reads-grid { grid-template-columns: 1fr; gap: 26px; }
  .vt-read-ph::before { inset: 12px; }
  .vt-read-ph2::before { inset: 17px; }
  .vt-read-ph3::before { inset: 9px; }
  .vt-read-body { padding-top: 13px; }
  /* 单列下标题不必再锁两行的高：一行一张卡，没有「同排对齐」这回事，
     锁着反而会在单行标题下面留一条空带。 */
  .vt-read-title { font-size: .98rem; min-height: 0; }
  .vt-read-excerpt { font-size: .82rem; line-height: 1.75; }

  /* —— 区块 #17 联系方式：卡片塌单列 ——
     390px 减去 .vt-wrap 的左右 18px 只剩 354px，auto-fit 的 240px 下限本来就塞不下两列，
     这里显式写单列只是把意图写明（别让下一个人以为是碰巧）。内边距同时收一档：
     28px 左右留白在 354px 上会把邮箱挤到折两行。 */
  .vt-contact { padding: clamp(40px, 9vw, 56px) 0 0; }
  .vt-contact-grid { grid-template-columns: 1fr; gap: 14px; margin-top: 26px; }
  .vt-contact-card { padding: 22px 18px; }
  .vt-contact-intro { font-size: .84rem; line-height: 1.75; }

  /* —— tab 条在窄屏：**横滚**，不折行 ——
     为什么不折行（三种做法实测比过）：
       · 折行 → 行数随店主起的分类名长度变，同一个区块在不同店里高度不同，
         而且「第二行只有一个 tab」的残行看起来像出了 bug；
       · 缩字号硬塞 → 5 个四字中文分类名在 390px 下要缩到 .5rem 才排得开，读不了；
       · 横滚 → 右边露出半个 tab，天然给出「还有更多」的可供性（与轮播本身窄屏降级
         成横滚是同一条设计语言，用户只要学一次）。
     负 margin + 等量 padding：让滚动区通到屏幕两边（首尾 tab 仍与正文左右对齐），
     否则 tab 会在离屏幕边 18px 的地方被切掉，看着像被裁了一刀。
     数值必须与 .vt-wrap 在本断点下的 padding（18px）一致 —— 改那个就得改这个。
     ⚠️ overflow 会把 :focus-visible 的外发光在下沿裁掉约 2px（outline-offset 3px，
        而 tab 的下边界就是滚动区下沿）。左右上三面完整，键盘焦点仍然一眼可辨；
        要彻底不裁只能把 outline 画到元素内部，那会横穿文字，更糟。
     实测 390px：tablist 宽 380 / scrollWidth 601 → 可横滚；首个 tab 左沿 x=18（与正文对齐）；
     键盘按 End 后 scrollLeft 自动跟到 221、末个 tab 完整可见（focus() 自带 scrollIntoView）。 */
  .vt-tablist {
    justify-content: flex-start; gap: 22px;
    margin-left: -18px; margin-right: -18px;
    padding-left: 18px; padding-right: 18px;
    overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .vt-tablist::-webkit-scrollbar { display: none; }
  .vt-tab { font-size: .66rem; letter-spacing: .13em; padding-bottom: 10px; }

  /* 系列卡：手机上一屏只露一张半（.vt-carousel-cell 的 62vw / 上限 280px），
     标题与描述跟着收一档，描述的两行锁高同步跟着字号走。 */
  .vt-cc-body { padding-top: 13px; }
  .vt-cc-title { font-size: .95rem; }
  .vt-cc-desc { font-size: .76rem; margin-top: 7px; min-height: calc(.76rem * 1.65 * 2); }
  .vt-cc-foot { padding-top: 11px; }
  .vt-cc-go svg { width: 18px; height: 18px; }
}

/* ============================================================================
   collection 形态（?cat=<id>）—— 配 index.ejs 的 _isColl 分支
   ----------------------------------------------------------------------------
   同一个 view 两种形态：无 ?cat= 是纯营销首页（上面那些区块），有 ?cat= 是 collection 页
   （分类 hero + 筛选面板 + 商品网格 + 分页，营销区块整块不渲染）。对标 bloomthis.co 的
   /collections/xxx。本段的类前缀两组：
     · .vt-cathero-  分类 hero（本页的 LCP 本体）
     · .vt-coll-     collection 主体（工具条 / 两栏骨架 / 网格 / 空态）
     · .vt-filters- / .vt-filter-  筛选面板（桌面常驻侧栏 = 窄屏抽屉，同一份 DOM）
     · .vt-pager-    分页
   商品卡本身复用 .vt-card-（上面已定义），本段一个字都不重写。

   金色纪律不变：金只出现在「筛选面板当前项的左侧细条」与「无图分类 hero 的内框」两处描边上，
   不承载任何文字（金配浅字 2.25 ✗）。
   ⚠️ 改本段同样要 bump layout.ejs 里 vitrine-theme.css 的 ?v= 。 */

/* —— 分类 hero ——
   高度用 min-height 而不是固定 height：分类描述由店主填、长度不可控，写死高度会把长描述切掉。
   两种底：
     · has-photo → 店主配了 homepage_image：铺满 + 压暗层（同首页 hero 的常数平台做法）
     · no-photo  → 近黑排版贴片 + 金色发丝内框（**不是**近白底）。
       近白底叠上压暗层会被读成「图没加载完」—— 这是区块 #6 无图占位实测推翻过一次的结论，
       同一个坑不踩第二遍。底色用 --scrim（明暗恒定的暗底），前景才能用 mode-invariant 的单值。 */
.vt-cathero {
  position: relative; overflow: hidden;
  display: flex; align-items: flex-end;
  min-height: clamp(300px, 34vh, 420px);
}
.vt-cathero.has-photo { background: var(--blush); } /* 图到达前的底：不留纯白闪一下 */
/* 🔴 2026-09-09 业主报障：分类没配图时，这里原来是 `background: var(--scrim)`（#0B0B0B）——
   一块 300~420px 高的**纯黑板**顶在分类页最上面，再加一圈金色发丝内框。
   与首页系列卡那个「近黑贴片」是同一种做法，业主对两处的裁定也一致：**没配图就别摆黑块**。
   改成普通标题区：页面底色 + 深色字，高度按内容走。
   连带三处必须一起改，少一处就露馅：
     ① margin-top 的负值要撤 —— 没有照片就不该钻到 nav 底下（下方 .no-photo 那条）；
     ② 前景色要从「照片上的近白」换回 --ink，否则浅底白字；
     ③ nav 不能再变透明（JS 里 .no-photo 直接早退），否则白 nav 压浅底。 */
.vt-cathero.no-photo {
  background: transparent;
  min-height: 0;
  margin-top: 0;          /* 撤掉上移：上面那条 .vt-hero, .vt-cathero 的负边距对它不适用 */
}
.vt-cathero.no-photo .vt-cathero-inner { padding: 28px 40px 4px; }
.vt-cathero.no-photo .vt-cathero-inner,
.vt-cathero.no-photo .vt-cathero-back,
.vt-cathero.no-photo .vt-cathero-title,
.vt-cathero.no-photo .vt-cathero-desc {
  color: var(--ink); text-shadow: none;
}
.vt-cathero.no-photo .vt-cathero-back { opacity: .62; }
.vt-cathero-photo {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; object-fit: cover;
}

/* 压暗层 —— 与 .vt-hero-scrim / .vt-occ-scrim **同一个常数**（.74），
   所以全主题「压在照片上的文字」只有一个对比度要记：
     最坏情况（纯白照片）合成 = .74 x 17 + .26 x 255 = 78.9 ≈ #4F4F4F
     与 --vt-on-photo #FAF9F7 = 7.80:1 ✓ AAA（AA 只要 4.5）
   平台取到 75%：本 hero 的文案块（返回链 + 标题 + 描述）高度有上界 ——
   标题与描述都做了两行截断（见下方 line-clamp），实测最坏约 195px；
   min-height 上界 420px 时文案块占 57%，内容更长时 hero 跟着长高、占比反而下降到 65% 封顶
   （分子只多了描述那两行，分母还多了上下 padding）。两档都在 75% 之内。
   ⚠️ 放开那两处截断或加大字号，必须重核这个余量 —— 越过平台上沿的那一行会掉进渐变段里。
   ⚠️ 无图态**不渲染**这一层（EJS 里就没输出）：底本来就是近黑，再压一层只是把内框压死。
   ⚠️ 前景不能用 --on-accent（跨明暗翻转，而照片不翻），详见段1 --vt-on-photo 的注释。 */
.vt-cathero-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top,
    rgba(17, 17, 17, .74) 0%,
    rgba(17, 17, 17, .74) 75%,
    rgba(17, 17, 17, .32) 90%,
    rgba(17, 17, 17, 0) 100%);
}
.vt-cathero-inner {
  position: relative; z-index: 1; width: 100%;
  max-width: var(--content-max); margin: 0 auto;
  padding: 64px 40px 44px;
}
/* 返回链：回的是店面首页（= 分类网格所在的页）。全大写小字距，与 nav 菜单同一套排版语言。
   color 显式写在自己身上、不靠从祖先继承：基底有一条全局 a:hover 会改 <a> 的 color，
   继承的话 hover 时它会被染成近黑/近白，在压暗过的照片上直接消失（同 .vt-occ-label 的教训）。 */
.vt-cathero-back {
  display: inline-flex; align-items: center; gap: 7px;
  margin-bottom: 18px;
  color: var(--vt-on-photo); text-decoration: none;
  font-family: var(--apple-font); font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
}
.vt-cathero-back:hover { color: var(--vt-on-photo); text-decoration: underline; text-underline-offset: 4px; }
.vt-cathero-back:focus-visible { outline: 2px solid var(--vt-on-photo); outline-offset: 3px; }
.vt-cathero-back i { font-size: .8rem; }
/* 标题：Didone 大字（同首页 hero 的刊头气质）。两行截断 —— 上面压暗层的余量账按两行算的。 */
.vt-cathero-title {
  margin: 0; color: var(--vt-on-photo);
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.75rem, 4.4vw, 3rem); line-height: 1.15; letter-spacing: .01em;
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* 描述同样两行截断：店主可以填很长，不截的话文案块会顶穿压暗层的平台。
   不用 opacity 压低层级（那会把 7.80 这个数字变成另一个要单独核的数）——
   改用字号与字距拉开层级，对比度全页只有一个值。 */
.vt-cathero-desc {
  margin: 14px 0 0; max-width: 62ch; color: var(--vt-on-photo);
  font-family: var(--apple-font); font-size: .92rem; line-height: 1.65;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* —— collection 主体 —— */
.vt-coll { padding: clamp(28px, 4vw, 44px) 0 clamp(56px, 8vw, 96px); }

/* 工具条：窄屏筛选入口 + 件数 + 排序下拉，下沿一道发丝线把它与网格分开 */
.vt-coll-bar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding-bottom: 18px; border-bottom: 1px solid var(--vt-hairline);
}
.vt-coll-count {
  margin: 0 auto 0 0; /* 右侧 auto 把排序推到最右 */
  font-family: var(--apple-font); font-size: .7rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* 窄屏筛选入口。描边式（本主题「次要操作」的语言就是描边不填色）。 */
.vt-coll-filterbtn {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 42px; padding: 0 15px;
  background: transparent; color: var(--ink);
  border: 1px solid var(--ink); border-radius: var(--radius-field);
  font-family: var(--apple-font); font-size: .7rem; font-weight: 600;
  letter-spacing: .16em; text-transform: uppercase; cursor: pointer;
  transition: background .18s;
}
.vt-coll-filterbtn:hover { background: var(--vt-hover-surface); }
.vt-coll-filterbtn:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* 生效筛选条数：抽屉关着时用户看不见面板，没有这个数就不知道结果为什么这么少。
   底走 --rose-deep、字走 --on-accent（成对翻转），绝不写死浅色字。 */
.vt-coll-fcount {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  background: var(--rose-deep); color: var(--on-accent, #fff);
  border-radius: var(--apple-radius-pill);
  font-size: .58rem; font-weight: 700; letter-spacing: 0;
}

/* 排序：原生 select（键盘 / 读屏器 / 移动端原生选择器全部免费，零自定义脚本）。
   appearance:none 去掉系统箭头后自己补一枚图标 —— 图标 pointer-events:none，
   否则点在箭头上打不开下拉。
   background 给 --card 而不是 transparent：多数浏览器的下拉选项面板跟随控件的
   background/color，透明底在暗色模式下会退回系统白面板配我们的近白字。 */
.vt-coll-sort { position: relative; display: inline-flex; align-items: center; gap: 9px; }
.vt-coll-sort-label {
  font-family: var(--apple-font); font-size: .66rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; color: var(--muted);
}
.vt-coll-select {
  appearance: none; -webkit-appearance: none;
  min-height: 42px; padding: 0 34px 0 13px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-field);
  font-family: var(--apple-font); font-size: .8rem; cursor: pointer;
  transition: border-color .18s;
}
.vt-coll-select:hover { border-color: var(--ink); }
.vt-coll-select:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.vt-coll-select-ic {
  position: absolute; right: 13px; pointer-events: none;
  font-size: .66rem; color: var(--muted);
}

/* 两栏骨架 —— **默认单栏**，两栏只在 >=992px 生效。
   为什么不能默认写成两栏再在窄屏改回单栏：窄屏下 .vt-filters 是 Bootstrap 的
   position:fixed 抽屉，脱离文档流后根本不是网格项，于是主区会被自动放进第一条
   （240px 的）轨道里 —— 商品网格缩成一条 240px 的窄带，且不报错。
   align-items:start 是侧栏 sticky 生效的前提（网格项默认 stretch，拉满高度就粘不住）。 */
.vt-coll-body { padding-top: clamp(24px, 3vw, 34px); }
.vt-coll-main { min-width: 0; }
/* 网格列宽下限比首页那套（208）略放宽：主区被侧栏切掉约 284px，
   1160 - 240 - 44 = 876 → 220 的下限恰好落三列（每格约 278px，与 EJS 里下发的 sizes 一致）。 */
.vt-coll-grid { --vt-card-min: 220px; }

/* —— 筛选面板：一份 DOM 两种形态 ——
   >=992px  Bootstrap 把 .offcanvas-lg 还原成普通静态块（它自己那条 media query 干的）
            → 这里只需补「常驻侧栏」的排版：sticky + 发丝右界。
   <=991.98 Bootstrap 接管成抽屉（遮罩 / ESC / 焦点管理全都是它的），
            这里只需把配色接到本主题的 token 上。
   ⚠️ --bs-offcanvas-bg 必须显式给：它默认是 --bs-body-bg（Bootstrap 的纯白），
      而本主题暗色下页面翻成近黑、正文 --ink 翻成近白 —— 白面板配近白字 = 看不见，
      正是 token-bg-hardcoded-fg 那条护栏在防的同一类事故，只是这次白底来自第三方 CSS 变量。 */
.vt-filters {
  --bs-offcanvas-bg: var(--cream);
  --bs-offcanvas-color: var(--ink);
  --bs-offcanvas-width: 308px;
  --bs-offcanvas-border-width: 0;
}
.vt-filters-head { border-bottom: 1px solid var(--vt-hairline); }
.vt-filters-title {
  margin: 0; font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: 1.2rem; color: var(--rose-ink);
}
/* 不用 Bootstrap 的 .btn-close：它靠一张写死深色的背景图，暗色下会变成看不见的按钮。
   图标字体 + currentColor 才跟着明暗翻。 */
.vt-filters-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  background: transparent; color: var(--ink);
  border: 0; border-radius: var(--radius-field); cursor: pointer;
}
.vt-filters-close:hover { background: var(--vt-hover-surface); }
.vt-filters-close:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* display:flex 两档都要：>=992px 时 Bootstrap 给 .offcanvas-body 设了 display:flex 但没给方向
   （默认 row，两个筛选组会并排挤成两列）；<=991.98 时它是 block。写死 column 两档统一。 */
.vt-filters-body { display: flex; flex-direction: column; }

.vt-filter-grp + .vt-filter-grp {
  margin-top: 26px; padding-top: 26px; border-top: 1px solid var(--vt-hairline);
}
.vt-filter-h {
  margin: 0 0 12px;
  font-family: var(--apple-font); font-size: .66rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase; color: var(--muted);
}
.vt-filter-list, .vt-filter-sublist { list-style: none; margin: 0; padding: 0; }
.vt-filter-sublist { padding-left: 13px; }
/* 当前项用一道**金色左细条** + 加粗：金在这里是描边/细节，不承载文字（全主题的金色纪律）。
   只靠加粗不够 —— 加粗在小字号上很不明显；只靠颜色也不够 —— 本主题正文与强调色几乎同色。
   语义上另有 aria-current="page"（EJS 里给的），颜色不是唯一信号。 */
.vt-filter-link {
  display: block; padding: 8px 8px 8px 11px;
  border-left: 2px solid transparent; border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-family: var(--apple-font); font-size: .84rem; line-height: 1.4;
  color: var(--ink); text-decoration: none;
  transition: background .18s, border-color .18s;
  overflow-wrap: anywhere;
}
.vt-filter-link:hover { background: var(--vt-hover-surface); color: var(--ink); text-decoration: none; }
.vt-filter-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.vt-filter-link.is-on { border-left-color: var(--rose); font-weight: 600; color: var(--rose-ink); }
.vt-filter-sub { font-size: .78rem; color: var(--muted); }
.vt-filter-sub:hover { color: var(--ink); }

.vt-filter-prow { display: flex; align-items: center; gap: 9px; }
.vt-filter-input {
  flex: 1 1 0; min-width: 0; width: 100%; min-height: 42px; padding: 0 11px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-field);
  font-family: var(--apple-font); font-size: .82rem;
  font-variant-numeric: tabular-nums;
}
.vt-filter-input:hover { border-color: var(--ink); }
.vt-filter-input:focus-visible { outline: 2px solid var(--ink); outline-offset: 1px; }
.vt-filter-sep { flex: 0 0 auto; color: var(--muted); font-size: .8rem; }
.vt-filter-apply {
  width: 100%; margin-top: 13px; min-height: 44px; padding: 0 16px;
  background: var(--rose-deep); color: var(--on-accent, #fff);
  border: 1px solid var(--rose-deep); border-radius: var(--radius-field);
  font-family: var(--apple-font); font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; cursor: pointer;
  transition: background .18s, border-color .18s;
}
.vt-filter-apply:hover { background: var(--vt-accent-hover); border-color: var(--vt-accent-hover); }
.vt-filter-apply:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.vt-filter-clear {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 12px;
  font-family: var(--apple-font); font-size: .72rem; color: var(--muted); text-decoration: none;
}
.vt-filter-clear:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.vt-filter-clear:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* —— 空态（两种，出口不同：筛没结果 → 清筛选；分类本身为空 → 回分类网格）——
   给一个真实存在的落点，不留一片白。 */
.vt-coll-empty {
  text-align: center; padding: clamp(44px, 8vw, 84px) 24px;
  background: var(--card); border: 1px solid var(--vt-hairline); border-radius: var(--radius);
}
.vt-coll-empty-ic { width: 54px; height: 54px; color: var(--rose); opacity: .34; }
.vt-coll-empty-t {
  margin: 18px 0 10px; font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(1.15rem, 2.6vw, 1.5rem); color: var(--rose-ink);
}
.vt-coll-empty-d {
  margin: 0 auto 26px; max-width: 46ch;
  font-family: var(--apple-font); font-size: .86rem; line-height: 1.75; color: var(--muted);
}
.vt-coll-empty-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 46px; padding: 0 26px;
  background: var(--rose-deep); color: var(--on-accent, #fff);
  border: 1px solid var(--rose-deep); border-radius: var(--radius-field);
  font-family: var(--apple-font); font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; text-decoration: none;
  transition: background .18s, border-color .18s;
}
.vt-coll-empty-btn:hover {
  background: var(--vt-accent-hover); border-color: var(--vt-accent-hover);
  color: var(--on-accent, #fff); text-decoration: none;
}
.vt-coll-empty-btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* —— 分页 ——
   页码窗口在 EJS 里算（首 / 末 / 当前±1 + 省略号），这里只管排版。
   当前页是 <span> 不是 <a>（跳到已经在的页对键盘用户是纯噪音），所以 hover 规则
   全部限定 a.vt-pager-link —— 否则鼠标划过当前页会给出「可点」的假暗示。 */
.vt-pager { margin-top: clamp(36px, 5vw, 58px); }
.vt-pager-list {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px;
  list-style: none; margin: 0; padding: 0;
}
.vt-pager-link {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 42px; height: 42px; padding: 0 9px;
  border: 1px solid transparent; border-radius: var(--radius-field);
  font-family: var(--apple-font); font-size: .8rem; color: var(--ink);
  text-decoration: none; font-variant-numeric: tabular-nums;
  transition: background .18s, border-color .18s;
}
a.vt-pager-link:hover {
  background: var(--vt-hover-surface); border-color: var(--line);
  color: var(--ink); text-decoration: none;
}
a.vt-pager-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.vt-pager-link.is-on {
  background: var(--rose-deep); color: var(--on-accent, #fff);
  border-color: var(--rose-deep); font-weight: 600;
}
/* 首/末页的禁用箭头：留在原地占位（拿掉会让整排页码在翻页时左右跳一格） */
.vt-pager-link.is-off { color: var(--muted); opacity: .38; }
.vt-pager-edge i { font-size: .78rem; }
.vt-pager-gap {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; height: 42px; color: var(--muted); font-size: .8rem;
}

/* —— >=992px：筛选面板成为常驻侧栏 —— */
@media (min-width: 992px) {
  .vt-coll-body {
    display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 44px;
    align-items: start;
  }
  /* sticky：分类多时侧栏比主区短得多，跟着页面滚走的话翻到网格中段就够不着筛选了。
     top 要让开 fixed 的 nav（--nav-h 是含边框的总占位高，见段1）。 */
  .vt-filters { position: sticky; top: calc(var(--nav-h) + 24px); }
}

/* —— <=767.98px：工具条与网格收一档 —— */
@media (max-width: 767.98px) {
  .vt-cathero { min-height: clamp(260px, 42vh, 360px); }
  .vt-cathero-inner { padding: 56px 18px 30px; }
  .vt-cathero-back { margin-bottom: 14px; font-size: .62rem; letter-spacing: .14em; }
  .vt-cathero-desc { margin-top: 11px; font-size: .84rem; line-height: 1.6; }

  /* 件数收一档字号：390px 下要与筛选钮、排序下拉挤同一行（实测三者合计约 325px < 可用 354px）。
     真放不下时 .vt-coll-bar 的 flex-wrap 会让它自然折行，不会挤变形。 */
  .vt-coll-count { font-size: .62rem; letter-spacing: .1em; }
  .vt-coll-bar { gap: 10px; padding-bottom: 14px; }
  .vt-coll-filterbtn { margin-right: auto; font-size: .64rem; letter-spacing: .12em; padding: 0 12px; }
  .vt-coll-sort { gap: 7px; }
  .vt-coll-sort-label { display: none; } /* 下拉里第一项就写着「默认排序」，标签在窄屏是冗余 */
  .vt-coll-select { font-size: .74rem; padding: 0 30px 0 10px; }
  .vt-coll-select-ic { right: 10px; }

  /* 两列：与首页分类网格 / 产品卡在手机上的列数一致，整站一个节奏 */
  .vt-coll-grid { --vt-card-min: 150px; }

  .vt-pager-link { min-width: 38px; height: 38px; padding: 0 6px; font-size: .76rem; }
  .vt-pager-gap { height: 38px; }

  /* 抽屉底部让开共享底部导航 .apple-bottom-nav（fixed，实测高 68px，z-index 1050
     高于 Bootstrap offcanvas 的 1045 → 它压在抽屉之上）。
     不去抬 offcanvas 的 z-index：那要连遮罩一起抬，而遮罩是 Bootstrap 挂在 <body> 上的
     全局元素，按类抬会波及本主题将来任何一个 offcanvas —— 为一个局部问题改全局层级不划算。
     底部导航在抽屉打开时仍可见可点，本身也是一条合理的退出路径。
     抽屉体是 overflow-y:auto，加了这段 padding 后最后一个筛选项能滚进可视区。 */
  .vt-filters-body { padding-bottom: 84px; }
}

/* 减弱动效偏好：缩放与淡入是纯装饰，关掉不影响任何信息。
   图框变色（hover 的真反馈）保留 —— 它不是动效。 */
@media (prefers-reduced-motion: reduce) {
  .vt-card-photo { transition: none; }
  .vt-card:hover .vt-card-photo { transform: none; }
  .vt-occ-photo { transition: none; }
  .vt-occ-card:hover .vt-occ-photo { transform: none; }
  .vt-carousel .vt-carousel-slide { transition: none; }
  /* hero 本就不自动轮播，但用户手动翻页时 Bootstrap 仍会做 600ms 位移 —— 一屏那么大的
     通栏图横向滑过去，对前庭敏感的人是最难受的一种。直接切换即可，信息一点不少。 */
  .vt-hero-slide { transition: none; }
}



/* ========== 段4：PDP 内页（橱窗编辑风 · 大图占主导；vt- 前缀，双模式全走 token） ==========
   编号跟随既有 5 套皮肤的惯例（段3 恒为首页、段4 恒为 PDP），配 themes/vitrine/product.ejs。
   设计口径（对标 bloomthis.co 的 PDP）：左大图占主导 · 右克制信息 · 发丝线分隔 ·
   价格当标题排（Didone 衬线）· CTA 近黑实心块 · 直角（radius 2–4px）。

   ⚠️ 金色纪律（--rose 亮 #C8A96A / 暗 #D4B87C）：金底配浅字实测只有 2.25:1，远低于 AA 的 4.5。
      故本段的金只出现在三个位置 —— 发丝描边、小图标、以及金底徽章（字色一律 --vt-on-gold 近黑，8.40 ✓）。
      正文与眉题一律 --muted（段1 已修到 5.04 ✓）或 --ink，绝不用金承载文字。

   钩子契约（见 _shared/README §8）：视觉类叠在 .apple-* 和 .fl-* 钩子上，不取代它们。
   注意 matchVariant 选中变体后会**整条重写** .apple-stock 的 className（只留 apple-stock +
   in-stock/out-stock），故库存色必须按 .apple-stock 自身的类写、不能挂在会被抹掉的 vt- 类上。 */

/* 外壳：不复用继承来的 .fl-wrap，PDP 要比功能页更宽的呼吸（与 vt-nav 同 --content-max 对齐） */
.vt-pdp-wrap { max-width: var(--content-max); margin: 0 auto; padding: 0 40px 10px; }

/* 面包屑：小字号全大写大字距（同 nav 菜单的排版语言） */
.vt-pdp-crumb {
  font-size: .68rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--muted); padding: 26px 0 4px;
}
.vt-pdp-crumb a { color: var(--muted); text-decoration: none; transition: color .18s; }
.vt-pdp-crumb a:hover { color: var(--ink); text-decoration: none; }
.vt-pdp-crumb-sep { color: var(--line); margin: 0 8px; }
.vt-pdp-crumb-cur { color: var(--ink); }

/* 两栏：图 1.15 : 信息 1（图占主导）。图列 sticky，长描述滚动时大图仍在视野 */
.vt-pdp { display: grid; grid-template-columns: 1.15fr 1fr; gap: 56px; padding: 26px 0 56px; align-items: start; }

/* —— 左：橱窗大图（4:5 竖幅，直角发丝框，无阴影） —— */
.vt-pdp-gal { position: sticky; top: calc(var(--nav-h, 65px) + 24px); }
.vt-pdp-hero { border: 1px solid var(--vt-hairline); border-radius: var(--radius); overflow: hidden; background: var(--card); }
.vt-pdp-hero-img { display: block; width: 100%; aspect-ratio: 4 / 5; object-fit: cover; }
/* 无图占位：中性近白 + 极淡金晕（README §7「PDP 无图占位」那条要求主题自换底色；
   基底 .fl-pdp-img 的粉紫渐变只作用于 florisse 自己的 PDP 外壳，vitrine 有了本视图后走这里） */
.vt-pdp-hero-ph {
  aspect-ratio: 4 / 5; display: grid; place-items: center;
  background:
    radial-gradient(300px 240px at 52% 42%, var(--blush) 0%, transparent 62%),
    var(--cream);
}
.vt-pdp-hero-ph svg { width: 92px; height: 92px; color: var(--rose); opacity: .38; }

/* 缩略图：直角小图，选中 .active 加近黑细框（钩子 .apple-gallery-thumb 挂在按钮上，见 product.ejs 文件头） */
.vt-pdp-thumbs { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.vt-pdp-thumb {
  width: 68px; height: 68px; padding: 0; overflow: hidden; cursor: pointer;
  background: var(--card); border: 1px solid var(--vt-hairline); border-radius: var(--radius-sm);
  opacity: .62; transition: opacity .2s, border-color .2s;
}
.vt-pdp-thumb:hover { opacity: .9; border-color: var(--rose); }
.vt-pdp-thumb.active { opacity: 1; border-color: var(--ink); }
.vt-pdp-thumb-img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* —— 图库滑轨（2026-08-28，对标 50gram.com.my 的 Swiper 图库；DOM 见 product.ejs）——
   轨道 = 横向 flex，每张 100% 宽；定位靠 transform（合成层动画，不触发布局）。
   拖动中去掉过渡（.is-dragging），手指到哪图到哪；松手后恢复过渡弹到位。 */
.vt-pdp-hero { position: relative; }
.vt-pdp-track {
  display: flex; width: 100%;
  transition: transform .45s cubic-bezier(.22, .61, .36, 1);
  will-change: transform; touch-action: pan-y; cursor: grab; user-select: none;
}
.vt-pdp-track.is-dragging { transition: none; cursor: grabbing; }
.vt-pdp-slide { flex: 0 0 100%; min-width: 0; }
.vt-pdp-slide .vt-pdp-hero-img { -webkit-user-drag: none; pointer-events: none; }
/* 箭头：圆钮压在图上，常态半透明、hover 图库时显；单张（.is-single）整个不出 */
.vt-pdp-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 40px; height: 40px; padding: 0; border-radius: var(--apple-radius-pill);
  background: var(--card); color: var(--ink); border: 1px solid var(--vt-hairline);
  display: grid; place-items: center; cursor: pointer;
  opacity: 0; transition: opacity .2s, border-color .2s;
}
.vt-pdp-hero:hover .vt-pdp-arrow, .vt-pdp-hero:focus-within .vt-pdp-arrow { opacity: .92; }
.vt-pdp-arrow:hover { opacity: 1; border-color: var(--ink); }
.vt-pdp-arrow:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; opacity: 1; }
.vt-pdp-arrow-prev { left: 12px; }
.vt-pdp-arrow-next { right: 12px; }
.vt-pdp-hero.is-single .vt-pdp-arrow { display: none; }
.vt-pdp-hero:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
@media (hover: none) { .vt-pdp-arrow { opacity: .92; } } /* 触屏没有 hover，箭头常显 */
@media (prefers-reduced-motion: reduce) { .vt-pdp-track { transition: none; } }

/* —— 右：克制信息栏 —— */
.vt-pdp-info { min-width: 0; padding-top: 2px; }

/* 眉题：金色发丝短横 + 中性小字（金不承字，只做那道横线） */
.vt-pdp-eyebrow {
  display: flex; align-items: center; gap: 10px;
  font-size: .66rem; letter-spacing: .26em; text-transform: uppercase; color: var(--muted);
  margin-bottom: 14px;
}
.vt-pdp-eyebrow::before { content: ''; width: 26px; height: 1px; background: var(--rose); flex: none; }

/* 标题：Didone 衬线刊头 */
.vt-pdp-title {
  font-family: var(--serif, Georgia, serif); font-weight: 400;
  font-size: clamp(28px, 3.4vw, 40px); line-height: 1.22; letter-spacing: .01em;
  color: var(--ink); margin: 0 0 18px;
}

/* 价格当标题排：衬线大字 + 表格数字（选变体改写时宽度不跳） */
.vt-pdp-priceline { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.vt-pdp-price {
  font-family: var(--serif, Georgia, serif); font-size: 1.62rem; font-weight: 400;
  letter-spacing: .02em; color: var(--rose-ink); font-variant-numeric: tabular-nums;
}
/* 会员价徽章：全站唯二的金底之一，字色必须走 --vt-on-gold（近黑 8.40 ✓；白字只有 2.25 ✗） */
.vt-pdp-ptag {
  background: var(--rose); color: var(--vt-on-gold);
  font-size: .62rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  padding: 3px 8px; border-radius: var(--radius-sm);
}

/* 短描述 lede：窄行宽（编辑感） */
.vt-pdp-lede { font-size: .88rem; line-height: 1.95; color: var(--ink); opacity: .74; max-width: 46ch; margin: 20px 0 0; }
.vt-pdp-lede p { margin: 0 0 10px; }

/* 发丝分隔线 */
.vt-pdp-rule { height: 1px; background: var(--vt-hairline); margin: 28px 0 4px; }

/* 通用小标签：全大写大字距 */
.vt-pdp-lbl {
  font-size: .66rem; font-weight: 600; letter-spacing: .22em; text-transform: uppercase;
  color: var(--rose-ink); margin: 26px 0 12px;
}
.vt-pdp-lbl-sub { font-weight: 400; letter-spacing: .1em; text-transform: none; color: var(--muted); }

/* 规格钮：直角描边，选中 = 近黑实心（暗色自动翻近白底近黑字），缺货 .opt-oos 灰划 */
.vt-pdp-opts { display: flex; gap: 10px; flex-wrap: wrap; }
.vt-pdp-opt {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-sm);
  color: var(--ink); font-size: .82rem; letter-spacing: .06em; padding: 9px 18px; cursor: pointer;
  transition: border-color .18s, background .18s, color .18s;
}
.vt-pdp-opt:hover { border-color: var(--ink); }
.vt-pdp-opt.selected { background: var(--rose-deep); border-color: var(--rose-deep); color: var(--on-accent); }
.vt-pdp-opt.opt-oos { opacity: .38; text-decoration: line-through; cursor: not-allowed; }

/* 库存：按 .apple-stock 自身的类写（matchVariant 会整条重写 className，vt- 类留不住） */
.vt-pdp-stock { margin-top: 16px; }
.vt-pdp-stock .apple-stock {
  font-size: .7rem; letter-spacing: .18em; text-transform: uppercase; color: var(--muted);
}
.vt-pdp-stock .apple-stock.in-stock { color: var(--sage-deep); }
.vt-pdp-stock .apple-stock.out-stock { color: var(--danger); }

/* 贺卡：直角发丝框输入 */
.vt-pdp-field { margin-top: 2px; }
.vt-pdp-note {
  width: 100%; font-family: var(--apple-font); font-size: .86rem; line-height: 1.7; color: var(--ink);
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-field);
  padding: 11px 13px; resize: vertical;
}
.vt-pdp-note::placeholder { color: var(--muted); }
.vt-pdp-note:focus { outline: none; border-color: var(--ink); }
.vt-pdp-counter { font-size: .66rem; letter-spacing: .1em; color: var(--muted); text-align: right; margin-top: 5px; }

/* PWP 心意加购：直角卡（覆写基底写死的 14px 圆角，见 README §7 首条），选中 .on 近黑边 + 软底 */
.vt-pdp-addons { display: flex; flex-direction: column; gap: 9px; margin-top: 0; }
.vt-pdp-addons .fl-addon {
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 10px 13px; gap: 12px;
  background: var(--card);
}
.vt-pdp-addons .fl-addon:hover { border-color: var(--rose); }
.vt-pdp-addons .fl-addon.on { border-color: var(--ink); background: var(--blush); }
/* 图槽 84px（基底 42px 的 2 倍）：加购项本身就是一件商品，缩略图太小看不出是什么、也没有购买欲；
   2026-08-28 业主要求放大。⚠️ product.ejs 里 imgTag 的 sizes 与这里成对（'84px'），改一处必改另一处，
   否则 srcset 会按旧尺寸挑 256w 档、放大后发糊。 */
.vt-pdp-addons .fl-addon-ic { width: 84px; height: 84px; flex: none; border-radius: var(--radius); background: var(--blush); overflow: hidden; }
.vt-pdp-addons .fl-addon-ic svg { width: 30px; height: 30px; color: var(--rose); }
.vt-pdp-addons .fl-addon { gap: 16px; }
.vt-pdp-addons .fl-addon-name { font-size: .86rem; font-weight: 500; letter-spacing: .02em; }
.vt-pdp-addons .fl-addon-save { font-size: .68rem; font-weight: 500; letter-spacing: .06em; }
.vt-pdp-addons .fl-addon-price b { font-family: var(--serif, Georgia, serif); font-size: .96rem; font-weight: 400; }
.vt-pdp-addons .fl-addon-price small { font-size: .68rem; }

/* —— PWP 瓦片（2026-08-28，对标 floristique.sg）——
   网格 4 列（手机 3 列），图正方占满瓦片宽，名字与价格在图下；选中 = 主色描边 + 右上角勾。
   ⚠️ 上面 .vt-pdp-addons 是 flex 纵排、.fl-addon-ic 钉死 84px —— 这两条是给「一行一条」的形态写的，
   florisse 基底仍在用，不能删；这里同特异度靠**源序**压过（新规则必须写在它们后面）。
   product.ejs 里 imgTag 的 sizes（桌面 120px）与 4 列网格成对。 */
.vt-tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.vt-tiles .fl-addon { flex-direction: column; align-items: stretch; gap: 8px; padding: 8px; border-radius: var(--radius); }
.vt-tiles .vt-tile-media { position: relative; width: 100%; height: auto; aspect-ratio: 1 / 1; border-radius: var(--radius-sm); }
.vt-tiles .vt-tile-media img { width: 100%; height: 100%; object-fit: cover; }
.vt-tile-check {
  position: absolute; top: 6px; right: 6px; width: 22px; height: 22px; border-radius: var(--apple-radius-pill);
  background: var(--rose-deep); color: var(--on-accent, #fff); font-size: .8rem; line-height: 22px; text-align: center;
  opacity: 0; transform: scale(.6); transition: opacity .15s, transform .15s;
}
.vt-tiles .fl-addon.on .vt-tile-check { opacity: 1; transform: scale(1); }
.vt-tiles .fl-addon-name { font-size: .8rem; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; min-height: 2.7em; }
/* 名字与价格同左对齐、原价划线同行（基底 .fl-addon-price{margin-left:auto;text-align:right} + small{display:block} 是给横排卡写的） */
.vt-tiles .fl-addon-price { margin-left: 0; text-align: left; }
.vt-tiles .fl-addon-price b { font-size: .9rem; }
/* 原价整体不拆行：瓦片才 ~100px 宽，不写 nowrap 会在货币符号与数字之间断开，划线的原价读成两截 */
.vt-tiles .fl-addon-price small { display: inline; margin-left: 4px; text-decoration: line-through; white-space: nowrap; }
.vt-tiles .fl-addon:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.vt-pwp-summary { min-height: 1.2em; margin-bottom: 8px; }
@media (max-width: 767.98px) { .vt-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } }

/* 批量优惠：直角软块（基底写死 14px 圆角，直角主题按类覆写） */
.vt-pdp-bulk {
  background: var(--blush); border: 1px solid var(--vt-hairline); border-radius: var(--radius-sm);
  padding: 13px 16px; margin: 24px 0 0; font-size: .8rem;
}
.vt-pdp-bulk .fl-bulk-title {
  font-size: .66rem; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; color: var(--rose-ink);
}
.vt-pdp-bulk .fl-bulk-title i { color: var(--rose); } /* 金只做图标 */
.vt-pdp-bulk ul { color: var(--ink); opacity: .82; }

/* 数量：直角发丝框步进器（− / input#qty / + 三者相邻，勿拆） */
.vt-pdp-qty-wrap { margin-top: 24px; }
.vt-pdp-qty { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.vt-pdp-qty button {
  border: none; background: none; color: var(--ink); font-size: 15px;
  width: 42px; height: 44px; cursor: pointer; transition: background .18s;
}
/* 同 .vt-icon-btn：--blush 骑在步进器的 --card 白底上 ΔL* 只有 3.6，改用 --vt-hover-surface */
.vt-pdp-qty button:hover { background: var(--vt-hover-surface); }
.vt-pdp-qty input {
  border: none; border-left: 1px solid var(--line); border-right: 1px solid var(--line);
  width: 58px; text-align: center; background: var(--card); color: var(--ink);
  font-size: .9rem; font-variant-numeric: tabular-nums; -moz-appearance: textfield;
}
.vt-pdp-qty input:focus { outline: none; }
.vt-pdp-qty input::-webkit-outer-spin-button,
.vt-pdp-qty input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* CTA：加入花篮 = 近黑实心通栏块；立即购买 = 描边次钮（暗色下 --rose-deep 翻近白，字色由 --on-accent 跟随） */
.vt-pdp-cta { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 28px; }
.vt-pdp-buy {
  flex: 1 1 200px; min-height: 52px; padding: 15px 24px;
  background: var(--rose-deep); color: var(--on-accent);
  border: 1px solid var(--rose-deep); border-radius: var(--radius-sm);
  font-size: .74rem; font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
  cursor: pointer; transition: background .18s, border-color .18s, color .18s;
}
.vt-pdp-buy:hover:not(:disabled) { background: var(--vt-accent-hover); border-color: var(--vt-accent-hover); }
.vt-pdp-buy:disabled { background: var(--blush); border-color: var(--line); color: var(--muted); cursor: not-allowed; }
/* 次钮：透明描边（同一块尺寸，靠填充与否分主次） */
.vt-pdp-buy-sub { flex: 0 1 auto; background: none; color: var(--ink); border-color: var(--ink); }
.vt-pdp-buy-sub:hover:not(:disabled) { background: var(--ink); color: var(--on-accent); border-color: var(--ink); }
.vt-pdp-buy-sub:disabled { background: none; border-color: var(--line); color: var(--muted); }

/* 服务承诺细列：发丝线逐条断开，金只做小图标 */
.vt-pdp-trust { list-style: none; margin: 28px 0 0; padding: 0; border-top: 1px solid var(--vt-hairline); }
.vt-pdp-trust li {
  display: flex; align-items: center; gap: 11px; padding: 13px 2px;
  border-bottom: 1px solid var(--vt-hairline);
  font-size: .78rem; letter-spacing: .04em; color: var(--ink); opacity: .78;
}
.vt-pdp-trust i { font-size: .95rem; color: var(--rose); opacity: 1; }

/* 长描述：上缘发丝线 + 窄栏行宽（编辑感正文） */
.vt-pdp-desc { max-width: 720px; border-top: 1px solid var(--vt-hairline); padding-top: 26px; margin: 0 0 60px; }
.vt-pdp-desc-hd {
  font-size: .66rem; font-weight: 600; letter-spacing: .24em; text-transform: uppercase;
  color: var(--rose-ink); margin-bottom: 16px;
}
.vt-pdp-desc-body { font-size: .88rem; line-height: 2; color: var(--ink); opacity: .76; }
.vt-pdp-desc-body img { max-width: 100%; height: auto; }

/* —— PDP 响应式：窄屏塌单列、取消 sticky、CTA 通栏 —— */
@media (max-width: 991.98px) {
  .vt-pdp { grid-template-columns: 1fr; gap: 28px; padding: 20px 0 40px; }
  .vt-pdp-gal { position: static; top: auto; }
}
@media (max-width: 767.98px) {
  .vt-pdp-wrap { padding: 0 18px 10px; }
  .vt-pdp-title { font-size: clamp(24px, 7vw, 30px); }
  .vt-pdp-buy, .vt-pdp-buy-sub { flex: 1 1 100%; }
  /* 底部有共享 _bottom-nav 悬浮条，长描述末端留出让位空间 */
  .vt-pdp-desc { margin-bottom: 84px; }
}

/* ══════════════════════════════════════════════════════════════
   段 N · 店铺墙（只用在**平台首页** themes/vitrine/home.ejs）
   一张卡 = 一家店：店招 + 店名 + 最多 3 张商品缩略图，整卡是通往店面页的链接。
   ⚠️ 刻意不复用 .vt-cc-*（系列卡）：那套卡的数据面是分类的 homepage_* 编辑字段、
      版式是竖幅大图 + 描述；店铺卡要的是「一眼认出这家店」，横向、矮、信息密度低。
      复用会让两种语义共用一套尺寸，改一处必然弄坏另一处。
   ══════════════════════════════════════════════════════════════ */
.vt-stores-sec { padding: clamp(46px, 6vw, 78px) 0; }
.vt-stores {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: clamp(14px, 1.6vw, 20px);
}
.vt-store {
  display: flex; flex-direction: column; gap: 12px;
  padding: 16px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); text-decoration: none; color: inherit;
  transition: border-color .18s ease, transform .18s ease;
}
.vt-store:hover { border-color: var(--rose); transform: translateY(-2px); }
.vt-store:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.vt-store-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
.vt-store-logo {
  width: 44px; height: 44px; flex: 0 0 44px;
  border-radius: 50%; object-fit: cover; background: var(--cream);
}
/* 无 logo 时的首字占位：与商品卡的无图占位同一种语言（近黑底 + 金色发丝边） */
.vt-store-logo-ph {
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 18px; color: var(--rose);
  background: var(--scrim); border: 1px solid rgba(200, 169, 106, .45);
}
.vt-store-name {
  font-family: var(--serif); font-size: 1.02rem; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vt-store-thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.vt-store-thumb {
  aspect-ratio: 1 / 1; border-radius: var(--radius-sm); overflow: hidden;
  background: var(--cream); display: block;
}
.vt-store-thumb-img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (max-width: 767.98px) {
  .vt-stores { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .vt-store { transition: none; }
  .vt-store:hover { transform: none; }
}

/* 首页「按分类选购」格只给桌面：手机端底栏已有「分类」入口，首页再铺一版是重复（业主 2026-09-06 定）*/
@media (max-width: 767.98px) {
  .vt-occ { display: none; }
}

/* 手机端店面首页有 hero 时藏掉顶部通知条，让图从最顶端开始（桌面与其它页照旧）。
   hero 上移量按第 1 行高度算：手机端第 2 行 display:none，--nav-h 仍含第 2 行，照它算会把 hero 顶出视口。 */
@media (max-width: 767.98px) {
  html:has(.vt-hero) .vt-notice { display: none; }
  html:has(.vt-hero) .vt-hero { margin-top: calc(-1 * var(--nav-row1-h, 64px)); }
  /* 满屏后 hero 文案落在最底部，被 fixed 的底栏（68px）盖住 —— 实测截图里标题只露出上半截；多让出一截 */
  html:has(.vt-hero) .vt-hero-copy { padding-bottom: 104px; }
}
