/**
 * TopSites — 主题样式系统
 *
 * 命名约定：全部以 ts- 前缀，避免与旧版 main.css 冲突，支持逐页迁移。
 * 换肤原则：所有颜色 / 字体 / 圆角 / 间距一律走 :root 变量，任何组件内不得写死色值。
 *           改 :root 即可整站换肤，无需触碰组件规则。
 */

/* ═══════════════════════════════════════════
   1. 设计变量（换肤唯一入口）
   ═══════════════════════════════════════════ */
:root {
  /* 中性色 */
  --ts-bg:            #F7F5F0;
  --ts-bg-rgb:        247, 245, 240; /* = --ts-bg，给 hero 背景图的遮罩用 */
  --ts-surface:       #FDFCFA;
  --ts-surface-rgb:   253, 252, 250; /* = --ts-surface，给渐变用；直接对 hex 用 transparent 会在 Safari 出灰边 */
  --ts-ink:           #1A1A1A;
  --ts-ink-soft:      #2E2E2E;
  /* 2026-08-25：浅色这两档整体压暗一级（用户决定）。
     改之前 --ts-muted #8A8680 在**最差的一块底色**上只有 3.04:1、在 surface 上 3.53、
     在 bg 上 3.32 —— 全部低于 AA 正文的 4.5，而它是全站次要文本的默认色
     （卡片日期、规格行标签、FAQ 正文、页脚说明、面包屑……六十多处）。
     --ts-muted-light #C4C0BA 更糟，1.52～1.77，第 4 名往后的名次数字基本看不见。

     新值是量出来的，不是挑好看的：对 bg / surface / accent-pale / 三个奖牌 tint /
     positive-pale / info-pale 八种底色逐个算，取最小值。
       --ts-muted       #6B675F  min 4.72  ← 过 AA 正文（4.5）
       --ts-muted-light #8A8680  min 3.04  ← 过 AA 大字与非文本（3.0）
     色相保持不动（H≈36～40°、S≈5%，还是原来那支暖灰），只降明度。
     梯队没塌：ink 16 : ink-soft 12.5 : muted 4.7 : muted-light 3.0，四档仍然分得开。

     ⚠️ --ts-muted-light **只能用在大字和非文本上**（≥24px 或 ≥18.7px 粗体的名次数字、
        品牌首字兜底、面包屑分隔符、描边），3.04 不够给小字用。小字一律用 --ts-muted。
     ⚠️ 这两个变量都不作背景色用（全站零处），压暗不会牵动任何底色。
     ⚠️ 改这里要连 tests 的 ts-token-contrast 那一节一起看，那里按同一套底色重算。 */
  --ts-muted:         #6B675F;
  --ts-muted-light:   #8A8680;
  --ts-border:        #E8E4DE;
  --ts-border-strong: #D4CFC8;

  /* 主色。
     ⚠️ 2026-08-26 第六场：浅色这一族整体压暗一档（**用户决定**，交接零.2 那笔债还完了）。
        起因是浅色下三条同时不达标，而三条是同一个病 —— 紫色太浅：
          白字压 accent           4.33  差 AA 4.5
          accent 当文字用（39 处） 3.63～4.22  差 AA 4.5（最差的一档是压在 accent-pale 上）
          accent-soft 当 38px 斜体 2.28～2.42  差大字 3.0
        深色那一档（2.50）第五场已经用 --ts-on-accent 绕开了，**浅色绕不开** ——
        白字不能更白，只能动主色本身。这是换品牌色，不是技术判断，所以等的是用户拍板。

        新值不是挑好看的，是沿**同一色相**（H≈249°，S 不动）只降明度，逐档算到刚好全过：
          accent  #7B70B8 → #6C62A1（V 0.72 → 0.63）
            白字压它 4.33 → **5.39**；它当文字压八种底色 3.63～4.22 → **4.52～5.25**，
            最紧的一档是压在 accent-pale 上的 4.52 —— **这一档决定了不能再浅**，
            上一档 #7268AB 只有 4.11，导航 hover（.ts-nav__menu a:hover）那块就会漏。
          accent-soft #A89FC8 → #9089AC（V 0.78 → 0.67）
            压 bg 2.28 → 3.03、压 surface 2.42 → 3.22。
            ⚠️ 它当文字只有四处，全是大标题里的 em（.ts-h2 / .ts-hero__title /
               .ts-cta__title / .ts-page-hero__title），**不在 accent-pale / positive-pale /
               info-pale 上出现**，所以那三种底色下的 2.77～2.93 不收（守卫也按这个范围收）。
            ⚠️ .ts-cta__title em 压的是**反色区块**（--ts-invert-bg 近黑），
               压暗 soft 会让它**变糟**：7.01 → 5.27。仍远高于 3.0，可接受 —— 但下次再动
               soft 要先回来看这一行，它是唯一一个「调暗 = 变差」的用法。
          accent-deep #6A5FA8 → #5D5393（hover 档，按原来的明度步长比 0.913 等比跟下来）
            白字压它 5.50 → 6.72。
          accent-pale **一位没动** —— 它只当底色用，压暗它反而会把上面那 4.52 拉回去。
     ⚠️ 深色两套一位没动：深色的 accent 是提亮过的 #A79BE0，当文字压深底已经 5.96～7.39，
        本来就达标。**别顺手把深色也跟着调**。
     ⚠️ favicon 是另一份文件 assets/images/brand-mark.svg，里面这个紫是**写死的**
        （独立 SVG 文件吃不到 CSS 变量），已同步改成 #6C62A1。改主色记得改那边。
     ⚠️ 改这里要连 tests 的 ts-accent-text-contrast / ts-on-accent-contrast 两节一起看。 */
  --ts-accent:        #6C62A1;
  --ts-accent-soft:   #9089AC;
  --ts-accent-pale:   #EDE9F8;
  --ts-accent-deep:   #5D5393;
  /* 压在 --ts-accent / --ts-accent-deep 上的文字色（2026-08-25 第五场新增）。
     以前这批按钮和徽章各自把文字色写死成白，十处 —— 白字在浅色的紫底上还能看
     （4.33 / 5.50），但**深色调色板把 accent 提亮成了浅紫**（#A79BE0，为了能在深底上当
     文字用），白字压上去只有 **2.50:1**，hover 那档 #C3B9F2 更掉到 **1.82:1**。
     受影响的是全站最重要的点击目标：评测页侧栏的联盟 CTA、榜单卡 CTA、compare 页的
     CTA 与选择器、榜单徽章。

     所以文字色跟着调色板走，不再写死：
       浅色  #FFFFFF 压 #6C62A1 = 5.39 / 压 #5D5393 = 6.72   ← 2026-08-26 主色压暗后的值
       深色  #14131A 压 #A79BE0 = 7.39 / 压 #C3B9F2 = 10.14  ← 从 2.50 / 1.82 上来的

     ⚠️ 上面那两个浅色数字**原来是 4.33 / 5.50，差 AA 一点点**，第五场当时是知情欠着的
        （交接零.2）。2026-08-26 用户决定压暗品牌主色，那笔债在**主色那边**还掉了，
        **不是靠调这个 token 凑的** —— 这里浅色仍然是纯白 #FFFFFF，一位没动。
     ⚠️ 别为了凑数把这个 token 调暗：那只会让浅色按钮的字发灰，
        而「紫色当文字用」那一类（39 处）根本不经过这个 token，治不到。
     ⚠️ 深色值取 #14131A（= 深色的 --ts-bg），跟下面那条深色组件覆盖里原来写死的一致。
     ⚠️ 改这里要连 tests 的 ts-on-accent-contrast 那一节一起看，那里按同一批底色重算。 */
  --ts-on-accent:     #FFFFFF;

  /* 反色区块（顶部广告披露条 .ts-disclosure、页脚订阅 CTA .ts-cta）。
     2026-08-25 第五场新增，起因是这两块在深色下**整块不可读**：

       原来写的是 `background: var(--ts-ink)` + 写死的 rgba(253,252,250,…) 白字。
       浅色下 ink 是近黑，白字压上去很好看；**深色下 ink 翻成 #F2F0EC（浅色）**，
       底白了字还是白的 —— 实测 **1.03～1.10:1**，等于看不见。
       救它的那条 `[data-ts-theme="dark"] .ts-cta { background: #0F0E14 }` 只写了
       **手动切换**那一套，`@media (prefers-color-scheme: dark)` 一条都没有，
       而 header.php 明确写着「未存过偏好时不写 data-ts-theme」——
       **每个第一次来、系统是深色的访客都撞得上**。

     所以这块不再借用 ink/surface（那两个是要跟着主题翻转的），自己一套 token：
     **底在三套主题下都是近黑，只是深色下再暗一档**，字色因此可以固定，不用翻转。

     | token | 值 | 压 #1A1A1A（浅） | 压 #0F0E14（深） | 用在哪 |
     |---|---|---|---|---|
     | `--ts-invert-ink`      | #FDFCFA | 16.97 | 18.73 | 标题、披露条那个链接、输入框里的字 |
     | `--ts-invert-muted`    | #A7A6A5 |  7.16 |  7.90 | 披露条正文、CTA 导语 |
     | `--ts-invert-faint`    | #858483 |  4.66 |  5.15 | 眉标、NO SPAM 那行、输入框占位符 |
     | `--ts-invert-line`     | #676766 |  3.07 |  3.39 | 输入框描边、披露链接的点线（1.4.11 非文本 3.0） |

     ⚠️ **faint 这一档顺带还了浅色的一笔债**：原来眉标是 rgba(…,.42)=4.00、
        NO SPAM 那行是 .32=**2.87**、占位符 .38=3.51，**三个都低于 AA 的 4.5**，
        只是压在近黑底上不刺眼所以一直没人提。现在统一到 4.66，浅色下那几行会**亮一点点**。
     ⚠️ **别再往这两块里写 rgba(253,252,250,…)**。写死的白字在这里是错的：
        它假设底一定是深的，而底是不是深的由调色板说了算。
     ⚠️ `.ts-subform button` 仍然走 surface/ink（**故意的**）：它是块上的一个按钮，
        跟着主题翻转反而对 —— 浅色下近白药丸、深色下深药丸配亮字，四种状态实测 13.3～17.0，
        都够。改它会连带把 hover 的 --ts-accent-pale 拖进来（深色下 pale 是暗的），别动。 */
  --ts-invert-bg:       #1A1A1A;
  --ts-invert-ink:      #FDFCFA;
  --ts-invert-muted:    #A7A6A5;
  --ts-invert-faint:    #858483;
  --ts-invert-line:     #676766;
  --ts-invert-field:    #2A2A2A;
  --ts-invert-field-hi: #333333;

  /* 榜单前三名（奖牌色）。金 / 银 / 铜，同时用于名次数字与评分数字。
     这是「排名」语义，与品牌主色 --ts-accent（用于按钮等操作）刻意分开。 */
  --ts-rank-1:      #B0820A;
  --ts-rank-2:      #7C828C;
  --ts-rank-3:      #9C5A33;
  --ts-rank-1-pale: rgba(176, 130, 10, .10);
  --ts-rank-2-pale: rgba(124, 130, 140, .12);
  --ts-rank-3-pale: rgba(156, 90, 51, .10);
  /* 前三名卡片的底色 / 描边 / 柔光。刻意做得很淡——卡片是内容载体，底色一重就压过正文。 */
  --ts-rank-1-tint: #FCF7E8;
  --ts-rank-2-tint: #F5F6F9;
  --ts-rank-3-tint: #FBF3EC;
  --ts-rank-1-edge: #E2CB8C;
  --ts-rank-2-edge: #D3D8E0;
  --ts-rank-3-edge: #E0C1A6;
  --ts-rank-1-glow: rgba(176, 130, 10, .13);
  --ts-rank-2-glow: rgba(124, 130, 140, .13);
  --ts-rank-3-glow: rgba(156, 90, 51, .12);

  /* 语义色 */
  --ts-positive:      #8FA89C;
  --ts-positive-pale: #EBF0EE;
  --ts-info:          #4A6CF7;
  --ts-info-pale:     #EEF1FE;

  /* 品牌 Logo 底板。第三方品牌 Logo 基本都是深色字 + 透明底，
     深色模式下会直接糊进卡片底色里，所以深色调色板要垫一块浅色板。
     浅色模式不需要，设成 transparent 等于那条规则不存在。 */
  --ts-logo-plate:    transparent;

  /* 站点字标的配色。
     ⚠️ 三套调色板都要给值，少一套那套主题下字母会退回继承色。测试守着这条。
     ⚠️ 这几个色**只服务字标**，别拿去做正文或按钮：它们是品牌标识，
        WCAG 1.4.3 明确豁免 logotype 的对比度要求，正文可不豁免。

     2026-09-03 从逐字四色（紫/青/橙/橙，2026-08-21 定的）改成**单色**，主管要求。
     ⚠️ **深色模式必须给近白，不能也给黑** —— 深色底是 #14131A，黑字标等于不存在。
        这是「一个决定在三套调色板里有三种正确答案」的典型，别只改一套就以为完事。
     取值刻意 = 各自的 --ts-ink（浅 #1A1A1A / 深 #F2F0EC），跟同一行的 MATTRESS
     和图标轮廓（两者本来就走 --ts-ink）保持一致。改 --ts-ink 时记得回来对一眼。 */
  --ts-logo-t:    #1A1A1A;
  --ts-logo-o:    #1A1A1A;
  --ts-logo-p:    #1A1A1A;
  --ts-logo-3:    #1A1A1A;
  /* o 字腔里原来垫着一个琥珀点。2026-09-03 主管要求去掉，三套都设成 transparent。
     🛑 **必须是 transparent，不能设成字母色或底色**：
        那个圆是垫在 o 的字腔（透空的洞）底下的 —— 设成字母色，o 会变成一个实心圆点；
        设成某个底色，换到另一处（页头半透明 / 页脚实色 / 深色两套）就会露出一块补丁。
        transparent 时字腔透出页面本身的背景，等于这个圆不存在，在四种底色上都对。
     ⚠️ 关掉而不是删掉：`.ts-logo__l--o::before` 和它那条 `isolation: isolate` 还在，
        主管要是改主意，把这一行改回 #E8A63C（深色 #F5C165）就回来了。
        真要彻底删，得连 ::before、isolation 和测试里那两条断言一起收拾。 */
  --ts-logo-dot:  transparent;
  /* 描边（-webkit-text-stroke）现在和填充同色 —— 单色之后它的作用只剩加粗字形。
     ⚠️ 深色那两套必须跟着翻成近白，否则近白的字被近黑的边框起来，成了空心字。 */
  --ts-logo-edge: #1A1A1A;

  /* 玻璃与阴影 */
  --ts-glass:         rgba(253, 252, 250, .72);
  --ts-shadow-soft:   0 2px 32px rgba(0, 0, 0, .06);
  --ts-shadow-card:   0 8px 48px rgba(0, 0, 0, .08);
  --ts-shadow-hover:  0 16px 64px rgba(0, 0, 0, .12);

  /* 圆角 */
  --ts-r-sm: 8px;
  --ts-r-md: 16px;
  --ts-r-lg: 24px;
  --ts-r-xl: 32px;
  --ts-r-pill: 100px;

  /* 字体 */
  --ts-serif: 'DM Serif Display', Georgia, 'Times New Roman', serif;
  --ts-sans:  'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --ts-mono:  'Space Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* 基准字号：整站文字大小的统一入口 */
  --ts-fs-base: 17px;

  /* 动效与布局 */
  --ts-ease:    .3s cubic-bezier(.16, 1, .3, 1);
  --ts-wrap:    1180px;
  --ts-gutter:  32px;
  --ts-section: 104px;
  /* 吸顶 header 的高度。下面四处都靠它算偏移：.ts-anchors 的吸顶位置、
     .ts-section / .ts-block / .ts-prose h2 的 scroll-margin-top。
     ⚠️ 2026-08-24 之前这个数是 70px 且**四处各硬写一遍**，改 header 高度时
     很容易只改一处 —— 症状是点锚点跳过去、标题正好被压在吸顶栏底下，
     而 DOM 和样式都「正常」，只有肉眼滚一次才看得出来。改高度只改这里。 */
  --ts-header-h: 92px;

  /* 页脚里那两段说明文字（标志块下的介绍语 / Get in touch）的宽度上限。
     它们左边缘对齐、上下相邻，读起来是同一列 —— 所以 measure 必须相同，
     两处共用这一个变量就是为了**不可能只改一个**。见 .ts-footer__about 那条注释。
     420 ≈ 55 个字符，是这两段（135 字 / 220 字）都排得下又不至于成长条的档。 */
  --ts-footer-measure: 420px;
}

/* ── 深色模式 ────────────────────────────────
 * 三态：显式 light / 显式 dark / 跟随系统（未设置 data-theme）。
 * 只覆盖变量，不碰任何组件规则——这就是全站走 token 的意义。
 * ------------------------------------------ */
@media (prefers-color-scheme: dark) {
  :root:not([data-ts-theme="light"]) {
    --ts-bg:            #14131A;
    --ts-bg-rgb:        20, 19, 26;
    --ts-surface:       #1C1B24;
    --ts-surface-rgb:   28, 27, 36;
    --ts-ink:           #F2F0EC;
    --ts-ink-soft:      #D6D3CE;
    /* --ts-muted 深色下本来就够（对八种底色最低 5.10:1），这次没动。
       --ts-muted-light 差一点：#6E6A78 最低 2.83，在三个奖牌 tint 和 positive-pale 上
       连 3.0 都不到。提到 #7B7688 后最低 3.39，够大字和描边用。
       ⚠️ 深色这两档是**两套写法各一份**（这里的 @media 和下面的 data-ts-theme），
          少改一套的症状是「跟随系统的用户看着正常、手动切深色的看不清」，反过来也一样。 */
    --ts-muted:         #9A95A3;
    --ts-muted-light:   #7B7688;
    --ts-border:        #2C2A36;
    --ts-border-strong: #3B3846;

    --ts-accent:        #A79BE0;
    --ts-accent-soft:   #BDB2EC;
    --ts-accent-pale:   #272338;
    --ts-accent-deep:   #C3B9F2;
    /* 深色下按钮上的字翻成深色，理由和实测见浅色那一份的注释。
       ⚠️ 跟 muted 那两档一样，深色是**两套写法各一份**（这里的 @media 和下面的
          data-ts-theme），少改一套的症状是「跟随系统的用户看不清、手动切深色的正常」。
          测试里 ts-on-accent-contrast 三套调色板逐套算，还额外比对两套深色是否逐字一致。 */
    --ts-on-accent:     #14131A;

    /* 反色区块的底：深色下比页面底（#14131A）再暗一档，块才分得出来。
       ⚠️ 只覆盖底色 —— 字色三套通用，因为底在三套下都是近黑。见浅色那份注释。 */
    --ts-invert-bg:     #0F0E14;

    --ts-logo-plate:    #F2F0EC;

    /* 字标：深色底上四个字母要提亮，原色在 #14131A 上会闷掉（紫的尤其明显）。
       描边不能再用黑——黑边贴着深色背景等于没有边，字母之间的分隔就没了，
       改成比背景更暗的一档，边还在，但不喧宾夺主。 */
    /* 单色字标的深色版（2026-09-03）。= 深色的 --ts-ink。
       🛑 这里不能给黑：底色是 #14131A。 */
    --ts-logo-t:    #F2F0EC;
    --ts-logo-o:    #F2F0EC;
    --ts-logo-p:    #F2F0EC;
    --ts-logo-3:    #F2F0EC;
    /* 琥珀点已关掉（2026-09-03，主管要求）。transparent 的理由见浅色那一处。 */
    --ts-logo-dot:  transparent;
    /* 跟着翻成近白：留着原来的 #0B0A0F 会把近白的字描成空心字。 */
    --ts-logo-edge: #F2F0EC;

    /* 深色底上需要提亮，否则金/铜会糊成褐色 */
    --ts-rank-1:      #E8BE55;
    --ts-rank-2:      #B9C0CC;
    --ts-rank-3:      #CE7F42;
    --ts-rank-1-pale: rgba(232, 190, 85, .14);
    --ts-rank-2-pale: rgba(185, 192, 204, .14);
    --ts-rank-3-pale: rgba(206, 127, 66, .14);
    --ts-rank-1-tint: #2A2419;
    --ts-rank-2-tint: #23242C;
    --ts-rank-3-tint: #2B2118;
    --ts-rank-1-edge: #5A4A22;
    --ts-rank-2-edge: #454B57;
    --ts-rank-3-edge: #5B4029;
    --ts-rank-1-glow: rgba(232, 190, 85, .10);
    --ts-rank-2-glow: rgba(185, 192, 204, .10);
    --ts-rank-3-glow: rgba(206, 127, 66, .10);

    --ts-positive:      #7FA492;
    --ts-positive-pale: #1E2A24;
    --ts-info:          #7D95FA;
    --ts-info-pale:     #1C2036;

    --ts-glass:         rgba(28, 27, 36, .78);
    --ts-shadow-soft:   0 2px 32px rgba(0, 0, 0, .34);
    --ts-shadow-card:   0 8px 48px rgba(0, 0, 0, .42);
    --ts-shadow-hover:  0 16px 64px rgba(0, 0, 0, .52);
  }
}

:root[data-ts-theme="dark"] {
  --ts-bg:            #14131A;
  --ts-bg-rgb:        20, 19, 26;
  --ts-surface:       #1C1B24;
  --ts-surface-rgb:   28, 27, 36;
  --ts-ink:           #F2F0EC;
  --ts-ink-soft:      #D6D3CE;
  /* 与上面 prefers-color-scheme 那套逐字一致，理由见那里。 */
  --ts-muted:         #9A95A3;
  --ts-muted-light:   #7B7688;
  --ts-border:        #2C2A36;
  --ts-border-strong: #3B3846;

  --ts-accent:        #A79BE0;
  --ts-accent-soft:   #BDB2EC;
  --ts-accent-pale:   #272338;
  --ts-accent-deep:   #C3B9F2;
  /* 与上面 prefers-color-scheme 那套逐字一致，见那里的注释。 */
  --ts-on-accent:     #14131A;
  --ts-invert-bg:     #0F0E14;

  --ts-logo-plate:    #F2F0EC;

  /* 字标：与上面 prefers-color-scheme 那套保持一致，见那里的注释。 */
  /* 单色字标的深色版（2026-09-03）。= 深色的 --ts-ink。
     🛑 这里不能给黑：底色是 #14131A。 */
  --ts-logo-t:    #F2F0EC;
  --ts-logo-o:    #F2F0EC;
  --ts-logo-p:    #F2F0EC;
  --ts-logo-3:    #F2F0EC;
  /* 琥珀点已关掉（2026-09-03，主管要求）。transparent 的理由见浅色那一处。 */
  --ts-logo-dot:  transparent;
  /* 跟着翻成近白：留着原来的 #0B0A0F 会把近白的字描成空心字。 */
  --ts-logo-edge: #F2F0EC;

  /* 深色底上需要提亮，否则金/铜会糊成褐色 */
  --ts-rank-1:      #E8BE55;
  --ts-rank-2:      #B9C0CC;
  --ts-rank-3:      #CE7F42;
  --ts-rank-1-pale: rgba(232, 190, 85, .14);
  --ts-rank-2-pale: rgba(185, 192, 204, .14);
  --ts-rank-3-pale: rgba(206, 127, 66, .14);
  --ts-rank-1-tint: #2A2419;
  --ts-rank-2-tint: #23242C;
  --ts-rank-3-tint: #2B2118;
  --ts-rank-1-edge: #5A4A22;
  --ts-rank-2-edge: #454B57;
  --ts-rank-3-edge: #5B4029;
  --ts-rank-1-glow: rgba(232, 190, 85, .10);
  --ts-rank-2-glow: rgba(185, 192, 204, .10);
  --ts-rank-3-glow: rgba(206, 127, 66, .10);

  --ts-positive:      #7FA492;
  --ts-positive-pale: #1E2A24;
  --ts-info:          #7D95FA;
  --ts-info-pale:     #1C2036;

  --ts-glass:         rgba(28, 27, 36, .78);
  --ts-shadow-soft:   0 2px 32px rgba(0, 0, 0, .34);
  --ts-shadow-card:   0 8px 48px rgba(0, 0, 0, .42);
  --ts-shadow-hover:  0 16px 64px rgba(0, 0, 0, .52);
}

/* 深色下需要单独处理的少数写死色值。
 *
 * 🛑 **组件级的深色规则必须两套选择器都写**：`@media (prefers-color-scheme: dark)`
 *    （系统深色、用户没点过切换按钮）和 `:root[data-ts-theme="dark"]`（手动切换）。
 *    `header.php` 里写着「未存过偏好时不写 data-ts-theme」，所以**第一次来的访客走的是
 *    前一条**，只写后一条等于对他们没生效。
 *    2026-08-25 之前这里就只有后一条，代价是顶部披露条和页脚 CTA 在系统深色下整块看不见
 *    （1.03:1，见 HANDOFF 二之五.2）—— 那两条现在改走 --ts-invert-* token 了，
 *    剩下这两组仍然是组件规则，所以两套都写。
 *    ⚠️ 测试里 `ts-dark-scope-twins` 那节双向核对这件事：任何一套里有、另一套里没有
 *       （或者两边声明不一致）都当场红。**能用 token 表达的就别写在这里。** */
@media (prefers-color-scheme: dark) {
  :root:not([data-ts-theme="light"]) .ts-score-panel__img,
  :root:not([data-ts-theme="light"]) .ts-ccol__img,
  :root:not([data-ts-theme="light"]) .ts-inline-card__img,
  :root:not([data-ts-theme="light"]) .ts-featured__img,
  :root:not([data-ts-theme="light"]) .ts-article__hero {
    background: linear-gradient(135deg, #2A2540 0%, #221F2E 100%);
  }
  :root:not([data-ts-theme="light"]) .ts-btn--primary,
  :root:not([data-ts-theme="light"]) .ts-rk__cta,
  :root:not([data-ts-theme="light"]) .ts-bcard__cta,
  :root:not([data-ts-theme="light"]) .ts-product__cta,
  :root:not([data-ts-theme="light"]) .ts-footer__form button,
  :root:not([data-ts-theme="light"]) .ts-cform .submit,
  :root:not([data-ts-theme="light"]) .ts-cform input[type="submit"] {
    background: var(--ts-accent);
    color: var(--ts-on-accent);
  }
}
:root[data-ts-theme="dark"] .ts-score-panel__img,
:root[data-ts-theme="dark"] .ts-ccol__img,
:root[data-ts-theme="dark"] .ts-inline-card__img,
:root[data-ts-theme="dark"] .ts-featured__img,
:root[data-ts-theme="dark"] .ts-article__hero {
  background: linear-gradient(135deg, #2A2540 0%, #221F2E 100%);
}
:root[data-ts-theme="dark"] .ts-btn--primary,
:root[data-ts-theme="dark"] .ts-rk__cta,
:root[data-ts-theme="dark"] .ts-bcard__cta,
:root[data-ts-theme="dark"] .ts-product__cta,
:root[data-ts-theme="dark"] .ts-footer__form button,
:root[data-ts-theme="dark"] .ts-cform .submit,
:root[data-ts-theme="dark"] .ts-cform input[type="submit"] {
  background: var(--ts-accent);
  color: var(--ts-on-accent);
}

/* 品牌 Logo 底板（5 处 Logo 槽位共用）
 * 底板颜色走 --ts-logo-plate，三套调色板各自定义，所以「显式深色」和
 * 「跟随系统深色」两种状态都覆盖得到，不用写 :root[data-ts-theme] 组件规则。
 * 用 box-shadow 的扩散做底板而不是 padding：不占布局，浅色模式下 token 是
 * transparent，等于这条规则不存在，切换主题时 Logo 零位移。 */
.ts-rk__logo,
.ts-brand-logo,
.ts-bcard__logo,
.ts-ccol__logo {
  background: var(--ts-logo-plate);
  box-shadow: 0 0 0 5px var(--ts-logo-plate);
  border-radius: var(--ts-r-sm);
}

/* 方形徽标型 Logo 放大（2026-08-24，用户指出 Sealy 明显比别家小一圈）。
   类由 topsites_get_logo_shape_class() 按宽高比发出（< 2.0 才发），四个模板共用。

   ⚠️ **为什么只能加类、不能直接调那几个固定高度**：框是「固定高度 + max-width + contain」，
   九个 Logo 里八个横向字标被 132px 的**宽度**卡住，只有 Sealy（比例 1.17）被**高度**卡住。
   把共享高度从 32 提到 50，Sealy 是够了，但 TEMPUR（2.81）会翻成被宽度卡住，
   面积从 2880 冲到 6204 变成全场最大 —— 按面积量过，不是估的。

   ⚠️ 这几个值是按「画出来的面积向其余八个的中位数靠」定的，不是随手乘 1.5。
   榜单卡实测：32px 时 Sealy 38×32 = 1200 px²，其余 1855～4219、中位数 2880；
   提到 46px 后是 54×46 = 2484，落进区间。**改高度前先量面积，别只看高度数字。** */
.ts-rk__logo.ts-logo--badge { height: 46px; }
.ts-brand-logo.ts-logo--badge,
.ts-bcard__logo.ts-logo--badge { height: 62px; }
.ts-ccol__logo.ts-logo--badge { height: 56px; }

/* ═══════════════════════════════════════════
   2. 基础
   ═══════════════════════════════════════════ */
.ts-body {
  margin: 0;
  font-family: var(--ts-sans);
  font-size: var(--ts-fs-base);
  font-weight: 300;
  line-height: 1.68;
  background: var(--ts-bg);
  color: var(--ts-ink);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* 基础重置层：一律用 :where() 包裹，优先级恒为 (0,0,N)，永远不会盖住组件规则。
 *
 * 踩过的坑：早期写成 .ts-body a { color: inherit }，优先级 (0,1,1) 高于
 * .ts-rk__cta { color: ... } 的 (0,1,0)，结果深色按钮上的浅色文字被改回继承的
 * 深色，肉眼看就是「按钮没有字」。同理 .ts-body p { margin: 0 } 会压掉组件
 * 自己的 margin-top / margin-bottom，导致间距塌陷。
 *
 * 规矩：本层只允许出现 :where(...) 选择器。新增重置请照此办理。 */
:where(.ts-body) *,
:where(.ts-body) *::before,
:where(.ts-body) *::after { box-sizing: border-box; }
:where(.ts-body) img { max-width: 100%; height: auto; display: block; }
:where(.ts-body) h1,
:where(.ts-body) h2,
:where(.ts-body) h3,
:where(.ts-body) h4 { margin: 0; font-weight: 400; }
:where(.ts-body) p { margin: 0; }
:where(.ts-body) ul { margin: 0; padding: 0; list-style: none; }
:where(.ts-body) a { color: inherit; text-decoration: none; }
:where(.ts-body) button { font-family: inherit; }

/* 锚点跳转：平滑滚动 + 抵消吸顶 header（--ts-header-h），否则标题会被压在导航栏底下 */
html:has(.ts-body) { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html:has(.ts-body) { scroll-behavior: auto; }
}

.ts-wrap { max-width: var(--ts-wrap); margin: 0 auto; padding: 0 var(--ts-gutter); }
.ts-section { padding: var(--ts-section) 0; scroll-margin-top: calc(var(--ts-header-h) + 16px); }
.ts-section--tint {
  background: var(--ts-surface);
  border-top: .5px solid var(--ts-border);
  border-bottom: .5px solid var(--ts-border);
}

.ts-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Skip link —— 平时移出视口，得到焦点才落下来。
   z-index 必须高过披露条（120）和它的浮层（130），否则弹出来就被盖住。
   不做 transition：动了就得再进 prefers-reduced-motion 里关一次，不值当。 */
.ts-skip {
  position: fixed;
  top: -100px;
  left: 16px;
  z-index: 200;
  background: var(--ts-ink);
  color: var(--ts-surface);
  font-family: var(--ts-sans);
  font-size: 15px;
  font-weight: 400;
  padding: 12px 22px;
  border-radius: var(--ts-r-md);
  text-decoration: none;
}
.ts-skip:focus { top: 16px; }

/* 跳过来之后焦点要真的落在 main 上，否则下一次 Tab 又从页头开始，
   skip link 等于没做——tabindex="-1" 就是为这个。
   ⚠️ 这条 outline:none 与表单字段那几处不是一回事：main 不是交互控件，
   它只是个程序化焦点落点，给整个内容区描一圈边没有意义。别一起删。 */
.ts-main:focus { outline: none; }

/* ═══════════════════════════════════════════
   3. 排版组件
   ═══════════════════════════════════════════ */
.ts-eyebrow {
  display: block;
  font-family: var(--ts-mono);
  font-size: 11.5px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin-bottom: 18px;
}
.ts-h2 {
  font-family: var(--ts-serif);
  font-size: clamp(38px, 4.2vw, 58px);
  line-height: 1.1;
  letter-spacing: -1px;
}
.ts-h2 em { font-style: italic; color: var(--ts-accent-soft); }
.ts-sub {
  font-size: 17px;
  color: var(--ts-muted);
  max-width: 560px;
  margin-top: 16px;
  line-height: 1.65;
}
.ts-sec-head { margin-bottom: 52px; }

/* ═══════════════════════════════════════════
   4. 按钮
   ═══════════════════════════════════════════ */
.ts-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 500;
  padding: 14px 30px;
  border-radius: var(--ts-r-pill);
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: all var(--ts-ease);
}
.ts-btn--primary { color: var(--ts-surface); background: var(--ts-ink); }
.ts-btn--primary:hover {
  background: var(--ts-ink-soft);
  transform: translateY(-1px);
  box-shadow: 0 8px 32px rgba(0, 0, 0, .18);
}
.ts-btn--ghost {
  color: var(--ts-ink);
  background: transparent;
  border-color: var(--ts-border-strong);
}
.ts-btn--ghost:hover { background: var(--ts-surface); border-color: var(--ts-muted); }
.ts-btn--accent { color: var(--ts-on-accent); background: var(--ts-accent); }
.ts-btn--accent:hover { background: var(--ts-accent-deep); transform: translateY(-1px); }

/* ═══════════════════════════════════════════
   5. 顶部披露条
   ═══════════════════════════════════════════ */
.ts-disclosure {
  background: var(--ts-invert-bg);
  color: var(--ts-invert-muted);
  font-family: var(--ts-mono);
  font-size: 11.5px;
  letter-spacing: .6px;
  text-align: center;
  padding: 9px 20px;
  position: relative;
  z-index: 120;
}
/* position: relative 是给下面那个浮层当定位父级用的（2026-08-25 从 .ts-disclosure__wrap
   挪过来）。⚠️ 别删，删了浮层会退回去锚在 __wrap 上，右溢出立刻回来 —— 理由见 __tip 那段。 */
.ts-disclosure__inner {
  position: relative;
  max-width: var(--ts-wrap);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}
.ts-disclosure__toggle {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--ts-invert-ink);
  border-bottom: 1px dotted var(--ts-invert-line);
  cursor: help;
}
.ts-disclosure__tip {
  position: absolute;
  top: calc(100% + 10px);
  /* ⚠️ 2026-08-25：改之前是「以 .ts-disclosure__wrap 为定位父级 + left:50% + translateX(-50%)」，
     也就是**居中对齐到那个 Advertiser Disclosure 链接**。而那个链接位于居中排布的一行的
     最右端，1280 视口下中心在 x=1060 —— 460 宽的浮层往两边一摊，右边缘到 1290，
     而可用宽度只有 1265。**实测右溢出 25px，整页因此多出一条水平滚动条**
     （`visibility: hidden` 的元素照样算进滚动区域，所以浮层没打开时也在溢出）。

     现在改成**贴住 .ts-disclosure__inner 的右边缘**（定位父级挪到了 __inner）。
     __inner 是那条 1180 的内容带，它的右边缘永远在视口内，所以任何宽度都溢不出去；
     而链接本身也在这条带子的右段，浮层照样罩得住它。

     ⚠️ **不要改回按链接居中。** 那行文案是后台可编辑的，文案一长一短链接就左右移动，
     任何写死的偏移量都会在某个文案长度上再次溢出 —— 贴内容带右边缘跟文案长度无关。

     ⚠️ **宽度从 `calc(100vw - 40px)` 换成 `100%`，这是第二个坑，不是顺手改的**：
     `100vw` **含滚动条**（实测 vw 按 1280 算，而可用宽度 1265），窄屏上光靠 vw 算宽度
     本身就能溢出一个滚动条的量。`100%` 是 __inner 的宽度，天然不含滚动条。 */
  left: auto;
  right: 0;
  transform: translateY(-6px);
  width: min(460px, 100%);
  /* 老问题，2026-08-24 量出来的：375×812 下这个浮层高 710px，底边落在 809，
     离视口底只剩 3px。它是 absolute + overflow:visible，一旦超出就是直接跑到
     屏幕外，**没有滚动条也滚不动**，等于后半段披露文案在小屏上读不到。
     （改文案前后都是这个数 —— 709.7 vs 711.3，不是文案改出来的。）
     320px 宽的小屏或系统放大字号就会真的超。这里给上限 + 内部滚动兜底：
     桌面下 510 < 上限，规则不生效；只有小屏才会真的滚。 */
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--ts-surface);
  color: var(--ts-ink);
  font-family: var(--ts-sans);
  font-size: 14px;
  letter-spacing: 0;
  line-height: 1.6;
  text-align: left;
  padding: 18px 20px;
  border-radius: var(--ts-r-md);
  border: .5px solid var(--ts-border);
  box-shadow: var(--ts-shadow-card);
  opacity: 0;
  visibility: hidden;
  transition: all var(--ts-ease);
  z-index: 130;
}
/* ⚠️ 这里**故意不写 position: relative**（2026-08-25 移除）。一写它就重新成为浮层的
   定位父级，浮层跟着回到「居中于这个链接」，右溢出立刻复发。见 .ts-disclosure__tip 那段。 */
.ts-disclosure__wrap { display: inline-block; }
/* 披露文案 2026-08-24 从一个 <p> 拆成三段（见 inc/data/options.php）。
   重置层把 p 的 margin 清成 0（约定三），单段时看不出来，三段就糊成一堵墙。
   这里补段距 —— 用 + 相邻兄弟，首段不留上边距。 */
.ts-disclosure__tip p + p { margin-top: 12px; }

.ts-disclosure__wrap:hover .ts-disclosure__tip,
.ts-disclosure__wrap:focus-within .ts-disclosure__tip {
  opacity: 1;
  visibility: visible;
  /* ⚠️ 这里的 transform 必须跟隐藏态**同构**，只差 translateY —— 隐藏态那条
     2026-08-25 去掉了 translateX(-50%)，这条漏改的话浮层一显示就往左跳半个身位，
     而且只有 hover 的瞬间才看得见。 */
  transform: translateY(0);
}

/* ═══════════════════════════════════════════
   6. 导航
   ═══════════════════════════════════════════ */
.ts-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--ts-glass);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border-bottom: .5px solid var(--ts-border);
}
.ts-header__inner {
  max-width: var(--ts-wrap);
  margin: 0 auto;
  padding: 0 var(--ts-gutter);
  height: var(--ts-header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.ts-logo {
  display: flex;
  align-items: center;
  gap: 9px;
  text-decoration: none;
  font-family: var(--ts-serif);
  font-size: 34.5px;
  letter-spacing: -.3px;
  white-space: nowrap;
}
/* 自有标志。三层横杠的 SVG 走 currentColor + var(--ts-accent)，深浅色模式自动跟随，
   所以不需要 --ts-logo-plate 那套给外来位图准备的底板。 */
.ts-logo__mark {
  width: 39px; height: 39px;
  flex: none;
  color: var(--ts-ink);
  /* 视觉重心：三条横杠的光学中心比几何中心略高，压 1px 让它和字基线对齐 */
  margin-top: 1px;
}
/* 字标改成两行：「Top3」逐字上色在上，「MATTRESS」黑色小字在下。
   ⚠️ 整块用 flex 列，不要回到 inline 排版——inline 时 MATTRESS 的
   letter-spacing 会在末尾多留一个字距的空，两行右边缘就对不齐了。 */
.ts-logo__word {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1;
  white-space: nowrap;
}
.ts-logo__name {
  font-family: var(--ts-sans);
  font-weight: 800;
  font-size: 1.18em;
  letter-spacing: .045em;
  /* 描边画在填充**下面**，否则 1px 的边会从内侧啃掉字形，800 字重也会显瘦。
     paint-order 不支持时退化成「边压在字上」，字还在，只是略瘦一点。 */
  paint-order: stroke fill;
  -webkit-text-stroke: 1.4px var(--ts-logo-edge);
}
.ts-logo__l--t     { color: var(--ts-logo-t); }
/* isolation 不是可有可无的：下面那个点用 z-index:-1 垫在字形底下，没有这一行，
   它会去最近的祖先层叠上下文里排位——现在恰好是 .ts-header（sticky + backdrop-filter），
   点还能画在它的背景之上；哪天 header 不再创建上下文，点就会掉到页面背景后面消失。
   自己 isolate 一层，这件事就跟祖先无关了。 */
.ts-logo__l--o     { color: var(--ts-logo-o); position: relative; isolation: isolate; }
.ts-logo__l--p     { color: var(--ts-logo-p); }
.ts-logo__l--three { color: var(--ts-logo-3); }
/* o 字腔里那个琥珀点。字母本身只有描边和填充，字腔是透空的，
   所以在它下面垫一个圆就能透出来。
   ⚠️ 尺寸和位置全用 em，跟着字号缩放；但它们是照 DM Sans 800 的字腔量的，
      换字体必须重新对一遍。z-index 为负是让圆落到字形下面——
      父级不能建立新的层叠上下文，否则圆会掉到页面背景后面看不见。 */
.ts-logo__l--o::before {
  content: '';
  position: absolute;
  z-index: -1;
  /* .755em 是量出来的，不是估的：canvas 的 actualBoundingBox 显示 DM Sans 800 的
     小写 o，墨迹中心落在 span content box 顶部下方 0.755em。按「看起来居中」
     拍一个 .5em，点会整个跑到字母上边缘外面——而 DOM 里一切正常，量不出问题。 */
  top: .755em;
  left: 50%;
  width: .34em;
  height: .34em;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--ts-logo-dot);
}
/* MATTRESS 单数——与域名 top3mattress.com 对齐。它是限定词不是主体，
   所以字号压到「Top3」的三分之一、拉开字距，读者记住的仍是「Top3」。
   ⚠️ 末尾那个 margin-right 是抵消 letter-spacing 在最后一个字母后面
      多加的一份字距，不补的话这一行的视觉中心会偏左。 */
.ts-logo__sub {
  font-family: var(--ts-sans);
  font-size: .40em;
  font-weight: 600;
  /* 字距是量出来的，不是拍的：两行要等宽。.34em 时 MATTRESS 是 71.6px 而
     Top3 只有 65.9px，下面那行反而更宽，整个字标看着头轻脚重。 */
  letter-spacing: .347em;
  text-transform: uppercase;
  color: var(--ts-ink);
  margin-top: .28em;
  margin-right: -.347em;
}
.ts-logo__dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--ts-accent);
  flex: none;
}
.ts-logo__img { max-height: 26px; width: auto; }

.ts-nav { display: flex; align-items: center; gap: 30px; }
.ts-nav__item { position: relative; }
.ts-nav__link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 15px;
  color: var(--ts-muted);
  text-decoration: none;
  transition: color var(--ts-ease);
  white-space: nowrap;
}
.ts-nav__link:hover,
.ts-nav__link.is-active { color: var(--ts-ink); }
.ts-nav__chevron {
  width: 6px; height: 6px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--ts-ease);
}
.ts-nav__item:hover .ts-nav__chevron { transform: rotate(225deg) translateY(-2px); }

.ts-nav__menu {
  position: absolute;
  top: calc(100% + 14px);
  left: 50%;
  transform: translateX(-50%) translateY(-6px);
  /* ⚠️ 绝对定位元素是 shrink-to-fit，可用宽度取的是**包含块**的宽度 ——
     这里包含块是 .ts-nav__item，只有 "Reviews" 那几个字宽。所以长条目
     （Sealy Posturepedic Exquisite Rosewood）会被压到 min-width 210px 上换行，
     一条占两行、左边缘参差。
     width: max-content 让面板按**最长那条**一次定宽，脱开包含块的限制；
     配合下面 a 的 nowrap，九个品牌名各占一行。
     max-width 是兜底：.ts-nav 在 860px 以下整个 display:none（走抽屉），
     所以只有 861px～ 这一小段窄桌面需要防顶出视口。 */
  width: max-content;
  min-width: 210px;
  max-width: calc(100vw - 32px);
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md);
  box-shadow: var(--ts-shadow-card);
  padding: 10px;
  opacity: 0;
  visibility: hidden;
  transition: all var(--ts-ease);
}
.ts-nav__item:hover .ts-nav__menu,
.ts-nav__item:focus-within .ts-nav__menu {
  opacity: 1; visibility: visible;
  transform: translateX(-50%) translateY(0);
}
.ts-nav__menu::before {
  content: ''; position: absolute; top: -14px; left: 0; right: 0; height: 14px;
}
.ts-nav__menu a {
  display: block;
  white-space: nowrap;
  padding: 9px 14px;
  font-size: 15.5px;
  color: var(--ts-ink-soft);
  text-decoration: none;
  border-radius: var(--ts-r-sm);
  transition: all var(--ts-ease);
}
.ts-nav__menu a:hover { background: var(--ts-accent-pale); color: var(--ts-accent); }

.ts-header__cta {
  font-size: 15px;
  font-weight: 500;
  background: var(--ts-bg);
  border: .5px solid var(--ts-border-strong);
  padding: 9px 20px;
  border-radius: var(--ts-r-pill);
  text-decoration: none;
  transition: all var(--ts-ease);
  white-space: nowrap;
}
.ts-header__cta:hover {
  background: var(--ts-ink); color: var(--ts-surface); border-color: var(--ts-ink);
}

/* 开灯 / 关灯 切换 */
.ts-theme-toggle {
  width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--ts-bg);
  border: .5px solid var(--ts-border-strong);
  border-radius: 50%;
  cursor: pointer;
  color: var(--ts-ink);
  transition: all var(--ts-ease);
  flex: none;
  padding: 0;
}
.ts-theme-toggle:hover { border-color: var(--ts-accent-soft); color: var(--ts-accent); transform: rotate(18deg); }
.ts-theme-toggle svg { width: 17px; height: 17px; display: block; }
.ts-theme-toggle .ts-icon-moon { display: none; }
:root[data-ts-theme="dark"] .ts-theme-toggle .ts-icon-sun { display: none; }
:root[data-ts-theme="dark"] .ts-theme-toggle .ts-icon-moon { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-ts-theme="light"]) .ts-theme-toggle .ts-icon-sun { display: none; }
  :root:not([data-ts-theme="light"]) .ts-theme-toggle .ts-icon-moon { display: block; }
}

.ts-burger {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 40px; height: 40px;
  background: none;
  border: .5px solid var(--ts-border-strong);
  border-radius: var(--ts-r-pill);
  cursor: pointer;
  padding: 0 11px;
}
.ts-burger span {
  display: block; height: 1.5px; background: var(--ts-ink); border-radius: 2px;
  transition: all var(--ts-ease);
}
.ts-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.ts-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.ts-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.ts-mobile-nav {
  display: none;
  border-top: .5px solid var(--ts-border);
  background: var(--ts-surface);
  padding: 14px var(--ts-gutter) 24px;
}
.ts-mobile-nav[data-open="true"] { display: block; }
.ts-mobile-nav a,
.ts-mobile-nav summary {
  display: block;
  padding: 13px 0;
  font-size: 17px;
  color: var(--ts-ink-soft);
  text-decoration: none;
  border-bottom: .5px solid var(--ts-border);
  cursor: pointer;
}
.ts-mobile-nav details a { padding-left: 18px; font-size: 16px; color: var(--ts-muted); }
.ts-mobile-nav summary { list-style: none; }
.ts-mobile-nav summary::-webkit-details-marker { display: none; }
/* 父项那一行是**两个点击区**（2026-08-28）：左边文字进父页面，右边 +/− 开合子菜单。
   ⚠️ 之前父项只是纯文本，移动端到不了父页面，而桌面端是能点的 —— 两端行为不一致。 */
.ts-mobile-nav summary { display: flex; align-items: center; }
/* summary 自己已经有 padding 和分隔线，里面的链接不能再叠一层，
   否则那一行会变成两条线、上下间距翻倍。 */
.ts-mobile-nav summary > a {
  flex: 1;
  padding: 0;
  border-bottom: 0;
  font-size: inherit;
  color: inherit;
}
/* ⚠️ 左边这 18px 内边距不是留白，是**把 +/− 的点击区撑开**。
   ::after 的盒子属于 summary，点它才是开合；不撑开的话可点区域只有一个字符宽（约 10px），
   在手机上基本点不中，用户会一直误点到左边的链接、跳走。 */
.ts-mobile-nav summary::after {
  content: '+';
  padding: 0 2px 0 18px;
  font-family: var(--ts-mono);
  color: var(--ts-muted);
}
.ts-mobile-nav details[open] summary::after { content: '−'; }

/* ═══════════════════════════════════════════
   7. Hero
   ═══════════════════════════════════════════ */
/* 顶部 40 < 底部 48：标题上方比下方紧一点，整屏视觉上被往上提。
   ⚠️ 原来是 56，那是**标题上方还有个小标签**时定的；标签 2026-08-24 删掉后
   再留 56px，标题和 header 之间会空出一块没人认领的白。 */
.ts-hero { padding: 40px 0 48px; position: relative; overflow: hidden; }
.ts-hero::after {
  content: '';
  position: absolute;
  top: -180px; right: -160px;
  width: 620px; height: 620px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--ts-accent-pale) 0%, transparent 68%);
  opacity: .85;
  pointer-events: none;
}
.ts-hero__inner { position: relative; z-index: 2; }
.ts-hero__copy { min-width: 0; max-width: 620px; }

/* ── Hero 背景图 ────────────────────────────────────────────
   后台配了 Hero 图片就整幅铺底，文字压在上面（用户 2026-08-21，
   此前是左文右图两栏）。没配图时以下规则一条都不生效，hero 保持纯文字。

   图里主体在右侧、左边留白，所以 background-position 靠右：窄屏被裁时
   优先保住人和宠物，让左边的空墙先被切掉——空墙切掉不心疼，人切一半就废了。 */
.ts-hero--bg {
  background-image:
    linear-gradient(rgba(var(--ts-bg-rgb), var(--ts-hero-dim, 0)), rgba(var(--ts-bg-rgb), var(--ts-hero-dim, 0))),
    var(--ts-hero-bg);
  background-size: cover;
  background-position: 78% center;
  background-repeat: no-repeat;
}

/* 文字侧遮罩。
   ⚠️ 这层不能省，即使当前这张图的左半边已经够干净（实测繁忙度 0.010–0.016、
      深色文字对比度 7.2–9.5:1）。图是外部产物，后台随时能换一张；遮罩是我们
      能控制的那道保险，换图之后文字仍然读得清，靠的是它不是运气。
   收尾用 rgba(...,0) 而不是 transparent：Safari 对 transparent 插值会走灰，
   渐变中段会出脏边（--ts-surface-rgb 那条注释记的是同一个坑）。 */
.ts-hero--bg::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    /* 横向：护住标题 / 导语 / 按钮那一列 */
    linear-gradient(
      to right,
      rgba(var(--ts-bg-rgb), .94) 0%,
      rgba(var(--ts-bg-rgb), .88) 30%,
      rgba(var(--ts-bg-rgb), .55) 52%,
      rgba(var(--ts-bg-rgb), .12) 72%,
      rgba(var(--ts-bg-rgb), 0) 88%
    ),
    /* 纵向：护住底部那条统计数字。
       ⚠️ 这层是实测逼出来的，不是保险起见加的：统计条横跨到视口 88%，
          远超横向遮罩的衰减终点，深色数字直接压在图的暗部（合成色 rgb(39,20,3)），
          实测对比度 1.02:1 —— 等于看不见。
          只吃底部 30%：统计条落在 hero 的 76%–100%，而狗在纵向 33%–58%，
          够得着统计条，够不着狗。 */
    linear-gradient(
      to top,
      rgba(var(--ts-bg-rgb), .95) 0%,
      rgba(var(--ts-bg-rgb), .90) 14%,
      rgba(var(--ts-bg-rgb), .40) 23%,
      rgba(var(--ts-bg-rgb), 0) 30%
    );
  pointer-events: none;
}

/* 有背景图时关掉那圈紫色光晕：它本来是给纯文字首屏补视觉重量的，
   压在照片上只会脏。 */
.ts-hero--bg::after { display: none; }

/* 导语和统计标签换用 --ts-ink-soft，只在有背景图时。
   历史：这两处平时用 --ts-muted，当时那个值（#8A8680）压在纯色底上只有 3.07:1，
   叠一张照片实测掉到 2.18:1，那就真读不了了。
   ⚠️ 2026-08-25 把 --ts-muted 整体压暗到 #6B675F（纯色底最低 4.72）之后，
      **这条规则照样不许撤**：上面那两个数是「纯色 → 照片」掉了 0.89 个比值，
      照片的亮度是逐像素变的，AA 那套公式只对单一底色成立，量出来的最差点才算数。
      要撤得先在真实首屏上按像素重量一遍；没量过就留着 ink-soft。有断言守着。 */
.ts-hero--bg .ts-hero__lede,
.ts-hero--bg .ts-stat span { color: var(--ts-ink-soft); }

/* 深色模式：照片本身是一间洒满晨光的房间，亮度 0.26–0.58，直接压在 #14131A 上
   会像开了盏灯。--ts-hero-dim 往上面那层 background-image 的纯色渐变里灌不透明度，
   把整张图压下去一档，再让 ::before 的遮罩接着做文字侧。
   两套选择器都要写：系统深色走 @media，手动切换走 data-ts-theme。 */
@media (prefers-color-scheme: dark) {
  :root:not([data-ts-theme="light"]) .ts-hero--bg { --ts-hero-dim: .58; }
}
:root[data-ts-theme="dark"] .ts-hero--bg { --ts-hero-dim: .58; }
.ts-hero__badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--ts-mono);
  font-size: 12.5px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ts-accent);
  /* ⚠️ 这是**写死的品牌紫**（= 浅色 --ts-accent 的 rgb 三元组），全站另有两处
     （.ts-anchors 的外发光、.ts-score-panel__cta 的投影）。**改主色必须连它们一起改** ——
     2026-08-26 压暗主色时按 hex 搜差点漏掉这三处，因为它们写的是 rgba 不是 #hex。
     没收成 --ts-accent-rgb 是因为深色调色板的 accent 是另一个色（#A79BE0），
     收成 token 会让这三处在深色下跟着变紫，那是另一个决定，不在这次范围内。
     守卫：tests 的 ts-accent-literal-rgba 那节，扫出紫系 rgba 就比对当前主色。 */
  border: .5px solid rgba(108, 98, 161, .28);
  padding: 7px 15px;
  border-radius: var(--ts-r-pill);
  margin-bottom: 18px;
}
.ts-hero__badge::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--ts-accent);
}
.ts-hero__title {
  font-family: var(--ts-serif);
  /* 2026-08-25 收了一档（原 clamp(42px, 5vw, 70px)，1280 下 64px，用户嫌大）。
     现在 1280 下约 52px。⚠️ 下限跟着从 42 降到 36 是**有理由的**，不是顺手：
     标题按逗号断成两半后，下半句「not just the verdict」在 375 宽下
     42px 排不下会再折一次（总共三行），36px 正好一行。改上限时回来核一眼下限。 */
  font-size: clamp(36px, 4.1vw, 56px);
  line-height: 1.05;
  /* ⚠️ 字距用 em 不用 px。原来写死 -1.6px，在 64px 上是 -.025em，
     字号一收到 52px 就变成 -.031em —— **字越小反而挤得越紧**，正好反了。 */
  letter-spacing: -.025em;
  max-width: 820px;
  margin-bottom: 18px;
}
/* 标题按第一个逗号分成的两半（topsites_format_hero_title()）。
   ⚠️ **靠 inline-block 断行，没有写死 <br>，也没有媒体查询。** 原理：
   inline-block 是个整体，放不下时整块挪到下一行 —— 所以整句排不下时，
   断点**只可能落在这两半之间**，也就是逗号后面。
   好处是它自适应：标题短到一行放得下就自然不断；下半句在窄屏上放不下时，
   它自己还能在内部再折一次，不会溢出。
   ⚠️ **别改成 `display: block`**：那样任何宽度都强制两行，标题改短了也一样。
   ⚠️ **两个 span 之间那个空格必须留着**（在 PHP 里拼的），它就是句中那个词间距；
      删了两半会贴在一起。同理别在 HTML 里给它们之间加换行以外的空白。 */
.ts-hero__title-part { display: inline-block; }
.ts-hero__title em { font-style: italic; color: var(--ts-accent-soft); }
.ts-hero__lede {
  font-size: 18px;
  color: var(--ts-muted);
  max-width: 520px;
  line-height: 1.6;
  margin-bottom: 28px;
}
.ts-hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }
.ts-hero__stats {
  /* 两栏布局下横跨整行，压在图文下方 */
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 20px;
  border-top: .5px solid var(--ts-border);
  margin-top: 44px;
  padding-top: 28px;
}
.ts-stat b {
  display: block;
  font-family: var(--ts-serif);
  font-size: 42px;
  letter-spacing: -1px;
  font-weight: 400;
  line-height: 1;
  margin-bottom: 8px;
}
/* 「Aug 2026」比纯数字长，缩一档才不会挤到相邻列 */
.ts-stat--date b { font-size: 30px; letter-spacing: -.5px; }
.ts-stat span {
  font-family: var(--ts-mono);
  font-size: 11.5px;
  letter-spacing: 1.2px;
  color: var(--ts-muted);
  text-transform: uppercase;
}

/* 首屏文字入场：从下往上淡入（用户 2026-08-25 要的，见 HANDOFF 二之四.7）。
   标题 → 说明 → 按钮 → 四个数字，依次错开。

   ⚠️ **只动 opacity 和 transform。** 这两个走合成层，不引起重排，首屏不会抖。
      改成动 margin-top / top 那类属性就是每帧重排，还会计进 CLS。
   ⚠️ **fill-mode 必须是 both**：延迟期间保持 from（否则会先完整显示一下再跳回去动），
      结束后停在 to（万一动画被中断，元素停在**可见**状态，不会把首屏内容留成透明）。
   ⚠️ **H1 的 delay 必须是最短的那个（40ms）。** 它是这一页的 LCP 元素，
      而 opacity 一离开 0 才算「画出来了」——delay 有多长，LCP 就晚多少。
      **别为了「更有层次」把 H1 往后排**，那是拿真实性能换观感。
      时长本身不计入 LCP（只要开始变亮就算），所以 .62s 是安全的。 */
@keyframes ts-rise-in {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

.ts-hero__title,
.ts-hero__lede,
.ts-hero__actions,
.ts-hero__stats .ts-stat {
  /* 曲线跟 --ts-ease 里那条是同一根。--ts-ease 是 transition 的简写（含时长），
     animation 这里只能要曲线本身，所以写开而不是复用那个变量。 */
  animation: ts-rise-in .62s cubic-bezier(.16, 1, .3, 1) both;
}

.ts-hero__title   { animation-delay: 40ms; }
.ts-hero__lede    { animation-delay: 140ms; }
.ts-hero__actions { animation-delay: 240ms; }
.ts-hero__stats .ts-stat:nth-child(1) { animation-delay: 330ms; }
.ts-hero__stats .ts-stat:nth-child(2) { animation-delay: 390ms; }
.ts-hero__stats .ts-stat:nth-child(3) { animation-delay: 450ms; }
.ts-hero__stats .ts-stat:nth-child(4) { animation-delay: 510ms; }

/* 关掉动效的人直接看最终状态。
   ⚠️ 写 `animation: none` 而不是把时长调成 0s —— none 连 delay 和 fill 一起清掉，
      不用担心「0s 但还有 500ms 延迟」这种把内容按住不放的组合。 */
@media (prefers-reduced-motion: reduce) {
  .ts-hero__title,
  .ts-hero__lede,
  .ts-hero__actions,
  .ts-hero__stats .ts-stat {
    animation: none;
  }
}

/* ═══════════════════════════════════════════
   8. 分类快捷入口
   ═══════════════════════════════════════════ */
/* 6 张卡固定一排。用 repeat(6,1fr) 而不是 auto-fit，
   auto-fit 会按最小宽度回绕，导致最后一张单独掉到第二行。 */
/* ⚠️ 列数必须是**分类数的因数**，否则末行不满会留半排空位 —— 1fr 栅格里那种
   残行是左对齐的，右边空一截，看着像坏了。
   2026-08-21 删掉 Organic & Latex 和 RV 之后是 8 个分类，所以列数从 5 改成 4
   （5 列会排成 5 + 3）。以后再加减分类，这三处断点的列数都要跟着重算：
   8 的因数是 4 和 2，桌面 4×2、窄屏 2×4。 */
.ts-finder {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
}
.ts-fcard {
  background: var(--ts-bg);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md);
  padding: 28px 16px 24px;
  text-decoration: none;
  transition: all var(--ts-ease);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.ts-fcard:hover {
  background: var(--ts-surface);
  border-color: var(--ts-accent-soft);
  transform: translateY(-3px);
  box-shadow: var(--ts-shadow-soft);
}
.ts-fcard__icon {
  width: 52px; height: 52px;
  border-radius: 14px;
  background: var(--ts-accent-pale);
  color: var(--ts-accent);
  margin-bottom: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--ts-ease);
}
.ts-fcard:hover .ts-fcard__icon { background: var(--ts-accent); color: var(--ts-surface); }
/* 内联 SVG：统一画布，居中不偏；颜色继承自 .ts-fcard__icon 的 color */
.ts-fcard__icon .ts-icon { width: 26px; height: 26px; display: block; }
.ts-fcard__icon img { max-width: 24px; max-height: 24px; }
.ts-fcard__label { display: block; font-size: 16px; font-weight: 500; margin-bottom: 6px; line-height: 1.3; }
.ts-fcard__desc { font-size: 13px; color: var(--ts-muted); line-height: 1.5; }

/* ═══════════════════════════════════════════
   9. 榜单（核心组件）
   ═══════════════════════════════════════════ */
.ts-rank { display: flex; flex-direction: column; gap: 16px; }
.ts-rk {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 28px;
  display: grid;
  /* 图片列 240 而不是 168：图片改成吃满卡片高度（约 235～260）之后，168 宽的框
     等于一条 0.68 的竖缝，而九张产品图有一半是 1.3～1.9 的横构图，cover 会把
     Emma / Ecosa 裁到只剩 36%～38% 宽——床垫整个不见了，画面上只剩人和地板。
     240 宽把最惨的两张拉回 52%～55%，九张的产品主体都还在。 */
  grid-template-columns: 52px 240px 1fr 176px;
  gap: 26px;
  align-items: center;
  transition: all var(--ts-ease);
}
.ts-rk:hover {
  transform: translateY(-3px);
  box-shadow: var(--ts-shadow-hover);
  border-color: var(--ts-border-strong);
}
/* 前三名卡片底色：奖牌色浅底 + 同色描边 + 同色柔光，与名次数字、评分同色系。
   第 1 名原本是紫色底（--ts-accent 系），与金色数字撞色，已一并改为金色系。
   注意：按钮与 Editor's Choice 徽章**保持品牌紫不变**——
   金银铜表达「第几名」，紫色表达「可点击的操作」，两套语义必须分开，
   否则读者分不清哪个是按钮。 */
.ts-rk--r1 {
  background: linear-gradient(135deg, var(--ts-rank-1-tint) 0%, var(--ts-surface) 62%);
  border-color: var(--ts-rank-1-edge);
  box-shadow: 0 6px 40px var(--ts-rank-1-glow);
}
.ts-rk--r2 {
  background: linear-gradient(135deg, var(--ts-rank-2-tint) 0%, var(--ts-surface) 62%);
  border-color: var(--ts-rank-2-edge);
  box-shadow: 0 6px 40px var(--ts-rank-2-glow);
}
.ts-rk--r3 {
  background: linear-gradient(135deg, var(--ts-rank-3-tint) 0%, var(--ts-surface) 62%);
  border-color: var(--ts-rank-3-edge);
  box-shadow: 0 6px 40px var(--ts-rank-3-glow);
}
.ts-rk__num {
  font-family: var(--ts-mono);
  font-size: 34px;
  font-weight: 700;
  color: var(--ts-muted-light);
  text-align: center;
  line-height: 1;
}
/* 前三名：名次数字用奖牌色，并加一个同色系的浅底圆盘。
   第 4 名往后维持原来的浅灰，视觉上自然拉开梯队。 */
.ts-rk--r1 .ts-rk__num,
.ts-rk--r2 .ts-rk__num,
.ts-rk--r3 .ts-rk__num {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  margin-inline: auto;
  border-radius: 50%;
}
.ts-rk--r1 .ts-rk__num { color: var(--ts-rank-1); background: var(--ts-rank-1-pale); }
.ts-rk--r2 .ts-rk__num { color: var(--ts-rank-2); background: var(--ts-rank-2-pale); }
.ts-rk--r3 .ts-rk__num { color: var(--ts-rank-3); background: var(--ts-rank-3-pale); }

/* 奖牌、徽章、产品图三层从上往下叠。全部走正常文档流：
   奖牌不用 absolute 吊着，就不会在窄屏顶出卡片，也不用为每种名次算偏移量。 */
.ts-rk__visual {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* 金银铜奖牌，坐在产品图正上方居中。填充取 currentColor，
   颜色由下面三条给，换肤 / 深色模式零分支。
   第 1 名大一号——金银铜只靠颜色区分，对色觉障碍读者等于没区分，
   尺寸差是第二条线索（第三条是左边的名次数字）。 */
.ts-medal {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  display: block;
  box-sizing: border-box;
  width: 34px;
  height: 34px;
  padding: 4px;
  border-radius: 50%;
  /* 圆底不是装饰：奖牌现在压在产品图上，而九张产品图的底色从纯白到深灰都有，
     金/银/铜任何一种都会在某张图上糊掉。给一层卡片底色把它托起来。 */
  background: var(--ts-surface);
  box-shadow: 0 1px 5px rgba(0, 0, 0, .14);
}
.ts-rk--r1 .ts-medal { width: 38px; height: 38px; color: var(--ts-rank-1); }
.ts-rk--r2 .ts-medal { color: var(--ts-rank-2); }
.ts-rk--r3 .ts-medal { color: var(--ts-rank-3); }
/* 图片槽按比例走，不写死高度。原来是 168×118 的横框 + contain + 8px 内边距，
   后台传正方形产品图时会被缩成 102×102 摊在框中间，四周露出一圈底色。 */
.ts-rk__img {
  /* 父级是居中的 flex 列，不 stretch 的话图片框会缩成内容宽度 */
  align-self: stretch;
  /* 正方形，九张一样高（用户 2026-08-21 指定）。
     ⚠️ 别改回「吃满卡片高度」（flex:1 + align-self:stretch on __visual）：
     那样图片高度跟着正文长短走，一列排下来 233 / 259 / 291 参差不齐。
     ⚠️ 这里也是名次圆盘能对准图片中心的原因——__visual 里只有这一个流内元素
     （奖牌和徽章都 absolute），所以 __visual 的高度就是图片高度，
     .ts-rk 的 align-items:center 一居中，图片中心和名次中心自然重合。 */
  flex: none;
  aspect-ratio: 1 / 1;
  border-radius: var(--ts-r-md);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-family: var(--ts-serif);
  font-size: 28px;
  color: var(--ts-muted-light);
}
/* cover 而不是 contain：产品图常自带白边，contain 会把白边一起缩进来摊在卡片底色上。
   裁的时候靠上取（用户 2026-08-21 指定「留上方、裁下方」）：竖构图的产品照下半截
   基本是地板和床脚，床垫和场景都在上半部分。
   注意只对竖图有效——九张里有六张是 1.3～1.9 的横构图，框是 1:1，它们裁的是左右，
   垂直方向根本没有可裁的余量，靠上取对它们是空操作。 */
.ts-rk__img img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }
/* 底色 + 大号衬线字是「后台没传图」时的占位形态，有图时不能露出来。 */
.ts-rk__img:not(:has(img)) {
  background: linear-gradient(135deg, var(--ts-bg) 0%, var(--ts-border) 100%);
}
.ts-rk--r1 .ts-rk__img:not(:has(img)) { background: var(--ts-rank-1-tint); color: var(--ts-rank-1); }
.ts-rk--r2 .ts-rk__img:not(:has(img)) { background: var(--ts-rank-2-tint); color: var(--ts-rank-2); }
.ts-rk--r3 .ts-rk__img:not(:has(img)) { background: var(--ts-rank-3-tint); color: var(--ts-rank-3); }
/* 徽章改成走文档流 + 负下边距：下半截照旧压在产品图上边缘，
   但它现在参与排版，所以上面能稳稳接住奖牌。
   原来是 absolute 吊在 .ts-rk__visual 顶上的，奖牌一进来就会被它盖住。 */
.ts-rk__badge {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 10px;
  z-index: 2;
  width: max-content;
  max-width: calc(100% - 16px);
  margin-inline: auto;
  text-align: center;
  font-family: var(--ts-mono);
  font-size: 10px;
  letter-spacing: 1.3px;
  text-transform: uppercase;
  padding: 5px 13px;
  border-radius: var(--ts-r-pill);
  background: var(--ts-accent);
  color: var(--ts-on-accent);
  /* 图片框只有 168px 宽，nowrap 会让「DUAL SPRING CORE」这种长徽章直接横向溢出。
     换行比溢出好看。 */
  white-space: normal;
  line-height: 1.35;
}
/* Logo 只锁高度是不够的：横版字标在 24px 高时还有一百多像素宽，
   方块型 Logo（比如带底色的品牌章）就只剩 24×24，字都看不清。
   给一个宽高都有界的框 + contain，两种比例都落在差不多的视觉体量上。
   注意 Logo 一律 contain 不能 cover——产品图可以裁，Logo 裁了就是残的。 */
.ts-rk__logo {
  height: 32px; width: auto; max-width: 132px;
  object-fit: contain; object-position: left center;
  margin-bottom: 10px;
}
.ts-rk__name {
  font-family: var(--ts-serif);
  font-size: 28px;
  letter-spacing: -.5px;
  line-height: 1.15;
  margin-bottom: 6px;
}
.ts-rk__name a { text-decoration: none; transition: color var(--ts-ease); }
.ts-rk__name a:hover { color: var(--ts-accent); }
/* 产品名底下那行规格小字（硬度 · 厚度 · 试睡期），2026-08-24 新开的槽位。
   照主题里既有的「技术性小字」写法：mono + 小字号 + 微字距，跟徽章、评分行同一套，
   这样它读起来是一条规格带，而不是又一句正文 —— 下面 .ts-rk__tag 的价格才是无衬线正文。

   颜色用 --ts-ink-soft（12.6:1）。历史原因是隔壁 .ts-rk__tag 那个 --ts-muted
   当时只有 3.53:1，而这行字比价格还小。
   ⚠️ 2026-08-25 --ts-muted 已经补到 4.72，这条不再是对比度问题，
      但**别顺手改回 --ts-muted**：现在它是层级选择 —— 12.5px 的规格带跟 15.5px 的
      价格行同色会分不出主次，而这块卡片的视觉重量是逐档排的。 */
.ts-rk__claim {
  font-family: var(--ts-mono);
  font-size: 12.5px;
  letter-spacing: .3px;
  line-height: 1.5;
  color: var(--ts-ink-soft);
  margin-bottom: 10px;
}
.ts-rk__tag { font-size: 15.5px; color: var(--ts-muted); margin-bottom: 16px; }
.ts-rk__feats { display: flex; flex-direction: column; gap: 7px; margin-bottom: 16px; }
.ts-rk__feats li {
  font-size: 15px;
  color: var(--ts-ink-soft);
  display: flex;
  align-items: flex-start;
  gap: 9px;
  line-height: 1.45;
}
.ts-rk__feats li::before {
  content: '';
  flex: none;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--ts-positive-pale);
  border: 1px solid var(--ts-positive);
  margin-top: 3px;
}
.ts-rk__specs { display: flex; flex-wrap: wrap; gap: 6px; }
.ts-rk__spec {
  font-family: var(--ts-mono);
  font-size: 11.5px;
  letter-spacing: .5px;
  color: var(--ts-muted);
  background: var(--ts-bg);
  border: .5px solid var(--ts-border);
  padding: 5px 11px;
  border-radius: var(--ts-r-pill);
}

.ts-rk__side {
  text-align: center;
  border-left: .5px solid var(--ts-border);
  padding-left: 26px;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
}
.ts-rk__score b {
  display: block;
  font-family: var(--ts-serif);
  font-size: 48px;
  line-height: 1;
  letter-spacing: -1.5px;
}
/* 评分与名次同色，让「第几名」和「几分」在视觉上绑定 */
.ts-rk--r1 .ts-rk__score b { color: var(--ts-rank-1); }
.ts-rk--r2 .ts-rk__score b { color: var(--ts-rank-2); }
.ts-rk--r3 .ts-rk__score b { color: var(--ts-rank-3); }
.ts-rk__score span {
  font-family: var(--ts-mono);
  font-size: 11px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ts-muted);
  display: block;
  margin-top: 7px;
}
.ts-rk__actions { display: flex; flex-direction: column; gap: 9px; }
.ts-rk__cta {
  font-size: 15px;
  font-weight: 500;
  padding: 11px 20px;
  border-radius: var(--ts-r-pill);
  text-decoration: none;
  background: var(--ts-ink);
  color: var(--ts-surface);
  text-align: center;
  transition: all var(--ts-ease);
}
.ts-rk__cta:hover { background: var(--ts-ink-soft); transform: translateY(-1px); }
.ts-rk--top .ts-rk__cta { background: var(--ts-accent); }
.ts-rk--top .ts-rk__cta:hover { background: var(--ts-accent-deep); }
.ts-rk__link {
  font-size: 14px;
  color: var(--ts-muted);
  text-decoration: none;
  transition: color var(--ts-ease);
}
.ts-rk__link:hover { color: var(--ts-ink); text-decoration: underline; }
.ts-rank__more { text-align: center; margin-top: 40px; }

/* 前三名与其余款式之间的分隔。
   .ts-rank 是 flex column gap:16px，这块直接当一个 flex item 插进去即可。
   上下留白不对称：跟上面的第 3 名拉开得多一点，跟下面的第 4 名近一点，
   这样它读起来是「后面这一组的开头」，而不是一条居中的装饰线。 */
.ts-rank__split {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 13px;
  padding: 30px 0 8px;
  text-align: center;
}
.ts-rank__split-chip {
  position: relative;
  font-family: var(--ts-mono);
  font-size: 11px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--ts-muted);
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-pill);
  padding: 7px 18px;
}
/* 左右两条发丝线用伪元素，不占布局；渐隐收尾，避免在窄容器里撞到边。 */
.ts-rank__split-chip::before,
.ts-rank__split-chip::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 130px;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--ts-border));
}
.ts-rank__split-chip::before { right: calc(100% + 16px); }
.ts-rank__split-chip::after {
  left: calc(100% + 16px);
  background: linear-gradient(to left, transparent, var(--ts-border));
}
.ts-rank__split-note {
  max-width: 560px;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--ts-muted);
}

/* 前三名之后的卡片：默认收起，点了才展开。
   用原生 <details>，所以无脚本时照样能开，展开状态也不用我们自己同步。 */
/* ⚠️ <details> 自己必须保持 display:block。给它设 flex/grid，浏览器会把
   summary 和内容一并当作 flex item 渲染，收起状态下卡片照样显示——
   实测踩过，别把下面这条 flex 挪上来。布局只能加在内层 .ts-rank__rest-list 上。 */
.ts-rank__rest-list {
  display: flex;
  flex-direction: column;
  gap: 16px;        /* 必须与 .ts-rank 的 gap 同值，否则展开后这几张卡的间距接不上前三名 */
  padding-top: 16px; /* 补上 summary 与第一张卡之间的距离——它们不在同一个 flex 容器里，吃不到 gap */
}
/* summary 默认是 list-item，会带一个三角。三种浏览器三种去法，都写上。 */
.ts-rank__rest-toggle {
  width: fit-content;
  margin: 0 auto;   /* details 是 block，居中只能靠这个；用 align-self 不管用 */
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  cursor: pointer;
  font-family: var(--ts-sans);
  font-size: 15px;
  font-weight: 500;
  color: var(--ts-ink);
  /* 刻意与 .ts-rank__split-chip、.ts-rk 同一套规格（surface 底 + 发丝边）：
     它属于榜单结构，不是导航。正下方的「View all reviews」是 1px 实边 + 透明底
     的 .ts-btn--ghost——两者原本长得一模一样，叠在一起读者分不出
     「就地展开」和「跳去别的页」。别把这里改回 ghost 那套。 */
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-pill);
  padding: 13px 28px;
  transition: border-color var(--ts-ease);
}
.ts-rank__rest-toggle::-webkit-details-marker { display: none; }
.ts-rank__rest-toggle::marker { content: ''; }
.ts-rank__rest-toggle:hover { border-color: var(--ts-muted); }
.ts-rank__rest-toggle:focus-visible { outline: 2px solid var(--ts-accent); outline-offset: 2px; }

/* 箭头自己画一个，省一次图片请求。展开后翻个面。
   只有 transform 的终态，没有 transition——加了动画就得再进 reduced-motion 关一次。 */
.ts-rank__rest-toggle::after {
  content: '';
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.ts-rank__rest[open] .ts-rank__rest-toggle::after { transform: translateY(2px) rotate(-135deg); }

/* 两段文案同时在 DOM 里，靠 [open] 切换——隐藏的那段 display:none，
   屏幕阅读器不会把两句都念出来。 */
.ts-rank__rest-less { display: none; }
.ts-rank__rest[open] .ts-rank__rest-more { display: none; }
.ts-rank__rest[open] .ts-rank__rest-less { display: inline; }

/* ═══════════════════════════════════════════
   10. 对比
   ═══════════════════════════════════════════ */
/* stretch 而不是 start：三张卡等高，才谈得上「按钮在同一条线上」。 */
.ts-cmp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: stretch; }
.ts-cmp {
  background: var(--ts-bg);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 28px;
  display: flex;
  flex-direction: column;
}
/* baseline 而不是 center：品牌名换成两行时，center 会把它整体上抬，
   分数就跟第一行文字错开半行。baseline 永远对齐名字的**第一行**基线。 */
.ts-cmp__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 18px;
  border-bottom: .5px solid var(--ts-border);
  margin-bottom: 20px;
}
/* 名字预留两行高度。「Newentor Hybrid Mattress」是两行、「Koala Mattress」是一行，
   不预留的话下面的评分行会整体差 29px，两张卡横着看就是错位的。
   不做 line-clamp 截断——真出现三行的名字，宁可那一张卡中间多撑开一点，
   也不要把品牌名截成「Sleeping Duck Origi…」。 */
.ts-cmp__name {
  font-family: var(--ts-serif);
  font-size: 23px;
  letter-spacing: -.4px;
  line-height: 1.25;
  min-height: 2.5em;
}
.ts-cmp__score { font-family: var(--ts-mono); font-size: 22px; font-weight: 700; color: var(--ts-accent); }
.ts-cmp__row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.ts-cmp__row > span { font-size: 14px; color: var(--ts-muted); width: 96px; flex: none; }
.ts-cmp__bar { flex: 1; height: 3px; background: var(--ts-border); border-radius: 2px; overflow: hidden; }
.ts-cmp__bar i { display: block; height: 100%; background: var(--ts-accent-soft); border-radius: 2px; }
.ts-cmp__row b { font-family: var(--ts-mono); font-size: 12.5px; width: 26px; text-align: right; }
/* margin-top:auto 把按钮顶到卡片底部。这样即使两张卡的评分条数不一样多，
   按钮那条线仍然是齐的（原来写在模板的行内 style 里，行内优先级更高，挪到这里）。
   auto 会把余量吃光，所以最小间距得由最后一行自己留出来，不能靠按钮的 margin。 */
.ts-cmp__row:last-of-type { margin-bottom: 20px; }
.ts-cmp__cta { display: block; margin-top: auto; }
.ts-cmp__add {
  border: 1px dashed var(--ts-border-strong);
  border-radius: var(--ts-r-lg);
  min-height: 290px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  color: var(--ts-muted);
  text-decoration: none;
  transition: all var(--ts-ease);
}
.ts-cmp__add:hover {
  border-color: var(--ts-accent-soft);
  color: var(--ts-accent);
  background: var(--ts-surface);
}
.ts-cmp__add i {
  width: 38px; height: 38px;
  border-radius: 50%;
  border: 1px solid currentColor;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; font-style: normal;
}

/* ═══════════════════════════════════════════
   11. 内容卡片
   ═══════════════════════════════════════════ */
.ts-guides { display: grid; grid-template-columns: repeat(auto-fit, minmax(258px, 1fr)); gap: 20px; }
.ts-gcard {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  overflow: hidden;
  text-decoration: none;
  transition: all var(--ts-ease);
  display: flex;
  flex-direction: column;
}
.ts-gcard:hover {
  transform: translateY(-4px);
  box-shadow: var(--ts-shadow-hover);
  border-color: var(--ts-border-strong);
}
.ts-gcard__img {
  height: 154px;
  background: linear-gradient(135deg, var(--ts-bg) 0%, var(--ts-border) 100%);
  overflow: hidden;
}
.ts-gcard__img img { width: 100%; height: 100%; object-fit: cover; }
.ts-gcard:nth-child(2) .ts-gcard__img { background: linear-gradient(135deg, var(--ts-accent-pale) 0%, #D9D2EE 100%); }
.ts-gcard:nth-child(3) .ts-gcard__img { background: linear-gradient(135deg, var(--ts-positive-pale) 0%, #CFDDD5 100%); }
.ts-gcard:nth-child(4) .ts-gcard__img { background: linear-gradient(135deg, var(--ts-info-pale) 0%, #D6DEFB 100%); }
.ts-gcard__body { padding: 24px; flex: 1; display: flex; flex-direction: column; }
.ts-gcard__cat {
  font-family: var(--ts-mono);
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--ts-accent);
  margin-bottom: 12px;
}
.ts-gcard__title {
  font-family: var(--ts-serif);
  font-size: 22px;
  line-height: 1.25;
  letter-spacing: -.3px;
  margin-bottom: 10px;
}
.ts-gcard__desc { font-size: 15px; color: var(--ts-muted); line-height: 1.6; flex: 1; }
/* 2026-08-25：从 --ts-muted-light 换过来。这行是阅读时长，11.5px 的小字，
   要按 AA 正文的 4.5 算；--ts-muted-light 只有 3.04，那一档只给大字和描边用。 */
.ts-gcard__meta {
  font-family: var(--ts-mono);
  font-size: 11.5px;
  color: var(--ts-muted);
  letter-spacing: .8px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: .5px solid var(--ts-border);
}

/* ═══════════════════════════════════════════
   12. FAQ
   ═══════════════════════════════════════════ */
.ts-faq { max-width: 800px; }
.ts-faq__item { border-bottom: .5px solid var(--ts-border); }
.ts-faq__q {
  width: 100%;
  background: none;
  border: 0;
  text-align: left;
  padding: 26px 40px 26px 0;
  font-family: var(--ts-serif);
  font-size: 22px;
  letter-spacing: -.2px;
  color: var(--ts-ink);
  cursor: pointer;
  position: relative;
  transition: color var(--ts-ease);
}
.ts-faq__q:hover { color: var(--ts-accent); }
.ts-faq__q::after {
  content: '+';
  position: absolute;
  right: 6px; top: 50%;
  transform: translateY(-50%);
  font-family: var(--ts-mono);
  font-size: 22px;
  color: var(--ts-muted);
  transition: transform var(--ts-ease);
}
.ts-faq__item.is-open .ts-faq__q::after { transform: translateY(-50%) rotate(45deg); }
.ts-faq__a { max-height: 0; overflow: hidden; transition: max-height .34s cubic-bezier(.16, 1, .3, 1); }
.ts-faq__item.is-open .ts-faq__a { max-height: 460px; }
.ts-faq__a p { font-size: 16.5px; color: var(--ts-muted); line-height: 1.7; padding: 0 40px 28px 0; }

/* ═══════════════════════════════════════════
   13. 订阅 CTA
   ═══════════════════════════════════════════ */
.ts-cta { background: var(--ts-invert-bg); color: var(--ts-invert-ink); text-align: center; padding: 112px 0; }
.ts-cta .ts-eyebrow { color: var(--ts-invert-faint); }
.ts-cta__title {
  font-family: var(--ts-serif);
  font-size: clamp(40px, 4.8vw, 64px);
  line-height: 1.08;
  letter-spacing: -1.4px;
  margin-bottom: 20px;
}
.ts-cta__title em { font-style: italic; color: var(--ts-accent-soft); }
.ts-cta__lede {
  font-size: 17px;
  color: var(--ts-invert-muted);
  max-width: 440px;
  margin: 0 auto 36px;
  line-height: 1.65;
}
/* justify-content 必须有：两个输入框撑满第一行后按钮会换行，
   没有它按钮会贴在第二行最左边，与整块居中的排版对不齐。 */
.ts-subform { display: flex; justify-content: center; gap: 10px; max-width: 470px; margin: 0 auto; flex-wrap: wrap; }
.ts-subform input {
  flex: 1;
  min-width: 190px;
  background: var(--ts-invert-field);
  border: .5px solid var(--ts-invert-line);
  border-radius: var(--ts-r-pill);
  padding: 14px 22px;
  color: var(--ts-invert-ink);
  font-family: var(--ts-sans);
  font-size: 16px;
  font-weight: 300;
  transition: all var(--ts-ease);
}
.ts-subform input::placeholder { color: var(--ts-invert-faint); }
.ts-subform input:focus { border-color: var(--ts-accent-soft); background: var(--ts-invert-field-hi); }
/* 这块底色是 --ts-invert-bg（三套主题下都是近黑），焦点环得用亮的那支紫，暗紫在上面看不见。
   :focus-visible 而非 :focus——鼠标点进输入框不该冒出一圈环。 */
.ts-subform input:focus-visible { outline: 2px solid var(--ts-accent-soft); outline-offset: 2px; }
.ts-subform button {
  background: var(--ts-surface);
  color: var(--ts-ink);
  border: 0;
  border-radius: var(--ts-r-pill);
  padding: 14px 30px;
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--ts-ease);
}
.ts-subform button:hover { background: var(--ts-accent-pale); transform: translateY(-1px); }
.ts-cta__note {
  display: block;
  font-family: var(--ts-mono);
  font-size: 11.5px;
  color: var(--ts-invert-faint);
  margin-top: 20px;
  letter-spacing: .6px;
}
.ts-form__trap { position: absolute; left: -9999px; opacity: 0; }

/* ═══════════════════════════════════════════
   14. 页脚
   ═══════════════════════════════════════════ */
.ts-footer { background: var(--ts-bg); padding: 72px 0 34px; border-top: .5px solid var(--ts-border); }
/* 页脚两排（2026-08-24 下半场，用户指定「Get in touch 下移一行、第一行左右再拉开」）：
   第一排 .ts-footer__grid = 标志块 + 链接组；第二排 .ts-footer__reach = 联系说明 + 表单。

   改之前是四列同宽一排（`1.9fr 1fr 1fr 1fr`），实测 Get in touch 那列只有 200.8px，
   220 字的说明被压成 9 行、表单也是 200 宽，整排被它一个人撑到 671.1px 高，
   而左边标志块的内容只有 150 出头 —— 右边又高又挤、左边一大片空。

   ⚠️ 第一排**不能再写死列数**。链接组的组数是后台 footer 菜单决定的
   （topsites_get_footer_nav_groups_from_menu），原来那套列数是按「正好两组」写死的，
   后台一挂三组的菜单当场错位。现在链接组包在 .ts-footer__nav 这个 flex 里，几组都不影响。
   ⚠️ 两列各占一半 + 链接组 space-between 是量出来的配比，不是随手写的。
   试过 `360px + flex-end`（链接组贴右边缘抱成一团）：标志块到 Quick Links 之间空出 485px，
   而两组链接之间只有 88px —— 一排里三块文字，空隙 485/88，看着是右边挤成一坨。
   现在这套实测 280 / 293，三块文字均匀铺开，右边缘仍然贴齐。 */
.ts-footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 36px 64px;
  margin-bottom: 56px;
}
.ts-footer__nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 32px 64px;
}
/* ⚠️ 第二排的列比例**必须跟第一排一样**（都是 1fr 1fr）。2026-08-25 之前这里是
   `1fr 1.25fr`，本意是给表单腾地方，但代价是：两排的第一列一个 526、一个 468，
   而 .ts-footer__about 和 .ts-footer__contact 这两段文字**左边缘是对齐的**
   （都从 75 起，视觉上就是同一列里上下两段），右边却差了 161px —— 用户看到的就是这个。
   表单从 584 收到 526 仍然是两列（每格约 231），够用。
   **别为了表单再把它调宽**：一调，上下两段文字的宽度就又不一样了。 */
.ts-footer__reach {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 32px 64px;
  align-items: start;
  margin-bottom: 52px;
}
.ts-footer__logo {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--ts-serif);
  font-size: 36px;
  letter-spacing: -.3px;
  margin-bottom: 16px;
  text-decoration: none;
}
/* 标志块下的介绍语 和 Get in touch 那段说明，**共用同一个宽度上限**。
   这两段在页面上是同一列里的上下两段（左边缘都是 75），measure 不同就是没对齐。
   2026-08-25 之前 about 写死 310、contact 完全没有上限，实测右边缘差 161px。
   ⚠️ 上限走变量，就是为了**不可能只改一个**。要调就调 --ts-footer-measure。
   ⚠️ 光统一上限还不够，两排的栅格列比例也得一样 —— 列比 310 窄的时候是列在决定换行，
      上限根本不生效。两处一起看，见上面 .ts-footer__reach 那条。 */
.ts-footer__about,
.ts-footer__contact { max-width: var(--ts-footer-measure); }
.ts-footer__about { font-size: 15.5px; color: var(--ts-muted); line-height: 1.7; }
.ts-footer__col h3 {
  font-family: var(--ts-mono);
  font-size: 11.5px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin-bottom: 18px;
}
.ts-footer__col a {
  display: block;
  font-size: 15.5px;
  color: var(--ts-ink-soft);
  text-decoration: none;
  margin-bottom: 11px;
  transition: color var(--ts-ease);
}
.ts-footer__col a:hover { color: var(--ts-accent); }
.ts-footer__contact { font-size: 15.5px; color: var(--ts-muted); line-height: 1.7; }
.ts-footer__contact a { color: var(--ts-accent); }
/* 第二排腾出宽度之后表单排成两列。竖排单列在 200px 宽的列里是唯一选择，
   在 590px 宽的格子里就是一根细长条，右边空半格。
   ⚠️ 只有五个可见子元素参与栅格：隐藏域是 display:none，蜜罐和 .ts-sr 标签是 absolute，
   三类都不生成栅格项。加字段要连 grid-template-areas 一起改，否则新字段会被自动放置扔到末尾。 */
.ts-footer__form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas:
    "name    email"
    "message message"
    "privacy submit";
  gap: 12px 14px;
  align-items: start;
  margin-top: 0;
}
.ts-footer__f-name { grid-area: name; }
.ts-footer__f-email { grid-area: email; }
.ts-footer__f-message { grid-area: message; }
.ts-footer__form input,
.ts-footer__form textarea {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md);
  padding: 12px 16px;
  font-family: var(--ts-sans);
  font-size: 15.5px;
  font-weight: 300;
  color: var(--ts-ink);
  transition: border-color var(--ts-ease);
  width: 100%;
}
.ts-footer__form textarea { min-height: 88px; resize: vertical; }
.ts-footer__form input:focus,
.ts-footer__form textarea:focus { border-color: var(--ts-accent-soft); }
.ts-footer__form input:focus-visible,
.ts-footer__form textarea:focus-visible { outline: 2px solid var(--ts-accent); outline-offset: 2px; }
.ts-footer__form button {
  grid-area: submit;
  justify-self: end;
  align-self: center;
  background: var(--ts-ink);
  color: var(--ts-surface);
  border: 0;
  border-radius: var(--ts-r-pill);
  padding: 12px 28px;
  font-size: 15.5px;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--ts-ease);
}
.ts-footer__form button:hover { background: var(--ts-ink-soft); }
/* 2026-08-25：从 --ts-muted-light 换过来 —— 这里是隐私声明，13px 的正文小字，
   而且是**必须读到**的合规文案，不能按装饰性小字处理。 */
.ts-footer__privacy { font-size: 13px; color: var(--ts-muted); line-height: 1.6; }
.ts-footer__privacy a { color: var(--ts-muted); }
.ts-footer__form .ts-footer__privacy { grid-area: privacy; align-self: center; }
.ts-footer__disc {
  font-size: 13.5px;
  color: var(--ts-muted);
  line-height: 1.65;
  max-width: 720px;
  margin-top: 22px;
  padding-top: 22px;
  border-top: .5px solid var(--ts-border);
}
.ts-footer__bottom {
  border-top: .5px solid var(--ts-border);
  padding-top: 26px;
  margin-top: 26px;
  display: flex;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  align-items: center;
}
.ts-footer__bottom p {
  font-family: var(--ts-mono);
  font-size: 12px;
  color: var(--ts-muted);
  letter-spacing: .5px;
}

/* ═══════════════════════════════════════════
   15. 响应式
   ═══════════════════════════════════════════ */
@media (max-width: 1000px) {
  :root { --ts-section: 80px; }
  /* 跟桌面一样 4 列（8 张卡刚好 4×2；3 列会剩末行 2 张）。卡片变窄，
     副标题这时候已经塞不下两行了，收掉；图标和字号同步缩一档。 */
  .ts-finder { grid-template-columns: repeat(4, 1fr); gap: 10px; }
  .ts-fcard { padding: 22px 10px 20px; }
  .ts-fcard__icon { width: 44px; height: 44px; border-radius: 12px; margin-bottom: 12px; }
  .ts-fcard__icon .ts-icon { width: 22px; height: 22px; }
  .ts-fcard__label { font-size: 14px; margin-bottom: 0; }
  .ts-fcard__desc { display: none; }
  /* 同桌面：图片吃满高度后列不能太窄，130 会把横构图裁到只剩三成 */
  .ts-rk { grid-template-columns: 44px 220px 1fr; gap: 20px; }
  .ts-rk__side {
    grid-column: 1 / -1;
    border-left: 0;
    border-top: .5px solid var(--ts-border);
    padding-left: 0;
    padding-top: 20px;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    text-align: left;
  }
  .ts-rk__actions { flex-direction: row; align-items: center; }
  .ts-cmp-grid { grid-template-columns: 1fr 1fr; }
  .ts-cmp__add { grid-column: 1 / -1; min-height: 120px; }
  /* 标志块收窄、间距收一档，两排的结构不变 */
  .ts-footer__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px 40px; }
  .ts-footer__nav { gap: 28px 40px; }
  /* ⚠️ 跟第一排保持同一个比例，理由见基础规则里那条 —— 这里原本是 1.15fr。 */
  .ts-footer__reach { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px 40px; }
  /* 窄屏放不下左文右图，图片落到文字下方 */
}

@media (max-width: 860px) {
  .ts-nav { display: none; }
  .ts-header__cta { display: none; }
  .ts-burger { display: flex; }
}

@media (max-width: 720px) {
  :root { --ts-gutter: 20px; --ts-section: 68px; }
  .ts-finder { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .ts-fcard { padding: 22px 12px 20px; }
  .ts-fcard__desc { display: none; }
  .ts-hero { padding: 32px 0 40px; }
  /* 窄屏的遮罩换成整体压一层，不再分左右。
     桌面上文字只占左半边，所以横向渐变能让右边的人和宠物露出来；手机上文字
     横跨整个宽度，右边没地方让了。而且 cover 在 390 宽只保留源图中间 23%——
     正好是人物那一段，是全图最花的地方，实测导语掉到 2.42:1。
     所以手机上图退成一层淡淡的氛围底纹，可读性优先。 */
  .ts-hero--bg::before {
    background: linear-gradient(
      to bottom,
      rgba(var(--ts-bg-rgb), .90) 0%,
      rgba(var(--ts-bg-rgb), .86) 50%,
      rgba(var(--ts-bg-rgb), .92) 100%
    );
  }
  .ts-stat b { font-size: 34px; }
  .ts-stat--date b { font-size: 26px; }
  /* 第一列按前三名那个奖牌圆盘的实际尺寸给（.ts-rk--r1/2/3 .ts-rk__num 是 46×46），
     写 38px 会让圆盘横向溢出 8px。 */
  .ts-rk { grid-template-columns: 46px 1fr; gap: 16px; padding: 22px; }
  /* ⚠️ 每一个直接子元素都得显式给列。少写一个，自动放置就会把它扔进只有 46px
     宽的第 1 列，正文会被压成每行一个词 —— 踩过一次。
     ⚠️ 第 1 列仍然保留 46px：2026-08-28 名次挪到顶部一行之后，那一列只剩名次
        圆盘在用（前三名的 .ts-rk__num 实测 46×46，写 38px 圆盘会横向溢出 8px）。 */
  .ts-rk__body { grid-column: 1 / -1; }
  /* 窄屏图片槽：**限宽，不限高**。
     ⚠️ 这里刻意不用桌面那个 1:1：手机上图片是整行宽（350 上下），正方形就是
     350 高——一张卡的图能吃掉小半屏，而窄屏的卡片本来就要多滑一屏。

     ⚠️⚠️ **原来写的是 `max-height: 210px`，那是个陷阱，2026-08-28 换掉了。**
     `max-height` 会**静默顶掉 `aspect-ratio`**：框一宽就只长宽不长高，比例自己就变了，
     而 CSS 里那行 `aspect-ratio: 4/3` 还明晃晃写着，看代码完全看不出来。实测：
        390px 宽 → 框 304×210 = **1.45**（不是 4:3），产品图纵向只剩 69%
        719px 宽 → 框 633×210 = **3.01**，纵向只剩 **33%** —— 床垫整个被切没了
     改成 `max-width` 之后 4:3 在每个宽度上都是真的，超宽时图片不再放大而已。
     420 是按 430 那档手机（框 346 宽）留了余量定的，719 那档会停在 420×315。
     ⚠️ **别改回 max-height。** 要限高就同时把 aspect-ratio 去掉，别让两个互相打架。 */
  .ts-rk__img { aspect-ratio: 4 / 3; max-width: 420px; }
  /* ⚠️ 桌面是 `center top`（1:1 的框，保顶部构图更稳）。窄屏的框是 4:3，
     从顶部裁等于**切掉照片下半部分，而床垫正好在下半部分** —— 用户 2026-08-28
     报「好多图片都看不到床垫了」，这是原因之一。窄屏改成居中裁。 */
  .ts-rk__img img { object-position: center; }
  .ts-cmp-grid { grid-template-columns: 1fr; }
  .ts-footer__grid { grid-template-columns: 1fr; }
  /* ⚠️ space-between 必须撤掉。单列之后链接组落在标志块**下面**、占满整行宽，
     再两头分开就是「Quick Links 贴左、Company 贴右」，中间空一大片，
     而上下所有内容都是左对齐的。 */
  .ts-footer__nav { justify-content: flex-start; gap: 28px 48px; }
  .ts-footer__reach { grid-template-columns: 1fr; }
  /* 单列宽度放不下并排两个输入框，栅格区域整个改成竖排。
     ⚠️ 提交按钮桌面端是 justify-self:end（跟隐私说明一行、各占一头），
     竖排之后那一行没有别的东西，贴右边就成了孤零零一个按钮，改回左对齐。 */
  .ts-footer__form {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "name"
      "email"
      "message"
      "privacy"
      "submit";
    gap: 10px;
  }
  .ts-footer__form button { justify-self: start; }
  /* 分数挪到卡片最上方，压在奖牌之上（用户 2026-08-20 指定，只改窄屏，桌面端不动）。
     移动端一整张卡一屏放不下，分数是读者第一眼要找的东西，压在最底下等于逼人
     先滑完整张卡才能判断值不值得看。

     做法：让 __side 自己不产生盒子，里面的 score / actions 才能各自成为 .ts-rk
     的 grid item，这样才有可能用 order 单独把分数拎到最前面——score 是 __side
     的子元素，不 contents 的话 order 只能在 __side 内部生效，动不了它在卡片里的位置。

     ⚠️ 代价一：1000px 断点给 __side 设的 border-top / padding-top 会一并失效
        （contents 元素不生成盒子，边框无处可画），那条分隔线下面补回给 __actions。
     ⚠️ 代价二：视觉顺序与 DOM 顺序不再一致，屏幕阅读器仍按 DOM 在最后读到分数。
        分数是对产品的补充判断，先听产品再听评分不影响理解，故接受。 */
  .ts-rk__side { display: contents; }
  /* ── 顶部一行：名次在最左，分数在卡片正中（用户 2026-08-28 指定，只改窄屏）──
     ⚠️ 名次和分数是**同一个网格行、刻意重叠**的：
        分数必须 `grid-column: 1 / -1` 才能落在**卡片**的正中；
        只放第 2 列的话它是「图片列的正中」，比卡片中线偏右 23px（46 的列宽 + 16 的 gap 折半）。
        名次钉在第 1 列的同一行，压在分数跨行的左段上。分数内容是居中的，
        两者中间留着一大段空白，不会撞。
     ⚠️ 所以这两条的 grid-row 都得写死 1。靠 order 自动排placement 做不到重叠。 */
  .ts-rk__num {
    grid-column: 1;
    grid-row: 1;
    align-self: center;
  }
  .ts-rk__score {
    grid-column: 1 / -1;
    grid-row: 1;
    /* 竖排：9.6 在 OUTSTANDING 上方。
       ⚠️ 2026-08-20 这里曾经刻意改成横排省高度，2026-08-28 用户要求改回竖排。
          竖排多吃约 20px 高，是用户看过效果后要的，别再"优化"回去。 */
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
  }
  .ts-rk__score span { margin-top: 4px; }
  /* 名次挪走之后图片不必再让出第 1 列，整行铺开 */
  .ts-rk__visual { grid-column: 1 / -1; }
  /* 卡片整体左对齐：按钮那块原来是居中的，跟上面所有内容的左对齐对不上。 */
  .ts-rk__actions {
    grid-column: 1 / -1;
    flex-direction: column;
    align-items: flex-start;
    text-align: left;
    border-top: .5px solid var(--ts-border);
    padding-top: 18px;
  }
  /* 窄屏放不下两侧发丝线，直接收掉，只留 chip */
  .ts-rank__split-chip::before,
  .ts-rank__split-chip::after { display: none; }
  .ts-rank__split { padding: 22px 0 4px; }
  .ts-rank__split-note { font-size: 13.5px; }
}

/* ═══════════════════════════════════════════
   16. 品牌评测详情页
   ═══════════════════════════════════════════ */

/* 面包屑 */
.ts-crumb {
  font-family: var(--ts-mono);
  font-size: 12px;
  letter-spacing: .8px;
  color: var(--ts-muted);
  padding: 26px 0 0;
}
.ts-crumb a { color: var(--ts-muted); transition: color var(--ts-ease); }
.ts-crumb a:hover { color: var(--ts-accent); }
/* 2026-08-25：分隔符从 --ts-muted-light 换成 --ts-muted。字号是从 .ts-crumb 继承的 12px，
   规则自己不写 font-size —— 所以 ts-token-contrast 那条小字守卫**抓不到它**，
   是在真实页面上逐元素扫出来的（3.32:1）。⚠️ 靠继承拿字号的还有别的，别以为守卫兜住了全部。 */
.ts-crumb i { font-style: normal; margin: 0 8px; color: var(--ts-muted); }

/* 详情页头部 */
.ts-brand-hero { padding: 40px 0 64px; position: relative; overflow: hidden; }
.ts-brand-hero::after {
  content: '';
  position: absolute;
  top: -220px; right: -180px;
  width: 640px; height: 640px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--ts-accent-pale) 0%, transparent 68%);
  opacity: .8;
  pointer-events: none;
}
.ts-brand-hero__grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 1fr 400px;
  gap: 56px;
  align-items: start;
}
.ts-brand-logo {
  height: 46px;
  max-width: 200px;
  object-fit: contain;
  object-position: left center;
  margin-bottom: 22px;
}
.ts-brand-title {
  font-family: var(--ts-serif);
  font-size: clamp(40px, 5vw, 64px);
  line-height: 1.05;
  letter-spacing: -1.6px;
  margin-bottom: 20px;
}
.ts-brand-summary {
  font-size: 17px;
  color: var(--ts-muted);
  line-height: 1.75;
  max-width: 620px;
  margin-bottom: 30px;
}

/* 作者 / 审核人 */
.ts-byline { display: flex; flex-wrap: wrap; gap: 28px; align-items: center; }
.ts-byline__person { display: flex; align-items: center; gap: 11px; }
.ts-byline__avatar {
  width: 40px; height: 40px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--ts-accent-pale);
  flex: none;
}
.ts-byline__role {
  display: block;
  font-family: var(--ts-mono);
  font-size: 10px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ts-muted);
}
.ts-byline__name { font-size: 15px; font-weight: 500; }

/* 评分面板 */
.ts-score-panel {
  background: var(--ts-surface);
  border: .5px solid var(--ts-accent-soft);
  border-radius: var(--ts-r-lg);
  padding: 28px;
  box-shadow: 0 6px 40px rgba(108, 98, 161, .12);
  position: sticky;
  top: 96px;
  /* 上限 + 内部滚动是兜底，不是常态。
     经过：2026-08-24 先把评分行从 4 条改成十条，面板从约 670px 涨到 878.8px，
     需要 999px 视口才放得下 —— 而**粘性面板一旦高过视口，下缘就永久卡在折线以下**，
     这个面板的最后一个子元素偏偏是「Visit Site」那个联盟 CTA（900 视口差 99px、
     800 差 199px，13 寸笔记本上点不到）。
     同一天又把十项整体搬进正文（#nav-scores），面板回到约 440px，这条上限因此
     平时根本不触发（只有 560px 以下的视口高度才会碰到）。留着是防以后往面板里
     再加东西时那个 CTA 又变成够不着的 —— 它挡的是「加内容不检查高度」这个动作。 */
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.ts-score-panel__img {
  height: 176px;
  border-radius: var(--ts-r-md);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  margin-bottom: 24px;
}
.ts-score-panel__img:not(:has(img)) {
  background: linear-gradient(135deg, var(--ts-accent-pale) 0%, #DDD6F2 100%);
}
.ts-score-panel__img img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
.ts-score-panel__overall {
  text-align: center;
  padding-bottom: 22px;
  margin-bottom: 22px;
  border-bottom: .5px solid var(--ts-border);
}
.ts-score-panel__overall b {
  display: block;
  font-family: var(--ts-serif);
  font-size: 58px;
  line-height: 1;
  letter-spacing: -2px;
  color: var(--ts-accent);
}
.ts-score-panel__overall span {
  display: block;
  font-family: var(--ts-mono);
  font-size: 10px;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin-top: 9px;
}
/* 正文里的十项评分（#nav-scores，2026-08-24 从 hero 侧栏搬进来的）。
   ⚠️ **列数写死 2，不用 `repeat(auto-fill, minmax(...))`。**
   第一版就是 auto-fill，在 1116px 的内容宽下自动排成了 **3 列** ——
   而 10 除不尽 3，末行剩一个孤项、右边空一截（HANDOFF 六.16：
   1fr 栅格的列数必须整除项目数，首页入口卡 8 项配 5 列踩过同一个坑）。
   2 是唯一既整除 10、又留得下 104px 标签 + 进度条 + 数字的列数（5 列每列太窄）。
   2 列也跟同页 pros/cons 的两列 548px 对齐，是这一页已确立的范式。 */
.ts-scores {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 56px;
}
@media (max-width: 720px) {
  .ts-scores { grid-template-columns: 1fr; }
}
.ts-scores__note {
  margin-top: 18px;
  font-size: 14px;
  color: var(--ts-ink-soft);
}
.ts-score-row { display: flex; align-items: center; gap: 12px; margin-bottom: 13px; }
/* 只作用于标签那一个 span。原来写的是 `.ts-score-row > span`，权重 (0,1,1) 压过
   下面 `.ts-score-row__bar` 的 (0,1,0)，把 width:84px 和 flex:none 一起套到了进度条上——
   进度条因此恒宽 84px，flex:1 从未生效，整行内容固定 222px，
   卡片一宽右边就空出一大片。 */
/* 104px 是「Motion isolation」「Pressure relief」不折行所需的宽度。
   原来 84px 会把这两项折成两行，那几行 45px 高、其余 22px，整块看着参差。 */
.ts-score-row > span:first-child { font-size: 14px; color: var(--ts-muted); width: 104px; flex: none; }
/* 进度条吃掉剩余空间，数字因此始终贴着卡片右边缘 */
.ts-score-row__bar { flex: 1; min-width: 48px; height: 3px; background: var(--ts-border); border-radius: 2px; overflow: hidden; }
.ts-score-row__bar i { display: block; height: 100%; background: var(--ts-accent-soft); border-radius: 2px; }
/* 等宽数字 + 右对齐，8.5 和 9 的小数点才对得齐 */
.ts-score-row b { font-family: var(--ts-mono); font-variant-numeric: tabular-nums; font-size: 12px; width: 30px; flex: none; text-align: right; }

.ts-promo {
  background: var(--ts-accent-pale);
  border-radius: var(--ts-r-md);
  padding: 14px 16px;
  font-size: 14px;
  color: var(--ts-accent-deep);
  text-align: center;
  margin: 22px 0 14px;
  line-height: 1.5;
}
.ts-score-panel__cta {
  display: block;
  text-align: center;
  background: var(--ts-accent);
  color: var(--ts-on-accent);
  font-size: 16px;
  font-weight: 500;
  padding: 14px 24px;
  border-radius: var(--ts-r-pill);
  transition: all var(--ts-ease);
}
.ts-score-panel__cta:hover { background: var(--ts-accent-deep); transform: translateY(-1px); }

/* 权益条 */
/* 2026-08-24：这排从整幅 1116px 收进了 hero 左栏（single-brand.php），
   同时从横排改成**单列纵向**。三个理由，都是量出来的：
   1. 填空效果最好 —— Newentor（3 项）下左栏 374.8 → 487.2，跟右边卡片 504.7
      只差 17.5px；横排 3 列时还差 129.8px。
   2. **彻底绕开整除问题。** 九款的 benefits 项数是 1 / 2 / 3 / 4 都有
      （Sealy 只有 1 项、TEMPUR 2 项 —— 它们的配送和分期是刻意留空的，见 HANDOFF 三.5）。
      横排无论定几列都会出事：3 列时四项的那五款末行留一个孤项（六.16），
      而原来的 auto-fit 在 Sealy 只有 1 项时会把那一项拉满整行（六.17）。
      单列时项数多少都不影响排版。
   3. 项高一致 58px。2 列那版实测 83 / 58 两种高度，参差。
   ⚠️ 别改回 `repeat(auto-fit, minmax(...))`，那是上面第 2 条里的两个坑。 */
.ts-benefits {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  margin-top: 32px;
  position: relative;
  z-index: 2;
}
.ts-benefit {
  display: flex; align-items: center; gap: 12px;
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md);
  padding: 16px 18px;
  font-size: 14.5px;
}
.ts-benefit img { width: 22px; height: 22px; object-fit: contain; flex: none; }
.ts-benefit .ts-icon { width: 22px; height: 22px; flex: none; color: var(--ts-accent); }

/* 章节锚点导航 */
.ts-anchors {
  position: sticky;
  top: var(--ts-header-h);
  z-index: 60;
  background: var(--ts-glass);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border-top: .5px solid var(--ts-border);
  border-bottom: .5px solid var(--ts-border);
}
.ts-anchors__list {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 12px 0;
}
.ts-anchors__list::-webkit-scrollbar { display: none; }
.ts-anchors__list a {
  white-space: nowrap;
  font-size: 14px;
  color: var(--ts-muted);
  padding: 8px 16px;
  border-radius: var(--ts-r-pill);
  transition: all var(--ts-ease);
}
.ts-anchors__list a:hover { background: var(--ts-accent-pale); color: var(--ts-accent); }

/* 正文区块
   scroll-margin-top 用于抵消吸顶 header(--ts-header-h) + 锚点条(~50px)，
   否则点锚点跳转后标题会被压在吸顶栏底下。 */
.ts-block { padding: 68px 0; border-bottom: .5px solid var(--ts-border); scroll-margin-top: calc(var(--ts-header-h) + 62px); }
.ts-block:last-of-type { border-bottom: 0; }
.ts-block__title {
  font-family: var(--ts-serif);
  font-size: clamp(30px, 3.2vw, 42px);
  letter-spacing: -.9px;
  line-height: 1.15;
  margin-bottom: 28px;
}
.ts-prose p { font-size: 17px; color: var(--ts-ink-soft); line-height: 1.8; margin-bottom: 18px; max-width: 780px; }

/* 评测页区块配图（2026-08-25，用户要求）：Our verdict 和 Pros & cons 各一张，共用这一套。
   标记由 topsites_render_block_figure() 输出，两处一模一样。

   ⚠️ **按原图比例显示，不裁切** —— 用户 2026-08-25 定的（我提过「两张比例不同、
   同一页上高度就不一致」，用户仍选原比例）。所以这里**没有** aspect-ratio、
   **没有** object-fit：高度完全由图自己的比例决定。
   **别加回来**，那是推翻用户的决定，不是修 bug。比例一致是上传时的事。

   ⚠️ **两张都撑满 .ts-wrap（1180），不跟着 .ts-prose 那 780 走。** 看着是不一致
   （verdict 的正文只有 780 宽），但这是刻意的：两张图宽度相同，同比例的图高度才会相同 ——
   而高度一致正是不裁切之后唯一还能守住的一致性。**改成 780 会当场把它弄丢。**

   ⚠️ `<figure>` 浏览器默认带 40px 左右外边距。重置层（约定三）只清了 p / h / ul，
   没管 figure，不在这儿清掉的话图会比同块的卡片各窄 40px，而且只在有图的页面上才看得出来。

   ⚠️ `width: 100%` 会把小图拉伸。后台字段说明里写了「建议 ≥1180px」，
   但没有任何东西强制 —— 传了张 600 宽的图，前台就是放大一倍的糊图，不报错。 */
.ts-block__figure {
  margin: 34px 0 0;
  /* ⚠️ 2026-08-25 加的上限（用户：「上传的图片宽度过大了」，见 HANDOFF 二之四.8）。
     在这之前图跟着 .ts-wrap 走，1280 视口下实测 **1116×1116** —— 传上来的是 1200×1200
     的正方形，撑满就是一整屏那么高的大方块。

     ⚠️ **上限必须写在这一条上，两个图位共用。** 上面那段说的「两张图宽度相同，
     同比例的图高度才会相同」是这块唯一还守得住的一致性 —— 加上限没有破坏它
     （两张一起被压到同一个数），但**分别给两个图位设不同的上限就当场破坏了**。
     ⚠️ 窄屏不受影响：375 视口下 wrap 只有 343，`max-width` 根本够不着，
     图还是满栏 —— 这是 max-width 而不是 width 的用意。 */
  max-width: 730px;
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  overflow: hidden;
}
/* height: auto 是给 <img> 上那对 width/height 属性兜底的（images.php 会写死原图尺寸）：
   只设 width 时浏览器确实会按属性推出比例，但一旦属性和实际文件对不上就是拉伸变形。
   ⚠️ 这不是 object-fit，上面那条「不裁切」的决定没有被推翻 —— auto 是等比，不是裁。 */
.ts-block__figure img { width: 100%; height: auto; }

@media (max-width: 720px) {
  .ts-block__figure { margin-top: 26px; border-radius: var(--ts-r-md); }
}
/* 导语段：「最终评价」是整段总结，排在「推荐给谁 / 谁别买」之前。
   稍大一号、颜色更实，让它读起来是结论而不是又一段正文。 */
.ts-prose__lead { font-size: 18.5px; color: var(--ts-ink); }

/* 优缺点 */
.ts-pc { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.ts-pc__col {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 30px;
}
.ts-pc__col--pros {
  border-color: var(--ts-positive);
  background: linear-gradient(160deg, var(--ts-positive-pale) 0%, var(--ts-surface) 45%);
}
.ts-pc__col--cons { border-color: var(--ts-border-strong); }
.ts-pc__head {
  font-family: var(--ts-mono);
  font-size: 11px;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin-bottom: 20px;
}
.ts-pc__list li {
  font-size: 15.5px;
  line-height: 1.5;
  color: var(--ts-ink-soft);
  display: flex;
  gap: 11px;
  align-items: flex-start;
  margin-bottom: 13px;
}
.ts-pc__list li::before {
  content: '';
  flex: none;
  width: 15px; height: 15px;
  border-radius: 50%;
  margin-top: 4px;
  border: 1px solid var(--ts-positive);
  background: var(--ts-positive-pale);
}
.ts-pc__col--cons .ts-pc__list li::before {
  border-color: var(--ts-muted-light);
  background: var(--ts-bg);
}

/* 商品与价格 */
.ts-products { display: flex; flex-direction: column; gap: 12px; }
.ts-product {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 24px;
  align-items: center;
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md);
  padding: 22px 26px;
  transition: all var(--ts-ease);
}
.ts-product:hover { border-color: var(--ts-accent-soft); box-shadow: var(--ts-shadow-soft); }
.ts-product__name { font-family: var(--ts-serif); font-size: 21px; letter-spacing: -.4px; }
.ts-product__meta {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: .8px;
  color: var(--ts-muted); margin-top: 5px;
}
.ts-product__price { font-family: var(--ts-serif); font-size: 26px; letter-spacing: -.6px; }
.ts-product__cta {
  font-size: 14px; font-weight: 500;
  background: var(--ts-ink); color: var(--ts-surface);
  padding: 11px 22px; border-radius: var(--ts-r-pill);
  transition: all var(--ts-ease); white-space: nowrap;
}
.ts-product__cta:hover { background: var(--ts-accent); }

/* 深度评测 */
.ts-depth { display: grid; grid-template-columns: 1fr 340px; gap: 44px; align-items: start; }
.ts-depth__features { display: flex; flex-wrap: wrap; gap: 8px; margin: 22px 0; }
.ts-depth__feature {
  font-size: 13.5px; color: var(--ts-ink-soft);
  background: var(--ts-bg); border: .5px solid var(--ts-border);
  padding: 7px 14px; border-radius: var(--ts-r-pill);
}
.ts-specs {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 26px 28px;
}
.ts-specs__row {
  display: flex; justify-content: space-between; gap: 16px;
  padding: 13px 0; border-bottom: .5px solid var(--ts-border);
  font-size: 15px;
}
.ts-specs__row:last-child { border-bottom: 0; }
.ts-specs__row span { color: var(--ts-muted); }
.ts-specs__row b { font-weight: 500; text-align: right; }

/* 刻度评分 */
.ts-scales { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 44px; }
.ts-scale {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 28px;
}
.ts-scale__title { font-family: var(--ts-serif); font-size: 22px; letter-spacing: -.4px; margin-bottom: 10px; }
.ts-scale__track {
  height: 6px; background: var(--ts-border); border-radius: 3px;
  position: relative; margin: 30px 0 12px;
}
.ts-scale__track i {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--ts-accent); border: 3px solid var(--ts-surface);
  box-shadow: 0 2px 10px rgba(108, 98, 161, .4);
}
.ts-scale__ends {
  display: flex; justify-content: space-between;
  font-family: var(--ts-mono); font-size: 11px;
  letter-spacing: 1px; text-transform: uppercase; color: var(--ts-muted);
  margin-bottom: 18px;
}
.ts-scale p { font-size: 15px; color: var(--ts-muted); line-height: 1.7; margin-bottom: 12px; }

/* 客服 */
.ts-service { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-top: 28px; }
.ts-service__card {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 26px;
}
.ts-service__card h4 { font-family: var(--ts-serif); font-size: 20px; letter-spacing: -.3px; margin-bottom: 10px; }
.ts-service__card p { font-size: 14.5px; color: var(--ts-muted); line-height: 1.65; }

/* 引用的第三方买家评价 */
/* ⚠️ auto-fill 而不是 auto-fit。auto-fit 会把空轨道塌掉，让剩下的项撑满整行——
   折叠块里通常只剩一两条，实测那一条会横跨 1116px，而上面五张是三列各 361px，
   展开之后两段完全接不上。auto-fill 保留空轨道，列宽在两个容器里一致。
   项数 ≥ 列数时两者行为相同，所以主列表不受影响。 */
.ts-ureviews { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
/* ⚠️ 这里原来有一条「末行只剩一张时挪到中列」的规则
   （`@media (min-width:1011px) { .ts-ureviews > .ts-ureview:last-child:nth-child(3n+1) { grid-column: 2 } }`，
   2026-08-24 用户指出末行缺一格难看时加的）。

   **2026-08-27 用户改主意，要左对齐，规则已删。别再加回来** ——
   测试里有一条反向断言盯着它，加回来就是红。

   同一天顺带把条数从 6/10 改成 **6/12**：折叠区变成 6 条，3 列下正好 3+3，
   **根本不残**，所以也不需要给残的那一格打补丁了。
   ⚠️ 但库里录不满 12 条时折叠区照样会残（10 条 → 折叠 4 条 → 3+1），
   那时候末行就是左对齐 —— 这是用户要的形态，不是没做完。 */
.ts-ureview {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 26px;
  display: flex; flex-direction: column;
}
.ts-ureview__top { display: flex; align-items: center; gap: 13px; margin-bottom: 16px; }
.ts-ureview__avatar {
  width: 42px; height: 42px; border-radius: 50%;
  background: var(--ts-accent-pale); color: var(--ts-accent);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--ts-mono); font-size: 13px; font-weight: 700;
  flex: none;
}
.ts-ureview__name { font-size: 15.5px; font-weight: 500; }
.ts-ureview__date { font-family: var(--ts-mono); font-size: 10.5px; color: var(--ts-muted); letter-spacing: .6px; }
.ts-ureview__verified {
  font-family: var(--ts-mono); font-size: 9px; letter-spacing: 1px; text-transform: uppercase;
  background: var(--ts-positive-pale); color: var(--ts-positive);
  border: .5px solid var(--ts-positive); padding: 3px 9px; border-radius: var(--ts-r-pill);
}
.ts-ureview__stars { color: var(--ts-accent); font-size: 14px; letter-spacing: 2px; margin-bottom: 10px; }
.ts-ureview__title { font-family: var(--ts-serif); font-size: 19px; letter-spacing: -.3px; margin-bottom: 9px; }
/* 是 <blockquote>，浏览器默认给它 40px 的左右缩进，得收掉。
   ⚠️ 颜色用 --ts-ink-soft 而不是全站次要文本那个 --ts-muted。
   最初的理由是对比度（当时 --ts-muted 对 surface 只有 3.53:1），
   2026-08-25 那笔全站债已经还了（现在 4.72，过 AA）。
   **但这三处仍然不许退回 --ts-muted，理由换成了定位**：买家原话、出处行、区块说明
   是这块的主体内容，不是脚注；出处行更是整块合规的落点，没有它这些引用就成了
   「本站用户评价」。「刚好达标」和「该有的分量」是两件事。有断言守着。 */
.ts-ureview__text { font-size: 15px; color: var(--ts-ink-soft); line-height: 1.65; flex: 1; margin: 0; }
/* 出处行。这是整块合规的落点——没有它，这些引用就成了「本站用户评价」。
   所以它跟正文同色（--ts-ink-soft），只靠字号退一档，不靠调淡来退。 */
.ts-ureview__source {
  font-size: 13px;
  color: var(--ts-ink-soft);
  margin-top: 18px;
  padding-top: 15px;
  border-top: .5px solid var(--ts-border);
}
.ts-ureview__source a { color: var(--ts-ink); }
.ts-ureview__source a:hover { color: var(--ts-accent); }
/* 出处名 + 出处站点徽标（2026-08-25，主管要求）。徽标在文字右边，同一个链接里。
   ⚠️ 下划线画在**里层的 <span> 上，不是 <a> 上**：画在 <a> 上会顺着延伸到徽标底下
   （行内格式化上下文里 <img> 也吃祖先的 text-decoration），成一条从文字穿到图片的横线。 */
.ts-ureview__source-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  /* ⚠️ 必须 baseline，**不能是 bottom**（2026-08-25 踩过，见 HANDOFF 二之四.6）。
     「Published on」是 <p> 里的裸文字，链接才是这个 inline-flex 盒；
     盒里 align-items:center 把文字居中在盒高上。盒高 = 徽标高。
     `bottom` 把盒底对到行盒底，于是盒里的文字被抬高约「(盒高 − 行高)/2」——
     徽标 16px 时这个差是 0，看不出来；提到 32px 当场变成**浅色差 5px、深色差 7px**
     （深色还多了底板那 2px 上下内边距，所以更歪）。
     `baseline` 走的是「inline-flex 的基线 = 第一个 flex 项的基线」，
     第一个项正是那段链接文字，于是两段文字落在同一条基线上，徽标照旧居中于文字。
     ⚠️ 徽标尺寸再改的话，回去重量一遍两段文字的大写字母顶边（做法见六.34）。 */
  vertical-align: baseline;
}
.ts-ureview__source-link > span { text-decoration: underline; }
/* 高度定死、宽度自适应 —— 各家徽标比例差得很远（横向字标到方形徽章都有），
   ⚠️ **别改成定宽**：那会让横的被压扁、方的撑爆行高。这跟榜单 logo 是同一个道理，
      只是这里字号小得多，统一按高度收更省事（见 topsites_get_logo_shape_class() 那段）。
   ⚠️ max-width 是给「万一传了张超长横标」兜底的，别删。 */
.ts-ureview__source-logo {
  /* ⚠️ 必须 content-box。重置层给所有元素上了 border-box（约定），而深色模式下这个
     徽标要垫一块带内边距的底板 —— border-box 会把那 2px 上下内边距从 height 里**扣掉**，
     深色下就比浅色矮一截。当年按 16px 量的：浅色 49×16、深色 41×12，矮 25%。
     content-box 之后 height 永远是图本身的高度，内边距加在外面。
     ⚠️ 2026-08-25 从 16px 提到 32px（用户：「太小了 小的离谱」，见 HANDOFF 二之四.6）。
     真浏览器实测四种情形都是 32px 高、两套主题一样高（content-box 这条仍然成立）：
     浅色 1280 / 深色 1280 / 375 / 320。底板 2px 4px 不动，实测板子 36×40，比例正好。 */
  box-sizing: content-box;
  height: 32px;
  width: auto;
  max-width: 120px;
  object-fit: contain;
  flex: none;
  /* 深色模式下第三方徽标基本都是深色字 + 透明底，直接糊进卡片底色里。
     跟品牌 Logo 用同一块底板 token —— 浅色模式下它是 transparent，等于这两行不存在。 */
  background: var(--ts-logo-plate);
  border-radius: 3px;
}
/* 底板只在深色下才真的出现，出现时得给一点内边距，否则字贴着板边。
   ⚠️ 两套深色选择器都要写（@media 跟随系统 / data-ts-theme 手动切），少一套那套下会贴边。 */
@media (prefers-color-scheme: dark) {
  :root:not([data-ts-theme="light"]) .ts-ureview__source-logo { padding: 2px 4px; }
}
:root[data-ts-theme="dark"] .ts-ureview__source-logo { padding: 2px 4px; }
/* 区块标题下面那句说明。同样走 --ts-ink-soft：它讲的是「这些引用是哪来的」，
   属于必须读到的内容，不能当装饰性小字处理。见上一段那条 ⚠️。 */
.ts-block__note {
  font-size: 14.5px;
  color: var(--ts-ink-soft);
  line-height: 1.6;
  max-width: 68ch;
  margin: -6px 0 22px;
}
/* 历史类名，前台已经不再渲染 helpful 和 verified（见 inc/helpers/quoted-reviews.php
   顶部的口径说明）。样式留着只为旧数据不至于裸奔，别拿它们做新东西。 */
/* 2026-08-25：颜色从 --ts-muted-light 换成 --ts-muted。10.5px 是小字，
   muted-light 那一档（3.04）只够大字和描边用。这条规则前台不渲染，改它零风险，
   但留着一个违规样例会被下面那条守卫抓，也会被将来的人照抄。 */
.ts-ureview__helpful {
  font-family: var(--ts-mono); font-size: 10.5px; letter-spacing: .8px; color: var(--ts-muted);
  margin-top: 18px; padding-top: 15px; border-top: .5px solid var(--ts-border);
}

/* 第 6 条往后：默认收起，点了才展开。跟首页榜单折叠同一套规格。
   ⚠️ <details> 自己必须保持 display:block——设 flex/grid 会让 summary 和内容
   一并当作 flex item 渲染，收起功能当场失效。布局只能加在内层 __rest-list 上。 */
.ts-ureviews__rest { margin-top: 16px; }
.ts-ureviews__rest-list { padding-top: 16px; }
.ts-ureviews__rest-toggle {
  width: fit-content;
  margin: 0 auto;   /* details 是 block，居中只能靠这个 */
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  cursor: pointer;
  font-family: var(--ts-sans);
  font-size: 15px;
  font-weight: 500;
  color: var(--ts-ink);
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-pill);
  padding: 13px 28px;
  transition: border-color var(--ts-ease);
}
.ts-ureviews__rest-toggle::-webkit-details-marker { display: none; }
.ts-ureviews__rest-toggle::marker { content: ''; }
.ts-ureviews__rest-toggle:hover { border-color: var(--ts-muted); }
.ts-ureviews__rest-toggle:focus-visible { outline: 2px solid var(--ts-accent); outline-offset: 2px; }
.ts-ureviews__rest-toggle::after {
  content: '';
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.ts-ureviews__rest[open] .ts-ureviews__rest-toggle::after { transform: translateY(2px) rotate(-135deg); }
/* 两段文案同时在 DOM 里，靠 [open] 切换——隐藏的那段 display:none，
   屏幕阅读器不会把两句都念出来。 */
.ts-ureviews__rest-less { display: none; }
.ts-ureviews__rest[open] .ts-ureviews__rest-more { display: none; }
.ts-ureviews__rest[open] .ts-ureviews__rest-less { display: inline; }

/* 视频 */
.ts-video {
  display: block; border-radius: var(--ts-r-lg); overflow: hidden;
  border: .5px solid var(--ts-border); position: relative; margin-top: 28px;
}
.ts-video img { width: 100%; display: block; }
.ts-video::after {
  content: '';
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 68px; height: 68px; border-radius: 50%;
  background: var(--ts-glass); backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, .6);
}
/* 没有视频时只借这个框的样式，不要那颗播放按钮。
   数据层无条件把产品图填进 video.image，所以这个分支是常态而不是例外。 */
.ts-video--still::after { content: none; }

/* Q&A 复用首页 FAQ 组件，无需额外样式 */

@media (max-width: 1000px) {
  .ts-brand-hero__grid { grid-template-columns: 1fr; gap: 36px; }
  .ts-score-panel { position: static; }
  .ts-depth { grid-template-columns: 1fr; gap: 30px; }
  .ts-scales { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
  .ts-pc { grid-template-columns: 1fr; }
  .ts-product { grid-template-columns: 1fr; gap: 14px; }
  .ts-product__cta { text-align: center; }
  .ts-block { padding: 48px 0; }
}

/* ═══════════════════════════════════════════
   17. 通用页面骨架
   ═══════════════════════════════════════════ */
.ts-page-hero { padding: 56px 0 48px; position: relative; overflow: hidden; }
.ts-page-hero::after {
  content: '';
  position: absolute; top: -240px; right: -180px;
  width: 620px; height: 620px; border-radius: 50%;
  background: radial-gradient(circle, var(--ts-accent-pale) 0%, transparent 68%);
  opacity: .75; pointer-events: none;
}
.ts-page-hero__inner { position: relative; z-index: 2; }
.ts-page-hero__title {
  font-family: var(--ts-serif);
  font-size: clamp(40px, 5vw, 66px);
  line-height: 1.04; letter-spacing: -1.8px;
  margin-bottom: 18px; max-width: 900px;
}
.ts-page-hero__title em { font-style: italic; color: var(--ts-accent-soft); }
.ts-page-hero__desc { font-size: 17px; color: var(--ts-muted); line-height: 1.7; max-width: 640px; }

/* 主内容 + 侧栏 */
.ts-layout { display: grid; grid-template-columns: 1fr 320px; gap: 52px; align-items: start; }
/* 栅格子项默认 min-width: auto（= 最小内容宽度），子项里只要有一个撑不小的东西，
   整条列就被顶宽，而**顶宽的是列不是那个元素**，所以浏览器里怎么找都找不到谁干的。
   2026-08-25 第六场实测：第一篇文章的正文里放了张表（`.ts-prose table` 有
   `min-width: 480px`），375 视口下整页横向溢出 **140px** —— 而表格外面那层
   `.ts-table-wrap` 明明写着 `overflow-x: auto`。**滚不起来的原因就是这条**：
   列的最小宽度先被算成 480，滚动容器根本没机会缩。
   ⚠️ 这不是那篇文章的问题，是**任何宽内容**（表格 / 代码块 / 长 URL）都会撞上，
      而 /articles/ 一直是 0 篇，所以从来没暴露过。
   ⚠️ `.ts-layout` 只有两个文章模板在用，改这条碰不到首页和评测页。 */
.ts-layout > * { min-width: 0; }
.ts-sidebar { position: sticky; top: 96px; display: flex; flex-direction: column; gap: 20px; }
.ts-panel {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 26px;
}
.ts-panel__title {
  font-family: var(--ts-mono);
  font-size: 11px; letter-spacing: 1.8px; text-transform: uppercase;
  color: var(--ts-muted); margin-bottom: 18px;
}
.ts-rec {
  display: flex; align-items: center; gap: 14px;
  padding: 13px 0; border-bottom: .5px solid var(--ts-border);
  transition: opacity var(--ts-ease);
}
.ts-rec:last-child { border-bottom: 0; }
.ts-rec:hover { opacity: .7; }
/* 这处本来就是个带底的小方块，底色换成同一个 token 即可，
   不用上面那条 box-shadow 底板（它自带 padding，撑开会顶到相邻元素）。 */
.ts-rec__logo {
  width: 54px; height: 34px; object-fit: contain; flex: none;
  background: var(--ts-logo-plate); border-radius: var(--ts-r-sm); padding: 4px;
}
.ts-rec__name { font-size: 15px; font-weight: 500; }
.ts-rec__go { margin-left: auto; font-family: var(--ts-mono); font-size: 11px; color: var(--ts-accent); }

/* ═══════════════════════════════════════════
   18. 品牌总览 /mattress-reviews/
   ═══════════════════════════════════════════ */
.ts-bgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }
.ts-bcard {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 30px;
  display: flex; flex-direction: column;
  transition: all var(--ts-ease);
}
.ts-bcard:hover { transform: translateY(-4px); box-shadow: var(--ts-shadow-hover); border-color: var(--ts-accent-soft); }
.ts-bcard__logo {
  height: 46px; max-width: 160px; object-fit: contain; object-position: left center;
  margin-bottom: 20px;
}
.ts-bcard__name { font-family: var(--ts-serif); font-size: 26px; letter-spacing: -.5px; margin-bottom: 12px; }
.ts-bcard__summary { font-size: 15px; color: var(--ts-muted); line-height: 1.65; flex: 1; margin-bottom: 24px; }
.ts-bcard__actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.ts-bcard__cta {
  font-size: 14px; font-weight: 500;
  background: var(--ts-ink); color: var(--ts-surface);
  padding: 11px 22px; border-radius: var(--ts-r-pill);
  transition: all var(--ts-ease);
}
.ts-bcard__cta:hover { background: var(--ts-accent); }
.ts-bcard__link { font-size: 14px; color: var(--ts-muted); transition: color var(--ts-ease); }
.ts-bcard__link:hover { color: var(--ts-accent); text-decoration: underline; }

/* ═══════════════════════════════════════════
   19. 文章列表 /guides/
   ═══════════════════════════════════════════ */
.ts-featured {
  display: grid; grid-template-columns: 1.15fr 1fr; gap: 0;
  background: var(--ts-surface);
  border: .5px solid var(--ts-accent-soft);
  border-radius: var(--ts-r-lg);
  overflow: hidden;
  margin-bottom: 44px;
  transition: all var(--ts-ease);
}
.ts-featured:hover { box-shadow: var(--ts-shadow-hover); }
.ts-featured__img { min-height: 300px; background: linear-gradient(135deg, var(--ts-accent-pale) 0%, #D9D2EE 100%); }
.ts-featured__img img { width: 100%; height: 100%; object-fit: cover; }
.ts-featured__body { padding: 40px; display: flex; flex-direction: column; justify-content: center; }
.ts-featured__tag {
  font-family: var(--ts-mono); font-size: 10px; letter-spacing: 1.8px;
  text-transform: uppercase; color: var(--ts-accent); margin-bottom: 16px;
}
.ts-featured__title {
  font-family: var(--ts-serif); font-size: clamp(26px, 2.6vw, 36px);
  line-height: 1.15; letter-spacing: -.8px; margin-bottom: 14px;
}
.ts-featured__excerpt { font-size: 15.5px; color: var(--ts-muted); line-height: 1.7; margin-bottom: 20px; }
.ts-featured__meta {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: .8px; color: var(--ts-muted);
  display: flex; align-items: center; gap: 8px;
}

.ts-alist { display: flex; flex-direction: column; gap: 16px; }
.ts-aitem {
  display: grid; grid-template-columns: 220px 1fr; gap: 26px;
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  overflow: hidden;
  transition: all var(--ts-ease);
}
.ts-aitem:hover { transform: translateY(-3px); box-shadow: var(--ts-shadow-hover); border-color: var(--ts-border-strong); }
.ts-aitem__img { background: linear-gradient(135deg, var(--ts-bg) 0%, var(--ts-border) 100%); min-height: 160px; }
.ts-aitem__img img { width: 100%; height: 100%; object-fit: cover; }
.ts-aitem__body { padding: 26px 30px 26px 0; display: flex; flex-direction: column; justify-content: center; }
.ts-aitem__title { font-family: var(--ts-serif); font-size: 23px; line-height: 1.2; letter-spacing: -.5px; margin-bottom: 10px; }
.ts-aitem__excerpt { font-size: 15px; color: var(--ts-muted); line-height: 1.65; margin-bottom: 14px; }
/* 2026-08-25：这三条（.ts-featured__meta / .ts-aitem__meta / .ts-comment__reply-to）
   原来都挂 --ts-muted-light，是文章列表和评论区的元信息行（日期 / 阅读时长 / 回复对象），
   10.5～11px 的小字。这块页面现在 0 篇文章，肉眼验收看不到，是新加的
   ts-token-contrast 守卫抓出来的 —— 手工挑「哪几处该改」一定漏，别再靠眼睛数。 */
.ts-aitem__meta { font-family: var(--ts-mono); font-size: 10.5px; letter-spacing: .8px; color: var(--ts-muted); }

.ts-pager { display: flex; gap: 8px; justify-content: center; margin-top: 44px; flex-wrap: wrap; }
.ts-pager a, .ts-pager span {
  min-width: 42px; height: 42px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--ts-mono); font-size: 13px;
  border: .5px solid var(--ts-border); border-radius: var(--ts-r-pill);
  background: var(--ts-surface); color: var(--ts-muted);
  padding: 0 14px; transition: all var(--ts-ease);
}
.ts-pager a:hover { border-color: var(--ts-accent-soft); color: var(--ts-accent); }
.ts-pager .is-current { background: var(--ts-accent); color: var(--ts-on-accent); border-color: var(--ts-accent); }
.ts-pager .is-dots { border: 0; background: none; }

/* ═══════════════════════════════════════════
   20. 文章详情
   ═══════════════════════════════════════════ */
.ts-article__hero {
  border-radius: var(--ts-r-lg); overflow: hidden;
  margin-bottom: 36px; max-height: 440px;
  background: linear-gradient(135deg, var(--ts-accent-pale) 0%, #D9D2EE 100%);
}
.ts-article__hero img { width: 100%; height: 100%; object-fit: cover; }
.ts-article__title {
  font-family: var(--ts-serif); font-size: clamp(34px, 4.2vw, 54px);
  line-height: 1.08; letter-spacing: -1.4px; margin-bottom: 22px;
}
.ts-article__intro { font-size: 19px; color: var(--ts-muted); line-height: 1.7; margin-bottom: 30px; }
.ts-article__meta {
  display: flex; align-items: center; gap: 22px; flex-wrap: wrap;
  padding: 22px 0; border-top: .5px solid var(--ts-border); border-bottom: .5px solid var(--ts-border);
  margin-bottom: 40px;
}
.ts-author { display: flex; align-items: center; gap: 11px; }
.ts-author__avatar {
  width: 42px; height: 42px; border-radius: 50%; object-fit: cover;
  background: var(--ts-accent-pale); color: var(--ts-accent);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--ts-mono); font-size: 13px; font-weight: 700; flex: none;
}
.ts-author__role {
  display: block; font-family: var(--ts-mono); font-size: 10px;
  letter-spacing: 1.2px; text-transform: uppercase; color: var(--ts-muted);
}
.ts-author__name { font-size: 15px; font-weight: 500; }
.ts-article__stats { margin-left: auto; display: flex; gap: 16px; align-items: center; }
.ts-like {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--ts-mono); font-size: 12px; letter-spacing: .6px;
  background: var(--ts-surface); border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-pill); padding: 9px 18px;
  cursor: pointer; color: var(--ts-muted); transition: all var(--ts-ease);
}
.ts-like:hover, .ts-like.is-liked { border-color: var(--ts-accent-soft); color: var(--ts-accent); background: var(--ts-accent-pale); }

/* 正文排版 */
.ts-prose h2 {
  font-family: var(--ts-serif); font-size: 32px; letter-spacing: -.8px;
  line-height: 1.2; margin: 46px 0 18px; scroll-margin-top: calc(var(--ts-header-h) + 30px);
}
.ts-prose h3 { font-family: var(--ts-serif); font-size: 25px; letter-spacing: -.5px; margin: 34px 0 14px; }
.ts-prose ul, .ts-prose ol { margin: 0 0 20px; padding-left: 0; max-width: 780px; }
.ts-prose ul li, .ts-prose ol li {
  font-size: 17px; color: var(--ts-ink-soft); line-height: 1.75;
  margin-bottom: 10px; padding-left: 26px; position: relative;
}
.ts-prose ul li::before {
  content: ''; position: absolute; left: 2px; top: 11px;
  width: 7px; height: 7px; border-radius: 50%; background: var(--ts-accent-soft);
}
.ts-prose ol { counter-reset: ts-ol; }
.ts-prose ol li { counter-increment: ts-ol; }
.ts-prose ol li::before {
  content: counter(ts-ol) '.'; position: absolute; left: 0; top: 0;
  font-family: var(--ts-mono); font-size: 13px; color: var(--ts-accent);
}
.ts-prose a { color: var(--ts-accent); text-decoration: underline; text-underline-offset: 3px; }
.ts-prose img { border-radius: var(--ts-r-md); margin: 26px 0; }
.ts-prose blockquote {
  border-left: 2px solid var(--ts-accent-soft);
  padding: 6px 0 6px 24px; margin: 28px 0;
  font-family: var(--ts-serif); font-size: 22px; line-height: 1.5; color: var(--ts-ink);
}
/* Gutenberg 的 core/quote 一定会在 blockquote 里套一层 <p>，而上面那条 22px 衬线是写在
   blockquote 上的 —— `.ts-prose p` 和它同优先级(0,1,1)且直接命中那个 <p>，
   结果引用块渲染成 17px 正文，跟旁边的段落一模一样，等于这条样式白写。
   2026-08-25 第六场写第四篇文章时量出来的（后台用引用块，前台没反应）。
   ⚠️ 这里连 max-width 一起收掉：正文 780 的行宽是给长段落定的，引用只有一两行，
      再截一次就会在中间断得莫名其妙。 */
.ts-prose blockquote p {
  font: inherit;
  color: inherit;
  margin: 0;
  max-width: none;
}
.ts-table-wrap { overflow-x: auto; margin: 26px 0; }
.ts-prose table { width: 100%; border-collapse: collapse; font-size: 15px; min-width: 480px; }
.ts-prose th, .ts-prose td {
  padding: 13px 16px; text-align: left;
  border-bottom: .5px solid var(--ts-border);
}
.ts-prose th {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--ts-muted); font-weight: 400;
}

/* 正文内商品卡 */
.ts-inline-card {
  display: grid; grid-template-columns: 130px 1fr auto; gap: 22px; align-items: center;
  background: var(--ts-surface); border: .5px solid var(--ts-accent-soft);
  border-radius: var(--ts-r-lg); padding: 22px; margin: 30px 0;
}
.ts-inline-card__img {
  height: 96px; border-radius: var(--ts-r-md);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.ts-inline-card__img:not(:has(img)) {
  background: linear-gradient(135deg, var(--ts-accent-pale) 0%, #DDD6F2 100%);
}
.ts-inline-card__img img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
.ts-inline-card__name { font-family: var(--ts-serif); font-size: 22px; letter-spacing: -.4px; margin-bottom: 6px; }
.ts-inline-card__text { font-size: 14.5px; color: var(--ts-muted); line-height: 1.6; }

/* ═══════════════════════════════════════════
   21. 评论区
   ═══════════════════════════════════════════ */
.ts-comments { margin-top: 56px; padding-top: 44px; border-top: .5px solid var(--ts-border); }
.ts-comments__title { font-family: var(--ts-serif); font-size: 30px; letter-spacing: -.7px; margin-bottom: 28px; }
.ts-comment {
  background: var(--ts-surface); border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg); padding: 24px; margin-bottom: 14px;
}
.ts-comment__top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.ts-comment__avatar {
  width: 40px; height: 40px; border-radius: 50%; overflow: hidden;
  background: var(--ts-accent-pale); color: var(--ts-accent);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--ts-mono); font-size: 13px; font-weight: 700; flex: none;
}
.ts-comment__name { font-size: 15.5px; font-weight: 500; }
.ts-comment__date { font-family: var(--ts-mono); font-size: 10.5px; color: var(--ts-muted); letter-spacing: .6px; }
.ts-comment__body { font-size: 15.5px; color: var(--ts-ink-soft); line-height: 1.7; }
.ts-comment__body p { margin-bottom: 12px; }
.ts-comment__actions { display: flex; gap: 10px; margin-top: 16px; align-items: center; }
.ts-helpful {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: .6px;
  background: var(--ts-bg); border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-pill); padding: 7px 14px;
  cursor: pointer; color: var(--ts-muted); transition: all var(--ts-ease);
}
/* is-helpful / is-liked 是既有 main.js 切换的类名，此处沿用以复用现成的 AJAX 逻辑 */
.ts-helpful:hover, .ts-helpful.is-helpful { border-color: var(--ts-positive); color: var(--ts-positive); background: var(--ts-positive-pale); }
.ts-helpful.is-submitting, .ts-like.is-submitting { opacity: .55; pointer-events: none; }
.ts-comment__reply { font-size: 13px; color: var(--ts-accent); }
.ts-comment .children { margin: 16px 0 0 32px; padding-left: 20px; border-left: .5px solid var(--ts-border); }

.ts-cform { margin-top: 34px; }
.ts-comment__reply-to {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: .8px;
  text-transform: uppercase; color: var(--ts-muted); margin-bottom: 8px;
}
.ts-cform__privacy { font-size: 13px; color: var(--ts-muted); margin-top: 10px; }
.ts-cform__privacy a { color: var(--ts-accent); }

.ts-cform textarea, .ts-cform input[type="text"], .ts-cform input[type="email"], .ts-cform input[type="url"] {
  width: 100%; background: var(--ts-surface);
  border: .5px solid var(--ts-border); border-radius: var(--ts-r-md);
  padding: 14px 16px; font-family: var(--ts-sans); font-size: 15.5px; font-weight: 300;
  color: var(--ts-ink); transition: border-color var(--ts-ease); margin-bottom: 12px;
}
.ts-cform textarea { min-height: 130px; resize: vertical; }
.ts-cform textarea:focus, .ts-cform input:focus { border-color: var(--ts-accent-soft); }
.ts-cform textarea:focus-visible,
.ts-cform input:focus-visible { outline: 2px solid var(--ts-accent); outline-offset: 2px; }
.ts-cform .submit, .ts-cform input[type="submit"] {
  background: var(--ts-ink); color: var(--ts-surface); border: 0;
  border-radius: var(--ts-r-pill); padding: 13px 30px;
  font-family: var(--ts-sans); font-size: 15px; font-weight: 500; cursor: pointer;
  transition: all var(--ts-ease);
}
.ts-cform .submit:hover, .ts-cform input[type="submit"]:hover { background: var(--ts-accent); }
.ts-cform label { font-size: 14px; color: var(--ts-muted); display: block; margin-bottom: 6px; }

/* ═══════════════════════════════════════════
   22. 对比页
   ═══════════════════════════════════════════ */
.ts-ctable-wrap { overflow-x: auto; padding-bottom: 8px; }
.ts-ctable { width: 100%; border-collapse: separate; border-spacing: 12px 0; min-width: 760px; }
.ts-ctable th, .ts-ctable td { vertical-align: top; padding: 0; }
/* ⚠️ `height: 1px` 不是笔误，是让里面 .ts-ccol 的 `height: 100%` 生效的必要条件。
   坑在这里：百分比高度要求父级有**确定**高度。<th> 不写 height 时行高是表格算出来的，
   浏览器不拿它解析子元素的百分比 —— 实测 th 的计算高度已经是 497.42px，
   而 .ts-ccol 仍然只有 458.8（自身内容高），height:100% 被直接忽略。
   给 th 写 `height: 100%` 也没用（它自己的百分比又要靠行高解析，循环依赖）。
   而表格单元格的 height 是当**最小值**用的：写 1px，格子照样长到行高，
   但「有确定值」这个条件满足了，子元素的 100% 就有依据。
   三种写法都实测过（1px / 0 / 给名字预留两行），都能对齐；取 1px 是因为它是
   有名字的通用做法，以后有人看不懂能搜到。
   ⚠️ 用 :has() 精确限定在放卡片的那一行，别影响 Overview / Ratings 那些格子。 */
.ts-ctable th:has(.ts-ccol) { height: 1px; }
/* 卡片必须撑满单元格（2026-08-24）。
   `<th>` 的高度由表格行自动拉平，三列一致 —— 但 .ts-ccol 是格子**里**的一个块，
   高度只等于自身内容。名字换成两行的那张把行撑高，其余卡片的边框在更早的位置就结束，
   于是底边不齐。量出来（1280 视口、对比 3 款）：
     单元格 481.4 / 481.4 / 481.4   ← 表格已经拉平了
     卡片   442.8 / 442.8 / 481.4   ← 差 38.64px，正好一行
     分数   1020.1 / 1020.1 / 1058.7
   顶边本来是齐的（三张都 647.9），图片底边也齐（4:3 那条修复还稳）。
   错的只有卡片底边和分数行两处，都差一行。 */
.ts-ccol {
  background: var(--ts-surface); border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg); padding: 26px; text-align: center;
  height: 100%;
  display: flex;
  flex-direction: column;
}
.ts-ccol__logo { height: 40px; max-width: 130px; margin: 0 auto 16px; object-fit: contain; flex: none; }
.ts-ccol__name {
  font-family: var(--ts-serif); font-size: 23px; letter-spacing: -.4px; margin-bottom: 8px;
  /* 两行时断得均匀些，不留孤字（Sealy Posturepedic Exquisite Rosewood 是最长的一个）。 */
  text-wrap: balance;
}
/* 分数吸到卡片底部。卡片已经等高，所以分数行自动对齐 ——
   不管名字占 1 行、2 行还是以后出现 3 行，也不管对比 2 款还是 3 款。
   ⚠️ **不要改成给名字写死 `min-height: 2 行`**：对比 2 款时列宽 548，九款名字全是
   1 行，那样会每张都白留一行空；而以后真出现 3 行的名字又会重新错位。
   吸底没有这两个问题，也不用写任何魔法数字。 */
.ts-ccol__score {
  font-family: var(--ts-mono); font-size: 26px; font-weight: 700; color: var(--ts-accent);
  margin-top: auto;
}
/* ⚠️ 这里不能写死 height。列宽是跟着对比款数走的（th 的 width 是 100/款数 %），
   而固定高度 + cover 意味着框的比例随款数变，裁掉多少也跟着变：
     3 款 → 每列约 380px，框 2.9:1，勉强看得出是张床
     2 款 → 约 570px，框 4.4:1
     1 款 → 整表一列 1146px，框 8.8:1，源图只剩 10% 的高度，就是一条
   改成「宽度上限 + 固定比例」，裁切比例就跟款数无关了，三种情况裁的是同一块。
   340 是照三列时的内容宽（列宽 380 − 两边各 26 的 padding ≈ 328）定的，
   再宽在三列下就会溢出卡片。 */
.ts-ccol__img {
  width: min(100%, 340px);
  aspect-ratio: 4 / 3;
  /* .ts-ccol 现在是 flex column，默认 flex-shrink:1 会在纵向空间紧张时压扁这个框，
     把上面刚定好的 4:3 又弄没了。定死不许收缩。 */
  flex: none;
  border-radius: var(--ts-r-md);
  margin: 16px auto;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.ts-ccol__img:not(:has(img)) {
  background: linear-gradient(135deg, var(--ts-accent-pale) 0%, #DDD6F2 100%);
}
.ts-ccol__img img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
.ts-crow-label {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: 1.6px;
  text-transform: uppercase; color: var(--ts-muted);
  padding: 30px 0 12px !important; text-align: left;
}
/* Overview 的一格 = 一张卡片的一条横切片。
   每项一个 <tr>，靠表格行保证三列等高、横线对齐（见模板里的说明）。
   纵向 border-spacing 为 0，切片之间没有缝，叠起来就是一张卡。
   选择器带上 .ts-ctable td 是为了盖过上面 `.ts-ctable th, .ts-ctable td { padding: 0 }`。 */
.ts-ctable td.ts-cspec {
  position: relative;
  background: var(--ts-surface); padding: 0 20px;
  border-left: .5px solid var(--ts-border); border-right: .5px solid var(--ts-border);
}
/* 分隔线画在格子上而不是里面那个 div 上。
   div 只有文字那么高，一行的列和两行的列线的位置会差一截；
   而 `height: 100%` 在表格单元格里解析不出来（td 自身高度是 auto），撑不满。
   伪元素贴着格子底边，左右各缩 20px 与文字对齐。 */
.ts-ctable td.ts-cspec::after {
  content: ""; position: absolute; left: 20px; right: 20px; bottom: 0;
  border-bottom: .5px solid var(--ts-border);
}
.ts-ctable td.ts-cspec.is-last::after { content: none; }
.ts-ctable td.ts-cspec.is-first {
  border-top: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md) var(--ts-r-md) 0 0; padding-top: 11px;
}
.ts-ctable td.ts-cspec.is-last {
  border-bottom: .5px solid var(--ts-border);
  border-radius: 0 0 var(--ts-r-md) var(--ts-r-md); padding-bottom: 11px;
}
.ts-cspec .ts-specs__row { padding: 9px 0; border-bottom: 0; }

.ts-cell {
  background: var(--ts-surface); border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md); padding: 20px; font-size: 15px;
  color: var(--ts-ink-soft); line-height: 1.6; height: 100%;
}
.ts-cell ul li { display: flex; gap: 9px; align-items: flex-start; margin-bottom: 9px; font-size: 14.5px; }
.ts-cell ul li::before {
  content: ''; flex: none; width: 13px; height: 13px; border-radius: 50%;
  margin-top: 4px; border: 1px solid var(--ts-positive); background: var(--ts-positive-pale);
}
.ts-cell--cons ul li::before { border-color: var(--ts-muted-light); background: var(--ts-bg); }
.ts-cell__price { font-family: var(--ts-serif); font-size: 26px; letter-spacing: -.5px; }
.ts-cell__cta {
  display: block; text-align: center; margin-top: 14px;
  background: var(--ts-accent); color: var(--ts-on-accent); font-size: 14px; font-weight: 500;
  padding: 11px 18px; border-radius: var(--ts-r-pill); transition: all var(--ts-ease);
}
.ts-cell__cta:hover { background: var(--ts-accent-deep); }
.ts-tag {
  display: inline-block; font-family: var(--ts-mono); font-size: 10px;
  letter-spacing: .8px; text-transform: uppercase;
  background: var(--ts-bg); border: .5px solid var(--ts-border);
  padding: 5px 11px; border-radius: var(--ts-r-pill); margin: 0 5px 5px 0; color: var(--ts-muted);
}

/* ═══════════════════════════════════════════
   23. 分类页
   ═══════════════════════════════════════════ */
.ts-cat-hero { padding: 48px 0 44px; position: relative; overflow: hidden; }
.ts-cat-hero__grid { display: grid; grid-template-columns: 1fr 420px; gap: 48px; align-items: center; position: relative; z-index: 2; }
.ts-cat-hero__img { border-radius: var(--ts-r-lg); overflow: hidden; background: var(--ts-accent-pale); }
.ts-cat-hero__updated {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: 1px;
  color: var(--ts-muted); margin-top: 18px;
}
.ts-tabs { border-bottom: .5px solid var(--ts-border); background: var(--ts-surface); }
.ts-tabs__viewport { position: relative; }
.ts-tabs__list {
  display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; padding: 12px 0;
  scroll-behavior: smooth;
  /* 键盘 Tab 到被箭头盖住的那一项时，别停在箭头底下 */
  scroll-padding-inline: 48px;
}
.ts-tabs__list::-webkit-scrollbar { display: none; }

/* 翻页箭头。默认带 hidden 属性，JS 量到确实溢出才去掉——
   没有 JS 时按钮不出现，标签条仍可触屏/触控板横向滚动，不会出现按了没反应的按钮。 */
.ts-tabs__arrow {
  position: absolute; top: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  width: 64px; padding: 0; border: 0; cursor: pointer;
  color: var(--ts-ink); background: none;
}
.ts-tabs__arrow[hidden] { display: none; }
.ts-tabs__arrow--prev { left: 0; justify-content: flex-start;
  background: linear-gradient(to right, var(--ts-surface) 42%, rgba(var(--ts-surface-rgb), 0)); }
.ts-tabs__arrow--next { right: 0; justify-content: flex-end;
  background: linear-gradient(to left,  var(--ts-surface) 42%, rgba(var(--ts-surface-rgb), 0)); }
/* 左箭头不另画图标，右尖角翻过来 */
.ts-tabs__arrow--prev .ts-icon { transform: scaleX(-1); }
.ts-tabs__arrow .ts-icon {
  width: 22px; height: 22px; border-radius: var(--ts-r-pill);
  background: var(--ts-surface); box-shadow: 0 0 0 1px var(--ts-border);
  transition: all var(--ts-ease);
}
.ts-tabs__arrow:hover .ts-icon { color: var(--ts-accent); box-shadow: 0 0 0 1px var(--ts-accent); }
.ts-tabs__arrow:focus-visible { outline: 2px solid var(--ts-accent); outline-offset: -2px; border-radius: var(--ts-r-pill); }
/* 已经到头了：留在原位但不可点，免得整行按钮忽隐忽现。
   ⚠️ pointer-events: none 是必须的，不是锦上添花。opacity:0 只是看不见——
   元素照样占着两侧各 64px 的整块高度参与命中测试，disabled 的 button 既不触发
   点击、又不让点击穿透下去，结果就是第一个（和最后一个）标签的大半个身子点不动。
   实测：滚到最左端时「Top Rated」102px 宽里有 64px 被这个隐形按钮吃掉，
   只有最右边那三成点得到。 */
.ts-tabs__arrow:disabled { cursor: default; opacity: 0; pointer-events: none; }

/* 箭头是给没法横滑的鼠标用户补的。触屏上横滑本来就是自然操作，
   而两侧各 64px 在 375px 的屏上要吃掉三分之一宽度，所以只给能悬停的设备显示。 */
@media (hover: none) {
  .ts-tabs__arrow { display: none; }
  .ts-tabs__list { scroll-padding-inline: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ts-tabs__list { scroll-behavior: auto; }
}
.ts-tabs__list a,
.ts-tabs__list span {
  white-space: nowrap; font-size: 14.5px; color: var(--ts-muted);
  padding: 9px 18px; border-radius: var(--ts-r-pill); transition: all var(--ts-ease);
}
.ts-tabs__list a:hover { background: var(--ts-accent-pale); color: var(--ts-accent); }
.ts-tabs__list a.is-active { background: var(--ts-accent); color: var(--ts-on-accent); }
/* 对比页选择器：药丸是开关，选中的再点一次会移出，所以悬停时要给个「可以去掉」的暗示。 */
.ts-tabs__list a.is-active:hover { background: var(--ts-accent); color: var(--ts-on-accent); opacity: .78; }
/* 选满 3 款（未选中项）或只剩 1 款（那一款自己）时点不动，渲染成非链接。 */
.ts-tabs__list .is-locked { opacity: .42; cursor: not-allowed; }
.ts-tabs__list span.is-active.is-locked { background: var(--ts-accent); color: var(--ts-on-accent); }
.ts-lineup { margin-bottom: 52px; }
.ts-lineup__title {
  font-family: var(--ts-serif); font-size: clamp(26px, 3vw, 36px);
  letter-spacing: -.7px; margin-bottom: 26px;
}
.ts-featured__avatar {
  width: 26px; height: 26px; border-radius: 50%; object-fit: cover;
  background: var(--ts-accent-pale); color: var(--ts-accent);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--ts-mono); font-size: 10px; margin: 0;
}
.ts-related { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 20px; }


/* ═══════════════════════════════════════════
   文章内嵌卡片（短代码 / 区块）
   —— 产品卡 ts-pcard、推荐位 ts-picks / ts-pick
   ═══════════════════════════════════════════ */
.ts-pcard {
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 28px;
  margin: 32px 0;
  box-shadow: var(--ts-shadow-soft);
}
.ts-pcard__name {
  font-family: var(--ts-serif);
  font-size: 26px;
  letter-spacing: -.5px;
  line-height: 1.2;
  margin: 0 0 20px;
}
.ts-pcard__main { display: grid; grid-template-columns: 220px 1fr; gap: 26px; align-items: start; }
.ts-pcard__img {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: var(--ts-r-md);
  overflow: hidden;
}
.ts-pcard__img:not(:has(img)) { background: var(--ts-bg); }
.ts-pcard__img img { width: 100%; height: 100%; object-fit: cover; object-position: center; margin: 0; }
.ts-pcard__specs { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin: 0; }
.ts-pcard__spec { display: flex; gap: 10px; align-items: flex-start; }
.ts-pcard__spec-icon { width: 20px; height: 20px; flex: none; margin: 2px 0 0; opacity: .7; }
.ts-pcard__spec dt {
  font-family: var(--ts-mono);
  font-size: 10.5px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin-bottom: 3px;
}
.ts-pcard__spec dd { font-size: 15px; color: var(--ts-ink); margin: 0; }
.ts-pcard__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 22px; }
.ts-pcard__offer {
  font-size: 14.5px;
  color: var(--ts-accent);
  background: var(--ts-accent-pale);
  border-radius: var(--ts-r-pill);
  padding: 9px 16px;
  text-decoration: none;
}
.ts-pcard__offer:hover { background: var(--ts-accent-soft); color: var(--ts-surface); }
.ts-pcard__cta {
  font-size: 15px;
  font-weight: 500;
  padding: 11px 22px;
  border-radius: var(--ts-r-pill);
  text-decoration: none;
  background: var(--ts-accent);
  color: var(--ts-on-accent);
  transition: all var(--ts-ease);
}
.ts-pcard__cta:hover { background: var(--ts-accent-deep); transform: translateY(-1px); color: var(--ts-on-accent); }
.ts-pcard__benefits { margin-top: 22px; padding-top: 20px; border-top: .5px solid var(--ts-border); }
.ts-pcard__benefits-head {
  font-family: var(--ts-mono);
  font-size: 10.5px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ts-muted);
  display: block;
  margin-bottom: 12px;
}
.ts-pcard__benefit-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.ts-pcard__benefit {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13.5px;
  color: var(--ts-ink-soft);
  background: var(--ts-bg);
  border-radius: var(--ts-r-pill);
  padding: 7px 14px;
  margin: 0;
}
.ts-pcard__benefit img { width: 16px; height: 16px; margin: 0; border-radius: 0; }
.ts-pcard__more { margin-top: 22px; }
.ts-pcard__more summary {
  cursor: pointer;
  font-size: 14.5px;
  color: var(--ts-accent);
  list-style: none;
}
.ts-pcard__more summary::-webkit-details-marker { display: none; }
.ts-pcard__more summary::after { content: ' +'; font-family: var(--ts-mono); }
.ts-pcard__more[open] summary::after { content: ' \2212'; }
.ts-pcard__more-body { padding-top: 16px; }
.ts-pcard__more-body h4 {
  font-family: var(--ts-mono);
  font-size: 10.5px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin: 18px 0 8px;
}
.ts-pcard__more-body h4:first-child { margin-top: 0; }
.ts-pcard__more-body p { font-size: 15.5px; color: var(--ts-ink-soft); line-height: 1.75; margin: 0 0 8px; }

.ts-picks {
  background: var(--ts-bg);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-lg);
  padding: 26px;
  margin: 32px 0;
}
.ts-picks__title {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--ts-serif);
  font-size: 24px;
  letter-spacing: -.5px;
  margin: 0 0 18px;
}
.ts-picks__icon { color: var(--ts-accent); display: inline-flex; }
.ts-picks__icon svg { width: 24px; height: 24px; }
.ts-pick {
  display: grid;
  grid-template-columns: 92px 1fr auto;
  gap: 18px;
  align-items: center;
  background: var(--ts-surface);
  border: .5px solid var(--ts-border);
  border-radius: var(--ts-r-md);
  padding: 16px;
}
.ts-pick + .ts-pick { margin-top: 12px; }
.ts-pick__img {
  display: block;
  aspect-ratio: 1;
  border-radius: var(--ts-r-sm);
  overflow: hidden;
}
.ts-pick__img:not(:has(img)) { background: var(--ts-bg); }
.ts-pick__img img { width: 100%; height: 100%; object-fit: cover; object-position: center; margin: 0; }
.ts-pick__name { font-size: 18px; font-weight: 500; margin: 0 0 6px; }
.ts-pick__score {
  display: inline-block;
  font-family: var(--ts-mono);
  font-size: 12px;
  color: var(--ts-accent);
  background: var(--ts-accent-pale);
  border-radius: var(--ts-r-pill);
  padding: 3px 10px;
}
.ts-pick__feats { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 8px 0 0; padding: 0; list-style: none; }
.ts-pick__feats li { font-size: 13.5px; color: var(--ts-muted); margin: 0; padding: 0; }
.ts-pick__side { display: flex; flex-direction: column; gap: 8px; text-align: center; }
.ts-pick__offer { font-size: 13px; color: var(--ts-muted); text-decoration: none; }
.ts-pick__offer:hover { color: var(--ts-accent); }
.ts-pick__cta {
  font-size: 14.5px;
  font-weight: 500;
  padding: 10px 18px;
  border-radius: var(--ts-r-pill);
  text-decoration: none;
  background: var(--ts-ink);
  color: var(--ts-surface);
  white-space: nowrap;
  transition: all var(--ts-ease);
}
.ts-pick__cta:hover { background: var(--ts-ink-soft); transform: translateY(-1px); color: var(--ts-surface); }

/* 这些卡片会落在 .ts-prose 里，正文的列表样式要在卡片内部失效 */
.ts-pcard ul li::before,
.ts-pick ul li::before { content: none; }
.ts-pcard ul li,
.ts-pick ul li { padding-left: 0; }
.ts-pcard a,
.ts-pick a { text-decoration: none; }

@media (max-width: 1000px) {
  .ts-layout { grid-template-columns: 1fr; gap: 40px; }
  .ts-sidebar { position: static; flex-direction: row; flex-wrap: wrap; }
  .ts-sidebar .ts-panel { flex: 1 1 280px; }
  /* 评分面板的高度上限只为解决**粘性**面板高过视口时下缘够不着（见 .ts-score-panel）。
     这个断点以下侧栏已经是静态的、面板是整行宽，没有那个问题；
     再留着上限就变成「页面里套一个可滚动的盒子」，比原问题更糟。这里撤掉。 */
  .ts-score-panel { max-height: none; overflow: visible; }
  .ts-featured { grid-template-columns: 1fr; }
  .ts-featured__img { min-height: 220px; }
  .ts-cat-hero__grid { grid-template-columns: 1fr; gap: 30px; }
}
@media (max-width: 720px) {
  .ts-aitem { grid-template-columns: 1fr; }
  .ts-aitem__body { padding: 24px; }
  .ts-aitem__img { min-height: 180px; }
  .ts-featured__body { padding: 28px; }
  .ts-inline-card { grid-template-columns: 1fr; text-align: center; }
  .ts-article__meta { gap: 14px; }
  .ts-article__stats { margin-left: 0; width: 100%; }
}
@media (max-width: 720px) {
  .ts-pcard__main { grid-template-columns: 1fr; }
  .ts-pcard__img { max-width: 240px; }
  .ts-pick { grid-template-columns: 72px 1fr; }
  .ts-pick__side { grid-column: 1 / -1; flex-direction: row; align-items: center; justify-content: space-between; }
}

/* ═══════════════════════════════════════════
   两两对比页 /compare/a-vs-b/（2026-08-26 第八场）
   模板 template-versus.php · 数据 inc/data/versus.php

   ⚠️ 这一节**一个色值都不写死**，全部走 token —— 三套调色板自动跟随。
      文件顶部那条「任何组件内不得写死色值」在这里同样有效，
      而且 tests 里有两条守卫在扫（写死的白字 / 紫系 rgba）。
   ⚠️ 也**没有任何组件级深色规则** —— 只要颜色都从 token 来，
      深浅两套就自动对了，不需要写 @media 孪生规则（那会触发 ts-dark-scope-twins 的检查）。
   ═══════════════════════════════════════════ */

/* 两列对垒。中间那个 vs 在窄屏收起来 —— 竖排时它夹在两张卡中间反而碍事。 */
.ts-vs__cols {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 20px;
  /* ⚠️ 2026-08-28 从 start 改成 stretch。原来两张卡各自按内容高，
     而卡里那张产品图是 height:auto —— 九款原图比例从 0.800（Onebed 竖图）
     到 1.874（Ecosa 宽图），于是同一页两张卡能差 186px 高（Onebed vs Ecosa）。
     36 个对比页都有这个问题，不是某一页的事。 */
  align-items: stretch;
}
.ts-vs__col {
  background: var(--ts-surface);
  border: 1px solid var(--ts-border);
  border-radius: var(--ts-r-lg, 16px);
  padding: 24px;
  text-align: center;
  min-width: 0;
  /* 纵向 flex 是为了让底部那个「Read the full … review」按钮沉到卡底对齐，
     否则两张卡等高之后按钮会各自吊在文字下面、左右不在一条线上。 */
  display: flex;
  flex-direction: column;
}
.ts-vs__divider {
  align-self: center;
  font-family: var(--ts-mono);
  font-size: 13px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ts-muted);
  padding: 0 4px;
}
.ts-vs__img {
  width: 100%;
  max-width: 260px;
  /* ⚠️ 固定 4:3 + contain，别改成 height:auto 或 cover：
     · height:auto → 卡高被原图比例牵着走，就是 2026-08-28 修的那个毛病
     · cover       → 会裁掉产品，本站对产品图一律不裁切
     4:3 是九款比例的**中位数**（0.800 / 1.000 / 1.000 / 1.333 / 1.333 /
     1.656 / 1.777 / 1.779 / 1.874），选它留白总量最小。
     换图导致比例分布变了就回来重算中位数。 */
  aspect-ratio: 4 / 3;
  object-fit: contain;
  margin: 0 auto 16px;
  display: block;
  border-radius: var(--ts-r-md, 12px);
}
.ts-vs__name {
  font-family: var(--ts-serif);
  font-size: 22px;
  line-height: 1.25;
  margin: 0 0 6px;
  color: var(--ts-ink);
}
.ts-vs__claim {
  font-size: 14px;
  color: var(--ts-muted);
  margin: 0 0 14px;
}
.ts-vs__score {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 2px;
  margin-bottom: 8px;
}
.ts-vs__score b {
  font-family: var(--ts-mono);
  font-size: 38px;
  /* ⚠️ 400 不是随手写的，**700 会让数字 4 看起来像被削掉了顶**。
     Space Mono 只有 400 / 700 两个字重，700 在这个字号下把 `4` 左上那个
     三角缺口糊满，顶端糊成一个钝楔形 —— 用户 2026-08-28 两次报「4 上面被切了一部分」，
     就是这个。**它不是 CSS 裁的**：查过祖先链的 overflow / contain / clip-path /
     mask / filter / transform 全部没有，同一串文字在 lh:1 / lh:normal / lh:1.6 下
     渲染完全一致，150px 下字形也是完整的 —— 纯粹是粗体在小字号下的栅格化。
     判据：把 700 / 500 / 400 并排渲染，400 的缺口张开、顶端是干净的尖。
     字号 34 → 38 是为了补回降字重掉的视觉分量。
     ⚠️ 改回 700 之前先在真实页面上看含 4 的分数（如 9.4），别只看 9.3。 */
  font-weight: 400;
  color: var(--ts-accent);
  line-height: 1;
  /* 这 2px 是另一件事，跟上面那个「看起来被切」无关：Space Mono 的 `4` 墨迹
     比步进宽度宽 0.19px（34px 下），而 <b> 是 flex 项、盒宽按步进算，
     墨迹正好压在盒子右沿。肉眼看不出来，留着是不让它贴边。 */
  padding-right: 2px;
}
.ts-vs__score span { font-size: 13px; color: var(--ts-muted); }
.ts-vs__price {
  font-family: var(--ts-mono);
  font-size: 15px;
  color: var(--ts-ink-soft);
  margin: 0 0 16px;
}
/* margin-top:auto 把按钮推到卡底 —— 配合上面的 stretch，两张卡的按钮才在一条线上。 */
.ts-vs__review { width: 100%; justify-content: center; margin-top: auto; }

/* 差异表。⚠️ 高亮那一格由模板按 facts 的 winner 加 .is-winner，
   CSS 只负责画，**不参与判断**。 */
.ts-vs__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 15px;
  min-width: 480px;
}
.ts-vs__table th,
.ts-vs__table td {
  padding: 13px 16px;
  text-align: left;
  border-bottom: 1px solid var(--ts-border);
  vertical-align: top;

  /* ⚠️ `position: relative` 不是装饰，是**修横向溢出的**。
     单元格里有读屏专用的 `<span class="ts-sr">(better of the two)</span>`
     （用来把「这一格被高亮了」这件事讲给看不见颜色的人听，WCAG 1.4.1）。
     `.ts-sr` 是 `position: absolute`，**没有定位祖先时它按初始包含块定位** ——
     于是它从 `.ts-ctable-wrap` 的横向滚动容器里逃出去，
     落在文档坐标 450px 处，把整页 scrollWidth 从 375 撑到 451。
     实测：375 宽下横向溢出 76px。**表格本身是被正确兜住的**（335 宽），
     肉眼只看到页面能左右晃一点，根本想不到是一个 1×1 的隐藏 span 干的。
     加了这一行之后四个宽度全部回到 0。
     ⚠️ 这跟「visibility:hidden 的元素照样算进滚动区域」是同一类坑，
        见 TOOL-TRAPS 六.54。**别删这一行。** */
  position: relative;
}
.ts-vs__table thead th {
  font-family: var(--ts-mono);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ts-muted);
  background: var(--ts-bg);
}
.ts-vs__table tbody th {
  font-weight: 600;
  color: var(--ts-ink);
  width: 30%;
}
.ts-vs__table td { color: var(--ts-ink-soft); }
.ts-vs__table td.is-winner {
  background: var(--ts-positive-pale);
  color: var(--ts-ink);
  font-weight: 600;
}
.ts-vs__table tbody tr:last-child th,
.ts-vs__table tbody tr:last-child td { border-bottom: 0; }

.ts-vs__note {
  font-size: 14px;
  color: var(--ts-muted);
  margin: 14px 0 0;
  max-width: 68ch;
}

/* 「选谁」两栏 */
.ts-vs__picks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}
.ts-vs__pick {
  background: var(--ts-surface);
  border: 1px solid var(--ts-border);
  border-radius: var(--ts-r-lg, 16px);
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
.ts-vs__pick h3 {
  font-family: var(--ts-serif);
  font-size: 20px;
  margin: 0;
  color: var(--ts-ink);
}
.ts-vs__pick p { margin: 0; color: var(--ts-ink-soft); font-size: 15px; line-height: 1.6; }
.ts-vs__pick .ts-btn { margin-top: auto; align-self: flex-start; }

/* 优缺点两栏 */
.ts-vs__pc {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}
.ts-vs__pc-col { min-width: 0; }
.ts-vs__pc-col h3 {
  font-family: var(--ts-serif);
  font-size: 20px;
  margin: 0 0 12px;
  color: var(--ts-ink);
}
.ts-vs__pc-head {
  font-family: var(--ts-mono);
  font-size: 11.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ts-muted);
  margin: 16px 0 8px;
}
.ts-vs__pros,
.ts-vs__cons {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.ts-vs__pros li,
.ts-vs__cons li {
  position: relative;
  padding-left: 22px;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--ts-ink-soft);
}
.ts-vs__pros li::before,
.ts-vs__cons li::before {
  position: absolute;
  left: 0;
  top: 0;
  font-weight: 700;
}
.ts-vs__pros li::before { content: '+'; color: var(--ts-positive, var(--ts-accent)); }
.ts-vs__cons li::before { content: '\2013'; color: var(--ts-muted); }

/* 相关对比 */
.ts-vs__related {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px;
}
.ts-vs__related a {
  display: block;
  padding: 12px 16px;
  border: 1px solid var(--ts-border);
  border-radius: var(--ts-r-md, 12px);
  background: var(--ts-surface);
  color: var(--ts-ink);
  font-size: 14.5px;
  text-decoration: none;
}
.ts-vs__related a:hover {
  border-color: var(--ts-accent-soft);
  color: var(--ts-accent);
}

@media (max-width: 760px) {
  .ts-vs__cols { grid-template-columns: 1fr; }
  .ts-vs__divider { display: none; }
}

/* ── 对比页选款控件（ts-cpick）─────────────────────────────────────
   2026-08-28 从 ts-tabs 换过来的。ts-tabs 是「横向滚动 + 翻页箭头」，
   项数固定时没问题（分类页八个），但对比页的项数跟着产品数长——
   录到 11 款时最后几款要点好几次箭头才够得着。
   现在：已选的留成药丸（最多就 TOPSITES_COMPARISON_MAX_COLUMNS 个，是常数），
   未选的收进 <select>，**宽度不随产品数变**。 */
.ts-cpick { border-bottom: .5px solid var(--ts-border); background: var(--ts-surface); }
.ts-cpick__inner {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px;
  padding-block: 12px;
}
.ts-cpick__chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; }
.ts-cpick__label {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--ts-muted); margin-right: 2px;
}
.ts-cpick__chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 14px; border-radius: var(--ts-r-pill);
  font-size: 14px; line-height: 1.2; text-decoration: none;
  border: .5px solid var(--ts-border); background: var(--ts-bg); color: var(--ts-text);
}
/* 已选的用主色实底——它和下面那个下拉框是两种动作（去掉 / 加上），
   靠颜色区分比靠位置区分可靠。 */
.ts-cpick__chips .ts-cpick__chip {
  background: var(--ts-accent); border-color: var(--ts-accent); color: var(--ts-on-accent);
}
.ts-cpick__chips .ts-cpick__chip.is-locked { opacity: .72; cursor: default; }
.ts-cpick__x { font-size: 15px; line-height: 1; opacity: .8; }
.ts-cpick__chips .ts-cpick__chip.is-locked .ts-cpick__x { display: none; }
.ts-cpick__add { margin-left: auto; }
/* 40px 高是为了和药丸的点击高度一致；min-width 让「Add a mattress…」不被挤成两行。
   ⚠️ 别给 max-width——选项文字最长的是 Sealy 那条，收起来时显示的是占位文案，不会撑宽。 */
.ts-cpick__select {
  min-width: 220px; height: 40px; padding: 0 34px 0 14px;
  border-radius: var(--ts-r-pill); border: .5px solid var(--ts-border);
  background: var(--ts-bg); color: var(--ts-text);
  font-family: inherit; font-size: 14px;
  appearance: none;
  /* 自绘箭头：原生的在深色下会跟着系统走，跟站上的调色板对不上。 */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 18px) 17px, calc(100% - 13px) 17px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.ts-cpick__select:disabled { opacity: .55; }
.ts-cpick__fallback { display: flex; flex-wrap: wrap; gap: 8px; }
/* 没有 JS 时才会看到这一排；有 JS 时 initComparePicker 把它 hidden 掉。 */
.ts-cpick__fallback[hidden] { display: none; }
@media (max-width: 720px) {
  .ts-cpick__add { margin-left: 0; width: 100%; }
  .ts-cpick__select { width: 100%; min-width: 0; }
}

/* ── /compare/ 的「挑两款」选择器（ts-vspick）──────────────────────
   2026-08-28 加的。原来那一块是 55 条链接铺开（<details open>），
   而这个数按 N(N-1)/2 涨 —— 再录两款就是 78 条。
   两个框的宽度不随款数变，所以选项里可以放**床垫全名**。
   ⚠️ 那 55 条没删，只是默认收起：它是没有 JS 时的唯一路径，也是 versus 页的站内链接来源。 */
.ts-vspick { margin: 18px 0 22px; }
.ts-vspick[hidden] { display: none; }
.ts-vspick__row {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px;
}
.ts-vspick__field { display: flex; flex-direction: column; gap: 6px; flex: 1 1 260px; min-width: 0; }
.ts-vspick__lab {
  font-family: var(--ts-mono); font-size: 11px; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--ts-muted);
}
/* 跟 ts-cpick__select 同一套外观，只是这里要撑满列宽（全名比「Add a mattress…」长得多）。 */
.ts-vspick__select {
  width: 100%; height: 44px; padding: 0 34px 0 14px;
  border-radius: var(--ts-r-md); border: .5px solid var(--ts-border);
  background: var(--ts-bg); color: var(--ts-text);
  font-family: inherit; font-size: 15px;
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 18px) 19px, calc(100% - 13px) 19px;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
/* 「vs」压在两个框之间，跟框底对齐（父级 align-items:flex-end）。 */
.ts-vspick__vs {
  font-family: var(--ts-serif); font-size: 20px; color: var(--ts-muted);
  padding-bottom: 10px; flex: none;
}
.ts-vspick__go { flex: none; height: 44px; }
.ts-vspick__go:disabled { opacity: .5; cursor: not-allowed; }
.ts-vspick__note { margin: 8px 0 0; font-size: 13px; color: var(--ts-muted); min-height: 1em; }
/* 收起来的那份索引现在是次要入口，summary 弱化一点，别跟上面的选择器抢。 */
.ts-vs__index > summary { cursor: pointer; color: var(--ts-muted); font-size: 14px; }
@media (max-width: 640px) {
  /* 窄屏下「vs」占一整行会很怪，藏掉——两个框上下排列，语义靠 label 撑着。 */
  .ts-vspick__vs { display: none; }
  .ts-vspick__field { flex: 1 1 100%; }
  .ts-vspick__go { width: 100%; }
}
