/*
 * card_fx.css — 全站卡片的悬停 / 点选动效与外观（2026-10-24）。
 *
 * 一、值从哪来
 *   全站默认 = 主题变量 --o-card-*（/website/theme.css 的 :root，建站器「主题 ▸ 卡片」改），
 *   积木覆盖 = 积木根上的 data-card-<组>="<值>"（建站器选项「卡片」组），本文件末尾把它换算成同一组变量。
 *   两层同时命中一张卡时，**离卡片最近的那层赢** —— 这只有变量继承做得到；属性选择器之间比的是
 *   特异度与先后，不是远近。所以每个值都要把本组变量写全，漏一个就从外层漏进来。
 *   变量表的唯一出处是 vertex 的 src/website/theme/card_fx.ts（card_fx.test.ts 逐条对本文件）。
 *
 * 二、哪些是卡片（下面那串 :is(…)，加新卡片往每一处都加，website_runtime.js 的 CARD_FX_SEL 同步）
 *   商品卡 .oe_product（11 套设计）、/shop 类目卡、配件卡、子类目卡、品牌卡、巨型菜单图卡、类目列表、
 *   统一搜索卡、课程卡、论坛首页卡、动态积木卡、积木里的 .s_card（时间线那种除外）。
 *   导航性质的（类目磁贴 / 逐级格 / 类目面板链接）不算：它们是筛选控件，有自己的选中态。
 *
 * 三、写法
 *   ★ 规则表只能拿数乘：开关一律 0/1，倍率写成数。
 *   ★ 悬停态只在卡片根上置一个私有量 --_hov（0/1），图、名称、按钮、遮罩都按它**算**，不各写一条
 *     「卡片:hover 里的 X」—— 那样每条都要和 X 静止态那条比特异度（名称那条带一长串 :not()，
 *     悬停那条永远比不过，表现是"悬停时显示名称"永远不显示）。
 *   ★ 阴影与描边光环叠在卡片自己的 box-shadow 上并带 !important：商品卡的 shadow-sm / shadow 是
 *     Bootstrap 工具类（!important），不这样写悬停阴影永远出不来；卡片原有的阴影经 --_rest 带上。
 *   ★ 位移 / 缩放用独立的 translate / scale 属性，不碰 transform —— 3D 倾斜要独占 transform。
 *
 * 四、触屏没有悬停：悬停只在 @media (hover: hover) 里置位，手机上只剩「点击」一组；
 *   「悬停时显示名称 / 按钮」在手机上一直显示（--_show，藏起来就再也点不到了）。
 *   系统开了「减少动态效果」：去掉位移、缩放、倾斜、扫光与波纹，只留颜色与阴影。
 */

/* ── 卡片根：公共底座 ─────────────────────────────────────────────── */
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card)) {
  --_d: var(--o-card-speed, 280ms);
  --_e: cubic-bezier(.2, .7, .2, 1);
  /* 圆角：设了「卡片圆角」用它，否则 Bootstrap 卡片照旧跟主题的「圆角」走（各卡自己写死的圆角
     在各自的样式表里改成了 var(--o-card-radius, 原值)） */
  --_R: var(--o-card-radius, var(--bs-border-radius, .375rem));
  --bs-card-border-radius: var(--_R);
  --bs-card-inner-border-radius: max(0px, calc(var(--_R) - var(--bs-card-border-width, 1px)));
  /* 状态量：悬停 / 按下 / 选中 / 触屏（静止时都是"不变"） */
  --_hov: 0;
  --_touch: 0;
  --_show: max(var(--_hov), var(--_touch));
  --_p: 1;
  --_sel: 0;
  --_sel0: 0;   /* 选中（不乘 --_box）：没有盒子的卡片把光环画在图上，见「没有盒子的类目卡片」 */
  /* 有没有"盒子"（无边无底的类目圆图之类没有，阴影与光环乘 0）、能不能动（贴边排的表格行不能） */
  --_box: 1;
  --_move: 1;
  --_rest: 0 0 0 0 transparent;
  /* 由状态量算出的效果量 */
  --_g: calc(1 + (var(--o-card-grow, 1) - 1) * var(--_move) * var(--_hov));
  --_s: calc(var(--o-card-shadow, 0) * var(--_box) * var(--_hov));
  --_ring: calc(var(--o-card-ring, 0) * var(--_box) * var(--_hov));
  position: relative;
  isolation: isolate;
  transition: translate var(--_d) var(--_e), scale var(--_d) var(--_e), transform var(--_d) var(--_e),
    box-shadow var(--_d) var(--_e), border-color var(--_d) var(--_e), background-color var(--_d) var(--_e),
    color var(--_d) var(--_e);
}
@media (hover: none) {
  :is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
      .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
      .s_card:not(.s_timeline_card)) { --_touch: 1; }
}
/* 卡片原有的阴影（Bootstrap 工具类）随悬停阴影一起叠，不被盖掉 */
:is(.oe_product, .o_wsale_acc_card, .o_learning_course_card, .o_forum_index_card, .s_card).shadow-sm { --_rest: var(--bs-box-shadow-sm); }
:is(.oe_product, .o_wsale_acc_card, .o_learning_course_card, .o_forum_index_card, .s_card).shadow { --_rest: var(--bs-box-shadow); }

/* 没有盒子的：类目列表的卡片 / 圆形、子类目圆形、商品「大」列表（无边无底） */
:is(.o_wsale_catlist_cards, .o_wsale_catlist_circles) .o_wsale_catlist_item,
.o_wsale_ccards_circles .o_wsale_ccard,
.o_wsale_d_list_large.oe_product { --_box: 0; }
/* 不能动的：贴边排成表格的几套与整行横幅（浮起会压住邻格的分隔线） */
:is(.o_wsale_d_grid, .o_wsale_d_list_grid, .o_wsale_d_condensed, .o_wsale_d_showcase).oe_product { --_move: 0; }
/* 表格那套是直角格子（wsale_snippets.css 把卡片圆角写成 0）：下面图框的圆角都按 --_R 算，不跟着归零
   图就在直角格子里圆出四个角 */
.o_wsale_d_grid.oe_product { --_R: 0px; }

/* ── 类目卡片：独立的一套（主题页签「类目卡片」，--o-catcard-*，card_fx.ts 的 CATEGORY_CARD_SELECTOR）──
 * 效果规则只读 --o-card-*；类目卡片根上把它们改接到 --o-catcard-*。没设的组在 :root 上回落到 --o-card-*
 * —— 用 :where(:root) 零特异度，主题里设了的（/website/theme.css 的 :root）赢。两套都没设的（圆角）
 * 在类目卡片上照样是"未设"，读它的地方走自己的兜底。积木覆盖见文件末尾。 */
:where(:root) {
  --o-catcard-lift: var(--o-card-lift);
  --o-catcard-grow: var(--o-card-grow);
  --o-catcard-shadow: var(--o-card-shadow);
  --o-catcard-ring: var(--o-card-ring);
  --o-catcard-tilt: var(--o-card-tilt);
  --o-catcard-glow: var(--o-card-glow);
  --o-catcard-zoom: var(--o-card-zoom);
  --o-catcard-swap: var(--o-card-swap);
  --o-catcard-gray: var(--o-card-gray);
  --o-catcard-veil: var(--o-card-veil);
  --o-catcard-reveal: var(--o-card-reveal);
  --o-catcard-shine: var(--o-card-shine);
  --o-catcard-press: var(--o-card-press);
  --o-catcard-ripple: var(--o-card-ripple);
  --o-catcard-select: var(--o-card-select);
  --o-catcard-speed: var(--o-card-speed);
  --o-catcard-cap-pos: var(--o-card-cap-pos);
  --o-catcard-cap-over: var(--o-card-cap-over);
  --o-catcard-cap-hide: var(--o-card-cap-hide);
  --o-catcard-img-border: var(--o-card-img-border);
  --o-catcard-img-inset: var(--o-card-img-inset);
  --o-catcard-radius: var(--o-card-radius);
}
:is(.o_wsale_cat_card, .o_wsale_ccard, .o_wsale_mega_card, .o_wsale_catlist_item) {
  --o-card-lift: var(--o-catcard-lift);
  --o-card-grow: var(--o-catcard-grow);
  --o-card-shadow: var(--o-catcard-shadow);
  --o-card-ring: var(--o-catcard-ring);
  --o-card-tilt: var(--o-catcard-tilt);
  --o-card-glow: var(--o-catcard-glow);
  --o-card-zoom: var(--o-catcard-zoom);
  --o-card-swap: var(--o-catcard-swap);
  --o-card-gray: var(--o-catcard-gray);
  --o-card-veil: var(--o-catcard-veil);
  --o-card-reveal: var(--o-catcard-reveal);
  --o-card-shine: var(--o-catcard-shine);
  --o-card-press: var(--o-catcard-press);
  --o-card-ripple: var(--o-catcard-ripple);
  --o-card-select: var(--o-catcard-select);
  --o-card-speed: var(--o-catcard-speed);
  --o-card-cap-pos: var(--o-catcard-cap-pos);
  --o-card-cap-over: var(--o-catcard-cap-over);
  --o-card-cap-hide: var(--o-catcard-cap-hide);
  --o-card-img-border: var(--o-catcard-img-border);
  --o-card-img-inset: var(--o-catcard-img-inset);
  --o-card-radius: var(--o-catcard-radius);
}

/* ── 悬停（只给有鼠标的设备）：置位 + 卡片自己的位移 / 缩放 / 倾斜 / 边框 ── */
@media (hover: hover) {
  :is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
      .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
      .s_card:not(.s_timeline_card)):is(:hover, :focus-visible, :has(:focus-visible)) {
    --_hov: 1;
    z-index: 1;
    translate: 0 calc(var(--o-card-lift, 0px) * var(--_move));
    scale: calc(var(--_g) * var(--_p));
    transform: perspective(900px) rotateX(calc(var(--o-card-rx, 0deg) * var(--o-card-tilt, 0)))
      rotateY(calc(var(--o-card-ry, 0deg) * var(--o-card-tilt, 0)));
    /* 描边高亮 / 光斑：边框一起换成主色（光环在 box-shadow 里） */
    border-color: color-mix(in srgb, var(--o-color-1, #714B67) calc(100% * var(--_ring)),
      var(--bs-card-border-color, var(--bs-border-color, #dee2e6)));
  }
}
/* 倾斜跟着鼠标走（website_runtime.js 写 --o-card-rx/ry 并挂这个类），过渡要短，否则卡片慢半拍 */
.o_card_tilting {
  transition: translate var(--_d) var(--_e), scale var(--_d) var(--_e), transform 90ms linear,
    box-shadow var(--_d) var(--_e), border-color var(--_d) var(--_e) !important;
}

/* ── 按下 / 选中 ──────────────────────────────────────────────────── */
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card)):active {
  --_p: var(--o-card-press, 1);
  scale: calc(var(--_g) * var(--_p));
  transition-duration: 90ms;
}
/* 选中描边：卡片里勾选了的单选/复选（付款方式、配送方式这类选项卡）、当前类目、显式标了选中的。
   键盘焦点与按下也给光环：看得见焦点在哪张卡上，比动效更要紧。 */
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card)):is(:has(input:checked), .is-selected, .is-active, .border-primary,
    [aria-current="page"], [aria-selected="true"], :focus-visible, :active) {
  --_sel: calc(var(--o-card-select, 0) * var(--_box));
  --_sel0: var(--o-card-select, 0);
}

/* 阴影 + 光环：叠在卡片原有阴影上（见文件头三）。只在会出阴影/光环的状态下写，
   免得静止态也被 !important 钉住、卡片自己的阴影过渡失效。 */
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card)):is(:hover, :focus-visible, :has(:focus-visible), :active, :has(input:checked),
    .is-selected, .is-active, .border-primary, [aria-current="page"], [aria-selected="true"]) {
  box-shadow: var(--_rest),
    0 calc(.75rem * var(--_s)) calc(1.75rem * var(--_s)) rgba(0, 0, 0, calc(.16 * var(--_s))),
    0 0 0 calc(2px * max(var(--_ring), var(--_sel))) var(--o-color-1, #714B67) !important;
}
/* 没有盒子的类目卡片（类目列表的卡片 / 圆形、子类目圆形，--_box: 0）：卡片本身无边无底，悬停阴影、描边高亮、
 * 选中光环画在卡片上只是一圈空框，于是此前这三样在它们身上怎么选都没反应。改画在图上（图是这几种卡片唯一的「盒子」）。
 * 类目列表圆形原来那圈 1px 细边（wsale_snippets.css）经 --_mrest 一起带上。 */
:is(.o_wsale_catlist.o_wsale_catlist_cards .o_wsale_catlist_media, .o_wsale_catlist.o_wsale_catlist_circles .o_wsale_catlist_media,
    .o_wsale_ccards.o_wsale_ccards_circles .o_wsale_ccard_img) {
  --_ms: calc(var(--o-card-shadow, 0) * var(--_hov, 0));
  --_mr: max(calc(var(--o-card-ring, 0) * var(--_hov, 0)), var(--_sel0, 0));
  box-shadow: var(--_mrest, 0 0 0 0 transparent),
    0 calc(.75rem * var(--_ms)) calc(1.75rem * var(--_ms)) rgba(0, 0, 0, calc(.16 * var(--_ms))),
    0 0 0 calc(2px * var(--_mr)) var(--o-color-1, #714B67);
  transition: box-shadow var(--o-card-speed, 280ms) cubic-bezier(.2, .7, .2, 1), border-color var(--o-card-speed, 280ms) ease;
}
.o_wsale_catlist.o_wsale_catlist_circles .o_wsale_catlist_media { --_mrest: 0 0 0 1px var(--bs-border-color, #dee2e6); }

/* 选中的对勾（只给明确"选中"的，不给焦点） */
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card))::before {
  content: "\2713";
  position: absolute;
  top: .5rem;
  right: .5rem;
  z-index: 3;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--o-color-1, #714B67);
  color: #fff;
  font-size: .8rem;
  line-height: 1;
  pointer-events: none;
  scale: 0;
  transition: scale 200ms cubic-bezier(.3, 1.6, .5, 1);
}
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card)):is(:has(input:checked), .is-selected, [aria-selected="true"])::before {
  scale: calc(var(--o-card-select, 0) * var(--_box));
}

/* 光斑跟随 + 光泽扫过：盖在卡片最上面的一层（不吃指针）。
   光泽：250% 宽的斜向高光，静止停在左外侧（150%），悬停扫到右外侧（-50%）；两端都在卡外，
   移开时无过渡地跳回起点也看不见。光斑：跟鼠标（--o-card-mx/my 由脚本写），只在悬停时有颜色。 */
:is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
    .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
    .s_card:not(.s_timeline_card))::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  pointer-events: none;
  background:
    linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, calc(.5 * var(--o-card-shine, 0))) 50%, transparent 60%)
      150% 0 / 250% 100% no-repeat,
    radial-gradient(260px circle at var(--o-card-mx, 50%) var(--o-card-my, 50%),
      color-mix(in srgb, var(--o-color-1, #714B67) calc(22% * var(--o-card-glow, 0) * var(--_hov)), transparent), transparent 70%);
}
@media (hover: hover) {
  :is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
      .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
      .s_card:not(.s_timeline_card)):hover::after {
    background-position: -50% 0, 0 0;
    transition: background-position 800ms ease;
  }
}

/* 波纹（website_runtime.js 插的 span）：裁在卡片自己的圆角里，不要求卡片 overflow:hidden */
.o_card_ripple_box {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}
.o_card_ripple {
  position: absolute;
  border-radius: 50%;
  background: var(--o-color-1, #714B67);
  opacity: .22;
  scale: 0;
  animation: o_card_ripple 650ms ease-out forwards;
}
@keyframes o_card_ripple { to { scale: 1; opacity: 0; } }

/* ── 图片：放大 / 换第二张图 / 灰度转彩色 ─────────────────────────────
 * 图框（下面这串）负责裁切；图本身只按 --_hov 算 scale / opacity / filter。 */
:is(.oe_product .o_wsale_product_images, .o_wsale_cat_card .ratio, .o_wsale_acc_card .ratio, .o_wsale_ccard_img,
    .o_wsale_brand_logo_wrap, .o_wsale_mega_card_media, .o_wsale_catlist_media, .o_search_card_media,
    .s_card .o_card_img_wrapper) {
  overflow: hidden;
}
:is(.oe_product .o_wsale_product_images, .o_wsale_cat_card .ratio, .o_wsale_acc_card .ratio, .o_wsale_ccard_img,
    .o_wsale_brand_logo_wrap, .o_wsale_mega_card_media, .o_wsale_catlist_media, .o_search_card_media,
    .s_card .o_card_img_wrapper) img {
  scale: calc(1 + (var(--o-card-zoom, 1) - 1) * var(--_hov, 0));
  filter: grayscale(calc(var(--o-card-gray, 0) * (1 - var(--_hov, 0))));
  transition: scale calc(var(--o-card-speed, 280ms) * 1.6) cubic-bezier(.2, .7, .2, 1),
    opacity var(--o-card-speed, 280ms) ease, filter var(--o-card-speed, 280ms) ease;
}
/* 第二张图平时透明、叠在主图上；有它的时候主图同步淡出（透明底的商品图不然会叠影） */
.oe_product .o_wsale_product_images img.o_card_img_alt { opacity: calc(var(--o-card-swap, 0) * var(--_hov, 0)); }
.oe_product .o_wsale_product_images:has(.o_card_img_alt) img:not(.o_card_img_alt) {
  opacity: calc(1 - var(--o-card-swap, 0) * var(--_hov, 0));
}
/* 图片放在卡片顶上的 Bootstrap 卡片：图框补上卡片内圆角，放大时角不会戳出卡片 */
:is(.o_wsale_cat_card, .o_wsale_acc_card) .ratio,
.s_card.o_card_img_top > .o_card_img_wrapper {
  border-top-left-radius: var(--bs-card-inner-border-radius);
  border-top-right-radius: var(--bs-card-inner-border-radius);
}
.s_card.o_card_img_horizontal > .o_card_img_wrapper {
  border-top-left-radius: var(--bs-card-inner-border-radius);
  border-bottom-left-radius: var(--bs-card-inner-border-radius);
}

/* ── 名称位置：图片下方 / 叠在图上 / 悬停时叠在图上 ──────────────────
 * 只对"图在上、正文在下"的卡片（网格商品卡、类目卡、配件卡、搜索卡、图在上的积木卡）。
 * 便当 / 优站精选本来就压在图上，Chips 的图内缩在卡里，横排的列表没有"图下方"可言 —— 都不参与。 */
:is(.oe_product:not(:has(> .row)):not(.o_wsale_d_bento):not(.o_wsale_d_showcase):not(.o_wsale_d_chips),
    .o_wsale_cat_card, .o_wsale_acc_card, .o_search_card, .s_card.o_card_img_top) > .card-body {
  --_over: var(--o-card-cap-over, 0);
  --_capv: calc(1 - var(--o-card-cap-hide, 0) * (1 - var(--_show, 1)));
  position: var(--o-card-cap-pos, static);
  /* 叠在图上时贴着图的边，不贴卡片的边：选了留白框，图缩进去了，深色渐变不能从图外的留白里漏出来 */
  left: var(--o-card-img-inset, 0px);
  right: var(--o-card-img-inset, 0px);
  bottom: var(--o-card-img-inset, 0px);
  z-index: 1;
  padding-top: calc(var(--bs-card-spacer-y, 1rem) + 2.5rem * var(--_over));
  border-bottom-left-radius: max(0px, calc(var(--bs-card-inner-border-radius, 0px) - var(--o-card-img-inset, 0px)));
  border-bottom-right-radius: max(0px, calc(var(--bs-card-inner-border-radius, 0px) - var(--o-card-img-inset, 0px)));
  background: linear-gradient(to top, rgba(0, 0, 0, calc(.74 * var(--_over))) 0,
    rgba(0, 0, 0, calc(.4 * var(--_over))) 55%, rgba(0, 0, 0, 0) 100%);
  color: color-mix(in srgb, #fff calc(100% * var(--_over)), currentColor);
  opacity: var(--_capv);
  translate: 0 calc(.75rem * (1 - var(--_capv)));
  transition: opacity var(--o-card-speed, 280ms) ease, translate var(--o-card-speed, 280ms) cubic-bezier(.2, .7, .2, 1);
}
:is(.oe_product:not(:has(> .row)):not(.o_wsale_d_bento):not(.o_wsale_d_showcase):not(.o_wsale_d_chips),
    .o_wsale_cat_card, .o_wsale_acc_card, .o_search_card, .s_card.o_card_img_top) > .card-body
  :is(.text-muted, .text-body-secondary, small) {
  color: color-mix(in srgb, rgba(255, 255, 255, .82) calc(100% * var(--o-card-cap-over, 0)),
    var(--bs-secondary-color, #6c757d)) !important;
}

/* 名称位置 · 不是 Bootstrap 卡片的三种类目卡片（没有 .card-body，上面那条够不着 —— 主题「类目卡片 ▸ 名称位置」
 * 曾只对 /shop 的类目卡生效，子类目卡、巨型菜单图卡、类目列表卡片怎么选都还在图下，2026-10-24 用户报「没效果」）。
 * 只管"图在上、名字在下"的那一档：子类目「卡片」、巨型菜单「图卡」、类目列表「卡片」。圆形 / 横排 / 文字 / 磁贴
 * 没有"图下方"可言，子类目便当本来就压在图上 —— 都不参与。
 * 静止（名称在图下）时下面每一项都乘 0，与没有这几条时一模一样。 */
:is(.o_wsale_ccards_cards .o_wsale_ccard, .o_wsale_mega_card, .o_wsale_catlist_cards .o_wsale_catlist_item) {
  --_over: var(--o-card-cap-over, 0);
  --_capv: calc(1 - var(--o-card-cap-hide, 0) * (1 - var(--_show, 1)));
  --_ins: var(--o-card-img-inset, 0px);
}
/* 名字（与件数）挪到图上的位置。子类目卡的名字、件数是两个并排的兄弟、没有外包层，只能各自定位（同子类目便当）。
 * 叠在图上时图一直铺到卡片底边（见下面「图片边框」那节），图底离卡片内沿 = 留白框的缩进 --_ins；名字在件数上面一行。 */
:is(.o_wsale_ccards_cards .o_wsale_ccard_name, .o_wsale_ccards_cards .o_wsale_ccard_count, .o_wsale_mega_card_name) {
  position: var(--o-card-cap-pos, static);
  left: calc(var(--_ins) + 10px);
  right: calc(var(--_ins) + 10px);
  z-index: 1;
  color: color-mix(in srgb, #fff calc(100% * var(--_over)), currentColor);
  opacity: var(--_capv);
  translate: 0 calc(.75rem * (1 - var(--_capv)));
  transition: opacity var(--o-card-speed, 280ms) ease, translate var(--o-card-speed, 280ms) cubic-bezier(.2, .7, .2, 1);
}
.o_wsale_ccards_cards .o_wsale_ccard_count {
  bottom: calc(var(--_ins) + 8px);
  color: color-mix(in srgb, rgba(255, 255, 255, .82) calc(100% * var(--_over)), var(--cc-muted, #6c757d));
}
.o_wsale_ccards_cards .o_wsale_ccard_name { bottom: calc(var(--_ins) + 8px + 1.3em + 2px); }
.o_wsale_ccards_cards .o_wsale_ccard_name:last-child { bottom: calc(var(--_ins) + 8px); }   /* 没有件数 */
.o_wsale_mega_card_name { left: calc(var(--_ins) + .5rem); right: calc(var(--_ins) + .5rem); bottom: calc(var(--_ins) + .4rem); }
/* 名字底下的深色渐变：子类目卡与巨型菜单图卡画在图框上（图框裁切、圆角都是现成的） */
:is(.o_wsale_ccards_cards .o_wsale_ccard_img, .o_wsale_mega_card_media) { position: relative; }
:is(.o_wsale_ccards_cards .o_wsale_ccard_img, .o_wsale_mega_card_media)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, .74) 0, rgba(0, 0, 0, .4) 35%, rgba(0, 0, 0, 0) 70%);
  opacity: calc(var(--_over) * var(--_capv));
  transition: opacity var(--o-card-speed, 280ms) ease;
}
/* 类目列表卡片：名字与件数本来就包在 .o_wsale_catlist_text 里，整块压到图底（同上面 .card-body 那条）。
 * 多带一个 .o_wsale_catlist：wsale_snippets.css 用同样的特异度写了它的 padding，两个模块的样式表谁后加载不由这里定。 */
.o_wsale_catlist.o_wsale_catlist_cards .o_wsale_catlist_text {
  position: var(--o-card-cap-pos, static);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  padding-top: calc(.65rem + 2.5rem * var(--_over));
  padding-inline: calc(.15rem + .6rem * var(--_over));
  padding-bottom: calc(.65rem * var(--_over));
  border-bottom-left-radius: var(--o-card-radius, .75rem);
  border-bottom-right-radius: var(--o-card-radius, .75rem);
  background: linear-gradient(to top, rgba(0, 0, 0, calc(.74 * var(--_over))) 0,
    rgba(0, 0, 0, calc(.4 * var(--_over))) 55%, rgba(0, 0, 0, 0) 100%);
  color: color-mix(in srgb, #fff calc(100% * var(--_over)), currentColor);
  opacity: var(--_capv);
  translate: 0 calc(.75rem * (1 - var(--_capv)));
  transition: opacity var(--o-card-speed, 280ms) ease, translate var(--o-card-speed, 280ms) cubic-bezier(.2, .7, .2, 1);
}

/* ── 叠层：遮罩 + 按钮 / 按钮上滑 / 光泽（光泽在上面的 ::after 里） ──────
 * 遮罩压在图框上；两种都把卡片里的按钮先藏起来、悬停才出 —— 位置照占，不让卡片高度跳。 */
:is(.oe_product .o_wsale_card_media, .o_wsale_cat_card .ratio, .o_wsale_acc_card .ratio, .o_search_card_media,
    .s_card .o_card_img_wrapper) {
  position: relative;
}
:is(.oe_product:not(.o_wsale_d_bento):not(.o_wsale_d_showcase) .o_wsale_card_media, .o_wsale_cat_card .ratio,
    .o_wsale_acc_card .ratio, .o_search_card_media, .s_card .o_card_img_wrapper)::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, .55), rgba(0, 0, 0, .12) 60%, rgba(0, 0, 0, 0));
  opacity: calc(var(--o-card-veil, 0) * var(--_hov, 0));
  transition: opacity var(--o-card-speed, 280ms) ease;
}
/* 同一层遮罩，给不是 Bootstrap 卡片的三种类目卡片（上面那串只认 .ratio，它们一样也没有 —— 此前「叠层」在它们身上没反应）：
 * 画在图框上（图框都已 overflow:hidden、带圆角）。子类目卡的 ::before 是「名称叠在图上」的渐变；子类目便当的
 * ::after 是它自己压字的渐变，便当的遮罩改用 ::before（便当没有「名称位置」）。 */
:is(.o_wsale_ccard_img, .o_wsale_mega_card_media, .o_wsale_catlist_media) { position: relative; }
:is(.o_wsale_ccards:not(.o_wsale_ccards_bento) .o_wsale_ccard_img, .o_wsale_mega_card_media, .o_wsale_catlist_media)::after,
.o_wsale_ccards_bento .o_wsale_ccard_img::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(to top, rgba(0, 0, 0, .55), rgba(0, 0, 0, .12) 60%, rgba(0, 0, 0, 0));
  opacity: calc(var(--o-card-veil, 0) * var(--_hov, 0));
  transition: opacity var(--o-card-speed, 280ms) ease;
}
:is(.oe_product .card-body :is(form, a.btn), .o_wsale_acc_card .card-body :is(form, .btn),
    .s_card :is(.card-body, .card-footer) .btn, .o_learning_course_card > .btn) {
  --_btnv: calc(1 - max(var(--o-card-veil, 0), var(--o-card-reveal, 0)) * (1 - var(--_show, 1)));
  opacity: var(--_btnv);
  translate: 0 calc(.6rem * var(--o-card-reveal, 0) * (1 - var(--_btnv)));
  transition: opacity var(--o-card-speed, 280ms) ease, translate var(--o-card-speed, 280ms) cubic-bezier(.2, .7, .2, 1);
}

/* 名称叠在图上（--o-card-cap-pos: absolute）时正文不占高，图框一直铺到卡片底边，下边两个角也要圆：
 * 卡片不 overflow:hidden（见 wsale_snippets.css），图框的方角白底 / 遮罩会从卡片圆角外戳出来，
 * 悬停时正文的深色渐变是圆的、底下方角更显眼（2026-10-24 用户截图）。下面「图片边框」那条管图本身。 */
.oe_product:not(:has(> .row)):not(.o_wsale_d_chips):not(.o_wsale_d_bento):not(.o_wsale_d_showcase) > .o_wsale_card_media {
  border-bottom-left-radius: calc(var(--bs-card-inner-border-radius, 0px) * var(--o-card-cap-over, 0));
  border-bottom-right-radius: calc(var(--bs-card-inner-border-radius, 0px) * var(--o-card-cap-over, 0));
}

/* ── 图片边框：无 / 细线 / 留白框 ─────────────────────────────────────
 * 留白框 = 图内缩一圈、自己带圆角（卡片圆角减去内缩）；不内缩时下边两个角是直的（正文接在下面），
 * 除非名称叠在图上（图铺到卡片底边，同上）。
 * 自带内缩或圆角图的几套（Chips / 便当 / 优站精选 / Condensed / 大）与横排列表不参与。 */
:is(.oe_product:not(:has(> .row)):not(.o_wsale_d_chips):not(.o_wsale_d_bento):not(.o_wsale_d_showcase) .o_wsale_product_images,
    .o_wsale_cat_card .ratio, .o_wsale_acc_card .ratio, .o_search_card_media, .s_card.o_card_img_top > .o_card_img_wrapper) {
  --_ins: var(--o-card-img-inset, 0px);
  width: auto;
  margin: var(--_ins);
  border: var(--o-card-img-border, 0px) solid var(--bs-border-color, #dee2e6);
  border-top-left-radius: max(0px, calc(var(--bs-card-inner-border-radius, 0px) - var(--_ins)));
  border-top-right-radius: max(0px, calc(var(--bs-card-inner-border-radius, 0px) - var(--_ins)));
  --_Rb: min(var(--_R, .375rem), var(--_ins) * 1000 + var(--bs-card-inner-border-radius, 0px) * var(--o-card-cap-over, 0));
  border-bottom-left-radius: max(0px, calc(var(--_Rb) - var(--_ins)));
  border-bottom-right-radius: max(0px, calc(var(--_Rb) - var(--_ins)));
}

/* 图片边框 · 不是 Bootstrap 卡片的类目卡片（上面那串只认 .ratio —— 此前「图片边框」在它们身上没反应）。
 * 只管图在上、名字在下的那一档（同「名称位置」）：子类目「卡片」、巨型菜单「图卡」、类目列表「卡片」。圆形本来就描着一圈、
 * 横排 / 磁贴的小方图、便当的满版图与文字档都不参与（同商品卡的 Chips / 便当 / 横排列表）。
 *
 * 「无」= 图铺到卡片边（同商品卡）。子类目卡与巨型菜单图卡的图原来缩在卡片内边距里（12px / .75rem），选「无」也像套着一圈
 * 白框（2026-10-24 用户报）：图框用负外边距吃掉内边距（--_pad），再按留白框缩回 --_ins；名称叠在图上时连下内边距也吃掉，
 * 图铺到卡片底边。圆角 = 卡片内圆角减去缩进；下边两个角只在留白框或名称叠在图上时圆（同商品卡的 --_Rb）。 */
.o_wsale_ccards_cards .o_wsale_ccard { --_pad: 12px; --_padb: 14px; --_gapb: .25rem; --_cr: var(--cc-radius, .5rem); }
.o_wsale_mega_card { --_pad: .75rem; --_padb: .75rem; --_gapb: 0px; --_cr: max(var(--o-wsale-mega-radius, 0px), var(--o-card-radius, 0px)); }
:is(.o_wsale_ccards_cards .o_wsale_ccard, .o_wsale_mega_card) { padding-bottom: calc(var(--_padb) * (1 - var(--_over))); }
:is(.o_wsale_ccards_cards .o_wsale_ccard_img, .o_wsale_mega_card_media) {
  --_ri: max(0px, calc(var(--_cr) - 1px));   /* 卡片 1px 边框以内的圆角 */
  --_rb: min(var(--_ri), var(--_ins) * 1000 + var(--_ri) * var(--_over));
  width: calc(100% + 2 * var(--_pad) - 2 * var(--_ins));
  margin: calc(var(--_ins) - var(--_pad)) calc(var(--_ins) - var(--_pad))
    calc(var(--_gapb) * (1 - var(--_over)) + var(--_ins) * var(--_over));
  border: var(--o-card-img-border, 0px) solid var(--bs-border-color, #dee2e6);
  border-radius: max(0px, calc(var(--_ri) - var(--_ins))) max(0px, calc(var(--_ri) - var(--_ins)))
    max(0px, calc(var(--_rb) - var(--_ins))) max(0px, calc(var(--_rb) - var(--_ins)));
}
.o_wsale_catlist.o_wsale_catlist_cards .o_wsale_catlist_media {
  margin: var(--_ins) var(--_ins) calc(var(--_ins) * var(--_over));   /* 名称叠在图上时卡片只剩图：下边也缩 */
  border: var(--o-card-img-border, 0px) solid var(--bs-border-color, #dee2e6);
}
/* 名称叠在图上时，类目列表卡片的文字块跟着图缩进去 */
.o_wsale_catlist.o_wsale_catlist_cards .o_wsale_catlist_text {
  left: var(--_ins);
  right: var(--_ins);
  bottom: var(--_ins);
}
/* /shop 类目卡的图自带 Bootstrap 的 p-2（!important）：图框已经铺到卡片边，图却在框里又缩了一圈，选「无」也看着有框 */
.o_wsale_cat_card > .ratio > img { padding: 0 !important; }

/* ── 减少动态效果 ─────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  :is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
      .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
      .s_card:not(.s_timeline_card)) {
    translate: none !important;
    scale: none !important;
    transform: none !important;
  }
  :is(.oe_product, .o_wsale_cat_card, .o_wsale_acc_card, .o_wsale_ccard, .o_wsale_brand_card, .o_wsale_mega_card,
      .o_wsale_catlist_item, .o_search_card, .o_learning_course_card, .o_forum_index_card, .o_dyn_card,
      .s_card:not(.s_timeline_card))::after { background-image: none, none; }
  :is(.o_wsale_product_images, .ratio, .o_wsale_ccard_img, .o_wsale_brand_logo_wrap, .o_wsale_mega_card_media,
      .o_wsale_catlist_media, .o_search_card_media, .o_card_img_wrapper) img { scale: none !important; }
  .o_card_ripple_box { display: none; }
}

/* ══ 积木覆盖：data-card-<组>="<值>" → 同一组变量（与 card_fx.ts 的 CARD_FX_GROUPS 逐条一致）══
 * 每条把同值的 --o-catcard-* 也写上（cardFxBlockVars）：积木上选的，对积木里的类目卡片也是最近的一层，
 * 不能被主题「类目卡片」一节从 :root 漏进来。 */
[data-card-hover="none"] { --o-card-lift: 0px; --o-card-grow: 1; --o-card-shadow: 0; --o-card-ring: 0; --o-card-tilt: 0; --o-card-glow: 0; --o-catcard-lift: 0px; --o-catcard-grow: 1; --o-catcard-shadow: 0; --o-catcard-ring: 0; --o-catcard-tilt: 0; --o-catcard-glow: 0; }
[data-card-hover="lift"] { --o-card-lift: -6px; --o-card-grow: 1; --o-card-shadow: 1; --o-card-ring: 0; --o-card-tilt: 0; --o-card-glow: 0; --o-catcard-lift: -6px; --o-catcard-grow: 1; --o-catcard-shadow: 1; --o-catcard-ring: 0; --o-catcard-tilt: 0; --o-catcard-glow: 0; }
[data-card-hover="scale"] { --o-card-lift: 0px; --o-card-grow: 1.03; --o-card-shadow: 1; --o-card-ring: 0; --o-card-tilt: 0; --o-card-glow: 0; --o-catcard-lift: 0px; --o-catcard-grow: 1.03; --o-catcard-shadow: 1; --o-catcard-ring: 0; --o-catcard-tilt: 0; --o-catcard-glow: 0; }
[data-card-hover="shadow"] { --o-card-lift: 0px; --o-card-grow: 1; --o-card-shadow: 1; --o-card-ring: 0; --o-card-tilt: 0; --o-card-glow: 0; --o-catcard-lift: 0px; --o-catcard-grow: 1; --o-catcard-shadow: 1; --o-catcard-ring: 0; --o-catcard-tilt: 0; --o-catcard-glow: 0; }
[data-card-hover="border"] { --o-card-lift: 0px; --o-card-grow: 1; --o-card-shadow: 0; --o-card-ring: 1; --o-card-tilt: 0; --o-card-glow: 0; --o-catcard-lift: 0px; --o-catcard-grow: 1; --o-catcard-shadow: 0; --o-catcard-ring: 1; --o-catcard-tilt: 0; --o-catcard-glow: 0; }
[data-card-hover="tilt"] { --o-card-lift: 0px; --o-card-grow: 1.02; --o-card-shadow: 1; --o-card-ring: 0; --o-card-tilt: 1; --o-card-glow: 0; --o-catcard-lift: 0px; --o-catcard-grow: 1.02; --o-catcard-shadow: 1; --o-catcard-ring: 0; --o-catcard-tilt: 1; --o-catcard-glow: 0; }
[data-card-hover="glow"] { --o-card-lift: 0px; --o-card-grow: 1; --o-card-shadow: 0; --o-card-ring: 1; --o-card-tilt: 0; --o-card-glow: 1; --o-catcard-lift: 0px; --o-catcard-grow: 1; --o-catcard-shadow: 0; --o-catcard-ring: 1; --o-catcard-tilt: 0; --o-catcard-glow: 1; }

[data-card-img="none"] { --o-card-zoom: 1; --o-card-swap: 0; --o-card-gray: 0; --o-catcard-zoom: 1; --o-catcard-swap: 0; --o-catcard-gray: 0; }
[data-card-img="zoom"] { --o-card-zoom: 1.08; --o-card-swap: 0; --o-card-gray: 0; --o-catcard-zoom: 1.08; --o-catcard-swap: 0; --o-catcard-gray: 0; }
[data-card-img="swap"] { --o-card-zoom: 1; --o-card-swap: 1; --o-card-gray: 0; --o-catcard-zoom: 1; --o-catcard-swap: 1; --o-catcard-gray: 0; }
[data-card-img="gray"] { --o-card-zoom: 1; --o-card-swap: 0; --o-card-gray: 1; --o-catcard-zoom: 1; --o-catcard-swap: 0; --o-catcard-gray: 1; }

[data-card-layer="none"] { --o-card-veil: 0; --o-card-reveal: 0; --o-card-shine: 0; --o-catcard-veil: 0; --o-catcard-reveal: 0; --o-catcard-shine: 0; }
[data-card-layer="overlay"] { --o-card-veil: 1; --o-card-reveal: 0; --o-card-shine: 0; --o-catcard-veil: 1; --o-catcard-reveal: 0; --o-catcard-shine: 0; }
[data-card-layer="slideup"] { --o-card-veil: 0; --o-card-reveal: 1; --o-card-shine: 0; --o-catcard-veil: 0; --o-catcard-reveal: 1; --o-catcard-shine: 0; }
[data-card-layer="shine"] { --o-card-veil: 0; --o-card-reveal: 0; --o-card-shine: 1; --o-catcard-veil: 0; --o-catcard-reveal: 0; --o-catcard-shine: 1; }

[data-card-press="none"] { --o-card-press: 1; --o-card-ripple: 0; --o-card-select: 0; --o-catcard-press: 1; --o-catcard-ripple: 0; --o-catcard-select: 0; }
[data-card-press="press"] { --o-card-press: 0.97; --o-card-ripple: 0; --o-card-select: 0; --o-catcard-press: 0.97; --o-catcard-ripple: 0; --o-catcard-select: 0; }
[data-card-press="ripple"] { --o-card-press: 1; --o-card-ripple: 1; --o-card-select: 0; --o-catcard-press: 1; --o-catcard-ripple: 1; --o-catcard-select: 0; }
[data-card-press="ring"] { --o-card-press: 1; --o-card-ripple: 0; --o-card-select: 1; --o-catcard-press: 1; --o-catcard-ripple: 0; --o-catcard-select: 1; }

[data-card-speed="fast"] { --o-card-speed: 160ms; --o-catcard-speed: 160ms; }
[data-card-speed="normal"] { --o-card-speed: 280ms; --o-catcard-speed: 280ms; }
[data-card-speed="slow"] { --o-card-speed: 460ms; --o-catcard-speed: 460ms; }

[data-card-caption="below"] { --o-card-cap-pos: static; --o-card-cap-over: 0; --o-card-cap-hide: 0; --o-catcard-cap-pos: static; --o-catcard-cap-over: 0; --o-catcard-cap-hide: 0; }
[data-card-caption="over"] { --o-card-cap-pos: absolute; --o-card-cap-over: 1; --o-card-cap-hide: 0; --o-catcard-cap-pos: absolute; --o-catcard-cap-over: 1; --o-catcard-cap-hide: 0; }
[data-card-caption="hover"] { --o-card-cap-pos: absolute; --o-card-cap-over: 1; --o-card-cap-hide: 1; --o-catcard-cap-pos: absolute; --o-catcard-cap-over: 1; --o-catcard-cap-hide: 1; }

[data-card-frame="none"] { --o-card-img-border: 0px; --o-card-img-inset: 0px; --o-catcard-img-border: 0px; --o-catcard-img-inset: 0px; }
[data-card-frame="line"] { --o-card-img-border: 1px; --o-card-img-inset: 0px; --o-catcard-img-border: 1px; --o-catcard-img-inset: 0px; }
[data-card-frame="inset"] { --o-card-img-border: 0px; --o-card-img-inset: 0.5rem; --o-catcard-img-border: 0px; --o-catcard-img-inset: 0.5rem; }

[data-card-radius="0"] { --o-card-radius: 0px; --o-catcard-radius: 0px; }
[data-card-radius="sm"] { --o-card-radius: 0.25rem; --o-catcard-radius: 0.25rem; }
[data-card-radius="md"] { --o-card-radius: 0.5rem; --o-catcard-radius: 0.5rem; }
[data-card-radius="lg"] { --o-card-radius: 1rem; --o-catcard-radius: 1rem; }
[data-card-radius="xl"] { --o-card-radius: 1.5rem; --o-catcard-radius: 1.5rem; }
