/* ============================================================
   开心菜园 · 主样式表
   目录：
     01 变量与重置        02 天空背景      03 通用组件
     04 顶部 HUD          05 布局          06 地块网格
     07 卡片/侧栏         08 弹窗          09 商店/仓库/邻居
     10 提示与飘字        11 登录注册页    12 响应式与动画
   ============================================================ */

/* ---------------- 01 变量与重置 ---------------- */
:root {
    --green-1: #4ade80;
    --green-2: #22c55e;
    --green-3: #16a34a;
    --green-4: #0f5132;
    --gold-1: #fde047;
    --gold-2: #fbbf24;
    --gold-3: #f59e0b;
    --sky-1: #a5e4ff;
    --sky-2: #cdf3d0;
    --soil-1: #ab7850;
    --soil-2: #8d5c36;
    --soil-3: #674424;
    --ink: #16311f;
    --ink-soft: #40634f;
    /* ★ v2.93.3 全站对比度提标：原 #7f9c8c 压白底只有 2.98:1 ✗。
       同色相（绿灰）压暗到 #456a58 = 6.08:1（白）/ 5.19:1（最暗的浅灰底）✓。
       只改明度，色相仍是原来的绿灰 ✓ */
    --muted: #456a58;
    /* ★ v2.93.3 文字专用色：**只给 `color:` 用**，背景/边框/渐变仍用原 token ✓
       —— 原来的 --green-3 #16a34a (3.30:1) / --gold-3 #f59e0b (2.15:1) /
       --danger #ef4444 (3.76:1) 当文字都 <4.5；同色相压暗后分别 5.02 / 7.09 / 6.47 ✓ */
    --green-ink: #15803d;
    --gold-ink: #8a5a00;
    --danger-ink: #b91c1c;
    --danger: #ef4444;
    --panel: rgba(255, 255, 255, .78);
    --panel-line: rgba(255, 255, 255, .92);
    --shadow-sm: 0 4px 14px rgba(23, 51, 38, .10);
    --shadow: 0 12px 32px rgba(23, 51, 38, .15);
    --shadow-lg: 0 26px 64px rgba(12, 40, 24, .28);
    --radius: 18px;
    --radius-sm: 12px;
    /* ★ v2.73 全站**数量框字号**只认这一个变量 ✓
       用户原话：「前面的数量1调整和按钮字体一样大小」（仓库种子页 / 作物页两张手机截图 ✗）
       —— .btn-sm 就是 13px，所以数量框也取 13px，五处**同一个值**：
         ① [data-scqtyadd] 仓库加入清单框  ② [data-rqtyadd] 仓库回收数量框
         ③ [data-scqty] 卖出清单行         ④ [data-cartqty] 购物车行
         ⑤ [data-qty] 商店卡片购买框
       改这一个值 = 五处一起改，不会再出现"这个框大一号、那个小一号" ✗ */
    --vf-qty-fs: 13px;
}

*, *::before, *::after { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
    min-height: 100vh;
    font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", sans-serif;
    color: var(--ink);
    background: linear-gradient(180deg, #a8e6ff 0%, #c8f0e0 26%, #e8f8cb 60%, #d5eeb2 100%);
    background-attachment: fixed;
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
}

h1, h2, h3, h4 { margin: 0; font-weight: 800; letter-spacing: .3px; }
p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
a { color: var(--green-ink); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font-family: inherit; }
small { font-weight: 400; }

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: rgba(34, 197, 94, .45); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(34, 197, 94, .7); }
::-webkit-scrollbar-track { background: transparent; }

/* ---------------- 02 天空背景 ---------------- */
.sky {
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    overflow: hidden;
}

.sun {
    position: absolute;
    top: 5vh;
    right: 7vw;
    width: 118px;
    height: 118px;
    border-radius: 50%;
    background: radial-gradient(circle at 36% 34%, #fffde7 0%, #fde047 42%, #fbbf24 72%, rgba(251, 191, 36, .1) 100%);
    box-shadow: 0 0 90px 28px rgba(253, 224, 71, .5), 0 0 200px 80px rgba(253, 224, 71, .22);
    animation: sunPulse 7s ease-in-out infinite;
}

.cloud {
    position: absolute;
    height: 54px;
    width: 150px;
    border-radius: 60px;
    background: rgba(255, 255, 255, .88);
    box-shadow: 0 12px 34px rgba(140, 185, 215, .22);
}
.cloud::before, .cloud::after {
    content: "";
    position: absolute;
    border-radius: 50%;
    background: inherit;
}
.cloud::before { width: 78px; height: 78px; top: -36px; left: 20px; }
.cloud::after  { width: 54px; height: 54px; top: -24px; right: 24px; }
.cloud.c1 { top: 8vh;  left: -18%;  animation: drift 78s linear infinite; }
.cloud.c2 { top: 22vh; left: -30%;  transform: scale(.72); opacity: .8; animation: drift 108s linear infinite 8s; }
.cloud.c3 { top: 3vh;  left: -45%;  transform: scale(.55); opacity: .65; animation: drift 140s linear infinite 20s; }

.hills { position: absolute; left: 0; right: 0; bottom: 0; height: 42vh; }
.hill {
    position: absolute;
    bottom: 0;
    border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}
.hill.h1 { left: -10%; width: 62%; height: 74%;  background: linear-gradient(180deg, #a6de84, #74c257); opacity: .5; }
.hill.h2 { right: -12%; width: 72%; height: 60%; background: linear-gradient(180deg, #93d46f, #55ab3a); opacity: .42; }
.hill.h3 { left: 24%; width: 54%; height: 92%;  background: linear-gradient(180deg, #bcec9c, #86cd64); opacity: .3; }

/* ---------------- 03 通用组件 ---------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 9px 15px;
    border: 1px solid rgba(255, 255, 255, .75);
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .8);
    color: var(--ink);
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    white-space: nowrap;
    box-shadow: var(--shadow-sm);
    transition: transform .16s ease, box-shadow .16s ease, background .16s ease, opacity .16s;
    -webkit-tap-highlight-color: transparent;
}
.btn:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.btn:active { transform: translateY(0) scale(.98); }
.btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
.btn-primary { background: linear-gradient(135deg, #15803d, #166534 55%, var(--green-4)); color: #fff; border-color: transparent; }
.btn-gold { background: linear-gradient(135deg, var(--gold-1), var(--gold-2) 60%, var(--gold-3)); color: #43290a; border-color: transparent; }
.btn-ghost { background: rgba(255, 255, 255, .45); }
.btn-danger { background: linear-gradient(135deg, #dc2626, #991b1b); color: #fff; border-color: transparent; }
.btn-sm { padding: 6px 11px; font-size: 13px; border-radius: 10px; }
/* ★ v2.73 仓库动作按钮里的 emoji：桌面端是 .btn 的 gap，窄屏把它整段藏掉
   （见 max-width:720px 那段 —— 393px 下少一个 emoji 的宽度这一行才排得整齐 ✓）
   ⚠️ HTML 里的 emoji 一个字没删 ✗（老测试 / vf_* 断言照旧能在 DOM 里找到） */
.btn > .no-emoji { display: inline-flex; align-items: center; }
.btn > .btn-text { display: inline; }
.btn-block { width: 100%; padding: 13px; font-size: 16px; }
.btn.pulse { animation: goldPulse 1.8s ease-in-out infinite; }
.btn .btn-label { font-size: 13px; }

.chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 11px;
    border-radius: 999px;
    background: rgba(34, 197, 94, .16);
    color: var(--green-4);
    font-size: 12px;
    font-weight: 700;
}
.chip-muted { background: rgba(255, 255, 255, .6); color: var(--ink-soft); }
.chip-muted::before {
    content: "";
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--green-2);
    box-shadow: 0 0 0 3px rgba(34, 197, 94, .22);
    animation: blink 2s ease-in-out infinite;
}

.link { border: 0; background: none; color: var(--green-ink); font-size: 12px; font-weight: 700; cursor: pointer; padding: 0; }
.link:hover { text-decoration: underline; }
.muted { color: var(--muted); }

.alert {
    padding: 11px 14px;
    border-radius: var(--radius-sm);
    font-size: 13.5px;
    line-height: 1.6;
    margin-bottom: 14px;
}
.alert-error   { background: rgba(239, 68, 68, .12);  border: 1px solid rgba(239, 68, 68, .35);  color: #9b1c1c; }
.alert-info    { background: rgba(59, 130, 246, .12); border: 1px solid rgba(59, 130, 246, .32); color: #1e40af; }
.alert-success { background: rgba(34, 197, 94, .14);  border: 1px solid rgba(34, 197, 94, .36);  color: #15803d; }

/* ---------------- 04 顶部 HUD ---------------- */
.hud {
    position: sticky;
    top: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
    padding: 11px 22px;
    background: rgba(255, 255, 255, .74);
    backdrop-filter: blur(18px) saturate(1.5);
    -webkit-backdrop-filter: blur(18px) saturate(1.5);
    border-bottom: 1px solid rgba(255, 255, 255, .9);
    box-shadow: 0 8px 26px rgba(23, 51, 38, .09);
}

.hud-brand { display: flex; align-items: center; gap: 10px; }
.hud-brand .logo { font-size: 30px; filter: drop-shadow(0 3px 6px rgba(0, 0, 0, .16)); animation: sway 3.6s ease-in-out infinite; }
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-text b { font-size: 17px; letter-spacing: .5px; }
.brand-text small { color: var(--muted); font-size: 11.5px; }

.hud-user { display: flex; align-items: center; gap: 10px; flex: 1 1 220px; min-width: 200px; }
.avatar {
    width: 46px; height: 46px;
    display: flex; align-items: center; justify-content: center;
    font-size: 26px;
    border-radius: 50%;
    background: linear-gradient(140deg, #fff5cc, #c9f0b4);
    border: 2px solid #fff;
    box-shadow: 0 6px 16px rgba(23, 51, 38, .16);
    flex: none;
}
.who { flex: 1; min-width: 0; }
.who-top { display: flex; align-items: center; gap: 8px; }
.nick { font-weight: 800; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 190px; }
.lv {
    flex: none;
    padding: 1px 9px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 800;
    color: #4a2c00;
    background: linear-gradient(135deg, var(--gold-2), var(--gold-3));
    box-shadow: 0 3px 8px rgba(245, 158, 11, .38);
}
.expbar {
    position: relative;
    height: 12px;
    margin-top: 4px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .14);
    overflow: hidden;
}
.expbar i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, #86efac, var(--green-2));
    box-shadow: 0 0 12px rgba(34, 197, 94, .55);
    transition: width .5s cubic-bezier(.3, 1, .4, 1);
}
.exp-text {
    position: absolute;
    inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 9.5px;
    font-weight: 700;
    color: #0d3a22;
    text-shadow: 0 1px 0 rgba(255, 255, 255, .6);
    letter-spacing: .3px;
}

.hud-stats { display: flex; gap: 8px; flex-wrap: wrap; }
.stat {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 7px 13px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .85);
    border: 1px solid rgba(255, 255, 255, .95);
    box-shadow: var(--shadow-sm);
    font-size: 14px;
}
.stat .ico { font-size: 16px; }
.stat b { font-variant-numeric: tabular-nums; }
.stat-coins b { color: var(--gold-ink); }
.stat-harvest b { color: var(--green-ink); }
.stat-steal b { color: #8a5a00; }
.stat.flash { animation: statPop .5s ease; }

.hud-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.menu-wrap { position: relative; }
.menu {
    /* ============================================================
     * v2.21：改回原来的「贴着按钮的下拉菜单」
     *
     * 我在这里绕了三版，把教训写清楚，免得下次又走一遍：
     *
     *   ① v2.17 下拉 + max-height + overflow:auto（想让它能滚）
     *   ② v2.18 fixed 居中浮层（transform 居中 + ::before 遮罩）
     *   ③ v2.19 fixed 全屏遮罩 + 内层卡片
     *
     * ② 和 ③ 都失败，症状一样：**面板贴在顶上被切、背景不变暗**。
     * 根因是同一个 —— **`position: fixed` 的包含块不一定是视口**：
     * 只要祖先元素上有 transform / filter / backdrop-filter / will-change，
     * fixed 就变成相对**那个祖先**定位。
     * 本项目的卡片/头部用了 `backdrop-filter: blur(...)`，
     * 所以我的 `top:0; bottom:0` 实际是"撑满 header 的高度"，
     * 于是面板被压在顶部、超出部分被切，遮罩也只盖住 header。
     *
     * **结论：这个页面里不能用 position:fixed 做浮层。**
     * 下面回到 relative 祖先（.menu-wrap，只有按钮那么大）内的 absolute —— 
     * 它的包含块是 .menu-wrap，不受 backdrop-filter 影响，所以定位是对的。
     * ============================================================ */
    position: absolute;
    right: 0;
    top: calc(100% + 8px);
    min-width: 176px;
    padding: 6px;
    border-radius: 14px;
    background: #fff;
    box-shadow: var(--shadow-lg);
    border: 1px solid rgba(23, 51, 38, .08);
    display: none;
    z-index: 50;

    /* 尽量让它可滚。注意这在移动端**不一定百分百管用**
       （vh 是"地址栏收起后的大视口"，算出来比实际高），
       所以真正可靠的办法是**别把菜单做太长** —— 见下面 menu-card 的说明。
       v2.37：再加一档 svh（小视口 = 地址栏可见时的真实高度）。
       三档按"越新越精确、且都不会超出屏幕"排：
         vh   —— 谁都有，但手机上偏大 → 留的余量给足（-150px）
         svh  —— 地址栏可见时的高度，**一定放得下**
         dvh  —— 动态视口，最贴合；不支持的浏览器会丢掉这一条、用上面的 */
    max-height: calc(100vh - 150px);
    max-height: calc(100svh - 96px);
    max-height: calc(100dvh - 96px);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}
.menu-wrap.open .menu { display: block; animation: popIn .18s ease; }

/* v2.19 引入的卡片层：改回下拉之后它没有存在意义了。
   留着这个 div（HTML 里还在）但让它不产生任何布局影响 ——
   用 display:contents 让它的子元素"穿透"上来，等于没有这层。 */
.menu-card { display: contents; }
.menu a {
    display: block;
    padding: 9px 12px;
    border-radius: 10px;
    color: var(--ink);
    font-size: 13.5px;
    font-weight: 600;
}
.menu a:hover { background: rgba(34, 197, 94, .12); text-decoration: none; }
.menu a.danger { color: var(--danger-ink); }

/* ---------------- 每日提示条 ---------------- */
.daily-tip {
    position: relative;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin: 14px 22px 0;
    padding: 12px 18px;
    border-radius: var(--radius);
    background: linear-gradient(100deg, #fff7d6, #ffeaa7 45%, #ffd86b);
    border: 1px solid rgba(255, 255, 255, .9);
    box-shadow: 0 12px 30px rgba(245, 158, 11, .28);
    font-size: 14px;
    animation: slideDown .4s ease;
}
.tip-close {
    margin-left: auto;
    border: 0;
    background: rgba(255, 255, 255, .55);
    width: 26px; height: 26px;
    border-radius: 50%;
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
    color: #7a5200;
}

/* ---------------- 05 布局 ---------------- */
.layout {
    position: relative;
    z-index: 10;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    gap: 20px;
    width: min(1840px, 100%);
    margin: 0 auto;
    padding: 20px 22px 48px;
    align-items: start;
}
.layout-side { display: flex; flex-direction: column; gap: 16px; }

.panel {
    background: var(--panel);
    backdrop-filter: blur(16px) saturate(1.3);
    -webkit-backdrop-filter: blur(16px) saturate(1.3);
    border: 1px solid var(--panel-line);
    border-radius: 24px;
    box-shadow: var(--shadow);
    padding: 20px;
}
.panel-head {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}
.panel-head h2 { font-size: 19px; display: flex; align-items: center; gap: 8px; }
.panel-head .ico { font-size: 22px; }
.panel-tools { margin-left: auto; display: flex; align-items: center; gap: 8px; }

/* 农场主面板：顶部一条若隐若现的绿地光带 */
.farm-panel { position: relative; overflow: hidden; }
.farm-panel::before {
    content: "";
    position: absolute;
    top: -60px;
    right: -40px;
    width: 240px;
    height: 160px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(134, 239, 172, .35), rgba(134, 239, 172, 0) 70%);
    pointer-events: none;
}

/* ---------------- 06 地块网格 ---------------- */
/* 地块网格：**auto-fit + 1fr**（规范 §3.1 / §1.2）
   ⚠️ v2.91.1 修 M1 —— 原来是 `auto-fill` + 轨道上限 228px + `justify-content:center`：
      1280 下内容盒 844px，3 条 228px 轨道 = 708px，剩下 136px 被居中**平均分到两边**
      ⇒ 实测右边空 **68px = 8.06%**（用户报的"右边空一块"✗）。
   现在：`auto-fit` 收掉空轨道 ✓ + `1fr` 让轨道把整行**铺满** ✓ ⇒ 右边空 0px。
      18 块地 / 3 列 = 6 行全是满行，最后一行不留缺口 ✓
      （1280 实测卡宽 228 → 273.33px，仍是 1/1 正方形、没有被拉变形 —— 手机档
        `aspect-ratio:auto` + 高度由内容决定那套在 560px 以下照旧 ✓）
   ⚠️ 不要再加回 `justify-content:center`：1fr 已经铺满，居中只会掩盖"没铺满"这件事 ✗ */
.plot-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 12px;
}

/* ★ v2.92.1：未开垦的地块不再画卡片（用户：「土地占的位置太多，未开放就不要显示了」）
   —— 网格下方只留**一行**「开垦条」，整条是一个按钮：
     · 高度 48px ≥ 44px 热区；手机上也只占一行（≤420px 缩到 13px 字号）
     · 文案「🔓 未开垦 N 块 · 开下一块 🪙X」由 game.js 写；X 是服务端 next_plot.cost
     · 全部开垦完 ⇒ 元素带 hidden（下面这条 [hidden] 必须有：作者样式
       display:flex 会盖掉 UA 的 [hidden]{display:none} ✗） */
.unlock-bar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    max-width: 100%;
    min-height: 48px;
    margin-top: 12px;
    padding: 10px 14px;
    border: 1px solid rgba(180, 83, 9, .35);
    border-radius: 14px;
    background: linear-gradient(135deg, var(--gold-1), var(--gold-3));
    color: #452a00;
    font-size: 14px;
    font-weight: 700;
    line-height: 1.4;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    transition: transform .16s ease, box-shadow .16s ease, filter .16s ease;
}
.unlock-bar[hidden] { display: none; }
.unlock-bar:hover { filter: brightness(1.04); box-shadow: 0 8px 22px rgba(245, 158, 11, .28); }
.unlock-bar:active { transform: translateY(1px); }
.unlock-bar:focus-visible { outline: 2px solid #452a00; outline-offset: 2px; }
.unlock-bar.busy { opacity: .65; pointer-events: none; }
.unlock-bar b { font-weight: 800; }
@media (max-width: 420px) {
    .unlock-bar { font-size: 13px; padding: 10px 8px; }
}
@media (max-width: 360px) {
    .unlock-bar { font-size: 11.5px; padding: 10px 4px; }
}

.plot {
    position: relative;
    aspect-ratio: 1 / 1;
    padding: 10px;
    border-radius: 20px;
    border: 2px solid rgba(255, 255, 255, .24);
    background: radial-gradient(circle at 50% 26%, var(--soil-1) 0%, var(--soil-2) 48%, var(--soil-3) 100%);
    box-shadow: inset 0 6px 16px rgba(0, 0, 0, .22), inset 0 -4px 12px rgba(255, 255, 255, .10), var(--shadow-sm);
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    overflow: hidden;
    transition: transform .18s ease, box-shadow .18s ease;
}
.plot::before {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        radial-gradient(rgba(255, 255, 255, .10) 1px, transparent 1px),
        radial-gradient(rgba(0, 0, 0, .16) 1px, transparent 1px);
    background-size: 13px 13px, 17px 17px;
    background-position: 0 0, 7px 9px;
    pointer-events: none;
    opacity: .75;
}
.plot:hover { transform: translateY(-4px); box-shadow: inset 0 7px 20px rgba(0, 0, 0, .38), 0 16px 34px rgba(23, 51, 38, .22); }
.plot.done { border-color: rgba(253, 224, 71, .85); box-shadow: inset 0 7px 20px rgba(0, 0, 0, .32), 0 0 0 3px rgba(253, 224, 71, .3), 0 16px 34px rgba(245, 158, 11, .3); }

.plot.locked {
    background: repeating-linear-gradient(45deg, #5d4632 0 10px, #4f3a29 10px 20px);
    justify-content: center;
    align-items: center;
    text-align: center;
    cursor: default;
}
.plot.empty { background: radial-gradient(circle at 50% 30%, #7d4f2c 0%, #5d3618 52%, #3d2110 100%); }

.plot-top {
    position: absolute;
    top: 8px; left: 8px; right: 8px;
    display: flex;
    align-items: center;
    gap: 5px;
    z-index: 3;
    pointer-events: none;
}
.tag {
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 10.5px;
    font-weight: 800;
    color: #fff;
    background: rgba(0, 0, 0, .42);
    backdrop-filter: blur(4px);
    white-space: nowrap;
}
.tag.ready { background: linear-gradient(135deg, var(--gold-1), var(--gold-3)); color: #4a2c00; box-shadow: 0 3px 10px rgba(245, 158, 11, .5); }
.tag.pest  { background: linear-gradient(135deg, #dc2626, #991b1b); animation: shake 1.6s ease-in-out infinite; }
.tag.stolen { background: rgba(0, 0, 0, .55); }
.tag.grow { background: #15803d; }

.crop-icon {
    margin: auto;
    font-size: clamp(30px, 5vw, 48px);
    line-height: 1;
    filter: drop-shadow(0 7px 9px rgba(0, 0, 0, .38));
    transition: transform .35s ease, filter .35s ease;
    z-index: 2;
    position: relative;
}
.plot.stage-0 .crop-icon { transform: scale(.42); filter: grayscale(.6) brightness(.92) drop-shadow(0 3px 4px rgba(0, 0, 0, .3)); opacity: .9; }
.plot.stage-1 .crop-icon { transform: scale(.6); filter: grayscale(.3) drop-shadow(0 5px 6px rgba(0, 0, 0, .32)); }
.plot.stage-2 .crop-icon { transform: scale(.82); }
.plot.stage-3 .crop-icon { animation: cropReady 1.7s ease-in-out infinite; }
.plot.stage-3::after {
    content: "";
    position: absolute;
    left: 50%; top: 42%;
    width: 76%; aspect-ratio: 1;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 236, 120, .55), rgba(255, 236, 120, 0) 68%);
    animation: glow 2.2s ease-in-out infinite;
    pointer-events: none;
}

.plot-meta {
    position: relative;
    z-index: 3;
    text-align: center;
    font-size: 11.5px;
    font-weight: 700;
    color: #fff;
    text-shadow: 0 1px 4px rgba(0, 0, 0, .7);
    margin-bottom: 5px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.plot-progress {
    position: relative;
    z-index: 3;
    height: 6px;
    border-radius: 6px;
    background: rgba(0, 0, 0, .45);
    overflow: hidden;
    margin-bottom: 7px;
}
.plot-progress i {
    display: block;
    height: 100%;
    border-radius: 6px;
    background: linear-gradient(90deg, #86efac, var(--green-2));
    transition: width .7s cubic-bezier(.3, 1, .4, 1);
}
.plot.stage-3 .plot-progress i { background: linear-gradient(90deg, var(--gold-1), var(--gold-3)); }

.plot-actions {
    position: relative;
    z-index: 4;
    display: flex;
    gap: 5px;
    justify-content: center;
    flex-wrap: wrap;
}
.pbtn {
    border: 0;
    padding: 5px 8px;
    border-radius: 10px;
    font-size: 12.5px;
    font-weight: 800;
    line-height: 1.1;
    cursor: pointer;
    background: rgba(255, 255, 255, .93);
    color: var(--ink);
    box-shadow: 0 3px 9px rgba(0, 0, 0, .3);
    transition: transform .14s ease, box-shadow .14s ease, filter .14s;
    -webkit-tap-highlight-color: transparent;
}
.pbtn:hover { transform: translateY(-2px) scale(1.05); filter: brightness(1.05); }
.pbtn:active { transform: scale(.94); }
.pbtn.primary { background: linear-gradient(135deg, #15803d, #166534); color: #fff; }
.pbtn.gold { background: linear-gradient(135deg, var(--gold-1), var(--gold-3)); color: #452a00; }
.pbtn.big { padding: 8px 16px; font-size: 14px; }
.pbtn.wide { padding: 8px 12px; font-size: 12.5px; }
.pbtn.disabled, .pbtn:disabled { opacity: .42; cursor: not-allowed; transform: none; filter: grayscale(.6); }

.lock-ico { font-size: 34px; filter: drop-shadow(0 5px 8px rgba(0, 0, 0, .4)); }
.lock-cost {
    margin-top: 8px;
    font-size: 12px;
    font-weight: 800;
    color: #ffe08a;
    text-shadow: 0 2px 5px rgba(0, 0, 0, .6);
    position: relative;
    z-index: 3;
}
.empty-hint {
    position: relative;
    z-index: 3;
    font-size: 12px;
    font-weight: 700;
    color: rgba(255, 236, 200, .78);
    text-align: center;
    text-shadow: 0 1px 4px rgba(0, 0, 0, .6);
}

.plot.skeleton { background: #6b4a30; animation: shimmer 1.4s linear infinite; }
.plot.skeleton::before { opacity: .3; }

.farm-foot {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px dashed rgba(23, 51, 38, .16);
}
.legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12.5px; color: var(--ink-soft); }
.legend span { display: inline-flex; align-items: center; gap: 5px; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot-water { background: #38bdf8; }
.dot-fert  { background: #a855f7; }
.dot-pest  { background: #ef4444; }
.dot-ready { background: var(--gold-2); }
.farm-foot .btn { margin-left: auto; }

/* ---------------- v2.66 一键操作区（④ 重新设计：分组 + 自动换行） ----------------
   按钮由 game.js 的 renderOneClickBar() 现算现画：没开通特权的、后台关掉的动作
   **根本不渲染**（不是灰的，也不是加锁的）。这里只管长什么样：
     · 一组一条「带子」= 小标题 + 按钮，组内和组间都能换行 —— 功能变多只多一行，
       永远不会横向滚动（手机上横滚体验极差，明确不要）；
     · 0 个可见按钮时 JS 会给容器加 hidden，所以下面 [hidden] 那条**必须**在，
       而且要压得住 .farm-actions 的 display:flex（同优先级靠后写是不够的，得靠选择器权重）。 */
.farm-actions {
    display: flex;
    align-items: center;
    gap: 10px 16px;
    margin-left: auto;
    flex-wrap: wrap;
}
.farm-actions[hidden] { display: none; }
/* ⚠️ 后台关掉的一键按钮（一键全卖 / 一键领取 / 一键交付）是 JS 给它们加 hidden 的，
   而 .btn 自己的 display:inline-flex 会盖掉 UA 样式里的 [hidden]{display:none} ——
   不显式压回去的话「藏」不动（这是实测最容易漏的一条）。 */
.btn[hidden] { display: none; }
.fa-group { display: inline-flex; align-items: center; gap: 8px; }
.fa-label {
    flex: none;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 800;
    color: var(--muted);
    background: rgba(23, 51, 38, .06);
    white-space: nowrap;
}
.fa-btns { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.farm-actions .btn { margin-left: 0; }
/* 热区 ≥44px（鼠标和手指都够点，用户明确要求）；
   长文案换行、不撑破卡片；touch-action 防「点一下变成双击缩放/滚屏」（移动端）。 */
.fa-btns .btn {
    min-height: 44px;
    padding: 10px 14px;
    max-width: 100%;
    white-space: normal;
    line-height: 1.3;
    overflow-wrap: anywhere;
    touch-action: manipulation;
}
/* 键盘可达：不靠 hover 也看得出焦点在哪个按钮上 */
.fa-btns .btn:focus-visible { outline: 2px solid var(--green-3); outline-offset: 2px; }

/* 批量播种模式下的提示条 */
.plant-tip {
    padding: 11px 14px;
    border-radius: 12px;
    background: rgba(34, 197, 94, .12);
    border: 1px dashed rgba(34, 197, 94, .45);
    color: var(--green-ink);
    font-size: 13px;
    font-weight: 600;
    line-height: 1.7;
}

.plant-tip b { color: var(--green-4); }

/* ---------------- 07 卡片 / 侧栏 ---------------- */
.card {
    background: var(--panel);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border: 1px solid var(--panel-line);
    border-radius: 20px;
    box-shadow: var(--shadow-sm);
    padding: 16px 18px;
}
.card-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    font-size: 14.5px;
    font-weight: 800;
    margin-bottom: 12px;
}
.announce-card { background: linear-gradient(140deg, #fffdf0, #f2fbe4); border-color: rgba(253, 224, 71, .55); }
.announce-card p { font-size: 13px; line-height: 1.75; color: var(--ink-soft); }

.mini-list li {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 6px;
    border-radius: 12px;
    transition: background .15s;
}
.mini-list li:hover { background: rgba(34, 197, 94, .1); }
.mini-list .mini-avatar {
    width: 34px; height: 34px;
    flex: none;
    display: flex; align-items: center; justify-content: center;
    font-size: 19px;
    border-radius: 50%;
    background: linear-gradient(140deg, #eaf9dc, #c9f0b4);
}
.mini-list .mini-info { flex: 1; min-width: 0; }
.mini-list .mini-name { font-size: 13.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-list .mini-sub { font-size: 11.5px; color: var(--muted); }
.mini-list .empty { justify-content: center; color: var(--muted); font-size: 13px; padding: 18px 0; }
.mini-list .mini-badge {
    flex: none;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    background: rgba(34, 197, 94, .18);
    color: var(--green-4);
}
.mini-list .mini-badge.none { background: rgba(0, 0, 0, .07); color: var(--muted); }

.tips li {
    position: relative;
    padding: 5px 0 5px 2px;
    font-size: 12.8px;
    line-height: 1.65;
    color: var(--ink-soft);
}

/* ---------------- 08 弹窗 ---------------- */
.modal {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}
.modal[hidden] { display: none; }
.modal-mask {
    position: absolute;
    inset: 0;
    background: rgba(8, 30, 18, .55);
    backdrop-filter: blur(7px);
    -webkit-backdrop-filter: blur(7px);
    animation: fadeIn .2s ease;
}
.modal-box {
    position: relative;
    width: 100%;
    max-width: 540px;
    max-height: 88vh;
    display: flex;
    flex-direction: column;
    border-radius: 26px;
    overflow: hidden;
    background: linear-gradient(180deg, #ffffff, #f5fcee);
    border: 1px solid rgba(255, 255, 255, .9);
    box-shadow: var(--shadow-lg);
    animation: popIn .28s cubic-bezier(.2, 1.3, .45, 1);
}
.modal-lg { max-width: 780px; }
.modal-xl { max-width: 1020px; }

.modal-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 15px 20px;
    background: linear-gradient(120deg, #15803d, #166534 60%, #14532d);
    color: #fff;
}
.modal-head h3 { font-size: 16.5px; display: flex; align-items: center; gap: 8px; }
.modal-head h3 small { font-size: 12px; font-weight: 500; opacity: 1; color: #eaf6ec; }
.modal-head .tabs { display: flex; gap: 6px; margin-left: 10px; }
.modal-head .btn-gold { margin-left: auto; }
.tab {
    border: 1px solid rgba(255, 255, 255, .4);
    background: rgba(255, 255, 255, .14);
    color: #fff;
    padding: 5px 14px;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 700;
    cursor: pointer;
    transition: background .15s;
}
.tab.active { background: #fff; color: var(--green-ink); border-color: #fff; }
.modal-x {
    margin-left: auto;
    width: 32px; height: 32px;
    flex: none;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, .2);
    color: #fff;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    transition: background .15s, transform .15s;
}
.modal-head .tabs + .modal-x { margin-left: 0; }
.modal-x:hover { background: rgba(255, 255, 255, .38); transform: rotate(90deg); }
.modal-body { padding: 18px 20px 22px; overflow: hidden auto; /* 同上：防 hover 动画引出横向滚动条 */ }

/* ---------------- 09 商店 / 仓库 / 邻居 ---------------- */

/* ================================================================
 * v2.55 商店「购物车合计」栏
 *
 * 位置：商店弹窗（#modal-shop）**内部**、.modal-body 之后，横跨整个弹窗宽度。
 *
 * ⚠️ 为什么不用 position: fixed / sticky：
 *   ① .modal-box 一系带 backdrop-filter（.modal-mask），fixed 的包含块会变成那个祖先，
 *      整个弹窗会跑到屏幕外 —— 这个坑踩过两次，别再试；
 *   ② .modal-box 本身是 display:flex + flex-direction:column，.modal-body 是
 *      唯一会滚的那一段（overflow: hidden auto）。合计栏作为它的**兄弟**放在后面，
 *      天然就是「钉在弹窗底部、不跟着内容滚、也不遮住购买按钮」的效果，
 *      不需要任何定位，手机上（媒体查询里 .modal-box 还是 flex 列）同样成立。
 * ================================================================ */
.shop-total {
    flex: none;                  /* 不被可滚动的 .modal-body 抢走高度 */
    display: flex;
    align-items: center;
    flex-wrap: wrap;             /* 窄屏自动折行，不横向撑破 */
    gap: 4px 10px;
    padding: 11px 20px;
    background: linear-gradient(180deg, #f7fdf1, #eaf7dd);
    border-top: 1.5px solid rgba(23, 51, 38, .1);
    font-size: 13px;
    color: var(--ink);
}
.shop-total-icon { font-size: 15px; line-height: 1; }
.shop-total-count { font-weight: 700; color: var(--ink-soft); }
.shop-total-sum {
    margin-left: auto;           /* 金额靠右（和截图里「共 X 金币」那种对位一致） */
    font-size: 15px;
    font-weight: 800;
    color: var(--gold-ink);
    font-variant-numeric: tabular-nums;
}
/* 买不起：数字变红 + 下面那句「还差 N 金币」也是红的 */
.shop-total-sum.is-short { color: var(--danger-ink); }
.shop-total-note { font-size: 11.5px; color: var(--muted); line-height: 1.4; }
.shop-total.is-short .shop-total-note { color: var(--danger-ink); font-weight: 700; }
/* 什么都没填：整栏只剩一句灰字提示（不显示 0） */
.shop-total.is-idle .shop-total-icon { opacity: .55; }
.shop-total.is-idle .shop-total-note { font-size: 12.5px; }

/* 数量框填了「用不了 / 会被夹住」的数字时的提示行（默认 hidden 属性） */
.shop-total-warn {
    flex: none;
    padding: 7px 20px;
    font-size: 11.5px;
    font-weight: 700;
    color: #8a5a00;
    background: rgba(245, 158, 11, .14);
    border-top: 1.5px solid rgba(23, 51, 38, .08);
    line-height: 1.4;
    word-break: break-word;
}

/* ⚠️ 本项目约定：只要组件自己的 class 设了 display（这里是 flex），
   `hidden` 属性就会被盖掉，必须显式补一条 .xxx[hidden] ——
   和文件末尾那一组（.order-fold-body[hidden] / .btn[hidden] …）同一个根因。 */
.shop-total[hidden] { display: none; }
.shop-total-warn[hidden] { display: none; }

/* ================================================================
 * v2.73 仓库里数量框的提示行（#boxQtyWarn）—— 和商店的 .shop-total-warn **同一副长相** ✓
 *   （用户要的是两处行为一致 ✓ 长相也就照抄，免得他以为又是两个功能 ✗）
 *   视觉刻意**朴素** ✗：就是一条浅黄底的小字，没有渐变 / 阴影 / 圆角 / 发光 ✓
 *   ⚠️ 本项目约定：class 自己设了 display 就得显式补 [hidden]，所以下面那条不能少 ✓
 * ================================================================ */
.box-qty-warn {
    flex: none;
    margin: 0 0 8px;
    padding: 6px 10px;
    border: 1.5px solid rgba(245, 158, 11, .28);
    border-radius: 10px;
    font-size: 11.5px;
    font-weight: 700;
    color: #8a5a00;
    background: rgba(245, 158, 11, .12);
    line-height: 1.4;
    word-break: break-word;
}
.box-qty-warn[hidden] { display: none; }

/* ================================================================
 * v2.55 购物车面板（#shopCartPanel）
 *
 * 位置和合计栏一样：.modal-box 正常流里、.modal-body 之后的兄弟节点，
 * **没有 position:fixed/sticky**（见上面那段注释：backdrop-filter + overflow:hidden）。
 * 列表自带 max-height + 滚动：加满 20 种也不会把商品区挤没。
 * ================================================================ */
.shop-cart {
    flex: none;
    padding: 10px 20px 12px;
    background: #fffdf5;
    border-top: 1.5px solid rgba(23, 51, 38, .1);
}
.shop-cart-head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 800;
    color: var(--ink);
    margin-bottom: 6px;
}
.shop-cart-head .shop-cart-title { flex: 1; min-width: 0; }
.shop-cart-head small { font-weight: 600; color: var(--muted); margin-left: 4px; }
.shop-cart-list {
    max-height: 156px;
    overflow: hidden auto;
    margin-bottom: 8px;
}
.shop-cart-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 0;
    font-size: 12.5px;
    border-bottom: 1px dashed rgba(23, 51, 38, .1);
}
.shop-cart-row:last-child { border-bottom: 0; }
.shop-cart-row .sc-icon { font-size: 16px; flex: none; }
.shop-cart-row .sc-name { flex: 1; min-width: 0; font-weight: 700; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* v2.55 数量调节：− / N / +（用户要「能减数量，不是只能整条删」）
   ⚠️ 点按目标：桌面上 30×28 够用；**手机上必须 ≥44px**（WCAG 2.5.5 / Apple HIG，
      这个项目里 tests/mobile_layout_test.py 一直在盯这条）—— 在 ≤560px 的
      媒体查询里放大到 44×44，并让 .sc-step 的 gap 也留出间距。 */
.shop-cart-row .sc-step {
    display: inline-flex;
    align-items: center;
    flex: none;
    gap: 3px;
}
.shop-cart-row .sc-btn {
    width: 30px;
    height: 28px;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1.5px solid rgba(23, 51, 38, .18);
    border-radius: 9px;
    background: #fff;
    color: var(--ink);
    font-size: 15px;
    font-weight: 800;
    line-height: 1;
    cursor: pointer;
}
.shop-cart-row .sc-btn:hover:not(:disabled) { border-color: var(--green-2); background: #f4fce9; }
.shop-cart-row .sc-btn:disabled { opacity: .42; cursor: not-allowed; }
.shop-cart-row .sc-qty {
    flex: none;
    min-width: 26px;
    text-align: center;
    font-size: 13px;
    font-weight: 800;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
/* ★ v2.73 购物车/卖出清单里的数量**从只读文字改成能直接键入的输入框** ✗
   为什么要自己写这几条（而不是把 .qty-input 的样式套过来）：
     · .qty-input 是**商店卡片**上的尺寸（52px 宽），放进 −/N/＋ 这一组里会把行撑开 ✗
     · 类型选择器 `.sc-qty-input` 只加在 input 上，所以 `<b class="sc-qty">` 那套老样式
       一行都不用改（现在也没有 b 了，但留着不碍事、也不给将来添乱 ✓）
   ⚠️ `type=number` 在 Chrome 上默认带上下箭头（会吃掉宽度、还和 −/＋ 重复）→ 隐掉 ✓
      （上下键照样能用 ✓ 那才是我们留着 number 类型的原因之一 ✓） */
.sc-qty-input {
    width: 52px;
    height: 28px;
    flex: none;
    padding: 0 4px;
    border: 1.5px solid rgba(23, 51, 38, .16);
    border-radius: 9px;
    background: #fff;
    color: var(--ink);
    font-size: var(--vf-qty-fs);   /* ★ 和同排 −/＋ 按钮的文字同号（v2.73） */
    font-weight: 800;
    text-align: center;
    font-family: inherit;
    font-variant-numeric: tabular-nums;
    -moz-appearance: textfield;
}
.sc-qty-input::-webkit-outer-spin-button,
.sc-qty-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.sc-qty-input:focus { outline: none; border-color: var(--green-2); }
.shop-cart-row .sc-sub { flex: none; min-width: 62px; text-align: right; color: var(--gold-ink); font-weight: 800; }
.shop-cart-row.is-blocked { opacity: .68; }
/* 项目约定：组件自己的 class 设了 display（.sc-step 是 inline-flex），
   要隐藏就必须显式补一条 [hidden]，否则 hidden 属性被盖掉 */
.shop-cart-row .sc-step[hidden] { display: none; }
.shop-cart-row .sc-bad {
    font-size: 10.5px; font-weight: 800; margin-left: 6px;
    padding: 1px 6px; border-radius: 999px;
    background: rgba(239, 68, 68, .14); color: var(--danger-ink);
}
.shop-cart-acts {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.shop-cart-total { flex: 1; min-width: 0; font-size: 13px; font-weight: 800; color: var(--gold-ink); }
.shop-cart-total.is-short { color: var(--danger-ink); }

.shop-cart[hidden] { display: none; }

/* 合计栏里那个「🛒 全加进购物车」按钮（默认 hidden，填了数量才给点） */
.shop-total .btn[hidden] { display: none; }

.seed-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }

.seed-card {
    position: relative;
    display: flex;
    gap: 12px;
    padding: 13px;
    border-radius: 16px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .09);
    box-shadow: var(--shadow-sm);
    transition: transform .16s, border-color .16s, box-shadow .16s;
}
.seed-card:hover { transform: translateY(-3px); border-color: rgba(34, 197, 94, .5); box-shadow: var(--shadow); }
.seed-card.locked { opacity: .62; filter: grayscale(.55); }
/* ★ v2.88.9：**余额不够**的卡片整体也有视觉提示（用户要求："别只灰按钮"✓）
   ⚠️ 故意**不用 opacity**：整卡半透明会连文字一起变淡 —— 实测 --muted 的说明文字
      在 opacity .72 下只剩 ~2.2:1，4.5:1 的对比度门槛当场破功 ✗。
      改成只**降饱和**（saturate，亮度基本不变 ⇒ 文字对比度一分不降 ✓）+ 灰边框
      + 图标底色变灰 —— 一眼就看得出"这张卡现在买不了"，字还照样看得清 ✓ */
.seed-card.is-poor { filter: saturate(.35); border-color: rgba(148, 163, 184, .5); }
.seed-card.is-poor .seed-icon { background: #f1f5f9; }
/* 等级不够的种子：**为什么灰**要说清（"等级不够（需 Lv.N）· 可从好友 / 交易获得"） */
.lv-hint {
    font-size: 11px;
    font-weight: 700;
    color: #8a5a00;
    background: rgba(245, 158, 11, .14);
    border-radius: 8px;
    padding: 3px 8px;
    margin: -2px 0 7px;
    line-height: 1.45;
}
.seed-icon {
    width: 52px; height: 52px;
    flex: none;
    display: flex; align-items: center; justify-content: center;
    font-size: 30px;
    border-radius: 14px;
    background: linear-gradient(140deg, #f0fbe4, #d6f2bf);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, .06);
}
.seed-body { flex: 1; min-width: 0; }
.seed-name { font-size: 14.5px; font-weight: 800; display: flex; align-items: center; gap: 6px; }
.seed-name .lv-req {
    font-size: 10.5px;
    font-weight: 800;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(245, 158, 11, .18);
    color: #8a5a00;
}
/* ★ v2.88.9：仓库行里的「需 Lv.N」—— `.box-name` 不是 flex（`.seed-name` 是，
   靠 gap 就有间距），所以这里自己给一个左边距 ✓ 只影响这一处，老样式一个字没改 ✗ */
.box-name .lv-req {
    font-size: 10.5px;
    font-weight: 800;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(245, 158, 11, .18);
    color: #8a5a00;
    margin-left: 6px;
    white-space: nowrap;
}
.seed-desc { font-size: 11.5px; color: var(--muted); margin: 3px 0 7px; line-height: 1.5; }
.seed-stats { display: flex; flex-wrap: wrap; gap: 5px 10px; font-size: 11.5px; color: var(--ink-soft); }
.seed-stats b { color: var(--ink); }
.seed-buy { display: flex; align-items: stretch; gap: 6px; margin-top: 10px; }
/* ★ v2.88.9 ★★ 四列对齐 ★★（用户原话：「卡片的说明文字长短不同，价格 / 数量框 /
   购物车 / 购买按钮几列参差，看着不整齐」✗）
   老写法是纯 flex + `.seed-buy .btn { margin-left: auto }`：只有**最后一列**贴右，
   中间的数量框 / 🛒 会跟着**价格块的宽度**左右漂 —— 真浏览器实测同一竖列里
   qty 的 left 在 431 / 451 / 464 / 522px 之间参差（差 90px）✗
   改用 **grid**：除第 1 列（价格块，1fr）外每一列都是**固定宽度**，于是
   「价格块 / 数量框 / 🛒 / 购买」在每张卡上落在同一条竖线上 ✓
     · 为什么用 grid 而不是各自 flex：flex 没有"列"这个概念，想让中间两列
       相对**右边**固定，只能靠 auto margin 平分剩余空间 —— 那又跟按钮宽度有关 ✗
     · 为什么价格块放第 1 列且是 1fr：价格文字长短差异最大（10 ~ 430,000），
       让它吃掉剩余空间，就不会把它后面的列顶得参差 ✓
     · 只作用于**同时有数量框和 🛒 的标准商品卡**（种子 / 道具的正常卡）：
       特权 / 头衔 / 宠物只有「价格 + 一个按钮」，没有列要对齐 ✗；
       偷菜手套那张多一个「🧤 使用」列、仓库扩容"已满"那张按钮文案长（6 个字，
       nowrap 塞不进 50px 的列会被挤出来），两张都**留在老 flex 布局**里 ✓ */
.seed-buy:has(.qty-input):has([data-cartone]):not(:has([data-useglove])) {
    display: grid;
    /* 52 / 42 / 50 = 老布局里「数量框 / 🛒 / 购买」的实测宽度（41.9 的 🛒 取整 42 ✓）；
       gap 4px 是量出来的：6px 时最贵的打折道具（16,200 ← 18,000）比价格列宽 1.8px，
       原价会被折到第二行 —— 4px 之后 393 / 402 / 1280 三档都能一行放下 ✓ */
    grid-template-columns: minmax(0, 1fr) 50px 42px 50px;
    align-items: stretch;
    gap: 4px;
}
.seed-buy:has(.qty-input):has([data-cartone]):not(:has([data-useglove])) .price-block { grid-column: 1; }
.seed-buy:has(.qty-input):has([data-cartone]):not(:has([data-useglove])) .qty-input { grid-column: 2; width: 100%; }
.seed-buy:has(.qty-input):has([data-cartone]):not(:has([data-useglove])) [data-cartone] { grid-column: 3; }
.seed-buy:has(.qty-input):has([data-cartone]):not(:has([data-useglove])) [data-buy] { grid-column: 4; margin-left: 0; }
/* ★ v2.73：align-items 由 center 改成 **stretch** —— 数量框和「购买」按钮**一样高** ✓
   （原来是 center：输入框 27px / 按钮 31px，同一行里差 4px，看着不齐 ✗）
   配合下面的 height:auto（不写死 px ✗，跟着行高走 ✓） */
.seed-buy .price { font-size: 13.5px; font-weight: 800; color: var(--gold-ink); }
.seed-buy .price::before { content: "🪙 "; }
/* ★ v2.88.9：折后价 + 原价 = **一个价格块**（用户原话：「原价划掉的图标数字看起来
   不整齐别扭，和折后价间距太远」✗）
     · 原来两个价钱各带一个 🪙（`::before`）+ 6px gap + 2px margin = 8px 空隙，
       看着就是"两块各自成块、中间空一大段" ✗
     · 现在：整块**只有一个 🪙**（挂在 .price 上 ✓）、原价小一号灰一点、
       块内 gap 4px **紧挨**折后价 ✓
     · `flex-wrap: wrap` 是保险丝：位置真的不够时原价折到**块内第二行**，
       **绝不让它压到数量框上**（宁可多一行，也不要两块相交 ✗）
     · 颜色 #64748b 是选过的：`--muted-2`(#94a3b8) 在卡片白底上只有 **2.8:1** ✗
       （用户点名的"灰一点"最容易不达标），#64748b 实测 **4.76:1** ✓ 仍然是灰的
   用 <s> 语义标签（不是 span），无障碍上也是「删除线」的意思 ✓ */
.seed-buy .price-block {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0 4px;
    min-width: 0;
}
.seed-buy .price-orig {
    font-size: 11px;
    font-weight: 600;
    color: #64748b;
    text-decoration: line-through;
    margin-left: 0;
}
/* ★ v2.88.9：原价**不再自带第二个 🪙**（用户原话：「两个都带金币图标」✗）——
   整块只留 .price 上那一个 ✓。这条覆盖块必须排在 `.seed-buy .price-orig`
   那条规则**之后**（同优先级后写的赢 ✓）。 */
.seed-buy .price-orig::before { content: none; }
.qty-input {
    width: 52px;
    height: auto;                  /* ★ v2.73：跟同排按钮等高（见 .seed-buy 的 stretch ✓） */
    padding: 5px 6px;
    border-radius: 9px;
    border: 1.5px solid rgba(23, 51, 38, .16);
    font-size: var(--vf-qty-fs);   /* ★ 和 `.seed-buy .btn`（.btn-sm=13px）同号（v2.73） */
    font-weight: 700;
    font-family: inherit;          /* ★ v2.73：同上（数字和按钮文字同一套字体 ✓） */
    text-align: center;
    color: var(--ink);
    background: #fff;
}
.qty-input:focus { outline: none; border-color: var(--green-2); }
.seed-buy .btn { margin-left: auto; }

.shop-grid { display: block; }
.shop-grid .seed-grid { display: grid; }
.item-card .seed-icon { background: linear-gradient(140deg, #f3e8ff, #e0cffc); }
/* 「已有 N」角标：只作用于商店卡片内部的徽章。
   注意不要写成全局 .owned —— 否则任何带 owned 的元素都会被绝对定位到右上角 */
.seed-card .owned {
    position: absolute;
    top: 10px; right: 10px;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    background: rgba(34, 197, 94, .16);
    color: var(--green-4);
}

.box-grid { display: flex; flex-direction: column; gap: 10px; }
.box-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 15px;
    border-radius: 15px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .08);
    box-shadow: var(--shadow-sm);
    transition: transform .15s, border-color .15s;
}
.box-row:hover { transform: translateX(3px); border-color: rgba(34, 197, 94, .45); }
.box-icon { font-size: 30px; flex: none; width: 46px; text-align: center; }
.box-info { flex: 1; min-width: 0; }
/* ★ v2.89.8（批次 2 · H6 层次）：仓库行「名称 vs 数量」原来 14.5/800 vs 15/800
   ⇒ Δ字号 0.5px、Δ字重 0 ⇒ R3 直接红（1280 那档）。按 §2.1 收敛到档位：
   名称 = **T4 15.5/600**（§2.1 T4 规定 600），数量 = **T5 17**（§2.1 T5 主数字）。
   ⇒ Δ字号 1.5px + **Δ字重 200**（600 vs 800）⇒ 满足 §2.2「Δ≥2px **或** Δ字重≥200」✓
   （`font-weight: 800` 那一档**没动** —— 它本来就是产品的数量强调值，不是新发明的 ✓） */
.box-name { font-size: 15.5px; font-weight: 600; }
.box-sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
/* ★ v2.73 单价那一行**不许把数字/单位折断**（用户今天第二次提这件事 ✗）：
   能卖的那一行（`.box-row:not(.is-nosell)` 里的 .box-sub）整句 nowrap ->
   要么整句在本行、要么整句掉下一行，**绝不会**出现「72,800 单独掉一行」那种半截断 ✓
   ⚠️ 为什么只加 CSS、**不给那一行加 class/span**：本项目的老回归按
      「<div class="box-sub">单价 🪙12 · 全部卖出可得 🪙108</div>」**整串**断言 ——
      多加一个 class 就红了 ✗（实测抓到过两次）。所以挑选择器、不动 DOM 结构 ✓
   ⚠️ 只作用于**能卖的那一行**（短句 ✓）—— 不能卖那行（is-nosell）的通用理由是长句，
      加了 nowrap 会横向溢出 ✗ 所以**不加** ✓ */
.box-row:not(.is-nosell) .box-sub { white-space: nowrap; }
.box-count {
    /* ★ v2.89.8（批次 2 · H6）：15px -> **T5 17px**（§2.1「卡片主数字」那一档）。
       字重 800 保持原样（不新造值 ✓）—— 和上面 `.box-name` 的 600 凑出 Δ字重 200 ✓ */
    font-size: 17px;
    font-weight: 800;
    color: var(--green-ink);
    font-variant-numeric: tabular-nums;
    min-width: 54px;
    text-align: right;
}
.box-count::after { content: " 个"; font-size: 11px; color: var(--muted); font-weight: 600; }
.box-actions { display: flex; gap: 6px; flex: none; }
/* ★ v2.73「同一句提醒只说一次」：整页级的**一条**提示放在列表顶部 ✓（用户原话：
   「不用每个都提醒，在关键位置提醒就行」）——原来每一行都顶着这条黄字，把单价那行挤到下一行 ✗。
   长相沿用本项目现成的浅黄提示条（.shop-total-warn 同一套配色 ✓ 没有新视觉 ✗） */
.box-notice {
    flex: none;
    padding: 6px 10px;
    border: 1.5px solid rgba(245, 158, 11, .28);
    border-radius: 10px;
    font-size: 12px;
    font-weight: 700;
    color: #8a5a00;
    background: rgba(245, 158, 11, .12);
    line-height: 1.45;
}
/* 行上那句**重复的**通用提醒：留在 DOM 里（老测试按字符串找它 ✓ / 屏幕阅读器不算它 ✓）
   但**不占位** ✗ —— 这样单价那一行才能回到一行 ✓ */
.box-why-dup, .box-why-dup[hidden] { display: none; }
.empty-box { text-align: center; padding: 40px 0; color: var(--muted); font-size: 14px; }
.empty-box .big { display: block; font-size: 44px; margin-bottom: 10px; opacity: .6; }

/* ---------------- v2.66 仓库：分类标签 + 容量条 + 「不能卖」的提示 ----------------
   三个新块都是 game.js 的 renderBox() 现算现画的：数据侧给什么就显示什么，
   字段缺失时（老 state）画面和改造前一模一样（没有容量条、没有标签、作物照旧能卖）。 */

/* 容量条：已用 N/M 格 + 回收记录；≥80% 变橙、满格变红（提醒，不是报警） */
.box-cap { margin-bottom: 12px; }
.box-cap[hidden] { display: none; }
.box-cap-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.box-cap-text { font-size: 13px; font-weight: 700; color: var(--ink-soft); white-space: nowrap; }
.box-cap-text b { color: var(--green-ink); }
.box-cap-track {
    flex: 1 1 70px;
    min-width: 56px;
    height: 8px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .10);
    overflow: hidden;
}
.box-cap-track > i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--green-1), var(--green-3));
    transition: width .25s ease;
}
.box-cap.is-warn .box-cap-text b { color: #8a5a00; }
.box-cap.is-warn .box-cap-track > i { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
.box-cap.is-full .box-cap-text b { color: #b91c1c; }
.box-cap.is-full .box-cap-track > i { background: linear-gradient(90deg, #f87171, #dc2626); }
/* 满了 -> 提示要**给下一步**（先去卖点东西 / 或者扩容），不是干说一句"仓库满了" */
.box-cap-tip {
    margin-top: 8px;
    padding: 9px 12px;
    border-radius: 12px;
    font-size: 12.5px;
    font-weight: 700;
    line-height: 1.6;
    color: #7f1d1d;
    background: rgba(248, 113, 113, .12);
    border: 1px dashed rgba(220, 38, 38, .45);
}
.box-cap .btn { flex: none; touch-action: manipulation; }
.box-cap .btn:disabled { opacity: .65; }   /* 到顶了：禁用但字还看得清 */

/* 分类标签：正文里的，配色和头部（深绿底）那套不一样，得自己来一遍 */
.box-tabs {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 2px;
    scrollbar-width: thin;
    scroll-padding-inline: 12px;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
}
.box-tabs[hidden] { display: none; }
.box-tabs .tab {
    flex: none;
    white-space: nowrap;
    touch-action: manipulation;
    background: rgba(23, 51, 38, .06);
    color: var(--ink-soft);
    border-color: rgba(23, 51, 38, .14);
}
.box-tabs .tab.active { background: #15803d; color: #fff; border-color: #15803d; }
.box-tabs .tab-n { opacity: .75; font-weight: 800; margin-left: 3px; }
.box-tabs .tab-lock { opacity: .65; margin-left: 4px; font-size: 11px; }
/* 还能往右滑 -> 右边淡出（和头部标签行同一套做法） */
.box-tabs.has-right {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
}
.box-tabs.has-left {
    -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to left, #000 calc(100% - 22px), transparent);
}
.box-tabs.has-left.has-right {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 22px, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to right, transparent, #000 22px, #000 calc(100% - 22px), transparent);
}

/* 不能卖的那一行：淡一点 + 一句明确的理由（🔒 不是"坏了"的样子，是"这类就是不给卖"） */
.box-row.is-nosell { background: rgba(23, 51, 38, .035); }
.box-row.is-nosell .box-name { color: var(--ink-soft); }
.box-why {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    margin-top: 3px;
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    color: #8a5a00;
    background: rgba(251, 191, 36, .18);
}
/* 数量后面的单位按分类换：作物/道具是「个」，种子「份」、特权「项」、头衔「张」、宠物「只」 */
.box-row[data-cat="seed"]      .box-count::after { content: " 份"; }
.box-row[data-cat="privilege"] .box-count::after { content: " 项"; }
.box-row[data-cat="title"]     .box-count::after { content: " 张"; }
.box-row[data-cat="pet"]       .box-count::after { content: " 只"; }

@media (max-width: 720px) {
    /* 窄屏：每一行的按钮也要 ≥44px（手指点得中）；标签行高度别太小 */
    .box-actions .btn { min-height: 44px; }
    .box-cap .btn { min-height: 44px; }
    .box-tabs .tab { padding: 7px 12px; }
    /* ★ v2.72：加入清单的数量框也一起放大（手指点得中 + 和其它控件齐平 ✓） */
    .box-actions .box-qty { height: 44px; }
}

/* ================================================================
 * ★ v2.73 手机上把仓库这一行**分成上下段**（用户原话：「移动端，输入框很大，
 *   数字看起来没对齐」+ 给了 iPhone 16 / 393×852 的截图 ✗）
 *
 * 截图里的三个问题，逐条对应下面的规则：
 *   ① 数量框"又宽又高像个大药丸" ✗ -> **收窄到 60px**（够放 2~3 位数 ✓）
 *      高度**和旁边按钮一致**（都是 44px ✓ 不比按钮高 ✗）
 *      内边距/圆角也收到和按钮一致（不再是药丸 ∵ radius 10px ✓）
 *   ② "数字看起来没对齐" ✗ -> 每行的**数字列统一右对齐**：
 *      数量「N 个」(.box-count) 和单价那一句里的数字都贴同一条右边界 ✓
 *      做法：.box-count 固定 min-width + 右对齐（本来就有的 ✓ 这里只在窄屏保持 ✗ 不改成左对齐）；
 *      .box-sub **整块左对齐**（它是文字行，不参与数字列 ✓）
 *      —— 关键是**每一行用同一套**：不能这行靠左那行靠右 ✗（选择器只按行、不按内容 ✓）
 *   ③ 价格那行被折成三截 ✗ -> 见上面 .box-row:not(.is-nosell) .box-sub { white-space: nowrap }
 *      （整句不拆 ✓ 放不下就整句掉下一行 —— **绝不会**把 🪙 和数字拆开 ✗）
 *   ④ 4 个控件在 393px 挤不下 ✗ -> **分两行**：第一行「数量框 + 主按钮」，
 *      第二行「次要按钮们」（flex-wrap + 动作区整行占满 ✓）——
 *      ⚠️ 「回收 / 全回收」**一个都没藏、没删** ✗（用户早前明确要留 ✓）
 *   ⑤ 桌面端（≥821px）不受影响 ✓（这一整段都在 max-width: 720px 里 ✓
 *      820px 那条今天刚修好的 gap 规则一个字没动 ✓）
 * ================================================================ */
@media (max-width: 720px) {
    /* 一行拆段：图标+名字+数量 占满第一段，动作区整行下移 */
    .box-row { flex-wrap: wrap; row-gap: 8px; }
    .box-info { flex: 1 1 100%; }              /* 名字占满一行，动作区被迫下移 ✓ */
    .box-row .box-count { min-width: 56px; text-align: right; }  /* 数字列右对齐（每行一样 ✓） */

    .box-row .box-actions {
        width: 100%;
        flex-wrap: wrap;
        justify-content: flex-start;           /* ★ 和左边内容同一起点（原来 flex-end 会让"掉行"更明显 ✗） */
        gap: 6px;                              /* ★ 8 -> 6：393px 下这一行才排得下（见下面的算术说明 ✓） */
    }
    /* 数量框：收窄 + 收矮 + 数字居中（用户三个诉求一次全中 ✓） */
    .box-actions .box-qty {
        width: 60px;
        min-width: 60px;
        height: 44px;
        padding: 0 4px;
        border-radius: 10px;                   /* 和按钮一致，不再是药丸 ✗ */
        text-align: center;                    /* 数字水平居中 ✓ */
        line-height: 44px;
        font-size: var(--vf-qty-fs);           /* ★ 和同排按钮**同号**（用户："数量1 比按钮文字小一号 ✗"） */
        -moz-appearance: textfield;
    }
    /* 数字框里的上下箭头隐掉（手机上没意义、还吃宽度 ✓ 上下键在桌面端照旧 ✓） */
    .box-actions .box-qty::-webkit-outer-spin-button,
    .box-actions .box-qty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
    /* 主按钮（加入清单）宽一点、次按钮窄一点 —— **每行一致** ✓ */
    .box-row .box-actions .btn { min-height: 44px; }
    /* ⚠️ 这两条必须**写在上面那条之后**（同优先级，后者赢）：393px 下 15px 的左右内边距
       会让这一行多出 ~48px → 「全卖 / 全回收」被挤到第三行 ✗
       收到 8px 之后：方案 A（数量框+加入清单+卖 1+全卖）≈ 60+16+2+52+16+2+45+6+42+6+42 ≈ 289px < 335px ✓
                     方案 B（数量框+加入清单+回收+全回收）≈ 296px ✓ */
    .box-row .box-actions .btn { padding: 6px 8px; }
    .box-row .box-actions [data-scadd],
    .box-row .box-actions [data-rcadd] { flex: 0 1 auto; }
    .box-row .box-actions [data-recycle],
    .box-row .box-actions [data-sell],
    .box-row .box-actions [data-sellitem] { flex: 0 1 auto; }
    /* ★ 393px 下把按钮里的 emoji 藏掉（省 ~16px/个）：四个控件 + 数量框恰好一行 ✓
       桌面端（>720px）emoji 照旧 ✓ —— HTML 里的 emoji 没删，只是窄屏不占宽 ✗ */
    .box-row .box-actions .btn > .no-emoji { display: none; }
}

/* ================================================================
 * ★ v2.73 数量框字号：**五处同一个值** ✓
 *   用户原话：「前面的数量1调整和按钮字体一样大小」（仓库种子页 / 作物页两张截图 ✗）
 *   一并处理的是这几处（全都取 --vf-qty-fs = 13px = .btn-sm 的字号 ✓）：
 *     ① 仓库「加入清单」框（[data-scqtyadd]）   ② 仓库「回收数量」框（[data-rqtyadd]）
 *     ③ 卖出清单行（[data-scqty]）             ④ 购物车行（[data-cartqty]）
 *     ⑤ 商店卡片购买框（[data-qty]）
 *   ⚠️ 输入框**尺寸**不在这里动（还是原来的 60px / 52px 宽 ✓ 不放大 ✗）；
 *      只统一字号，并且窄屏把高度补到和按钮一样（≥44px 触控 ✓）
 *   ⚠️⚠️ 这一块必须**留在文件最后**（见下面那段"级联顺序"说明）——
 *       `.box-actions .box-qty` 的**基础规则在后面**（width/height 写在 .box-actions 那一段），
 *       同优先级下**后写的赢**；放前面的话 height:44px 会被基础的 30px 盖掉 ✗
 *       （实测踩过：真 Chrome 里 getComputedStyle().height 还是 30px ✗）
 * ================================================================ */

/* ================================================================
 * ★ v2.72 仓库「卖出清单」（.sell-cart*）—— 用户：「后边这个卖作物做成和购物车类似的功能」
 *
 * 视觉**照抄商店购物车**（.shop-cart*，见上面的「v2.55 购物车」那一段 ✓）：
 * 同一套底色 / 边框 / 行距 / 圆角 / −＋按钮尺寸 —— 用户要的是"像购物车那样"，
 * 所以**不发明新视觉** ✗（不加渐变、不加发光、不加 hover 抬升 ✓ 朴素即可 ✓）。
 *
 * 位置也一样：.modal-box 正常流里、.modal-body **之后**的兄弟节点，
 * **没有 position:fixed/sticky**（backdrop-filter + overflow:hidden 两个坑都躲开 ✓）。
 * 列表自带 max-height + 滚动：加满 20 种也不会把仓库列表挤没 ✓。
 * ================================================================ */
/* 行内那个「要卖几件」的数量框（和 .box-actions 里其它控件同一高度 ✓） */
.box-actions .box-qty {
    width: 58px;
    height: 30px;
    flex: none;
    padding: 0 6px;
    border: 1.5px solid rgba(23, 51, 38, .18);
    border-radius: 10px;
    background: #fff;
    color: var(--ink);
    font-size: 13px;
    font-weight: 800;
    text-align: center;
    font-variant-numeric: tabular-nums;
    /* ★ v2.73：input 默认用**系统 UI 字体**（数字看着比按钮文字"小一号" ✗）——
       这里跟页面字体，和同排 .btn 完全同一套 ✓ */
    font-family: inherit;
}
.box-actions .box-qty:focus { outline: none; border-color: var(--green-2); }

.sell-cart {
    flex: none;
    padding: 10px 20px 12px;
    background: #fffdf5;
    border-top: 1.5px solid rgba(23, 51, 38, .1);
}
.sell-cart-head {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 800;
    color: var(--ink);
    margin-bottom: 6px;
}
.sell-cart-head .sell-cart-title { flex: 1; min-width: 0; }
.sell-cart-head small { font-weight: 600; color: var(--muted); margin-left: 4px; }
/* ★ v2.73 同一个面板里切换「🛒 卖出清单 / ♻️ 回收清单」（沿用现成的 .tabs/.tab ✓
   没有新视觉 ✗ 只是把默认的下外边距收一收，免得面板被撑高） */
.sell-cart-modes { margin: 0 0 6px; }
.sell-cart-modes[hidden] { display: none; }
.sell-cart-list {
    max-height: 156px;
    overflow: hidden auto;
    margin-bottom: 8px;
}
.sell-cart-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 0;
    font-size: 12.5px;
    border-bottom: 1px dashed rgba(23, 51, 38, .1);
}
.sell-cart-row:last-child { border-bottom: 0; }
.sell-cart-row .sc-icon { font-size: 16px; flex: none; }
.sell-cart-row .sc-name { flex: 1; min-width: 0; font-weight: 700; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sell-cart-row .sc-have { font-weight: 600; color: var(--muted); margin-left: 6px; }
.sell-cart-row .sc-step {
    display: inline-flex;
    align-items: center;
    flex: none;
    gap: 3px;
}
.sell-cart-row .sc-btn {
    width: 30px;
    height: 28px;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1.5px solid rgba(23, 51, 38, .18);
    border-radius: 9px;
    background: #fff;
    color: var(--ink);
    font-size: 15px;
    font-weight: 800;
    line-height: 1;
    cursor: pointer;
}
.sell-cart-row .sc-btn:hover:not(:disabled) { border-color: var(--green-2); background: #f4fce9; }
.sell-cart-row .sc-btn:disabled { opacity: .42; cursor: not-allowed; }
.sell-cart-row .sc-qty {
    flex: none;
    min-width: 26px;
    text-align: center;
    font-size: 13px;
    font-weight: 800;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.sell-cart-row .sc-sub { flex: none; min-width: 62px; text-align: right; color: var(--gold-ink); font-weight: 800; }
.sell-cart-acts {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.sell-cart-total { flex: 1; min-width: 0; font-size: 13px; font-weight: 800; color: var(--gold-ink); }
.sell-cart[hidden] { display: none; }

/* 清单的常驻合计条（清单非空才出现）：和商店的「这一页填的数量」合计栏对称 ✓ */
.sell-cart-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 10px;
    padding: 8px 12px;
    border: 1.5px solid rgba(23, 51, 38, .1);
    border-radius: 12px;
    background: #fffdf5;
    font-size: 12.5px;
    font-weight: 700;
    color: var(--ink-soft);
}
.sell-cart-bar-text { flex: 1; min-width: 0; }
.sell-cart-bar[hidden] { display: none; }

@media (max-width: 560px) {
    /* 手机上 −/＋ 的点击目标必须 ≥44px（WCAG 2.5.5 / Apple HIG ✓
       和上面 .shop-cart-row 的 ≤560px 那一档完全同一口径 ✓） */
    .sell-cart { padding: 8px 14px 10px; }
    .sell-cart-list { max-height: 132px; }
    .sell-cart-row { padding: 6px 0; gap: 6px; }
    .sell-cart-row .sc-btn { width: 44px; height: 44px; font-size: 18px; border-radius: 12px; }
    /* ★ v2.73：原来是 14px（比按钮文字 13px 还大一号 ✗）→ 统一取变量 ✓ */
    .sell-cart-row .sc-qty { min-width: 30px; font-size: var(--vf-qty-fs); }
    .sell-cart-row .sc-step { gap: 4px; }
    .sell-cart-row .sc-sub { min-width: 58px; font-size: 13px; }
}

.neighbor-list { display: flex; flex-direction: column; gap: 10px; }
.neighbor-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 14px;
    border-radius: 15px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .08);
    box-shadow: var(--shadow-sm);
    transition: transform .15s, border-color .15s;
}
.neighbor-row:hover { transform: translateX(3px); border-color: rgba(34, 197, 94, .45); }
.neighbor-row.hot { border-color: rgba(253, 224, 71, .9); background: linear-gradient(100deg, #fffdf0, #fff); }
.neighbor-avatar {
    width: 42px; height: 42px;
    flex: none;
    display: flex; align-items: center; justify-content: center;
    font-size: 23px;
    border-radius: 50%;
    background: linear-gradient(140deg, #eaf9dc, #c9f0b4);
}
.neighbor-info { flex: 1; min-width: 0; }
.neighbor-name { font-size: 14px; font-weight: 800; display: flex; align-items: center; gap: 7px; }
.neighbor-sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.neighbor-row .btn { flex: none; }

.visit-hint {
    margin-bottom: 14px;
    padding: 10px 14px;
    border-radius: 12px;
    font-size: 12.5px;
    background: rgba(59, 130, 246, .1);
    border: 1px solid rgba(59, 130, 246, .28);
    color: #1e40af;
}
.plot-grid-visit { grid-template-columns: repeat(auto-fill, minmax(210px, 228px)); justify-content: center; }
.plot-grid-visit .plot { pointer-events: auto; }

.help-body h4 { margin: 16px 0 6px; font-size: 14.5px; color: var(--green-ink); }
.help-body h4:first-child { margin-top: 0; }
.help-body p { font-size: 13px; line-height: 1.8; color: var(--ink-soft); }

/* 按钮请求中的状态 */
.btn.busy, .pbtn.busy {
    position: relative;
    overflow: hidden;
    opacity: .7;
    pointer-events: none;
}
.btn.busy::after, .pbtn.busy::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .6), transparent);
    animation: busySweep .9s linear infinite;
}
@keyframes busySweep {
    from { transform: translateX(-100%); }
    to   { transform: translateX(100%); }
}

/* ---------------- 10 提示与飘字 ---------------- */
.toasts {
    position: fixed;
    right: 20px;
    bottom: 20px;
    z-index: 300;
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: flex-end;
    pointer-events: none;
}
.toast {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 210px;
    max-width: 340px;
    padding: 12px 16px;
    border-radius: 15px;
    background: rgba(255, 255, 255, .97);
    box-shadow: var(--shadow-lg);
    border-left: 4px solid var(--green-2);
    font-size: 13.5px;
    font-weight: 600;
    line-height: 1.5;
    animation: toastIn .3s cubic-bezier(.2, 1.3, .45, 1);
}
.toast.out { animation: toastOut .28s ease forwards; }
.toast.error { border-left-color: var(--danger-ink); }
.toast.warn  { border-left-color: var(--gold-2); }
.toast .t-ico { font-size: 19px; flex: none; }

.float-layer { position: fixed; inset: 0; z-index: 250; pointer-events: none; overflow: hidden; }
.float-num {
    position: fixed;
    font-size: 20px;
    font-weight: 900;
    color: var(--gold-ink);
    text-shadow: 0 2px 6px rgba(0, 0, 0, .3), 0 0 12px rgba(255, 255, 255, .9);
    animation: floatUp 1.25s cubic-bezier(.2, .8, .3, 1) forwards;
    pointer-events: none;
    white-space: nowrap;
}
.float-num.green { color: var(--green-ink); }
.float-num.purple { color: #9333ea; }

/* 地块被操作时的反馈 */
.plot.pop { animation: plotPop .45s cubic-bezier(.2, 1.4, .4, 1); }
.plot.busy { opacity: .65; pointer-events: none; }

/* ---------------- 11 登录注册页 ---------------- */
.auth-body { display: flex; align-items: center; justify-content: center; padding: 34px 18px; }
.auth-wrap {
    position: relative;
    z-index: 10;
    display: grid;
    grid-template-columns: 1.05fr .95fr;
    width: min(1000px, 100%);
    border-radius: 30px;
    overflow: hidden;
    background: rgba(255, 255, 255, .55);
    backdrop-filter: blur(20px) saturate(1.35);
    -webkit-backdrop-filter: blur(20px) saturate(1.35);
    border: 1px solid rgba(255, 255, 255, .9);
    box-shadow: var(--shadow-lg);
    animation: popIn .4s cubic-bezier(.2, 1.2, .4, 1);
}

.auth-visual {
    position: relative;
    padding: 42px 38px;
    color: #fff;
    background: linear-gradient(150deg, #34d399 0%, #16a34a 45%, #065f46 100%);
    overflow: hidden;
}
.auth-visual::after {
    content: "";
    position: absolute;
    right: -70px;
    bottom: -90px;
    width: 280px; height: 280px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 255, 255, .22), rgba(255, 255, 255, 0) 70%);
}
.visual-badge {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 6px 15px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .2);
    font-size: 13px;
    font-weight: 700;
    margin-bottom: 24px;
    backdrop-filter: blur(6px);
}
.auth-visual h1 { font-size: 33px; line-height: 1.32; margin-bottom: 14px; letter-spacing: 1px; }
.auth-visual > p { font-size: 14px; opacity: .92; line-height: 1.8; margin-bottom: 26px; }

.visual-field {
    position: relative;
    height: 128px;
    border-radius: 20px;
    background: repeating-linear-gradient(90deg, rgba(0, 0, 0, .16) 0 3px, transparent 3px 26px), rgba(0, 0, 0, .16);
    margin-bottom: 26px;
    display: flex;
    align-items: flex-end;
    justify-content: space-around;
    padding: 0 12px 16px;
}
.visual-field .vf {
    font-size: 34px;
    filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .35));
    animation: cropReady 2.6s ease-in-out infinite;
}
.visual-field .vf:nth-child(2) { animation-delay: .3s; }
.visual-field .vf:nth-child(3) { animation-delay: .6s; }
.visual-field .vf:nth-child(4) { animation-delay: .9s; }
.visual-field .vf:nth-child(5) { animation-delay: 1.2s; }
.visual-field .vf:nth-child(6) { animation-delay: 1.5s; }

.visual-list { position: relative; z-index: 2; }
.visual-list li {
    position: relative;
    padding: 6px 0 6px 4px;
    font-size: 13px;
    line-height: 1.7;
    opacity: .95;
}
.visual-list b { color: #fff9c4; }

.auth-card { padding: 42px 40px; background: rgba(255, 255, 255, .82); }
.auth-card h2 { font-size: 25px; margin-bottom: 6px; }
.auth-sub { font-size: 13.5px; color: var(--muted); margin-bottom: 22px; }

.auth-form .field, .field { display: block; margin-bottom: 15px; }
.field > span {
    display: block;
    font-size: 13px;
    font-weight: 700;
    color: var(--ink-soft);
    margin-bottom: 6px;
}
.field > span small { color: var(--muted); font-size: 11.5px; margin-left: 4px; }
.field input {
    width: 100%;
    padding: 12px 15px;
    border-radius: 13px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #fff;
    font-size: 14.5px;
    color: var(--ink);
    transition: border-color .18s, box-shadow .18s;
}
.field input:focus {
    outline: none;
    border-color: var(--green-2);
    box-shadow: 0 0 0 4px rgba(34, 197, 94, .16);
}
.auth-form .btn-block { margin-top: 8px; }
.auth-foot {
    margin-top: 20px;
    padding-top: 18px;
    border-top: 1px dashed rgba(23, 51, 38, .16);
    text-align: center;
    font-size: 13.5px;
    color: var(--ink-soft);
}
.auth-foot a { font-weight: 800; }

/* ---------------- 12 响应式与动画 ---------------- */
@media (max-width: 1180px) {
    .layout { grid-template-columns: minmax(0, 1fr) 288px; }
}

@media (max-width: 1024px) {
    .layout { grid-template-columns: minmax(0, 1fr); }
    .layout-side { order: 2; }
    .auth-wrap { grid-template-columns: 1fr; max-width: 520px; }
    .auth-visual { display: none; }
}

@media (max-width: 820px) {
    .hud { gap: 10px; padding: 10px 14px; }
    .hud-brand .brand-text small { display: none; }
    .hud-user { flex: 1 1 100%; order: 2; }
    .hud-stats { order: 3; }
    .hud-actions { order: 4; margin-left: auto; }
    .layout { padding: 14px 12px 40px; gap: 14px; }
    .panel { padding: 15px; border-radius: 20px; }
    .daily-tip { margin: 12px 12px 0; }
    .plot-grid { grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); gap: 10px; }
    .plot-grid-visit { grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); }
    .seed-grid { grid-template-columns: 1fr; }
    /* v2.55 购物车合计：窄屏收紧内边距 / 缩小金额字号，让它永远是一行能放下的高度，
       不至于把可滚动的商品区挤掉太多（它自己不滚动、也不遮挡购买按钮） */
    .shop-total { padding: 9px 14px; font-size: 12.5px; }
    .shop-total-sum { font-size: 14px; }
    .shop-total-note { flex: 1 1 100%; }
    .shop-total-warn { padding: 6px 14px; }
    /* v2.55 购物车：窄屏收紧列表高度（它自己不滚动整个弹窗）。
       行变高了（44px 按钮），所以这里比桌面略矮一档：够看 2 行多，再多就滚动。 */
    .shop-cart { padding: 8px 14px 10px; }
    .shop-cart-list { max-height: 132px; }
    .shop-cart-row .sc-sub { min-width: 54px; }
    /* ⚠️ 手机上 −/+ 必须 ≥44px 点按目标（WCAG 2.5.5 / Apple HIG）。
       行会因此变高一点 —— 值得：手指点不中会让人以为"按钮坏了"。 */
    .shop-cart-row { padding: 6px 0; gap: 6px; }
    .shop-cart-row .sc-btn { width: 44px; height: 44px; font-size: 18px; border-radius: 12px; }
    /* ★ v2.73：原来是 14px（和卖出清单行同一处不一致 ✗）→ 统一取变量 ✓ */
    .shop-cart-row .sc-qty { min-width: 30px; font-size: var(--vf-qty-fs); }
    .shop-cart-row .sc-step { gap: 4px; }
    .shop-cart-row .sc-sub { min-width: 58px; font-size: 13px; }
    .toasts { left: 14px; right: 14px; align-items: stretch; }
    .toast { max-width: none; }
}

@media (max-width: 560px) {
    .plot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
    .plot-grid-visit { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .plot { border-radius: 16px; padding: 8px; }
    .crop-icon { font-size: 34px; }
    .pbtn { padding: 6px 9px; font-size: 12px; }
    /* 手机上原来这里把按钮文字 display:none 了，结果只剩一排图标完全看不懂。
       改成缩小字号 + 允许换行，宁可多占一行也要让人知道点的是啥 */
    .btn .btn-label { font-size: 11.5px; margin-left: 3px; }
    .hud-actions { gap: 6px; }
    .hud-actions .btn { padding: 8px 10px; font-size: 12px; }
    .brand-text b { font-size: 15px; }
    .panel-head h2 { font-size: 17px; }
    .stat { padding: 6px 10px; font-size: 13px; }
    .auth-card { padding: 30px 22px; }
    .modal { padding: 10px; }
    .modal-box { max-height: 92vh; border-radius: 20px; }
    .modal-body { padding: 14px; }
    .box-row { flex-wrap: wrap; }
    .box-row .box-actions { width: 100%; justify-content: flex-end; }
}

@keyframes sunPulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.06); opacity: .92; }
}
@keyframes drift {
    from { transform: translateX(0); }
    to { transform: translateX(160vw); }
}
@keyframes sway {
    0%, 100% { transform: rotate(-5deg); }
    50% { transform: rotate(6deg); }
}
@keyframes blink {
    0%, 100% { opacity: 1; }
    50% { opacity: .35; }
}
@keyframes cropReady {
    0%, 100% { transform: scale(1) translateY(0); }
    50% { transform: scale(1.08) translateY(-4px); }
}
@keyframes glow {
    0%, 100% { opacity: .45; transform: translate(-50%, -50%) scale(.9); }
    50% { opacity: .9; transform: translate(-50%, -50%) scale(1.12); }
}
@keyframes shake {
    0%, 100% { transform: translateX(0) rotate(0); }
    25% { transform: translateX(-2px) rotate(-5deg); }
    75% { transform: translateX(2px) rotate(5deg); }
}
@keyframes shimmer {
    0% { filter: brightness(.92); }
    50% { filter: brightness(1.14); }
    100% { filter: brightness(.92); }
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn {
    from { opacity: 0; transform: translateY(14px) scale(.96); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes slideDown {
    from { opacity: 0; transform: translateY(-14px); }
    to { opacity: 1; transform: translateY(0); }
}
@keyframes toastIn {
    from { opacity: 0; transform: translateX(30px) scale(.95); }
    to { opacity: 1; transform: translateX(0) scale(1); }
}
@keyframes toastOut {
    to { opacity: 0; transform: translateX(30px) scale(.95); }
}
@keyframes floatUp {
    0% { opacity: 0; transform: translateY(6px) scale(.7); }
    22% { opacity: 1; transform: translateY(-6px) scale(1.12); }
    100% { opacity: 0; transform: translateY(-72px) scale(1); }
}
@keyframes plotPop {
    0% { transform: scale(1); }
    40% { transform: scale(1.06); }
    100% { transform: scale(1); }
}
@keyframes statPop {
    0% { transform: scale(1); }
    45% { transform: scale(1.14); }
    100% { transform: scale(1); }
}
@keyframes goldPulse {
    0%, 100% { box-shadow: 0 4px 14px rgba(245, 158, 11, .35); }
    50% { box-shadow: 0 6px 24px rgba(245, 158, 11, .75), 0 0 0 5px rgba(253, 224, 71, .3); }
}

/* ============================================================
   v2.0 新增组件样式（全部为新增规则，不覆盖上方任何既有声明）
   目录：
     13 红点与徽标        14 天气徽章        15 装饰条与围栏
     16 土地品质与地块标记 17 每日任务        18 成就殿堂
     19 装饰商店          20 站内消息        21 排行榜
     22 好友面板          23 留言板          24 零散补丁
     25 新增动画          26 新增响应式
   ============================================================ */

/* ---------------- 13 红点与徽标 ---------------- */
/* 需要挂红点的 HUD 按钮作为定位参照（只补充定位上下文，不动其它声明） */
.hud-actions > .btn,
.hud-actions .menu-wrap > .btn { position: relative; }

.dot-badge,
.menu-badge,
.tab-badge {
    font-style: normal;
    font-weight: 800;
    color: #fff;
    background: #b91c1c;
    border-radius: 999px;
    line-height: 1;
    letter-spacing: .2px;
}

/* HUD 按钮右上角的小红点 */
.dot-badge {
    position: absolute;
    top: -5px;
    right: -5px;
    z-index: 5;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    font-size: 10px;
    border: 2px solid #fff;
    box-shadow: 0 2px 7px rgba(239, 68, 68, .5);
    animation: badgePulse 1.9s ease-in-out infinite;
    pointer-events: none;
}
.dot-badge[hidden] { display: none; }

/* v2.31：有未读、但**都被免打扰吃掉了** —— 不显示数字，只留一个小圆点。
   沿用上面 .dot-badge 的位置 / 颜色 / 白边 / 呼吸动画，只把胶囊收成圆的
   （不另造一套视觉，改的是同一个元素上的一个变体类）。 */
.dot-badge.is-quiet {
    min-width: 0;
    width: 11px;
    height: 11px;
    padding: 0;
    font-size: 0;
    top: -3px;
    right: -3px;
}

/* 下拉菜单里的小红数字 */
.menu-badge {
    display: inline-block;
    min-width: 17px;
    padding: 2px 6px;
    margin-left: 5px;
    font-size: 10.5px;
    text-align: center;
    vertical-align: 1px;
    box-shadow: 0 2px 6px rgba(239, 68, 68, .4);
}
.menu-badge[hidden] { display: none; }

/* 弹窗 tab 里的小红数字 */
.tab-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 16px;
    padding: 0 5px;
    margin-left: 5px;
    font-size: 10px;
    vertical-align: 1px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, .22);
}
.tab-badge[hidden] { display: none; }

/* 好友名后面的绿色在线小圆点 */
.online-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-left: 4px;
    border-radius: 50%;
    background: var(--green-2);
    box-shadow: 0 0 0 3px rgba(34, 197, 94, .22);
    vertical-align: 1px;
    animation: onlineBreath 2.2s ease-in-out infinite;
}

/* 浅色卡片里的高亮文字 */
.hl {
    /* 白底上用更深的琥珀色，保证对比度（--gold-3 在白底只有 ~2.2:1） */
    color: #8a5a00;
    font-weight: 800;
    text-shadow: 0 1px 0 rgba(255, 255, 255, .75);
}

/* ---------------- 14 天气徽章 ---------------- */
.weather-chip {
    --w-color: var(--green-2);
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 9px;
    padding: 5px 14px 5px 10px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .78);
    border: 1px solid rgba(255, 255, 255, .95);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    cursor: default;
    transition: box-shadow .18s ease;
}
.weather-chip:hover { box-shadow: var(--shadow); }

/* 左侧竖条：颜色来自 --w-color */
.weather-chip::before {
    content: "";
    flex: none;
    width: 4px;
    align-self: stretch;
    min-height: 28px;
    border-radius: 999px;
    background: var(--w-color);
    box-shadow: 0 0 10px var(--w-color);
    animation: wGlow 3s ease-in-out infinite;
}
/* 左侧光晕 */
.weather-chip::after {
    content: "";
    position: absolute;
    left: -16px;
    top: 50%;
    width: 96px;
    height: 96px;
    transform: translateY(-50%);
    border-radius: 50%;
    background: radial-gradient(circle, var(--w-color), rgba(255, 255, 255, 0) 70%);
    opacity: .16;
    pointer-events: none;
}
.weather-chip .w-icon {
    position: relative;
    z-index: 1;
    font-size: 22px;
    line-height: 1;
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .16));
}
.weather-chip .w-text {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    line-height: 1.2;
}
.weather-chip .w-text b { font-size: 13px; font-weight: 800; color: var(--ink); }
.weather-chip .w-text small { font-size: 10.5px; font-weight: 600; color: var(--muted); }

/* 天气系统关闭时的弱化态 */
.weather-chip.weather-off {
    --w-color: var(--muted);
    filter: grayscale(.85);
    opacity: .68;
}

/* ---------------- 15 土地品质与地块标记 ---------------- */
/* 标签变多时允许换行，避免溢出地块 */
.plot .plot-top { flex-wrap: wrap; align-content: flex-start; row-gap: 3px; }

/* 黑土地：更深的土色 + 深色颗粒纹理 */
.plot.land-black {
    --land-shadow: inset 0 8px 22px rgba(0, 0, 0, .42), inset 0 -4px 12px rgba(255, 255, 255, .08), var(--shadow-sm);
    background: radial-gradient(circle at 50% 26%, #644a35 0%, #443021 50%, #26180e 100%);
    border-color: rgba(255, 255, 255, .14);
    box-shadow: var(--land-shadow);
}
.plot.land-black::before {
    background-image:
        radial-gradient(rgba(255, 255, 255, .07) 1px, transparent 1px),
        radial-gradient(rgba(0, 0, 0, .34) 1px, transparent 1px);
    background-size: 11px 11px, 19px 19px;
    background-position: 0 0, 6px 8px;
    opacity: .9;
}

/* 金土地：金色土壤 + 金色内边框 + 微弱金光 */
.plot.land-gold {
    --land-shadow: inset 0 8px 20px rgba(0, 0, 0, .42), inset 0 0 0 2px rgba(253, 224, 71, .42), inset 0 -4px 12px rgba(255, 255, 255, .08), 0 0 20px rgba(253, 224, 71, .22), var(--shadow-sm);
    background: radial-gradient(circle at 50% 26%, #b8822a 0%, #7f5413 48%, #462c07 100%);
    border-color: rgba(253, 224, 71, .55);
    box-shadow: var(--land-shadow);
}
.plot.land-gold::before {
    background-image:
        radial-gradient(rgba(255, 240, 160, .38) 1px, transparent 1px),
        radial-gradient(rgba(110, 70, 8, .4) 1px, transparent 1px);
    background-size: 12px 12px, 18px 18px;
    background-position: 0 0, 6px 8px;
    opacity: .9;
}

/* 成熟时保留原有的金光（与土地品质叠加，不丢 .plot.done 的效果） */
.plot.land-black.done,
.plot.land-gold.done {
    border-color: rgba(253, 224, 71, .85);
    box-shadow: var(--land-shadow), 0 0 0 3px rgba(253, 224, 71, .3), 0 16px 34px rgba(245, 158, 11, .3);
}
/* 品质地块也要保留悬停反馈 */
.plot.land-black:hover,
.plot.land-gold:hover { box-shadow: var(--land-shadow), 0 16px 34px rgba(23, 51, 38, .22); }

/* 升级土地的小按钮：暗金色 */
.pbtn.land {
    background: linear-gradient(135deg, #fcd34d, #b45309);
    color: #3a2000;
    box-shadow: 0 3px 9px rgba(180, 83, 9, .45);
}
.pbtn.land:hover { filter: brightness(1.08); }

/* 地块标签：守卫犬 / 土地品质 / 丰收符 */
.tag.guard { background: linear-gradient(135deg, #64748b, #334155); box-shadow: 0 3px 9px rgba(71, 85, 105, .45); }
.tag.quality { background: linear-gradient(135deg, var(--gold-1), var(--gold-3)); color: #4a2c00; box-shadow: 0 3px 9px rgba(245, 158, 11, .45); }
.tag.charm { background: linear-gradient(135deg, #fca5a5, #dc2626); box-shadow: 0 3px 9px rgba(220, 38, 38, .4); }

/* ---------------- 17 每日任务 ---------------- */
/* 侧栏小卡 */
.task-card { background: linear-gradient(150deg, #fffdf3, #f3fce8); border-color: rgba(253, 224, 71, .5); }

.task-mini { display: block; }
.task-mini-bar {
    height: 10px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .12);
    overflow: hidden;
}
.task-mini-bar i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, #86efac, var(--green-2));
    box-shadow: 0 0 10px rgba(34, 197, 94, .5);
    transition: width .5s cubic-bezier(.3, 1, .4, 1);
}
.task-mini-text { margin-top: 7px; font-size: 12.5px; color: var(--ink-soft); }
.task-mini-text b { color: var(--green-ink); }

/* 弹窗里的统计块 */
.task-summary { margin-bottom: 14px; }
.task-summary-inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 14px;
    border-radius: 16px;
    background: linear-gradient(140deg, #f2fdf0, #e6f9dd);
    border: 1px solid rgba(255, 255, 255, .9);
    box-shadow: inset 0 1px 0 #fff, var(--shadow-sm);
}
.ts-num {
    font-size: 30px;
    font-weight: 900;
    line-height: 1.1;
    color: var(--green-ink);
    font-variant-numeric: tabular-nums;
    text-shadow: 0 2px 0 rgba(255, 255, 255, .8);
}
.ts-label { font-size: 12px; font-weight: 600; color: var(--muted); }

/* 任务列表 */
.task-list { display: flex; flex-direction: column; gap: 10px; }
.task-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    border-radius: 15px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .08);
    box-shadow: var(--shadow-sm);
    transition: transform .15s ease, border-color .15s ease, background .15s ease;
}
.task-row:hover { transform: translateX(3px); }
.task-row.done { border-color: rgba(34, 197, 94, .55); background: linear-gradient(100deg, #f2fdf1, #fff); }
.task-row.claimed { opacity: .6; }

.task-icon {
    width: 42px;
    height: 42px;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 23px;
    border-radius: 13px;
    background: linear-gradient(140deg, #f0fbe4, #d6f2bf);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, .06);
}
.task-main { flex: 1; min-width: 0; }
.task-name { font-size: 14px; font-weight: 800; }
.task-desc { font-size: 11.5px; color: var(--muted); line-height: 1.5; margin: 2px 0 7px; }
.task-bar { height: 7px; border-radius: 999px; background: rgba(23, 51, 38, .12); overflow: hidden; }
.task-bar i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, #86efac, var(--green-2));
    transition: width .5s cubic-bezier(.3, 1, .4, 1);
}
.task-reward {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    font-size: 12.5px;
    font-weight: 800;
    color: var(--gold-ink);
    font-variant-numeric: tabular-nums;
}
.task-reward small { font-size: 11px; font-weight: 700; color: var(--green-ink); }
.task-action { flex: none; min-width: 76px; display: flex; justify-content: flex-end; }
.task-need { font-size: 12.5px; font-weight: 800; color: var(--muted); font-variant-numeric: tabular-nums; }
.task-got { font-size: 12.5px; font-weight: 800; color: var(--green-ink); }

/* ---------------- 18 成就殿堂 ---------------- */
.ach-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
    gap: 12px;
}
.ach-item {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 16px 12px 13px;
    border-radius: 16px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .08);
    box-shadow: var(--shadow-sm);
    text-align: center;
    transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
}
.ach-item:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.ach-icon {
    width: 52px;
    height: 52px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28px;
    line-height: 1;
    border-radius: 50%;
    background: linear-gradient(140deg, #fff5cc, #e9fbd6);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, .06);
}
.ach-name { font-size: 13.5px; font-weight: 800; }
.ach-desc { font-size: 11px; color: var(--muted); line-height: 1.5; }
.ach-reward { margin-top: auto; padding-top: 6px; font-size: 11.5px; font-weight: 800; color: var(--gold-ink); }

/* 未解锁：整体灰度 + 半透明 + 右上角小锁 */
.ach-item:not(.unlocked) { filter: grayscale(.9); opacity: .58; }
.ach-item:not(.unlocked)::after {
    content: "🔒";
    position: absolute;
    top: 8px;
    right: 9px;
    font-size: 12px;
    opacity: .85;
}
/* 已解锁：彩色 + 金色边框 + 微光 */
.ach-item.unlocked {
    border-color: rgba(253, 224, 71, .85);
    background: linear-gradient(160deg, #fffdf0, #fff);
    box-shadow: 0 10px 26px rgba(245, 158, 11, .22);
}
.ach-item.unlocked::before {
    content: "";
    position: absolute;
    top: -22px;
    right: -22px;
    width: 90px;
    height: 90px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(253, 224, 71, .5), rgba(253, 224, 71, 0) 70%);
    pointer-events: none;
}
.ach-item.unlocked .ach-reward { color: var(--green-ink); }

/* ---------------- 20 站内消息 ---------------- */
.msg-list { display: flex; flex-direction: column; gap: 9px; }
.msg-row {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px 14px;
    border-radius: 15px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .08);
    box-shadow: var(--shadow-sm);
    transition: transform .15s ease, border-color .15s ease;
}
.msg-row:hover { transform: translateX(3px); }
.msg-row.unread {
    background: linear-gradient(100deg, #eff6ff, #fff);
    border-color: rgba(59, 130, 246, .35);
}
.msg-row.unread::before {
    content: "";
    position: absolute;
    left: 0;
    top: 10px;
    bottom: 10px;
    width: 4px;
    border-radius: 0 4px 4px 0;
    background: #3b82f6;
    box-shadow: 0 0 10px rgba(59, 130, 246, .5);
}
.msg-icon {
    width: 40px;
    height: 40px;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 21px;
    line-height: 1;
    border-radius: 12px;
    background: linear-gradient(140deg, #eef7ff, #dfeafd);
}
.msg-main { flex: 1; min-width: 0; }
.msg-title { font-size: 13.5px; font-weight: 800; }
.msg-row.unread .msg-title { font-weight: 900; }
.msg-body { font-size: 12.5px; color: var(--ink-soft); line-height: 1.6; margin: 3px 0 5px; }
.msg-meta { font-size: 11px; color: var(--muted); }
.msg-dot {
    flex: none;
    width: 9px;
    height: 9px;
    margin-top: 6px;
    border-radius: 50%;
    background: #3b82f6;
    box-shadow: 0 0 0 4px rgba(59, 130, 246, .16);
    animation: blink 2s ease-in-out infinite;
}

/* ---------------- 20b 消息：类别标签 + 每类免打扰（v2.31） ----------------
 *
 * 标签**照抄弹窗标题栏里 .tab 的几何**（胶囊 / 13px 粗体 / 5px 14px 内边距），
 * 只换配色：.tab 原本只出现在深绿的 .modal-head 上（白字半透明底），
 * 直接搬进浅色的弹窗正文就是白底白字，什么都看不见。
 */
.msg-cat-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 10px; }
.msg-cat-tabs .tab {
    background: #fff;
    color: var(--ink-soft);
    border-color: rgba(23, 51, 38, .14);
}
.msg-cat-tabs .tab:hover { background: #f2fbef; }
.msg-cat-tabs .tab.active {
    background: linear-gradient(135deg, #15803d, #166534);
    color: #fff;
    border-color: transparent;
    box-shadow: var(--shadow-sm);
}
/* 这一类当前一条消息都没有：**置灰，不隐藏**（藏了就没法提前给它开免打扰，
   标签行也会随消息来去一直蹦，玩家老是点错） */
.msg-cat-tabs .tab.zero { opacity: .55; }

/* 免打扰面板：展开式，不是浮层 —— 卡片有 backdrop-filter，
   position:fixed 的包含块会变成那个祖先（这里已经翻过两次车） */
.msg-mute-fold { margin-bottom: 11px; }
.msg-fold-head {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 9px 12px;
    border: 1px solid rgba(23, 51, 38, .10);
    border-radius: 12px;
    background: rgba(255, 255, 255, .72);
    color: var(--ink);
    font-size: 13px;
    font-weight: 800;
    cursor: pointer;
    text-align: left;
}
.msg-fold-head:hover { border-color: rgba(34, 197, 94, .4); }
.msg-fold-head small { font-size: 11px; font-weight: 500; color: var(--muted); }
.msg-fold-head .fold-arrow { font-size: 10px; color: var(--muted); }

.msg-mute-body {
    margin-top: 9px;
    padding: 11px 12px;
    border-radius: 14px;
    background: rgba(34, 197, 94, .07);
    border: 1px dashed rgba(34, 197, 94, .32);
}
/* 折叠面板的 hidden 必须显式写一条（本项目的约定，见文件末尾那一组 [hidden]）。
   这里 .msg-mute-body 目前没有设 display，浏览器默认的 [hidden] 是生效的 ——
   但 .msg-mute-list / .msg-cat-tabs 都是 display:flex/grid，哪天给这个容器也加上
   display，hidden 就会**静默失效**（订单折叠就栽在这上面）。补一条挡住以后。 */
.msg-mute-body[hidden] { display: none; }
/* 免打扰的口径说明必须紧贴在开关上面 */
.msg-mute-note { margin: 0 0 10px; }

.msg-mute-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
.msg-mute-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 7px 8px 7px 11px;
    border-radius: 13px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .09);
}
/* 这一类开着免打扰：给个暖色边框，一眼能扫出设过哪几类 */
.msg-mute-row.is-muted { border-color: rgba(245, 158, 11, .5); background: #fffbf0; }
.msg-mute-row .mm-name { flex: 1; min-width: 0; font-size: 12.5px; font-weight: 800; }
.msg-mute-row .btn { flex: none; }

/* 分组小标题（「全部」视图里每条消息前面那一行） */
.msg-group-head {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 4px 2px 0;
    font-size: 12.5px;
    font-weight: 800;
    color: var(--green-4);
}
.msg-group-head:first-child { margin-top: 0; }
.msg-group-head small { font-weight: 500; color: var(--muted); }
.msg-group-head .tab-badge { background: #3b82f6; box-shadow: none; }

@media (max-width: 560px) {
    /* 窄屏：免打扰开关改成一行一个，不然按钮会被挤成两行 */
    .msg-mute-list { grid-template-columns: 1fr; }
}

/* ---------------- 21 排行榜 ---------------- */
.rank-list { display: flex; flex-direction: column; gap: 9px; }
.rank-row {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 10px 14px;
    border-radius: 15px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .08);
    box-shadow: var(--shadow-sm);
    transition: transform .15s ease, border-color .15s ease;
}
.rank-row:hover { transform: translateX(3px); }
/* 前三名：金 / 银 / 铜 */
.rank-row:nth-child(1) { background: linear-gradient(100deg, #fffbe6, #fff); border-color: rgba(253, 224, 71, .85); }
.rank-row:nth-child(2) { background: linear-gradient(100deg, #f4f7fa, #fff); border-color: rgba(148, 163, 184, .6); }
.rank-row:nth-child(3) { background: linear-gradient(100deg, #fdf1e4, #fff); border-color: rgba(217, 119, 6, .45); }
/* 我自己：绿色高亮（写在名次之后，优先显示） */
.rank-row.is-me {
    border-color: rgba(34, 197, 94, .75);
    background: linear-gradient(100deg, #f0fdf4, #fff);
    box-shadow: 0 0 0 2px rgba(34, 197, 94, .14), var(--shadow-sm);
}
.rank-no {
    flex: none;
    width: 34px;
    text-align: center;
    font-size: 15px;
    font-weight: 900;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
.rank-row:nth-child(-n+3) .rank-no { font-size: 21px; line-height: 1; }
.rank-name {
    flex: 1;
    min-width: 0;
    font-size: 14px;
    font-weight: 800;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.rank-name small { margin-left: 6px; font-size: 11px; font-weight: 700; color: var(--muted); }
.rank-val {
    flex: none;
    font-size: 15px;
    font-weight: 900;
    color: var(--green-ink);
    font-variant-numeric: tabular-nums;
}
.rank-val small { margin-left: 3px; font-size: 10.5px; font-weight: 700; color: var(--muted); }
/* 底部固定的「我的排名」条 */
.rank-me {
    position: sticky;
    bottom: 0;
    margin-top: 12px;
    padding: 11px 14px;
    border-radius: 14px;
    text-align: center;
    font-size: 13px;
    font-weight: 700;
    color: #7a5200;
    background: linear-gradient(100deg, #fff7d6, #ffeaa7);
    border: 1px solid rgba(253, 224, 71, .8);
    box-shadow: var(--shadow-sm);
}
.rank-me b { color: #8a5a00; font-weight: 900; }
.rank-me[hidden] { display: none; }

/* ---------------- v2.66 排行榜自动刷新：轻微过渡 ----------------
   只有「内容真的变了」的时候 JS 才会加 .rank-updated（没变时一个节点都不动，
   见 game.js 的 paintIfChanged）：一次性的淡黄高亮，让名次/数字变化有轻微过渡，
   而不是硬跳一下。动画结束回到原样，不会永久改颜色。
   「减少动效」由文件上方那条全局 @media (prefers-reduced-motion) 统一降级。 */
@keyframes rankUpdated {
    from { background-color: rgba(253, 224, 71, .42); }
    to   { background-color: transparent; }
}
.rank-updated {
    animation: rankUpdated .9s ease-out;
    border-radius: 16px;
}

/* ---------------- 22 好友面板 ---------------- */
.friend-pane { animation: fadeIn .22s ease; }
.friend-pane[hidden] { display: none; }

.search-bar { display: flex; gap: 10px; margin-bottom: 14px; }
.search-bar input {
    flex: 1;
    min-width: 0;
    padding: 11px 15px;
    border-radius: 13px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #fff;
    font-size: 14px;
    color: var(--ink);
    transition: border-color .18s ease, box-shadow .18s ease;
}
.search-bar input:focus {
    outline: none;
    border-color: var(--green-2);
    box-shadow: 0 0 0 4px rgba(34, 197, 94, .16);
}
.search-bar .btn { flex: none; }

/* 参观弹窗头部（深绿底）里的按钮组 */
.visit-head-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-left: 6px;
}
.visit-head-actions .btn {
    background: rgba(255, 255, 255, .22);
    border-color: rgba(255, 255, 255, .55);
    color: #fff;
    box-shadow: none;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}
.visit-head-actions .btn:hover { background: rgba(255, 255, 255, .36); box-shadow: 0 6px 16px rgba(0, 0, 0, .18); }
.visit-head-actions .btn-primary {
    background: linear-gradient(135deg, #fff, #d9fbe4);
    border-color: #fff;
    color: var(--green-ink);
}
.visit-head-actions .btn-primary:hover { background: #fff; }
.visit-head-actions .mini-badge { background: rgba(255, 255, 255, .24); color: #fff; }

/* ---------------- 23 留言板 ---------------- */
.guestbook { margin-top: 18px; padding-top: 16px; border-top: 1px dashed rgba(23, 51, 38, .16); }
.guestbook h4 {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 11px;
    font-size: 14.5px;
    color: var(--green-ink);
}
.guestbook h4 small { font-size: 11.5px; font-weight: 500; color: var(--muted); }

.guest-form { display: flex; gap: 9px; margin-bottom: 12px; }
.guest-form input {
    flex: 1;
    min-width: 0;
    padding: 11px 15px;
    border-radius: 13px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #fff;
    font-size: 14px;
    color: var(--ink);
    transition: border-color .18s ease, box-shadow .18s ease;
}
.guest-form input:focus {
    outline: none;
    border-color: var(--green-2);
    box-shadow: 0 0 0 4px rgba(34, 197, 94, .16);
}
.guest-form .btn { flex: none; }

.guest-list { display: flex; flex-direction: column; gap: 8px; max-height: 260px; overflow: hidden auto; }
.guest-item {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 14px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .07);
    box-shadow: var(--shadow-sm);
    transition: border-color .15s ease, background .15s ease;
}
.guest-item:hover { border-color: rgba(34, 197, 94, .4); background: #fdfffb; }
.guest-avatar {
    width: 34px;
    height: 34px;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    line-height: 1;
    border-radius: 50%;
    background: linear-gradient(140deg, #eaf9dc, #c9f0b4);
}
.guest-main { flex: 1; min-width: 0; }
.guest-text { font-size: 13px; line-height: 1.6; word-break: break-word; }
.guest-meta { margin-top: 3px; font-size: 11px; color: var(--muted); }
.guest-del {
    flex: none;
    width: 24px;
    height: 24px;
    border: 0;
    border-radius: 50%;
    background: rgba(239, 68, 68, .12);
    color: var(--danger-ink);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity .16s ease, background .16s ease;
}
.guest-item:hover .guest-del,
.guest-del:focus-visible { opacity: 1; }
.guest-del:hover { background: rgba(239, 68, 68, .25); }
.guest-empty { padding: 22px 0; text-align: center; font-size: 13px; color: var(--muted); }
/* 触屏没有 hover，删除按钮直接显示 */
@media (hover: none) {
    .guest-del { opacity: 1; }
}

/* ---------------- 24 零散补丁 ---------------- */
.hint-text { margin: 12px 0 0; font-size: 13px; line-height: 1.8; color: var(--muted); }
.modal-body > .hint-text:first-child { margin-top: 0; }

/* 侧栏列表可点击项 */
.mini-list li.clickable { cursor: pointer; transition: background .15s ease, transform .15s ease; }
.mini-list li.clickable:hover { background: rgba(34, 197, 94, .16); transform: translateX(2px); }

/* v2.73 ⑥ 点「头像 / 名字」看资料（data-uinfo）—— 只加一个"能点"的手型光标，
   **一个尺寸/间距/颜色都不改** ✓（不重排版面 ✗：所以这里没有 hover 换色、
   也没有加下划线，免得用户觉得"好友列表又变了"）。
   好友行、侧栏「我的好友」小卡都用同一条规则。 */
.mini-list .mini-info[data-uinfo],
.neighbor-row [data-uinfo] { cursor: pointer; }

/* v2.78：首页「👥 邻居动态」「🏆 排行榜」小卡 + 「🏆 完整榜单 / 🏅 赛季」榜单行
   的头像与名字也能点开资料（data-uinfo）—— 照 v2.73 那条的写法，**只加手型光标** ✓
   （没有 hover 换色 / 下划线 / 尺寸变化：版式、配色、间距一个字都不改 ✗） */
.mini-list .mini-avatar[data-uinfo],
.rank-row [data-uinfo] { cursor: pointer; }

/* 蓝色的「可帮」徽章，区别于绿色的「可偷」 */
.mini-badge.help { background: rgba(59, 130, 246, .18); color: #1e40af; }

/* 图例里的金土地小圆点 */
.dot-gold { background: linear-gradient(135deg, var(--gold-1), var(--gold-3)); box-shadow: 0 0 8px rgba(245, 158, 11, .6); }

/* 邻居/好友一行里出现多个按钮时保持间距与对齐 */
.neighbor-list .neighbor-row { flex-wrap: wrap; row-gap: 8px; }
.neighbor-row > .btn { flex: none; min-width: 64px; }

/* ---------------- 25 新增动画 ---------------- */
@keyframes badgePulse {
    0%, 100% { transform: scale(1); box-shadow: 0 2px 7px rgba(239, 68, 68, .45); }
    50%      { transform: scale(1.14); box-shadow: 0 2px 13px rgba(239, 68, 68, .78); }
}
@keyframes onlineBreath {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 3px rgba(34, 197, 94, .22); }
    50%      { opacity: .5; box-shadow: 0 0 0 5px rgba(34, 197, 94, .1); }
}
@keyframes wGlow {
    0%, 100% { opacity: .75; }
    50%      { opacity: 1; }
}

/* ---------------- 26 新增响应式 ---------------- */
@media (max-width: 560px) {
    /* 成就 / 装饰网格收成 2 列 */
    .ach-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; }
    .ach-item { padding: 13px 9px 11px; }
    .ach-icon { width: 44px; height: 44px; font-size: 23px; }
    .ach-desc { font-size: 10.5px; }

    /* 任务行：次要说明隐藏，奖励与按钮换行 */
    .task-row { flex-wrap: wrap; gap: 9px 10px; padding: 11px 12px; }
    .task-icon { width: 36px; height: 36px; font-size: 20px; border-radius: 11px; }
    .task-desc { display: none; }
    .task-action { margin-left: auto; }
    .ts-num { font-size: 26px; }

    /* 排行榜：允许换行，按钮另起一行靠右 */
    .rank-row { flex-wrap: wrap; gap: 8px 10px; }
    .rank-no { width: 26px; font-size: 14px; }
    .rank-row:nth-child(-n+3) .rank-no { font-size: 19px; }
    .rank-val { font-size: 14px; }
    .rank-row .btn { margin-left: auto; }
    .rank-me { position: static; }

    /* 天气徽章收紧 */
    .weather-chip { padding: 4px 11px 4px 9px; gap: 7px; }
    .weather-chip .w-icon { font-size: 18px; }
    .weather-chip .w-text small { font-size: 10px; }

    /* 消息与留言板 */
    .msg-icon { width: 34px; height: 34px; font-size: 18px; border-radius: 10px; }
    .msg-row { padding: 11px 12px; }
    .search-bar { flex-wrap: wrap; }
    .search-bar .btn { width: 100%; }
    .guest-list { max-height: 200px; }
    .guest-del { opacity: 1; }
    .visit-head-actions { margin-left: 0; gap: 6px; }
}

/* 更窄的手机（安卓常见 360~414px）再收一档，保证一排能放下两三个按钮 */
@media (max-width: 400px) {
    .hud { padding: 9px 11px; gap: 10px; }
    .hud-actions .btn { padding: 7px 8px; }
    .btn .btn-label { font-size: 11px; margin-left: 2px; }
    .hud-stats .stat { padding: 5px 8px; font-size: 12px; }
    /* v2.66：一键操作区这一档的样式挪到文件末尾「一键操作区（移动端）」那块统一管，
       这里不再重复写 .farm-actions —— 两处写同一件事最容易改漏一处。 */
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
    }
}

/* ================================================================
 * v2.2 特权 / 头衔
 * ============================================================= */

/* HUD 上昵称旁边的头衔 */
.hud-title {
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    white-space: nowrap;
    color: #4a2c00;
    background: linear-gradient(135deg, var(--gold-1), var(--gold-3));
    box-shadow: 0 3px 10px rgba(245, 158, 11, .4);
}

/* 没开通特权的「一键」按钮：压暗 + 右上角一把锁 */
.btn.priv-locked {
    background: rgba(255, 255, 255, .55) !important;
    color: var(--ink-soft) !important;
    box-shadow: none !important;
    border: 1px dashed rgba(23, 51, 38, .28) !important;
}
.btn.priv-locked:hover { filter: none; transform: none; }

.priv-lock {
    margin-left: 5px;
    font-size: 12px;
    opacity: .85;
}

/* 商店里的说明条 */
.shop-note {
    grid-column: 1 / -1;
    padding: 11px 14px;
    border-radius: 12px;
    background: rgba(245, 158, 11, .12);
    border: 1px dashed rgba(245, 158, 11, .5);
    color: #8a5a00;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.7;
}

.priv-card.is-owned,
.title-card.is-owned {
    border-color: rgba(34, 197, 94, .5);
    background: linear-gradient(160deg, #f3fdf0, #fff);
}

.priv-card .seed-icon,
.title-card .seed-icon { font-size: 34px; }

.title-card .seed-stats { flex-wrap: wrap; }

/* v2.58 特权卡片上的「这条特权给什么」清单（动作 + 数值效果）
   ⚠️ 是新类，不复用 .seed-desc / .is-owned 那些已有语义的类：
      那些是"说明文字"和"已开通"，这里是"效果条目"，混用会二义。 */
.priv-fx {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 6px;
    margin: 2px 0 7px;
}
.priv-fx-item {
    display: inline-block;
    padding: 2px 7px;
    border-radius: 999px;
    font-size: 11px;
    line-height: 1.5;
    font-weight: 600;
    color: #14532d;
    background: rgba(34, 197, 94, .12);
    border: 1px solid rgba(34, 197, 94, .3);
    white-space: nowrap;
}

/* 剩余时长 */
.priv-left { color: var(--green-4); }

/* ================================================================
 * v2.6 玩家信息卡（点头像弹出）
 * ============================================================= */
.modal-box.modal-sm { max-width: 380px; }

.uc-top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.uc-avatar {
    flex: none; width: 54px; height: 54px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 28px; background: linear-gradient(140deg, #eafff1, #d6f5e3);
    border: 2px solid rgba(34, 197, 94, .3);
}
.uc-who { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.uc-name { font-size: 16px; font-weight: 800; color: var(--ink); }
.uc-meta { font-size: 12px; color: var(--ink-soft); }
.uc-on { color: var(--green-4); }
.uc-off { color: #475569; }

.uc-bar {
    height: 7px; border-radius: 7px; overflow: hidden;
    background: rgba(23, 51, 38, .1); margin-bottom: 5px;
}
.uc-bar i { display: block; height: 100%; border-radius: 7px; background: linear-gradient(90deg, #86efac, var(--green-2)); }
.uc-exp { font-size: 11.5px; color: var(--ink-soft); text-align: right; margin-bottom: 12px; }

.uc-title-desc {
    padding: 8px 12px; border-radius: 11px; margin-bottom: 12px;
    background: rgba(245, 158, 11, .12); border: 1px dashed rgba(245, 158, 11, .45);
    color: #8a5a00; font-size: 12.5px; font-weight: 600;
}

.uc-stats {
    list-style: none; margin: 0 0 12px; padding: 0;
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px;
}
.uc-stats li {
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    padding: 9px 4px; border-radius: 12px; background: rgba(23, 51, 38, .045);
}
.uc-stats b { font-size: 14.5px; font-weight: 800; color: var(--ink); }
.uc-stats span { font-size: 10.5px; color: var(--ink-soft); }

.uc-foot { font-size: 12px; color: var(--ink-soft); text-align: center; margin-bottom: 14px; }
.uc-acts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }

@media (max-width: 560px) {
    .uc-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .uc-acts .btn { flex: 1 1 auto; justify-content: center; }
}

/* 「⋯」菜单里标出当前页（个人中心 / 聊天中心） */
.menu a.on { background: rgba(34, 197, 94, .14); color: var(--green-4); }
.menu a.on::after { content: '· 当前页'; float: right; font-size: 11px; opacity: .7; font-weight: 600; }

/* 任务弹窗：达标但还没领奖时的那行提示 */
.task-summary-inner .ts-wait {
    margin-top: 4px; font-size: 12px; font-weight: 800;
    color: #8a5a00; padding: 3px 10px; border-radius: 999px;
    background: rgba(251, 191, 36, .18);
    border: 1px dashed rgba(251, 191, 36, .55);
}

/* v2.64 任务弹窗：全完成奖励那行（后台配的两个数，见 admin/settings.php） */
.task-summary-inner .ts-bonus {
    margin-top: 4px; font-size: 12px; font-weight: 700;
    color: #7c2d12; padding: 3px 10px; border-radius: 999px;
    background: rgba(253, 186, 116, .22);
    border: 1px solid rgba(249, 115, 22, .38);
}
/* 今天已经领过 -> 变灰，和「待领取」的黄区分开 */
.task-summary-inner .ts-bonus.is-got {
    color: var(--muted);
    background: rgba(148, 163, 184, .14);
    border-color: rgba(148, 163, 184, .35);
}

/* ================================================================
 * v2.8.1 手机端排版整理
 *
 * 原来手机上几个明显的问题：
 *   1. HUD 按钮是 flex 换行，9 个按钮排成 6+3 两行、每行宽度还参差不齐。
 *   2. 地块固定 2 列，每块 163px 见方，18 块要滚一千多像素。
 *   3. 三层容器内边距（22+22+20）在手机上白吃一百多像素。
 * ============================================================= */
@media (max-width: 560px) {

    /* ---- 顶部信息收紧：品牌、用户名、三个数据块各占一行 ---- */

    /* 副标题「种下第一颗种子…」在手机上是纯占地方 */
    .brand-text small { display: none; }
    .hud-brand { gap: 8px; }
    .hud-brand .logo { font-size: 24px; }

    .hud-user { flex: 1 1 100%; min-width: 0; gap: 9px; }

    /* 三个数据块等宽，不再各占各的宽度显得参差 */
    .hud-stats {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 6px;
        width: 100%;
    }
    .hud-stats .stat { justify-content: center; padding: 6px 8px; }

    /* ---- HUD 按钮：每行 5 个，等宽 ----
       为什么不用 grid 的 auto-fill：屏一宽它就会排出 6 列、7 列，
       9 个按钮变成 6+3 / 7+2，又回到参差的老问题。
       这里用百分比定宽，所以屏宽怎么变都是「每行 5 个」，
       而 max-width 兜住之后，按钮少的页面（聊天页只有 4 个）
       也不会被 flex 拉伸成大药丸。 */
    .hud-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
        width: 100%;
        margin-left: 0;
    }

    .hud-actions > .btn,
    .hud-actions .menu-wrap {
        flex: 1 1 calc(20% - 5px);
        max-width: calc(20% - 5px);
        min-width: 0;
    }

    .hud-actions .menu-wrap > .btn { width: 100%; }

    .hud-actions .btn,
    .hud-actions .menu-wrap > .btn {
        flex-direction: column;
        gap: 2px;
        padding: 7px 2px;
        font-size: 15px;
        line-height: 1.1;
    }

    .hud-actions .btn .btn-label {
        font-size: 10.5px;
        margin-left: 0;
        line-height: 1.2;
    }

    /* ---- 地块：3 列 + 紧凑空地块 ---- */
    .plot-grid,
    .plot-grid-visit {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 7px;
    }

    .plot { border-radius: 13px; padding: 6px; }
    .plot .crop-icon { font-size: 24px; }
    .plot.empty .crop-icon { font-size: 19px; }
    .empty-hint { font-size: 9.5px; }
    .plot-meta { font-size: 10px; margin-bottom: 4px; }
    .plot-progress { height: 5px; margin-bottom: 5px; }
    .plot-actions { gap: 4px; }
    .plot-actions .pbtn { padding: 4px 6px; font-size: 10.5px; border-radius: 8px; }
}

/* 320px 这种超窄屏再收一档：列数交给 auto-fill 自己算，这里只缩字号 */
@media (max-width: 380px) {
    /* 超窄屏（CSS 视口 320~380px，如 iPhone SE）只缩字号、不动列数。
       列数是 auto-fill 按屏宽算的，320px 下每格仍有 ~54px，超过 44px 点按标准。
       地块**仍然是 3 列**，不要退回 2 列，否则 18 块地又要滚一千多像素。 */
    .hud-actions { gap: 5px; }
    .hud-actions .btn { padding: 6px 1px; font-size: 14px; }
    .hud-actions .btn .btn-label { font-size: 9.5px; }

    .plot-grid,
    .plot-grid-visit { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
    .plot { padding: 4px; border-radius: 11px; }
    .plot .crop-icon { font-size: 20px; }
    .plot.empty .crop-icon { font-size: 16px; }
    .empty-hint { font-size: 8.5px; }
    .plot-meta { font-size: 9px; }
    .plot-actions { gap: 3px; }
    .plot-actions .pbtn { padding: 3px 5px; font-size: 10px; border-radius: 7px; }
}

/* 手机屏幕就这么宽，22+22+20+20 的内边距白白吃掉一百多像素，收一收 */
/* ★ v2.93.8（用户原话「空隙太大」）：页码点 bottom → 下一块 top 实测 **23.00px**：
     .hud padding-bottom 10 + border-bottom 1 + .layout padding-top 12 = 23。
   用户截图里圆点下面那条淡绿的空白带就是它。**撑大头的就是 .layout 的 12px**
   （`--vf-nav-keep` 只是 clip-path 的裁切线，不参与布局、不占高度 ✗）。
   改到规范 §1.1 的档位：`.layout` padding-top 12 → **0** ⇒ 10 + 1 + 0 = **11px（≤12）** ✓
   ⚠️ **.hud 的 padding-bottom 保持 10px 不动**：keepPx / 门闩线 / 收起态雾带
      都靠它，动了会把 navblur 的雾带像素断言和门闩语义一起带偏 ✗。
   有每日提示条时同一条带子：`.daily-tip` margin-top 12 → **0** ⇒ 也是 11px ✓ */
@media (max-width: 560px) {
    .hud { padding: 10px 12px; gap: 10px; }
    .layout { padding: 0 10px 40px; gap: 14px; }
    .panel { padding: 14px; border-radius: 20px; }
    .daily-tip { margin-top: 0; }
}

/* ================================================================
 * v2.8.2 农场面板头：换行后别一边倒
 *
 * .panel-tools 是 margin-left:auto（宽屏时右对齐，对的）。
 * 但窄屏换行之后它整条跑到第二排最右边，跟第一排的左对齐对不上，
 * 左边空一大块，看着就「别扭」。
 * 手机上让工具条自己占满一整排：状态在左、刷新在右。
 * 只作用于农场面板 —— 个人中心也用了 .panel-tools，别误伤。
 * ============================================================= */
@media (max-width: 560px) {
    .farm-panel .panel-head { row-gap: 9px; }
    .farm-panel .panel-tools {
        flex: 1 1 100%;
        margin-left: 0;
        justify-content: flex-start;
        gap: 7px;
    }
    /* 刷新按钮推到最右，状态类 chip 留在左边 */
    .farm-panel .panel-tools #btnRefresh { margin-left: auto; }
    .farm-panel .chip { font-size: 12px; padding: 5px 10px; }
}

/* ================================================================
 * v2.8.3 宠物（HUD 徽章 / 我的宠物面板 / 宠物商店）
 *
 * 只做「追加」，不动上面任何既有规则。
 * 所有新类名都带 pet- 前缀，避免和别的组件串味（.card / .owned 那种坑）。
 * ============================================================= */

/* ---- HUD 上的宠物小徽章（.hud-stats 里的第 4 个孩子） ---- */
.pet-chip {
    border: 0;
    font-family: inherit;
    font-size: 12px;
    line-height: 1.4;
    cursor: pointer;
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: transform .15s, filter .15s;
}
.pet-chip:hover { transform: translateY(-1px); filter: brightness(1.04); }
.pet-chip[hidden] { display: none; }
/* 饿了 / 饿透了：换成警示色，一眼能看出加成没生效 */
.pet-chip.pet-chip-hungry {
    background: rgba(239, 68, 68, .16);
    color: #b91c1c;
    box-shadow: inset 0 0 0 1px rgba(239, 68, 68, .35);
}
/* 宠物在窝里（没带身边） */
.pet-chip.pet-chip-idle { background: rgba(255, 255, 255, .65); color: var(--ink-soft); }

/* ---- 宠物面板 ---- */
.pet-list { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }

.pet-card {
    position: relative;
    display: flex;
    gap: 13px;
    padding: 13px 15px;
    border-radius: 18px;
    background: #fff;
    border: 1.5px solid rgba(23, 51, 38, .09);
    box-shadow: var(--shadow-sm);
    transition: border-color .16s, box-shadow .16s, transform .16s;
}
.pet-card:hover { transform: translateY(-2px); border-color: rgba(34, 197, 94, .45); box-shadow: var(--shadow); }
/* 带在身边的那只：高亮边框 + 左侧色条 */
.pet-card.is-active {
    border-color: rgba(34, 197, 94, .75);
    background: linear-gradient(160deg, #f2fdf0, #fff);
    box-shadow: 0 10px 26px rgba(34, 197, 94, .18);
}
.pet-card.is-active::before {
    content: "";
    position: absolute;
    left: 0; top: 14px; bottom: 14px;
    width: 4px;
    border-radius: 0 4px 4px 0;
    background: linear-gradient(180deg, var(--green-1), var(--green-3));
}

/* ================================================================
 * v2.55 整张宠物卡片可点（点了看它的技能）
 *
 * ⚠️ 为什么另起一个类、而不复用 `.is-active`：
 *   `.is-active` 是**「带在身边」**（加成生效的那只，绿色边框 + 左侧绿条）。
 *   用户截图里第一张卡的绿边就是它 —— **不是**"正在看它的技能"。
 *   如果拿它当真选态，会出现"绿边到底表示什么"的二义；所以这里用**琥珀色**
 *   （和绿色完全区分），两种状态可以同时存在、各表一义。
 * ================================================================ */
.pet-card { cursor: pointer; }
.pet-card.is-skill-open {
    border-color: var(--gold-ink);
    box-shadow: 0 0 0 2px rgba(245, 158, 11, .22), var(--shadow-sm);
}
/* 键盘焦点圈（只在键盘操作时出现，鼠标点不会留一圈） */
.pet-card:focus-visible {
    outline: 3px solid rgba(34, 197, 94, .55);
    outline-offset: 2px;
}
/* 「点卡片看技能」这句提示：把"可点"这件事说出来（用户就是不知道能点） */
.pet-skill-hint {
    margin-left: 6px;
    font-size: 11px;
    font-weight: 700;
    color: var(--gold-ink);
    white-space: nowrap;
}
.pet-card.is-skill-open .pet-skill-hint { color: var(--green-4); }

.pet-avatar {
    flex: none;
    width: 56px; height: 56px;
    display: flex; align-items: center; justify-content: center;
    font-size: 32px;
    border-radius: 16px;
    background: linear-gradient(140deg, #fff5cc, #d9f2c6);
    box-shadow: inset 0 2px 6px rgba(0, 0, 0, .06);
}
.pet-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.pet-row { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.pet-row-name { gap: 8px; }

.pet-name-text { font-size: 15.5px; font-weight: 800; }
.pet-rarity {
    flex: none;
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 10.5px;
    font-weight: 800;
    background: rgba(23, 51, 38, .09);
    color: var(--ink-soft);
}
.pet-rarity.c-gray   { background: rgba(23, 51, 38, .09);   color: #4b5563; }
.pet-rarity.c-blue   { background: rgba(59, 130, 246, .16); color: #1d4ed8; }
.pet-rarity.c-purple { background: rgba(147, 51, 234, .16); color: #7e22ce; }
.pet-rarity.c-gold   { background: rgba(245, 158, 11, .18); color: #8a5a00; }

.pet-active-btn { margin-left: auto; }

.pet-lv { font-size: 12.5px; font-weight: 800; color: var(--ink-soft); }
.pet-max {
    font-size: 12.5px; font-weight: 800;
    padding: 2px 10px; border-radius: 999px;
    color: #4a2c00;
    background: linear-gradient(135deg, var(--gold-1), var(--gold-3));
}
.pet-exp { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

.pet-bar {
    flex: 1 1 90px;
    min-width: 60px;
    height: 8px;
    border-radius: 999px;
    overflow: hidden;
    background: rgba(23, 51, 38, .12);
}
.pet-bar i { display: block; height: 100%; border-radius: 999px; transition: width .4s ease; }
.pet-bar-exp i { background: linear-gradient(90deg, #86efac, var(--green-2)); }

.pet-row-state { font-size: 11.5px; color: var(--ink-soft); }
.pet-sep { opacity: .45; }
.pet-hunger-num { font-size: 12.5px; font-variant-numeric: tabular-nums; }

/* 饱食度条的三种颜色：>=30 绿 / 1-29 橙 / 0 红 */
.pet-bar-hunger i { background: linear-gradient(90deg, #86efac, var(--green-2)); }
.pet-bar-hunger.is-hungry i { background: linear-gradient(90deg, #fdba74, #f97316); }
.pet-bar-hunger.is-starving i { background: linear-gradient(90deg, #fca5a5, #dc2626); }
.pet-hunger-text.is-hungry,
.pet-hunger-label.is-hungry { color: #c2410c; font-weight: 800; }
.pet-hunger-text.is-starving,
.pet-hunger-label.is-starving { color: #b91c1c; font-weight: 800; }

/* 加成行：没生效就压暗 + 红色警示 */
.pet-effect { font-size: 12.5px; }
.pet-eff-now { font-weight: 800; color: var(--green-4); }
.pet-eff-full { font-size: 11.5px; color: var(--muted); }
.pet-effect.is-off .pet-eff-now { color: var(--muted); text-decoration: line-through; }
.pet-eff-warn {
    font-size: 11px;
    font-weight: 800;
    padding: 1px 9px;
    border-radius: 999px;
    color: #b91c1c;
    background: rgba(239, 68, 68, .14);
    border: 1px dashed rgba(239, 68, 68, .45);
}

.pet-actions { gap: 6px; margin-top: 2px; }
.pet-actions .btn:disabled { opacity: .55; cursor: not-allowed; }

/* 改名：内联输入框（不用原生 prompt） */
.pet-rename { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pet-rename-input {
    width: 132px;
    padding: 5px 9px;
    border-radius: 9px;
    border: 1.5px solid rgba(23, 51, 38, .16);
    font-size: 13.5px;
    font-weight: 700;
    color: var(--ink);
    background: #fff;
    font-family: inherit;
}
.pet-rename-input:focus { outline: none; border-color: var(--green-2); }
/* v2.59 改名提示：这次是免费，还是要扣一张「宠物改名卡」（没卡时是警示色） */
.pet-rename-note {
    flex: 1 1 100%;
    font-size: 11.5px;
    font-weight: 700;
    color: var(--muted);
}
.pet-rename-note.is-warn { color: #8a5a00; }

/* ---- 宠物商店的图鉴卡片 ---- */
.pet-shop-card.is-owned {
    border-color: rgba(34, 197, 94, .5);
    background: linear-gradient(160deg, #f3fdf0, #fff);
}
.pet-shop-card .seed-name { flex-wrap: wrap; }
.pet-shop-icon { background: linear-gradient(140deg, #fff5cc, #ffe9b8); }

.pet-nosell {
    margin-left: auto;
    font-size: 11.5px;
    font-weight: 800;
    padding: 4px 10px;
    border-radius: 999px;
    color: #8a5a00;
    background: rgba(245, 158, 11, .14);
    border: 1px dashed rgba(245, 158, 11, .5);
    text-align: right;
}

/* ---- 手机端 ---- */
@media (max-width: 560px) {
    /* HUD 那排数据在手机上是 3 列网格，宠物徽章自己占一整行，别把网格挤歪 */
    .pet-chip {
        grid-column: 1 / -1;
        justify-content: center;
        padding: 5px 10px;
    }

    .pet-card { padding: 11px; border-radius: 15px; gap: 10px; }
    .pet-avatar { width: 46px; height: 46px; font-size: 26px; border-radius: 13px; }
    .pet-name-text { font-size: 14.5px; }
    .pet-active-btn { margin-left: 0; }
    /* 操作按钮按内容宽度自然换行（.btn 是 nowrap，硬撑成 50% 会挤到溢出） */
    .pet-actions .btn { flex: 0 1 auto; padding: 6px 9px; font-size: 12.5px; }
    .pet-rename-input { width: 100%; }
    .pet-bar { flex: 1 1 100%; }
}

/* ============================================================
   v2.9：商店「🐾 宠物」页的分段（领养宠物 / 宠物粮） + 喂食选择框
   ============================================================ */

/* ---- 商店宠物页的两段标题 ---- */
.pet-sec-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
    margin: 4px 0 10px;
    padding-bottom: 8px;
    border-bottom: 1.5px dashed rgba(23, 51, 38, .14);
}
.pet-sec-head + .seed-grid { margin-bottom: 22px; }
.pet-sec-head h4 { margin: 0; font-size: 14.5px; font-weight: 800; color: var(--ink); }
.pet-sec-sub { font-size: 11.5px; color: var(--muted); }

/* ---- 宠物粮卡片：图标底色偏暖，和宠物图鉴区分开 ---- */
.pet-food-card.is-owned {
    border-color: rgba(34, 197, 94, .5);
    background: linear-gradient(160deg, #f3fdf0, #fff);
}
.pet-food-icon { background: linear-gradient(140deg, #ffe9c7, #ffd9a0); }

/* 买 1 / 买 5 / 买 10 三个按钮：小屏自动换行，不撑破卡片 */
.pet-qty-btns { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-left: auto; }
.pet-qty-btns .btn { padding: 5px 10px; font-size: 12.5px; }
.pet-qty-btns .btn:disabled { opacity: .5; cursor: not-allowed; }

/* ---- 喂食选择框（塞在 vfConfirm 的 .vf-confirm-msg 里） ---- */
.pet-feed-picker { display: flex; flex-direction: column; gap: 8px; margin: 12px 0 10px; text-align: left; }
.pet-food-pick {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 11px;
    border-radius: 14px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #fff;
    cursor: pointer;
    transition: border-color .15s, background .15s;
}
.pet-food-pick:has(input:checked) { border-color: rgba(34, 197, 94, .8); background: #f2fdf0; }
.pet-food-pick input { flex: none; width: 16px; height: 16px; margin: 0; accent-color: #16a34a; }
.pet-food-pick-icon { flex: none; font-size: 24px; line-height: 1; }
.pet-food-pick-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.pet-food-pick-name { font-size: 13.5px; font-weight: 800; color: var(--ink); }
.pet-food-pick-sub { font-size: 11.5px; color: var(--muted); }
.pet-feed-note { display: block; font-size: 11.5px; color: var(--muted); }

/* ============================================================
   v2.25 宠物第一批：①新互动 ②技能
   ------------------------------------------------------------
   技能面板刻意做成**文档流里的展开区**（不是浮层）：
   项目卡片上有 backdrop-filter: blur()，它会让 position:fixed 的包含块
   变成那个祖先 —— 用 fixed 做浮层已经被这个坑掉过两次。
   ============================================================ */

/* 心情那一行（心情终于有用了，要让玩家看见它乘了多少） */
.pet-mood-line { font-size: 12px; color: var(--ink-soft); }
.pet-mood-line .dim { color: var(--muted); font-size: 11.5px; }

/* 卡片上的技能行 / 技能小徽章 */
.pet-skills { gap: 6px; }
.pet-skill-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    color: #1f4d33;
    background: rgba(34, 197, 94, .14);
    border: 1px solid rgba(34, 197, 94, .28);
    white-space: nowrap;
}
.pet-skill-chip b { font-variant-numeric: tabular-nums; }
.pet-skill-chip.is-dead {
    color: var(--muted);
    background: rgba(23, 51, 38, .07);
    border-color: rgba(23, 51, 38, .14);
    text-decoration: line-through;
}
.pet-skill-btn { margin-left: auto; }

/* 面板顶部的技能加成合计 */
.pet-skill-sum {
    margin: 8px 0 2px;
    padding: 9px 12px;
    border-radius: 14px;
    font-size: 12.5px;
    line-height: 1.8;
    color: #1f4d33;
    background: rgba(34, 197, 94, .10);
    border: 1px dashed rgba(34, 197, 94, .35);
}
.pet-skill-sum[hidden] { display: none; }

/* 展开的技能面板 */
.pet-skill-pane {
    margin-top: 14px;
    padding: 12px 13px;
    border-radius: 18px;
    background: rgba(255, 255, 255, .72);
    border: 1.5px solid rgba(34, 197, 94, .30);
}
.pet-skill-pane[hidden] { display: none; }

/* ================================================================
 * v2.55 技能面板改成**紧跟选中那张卡**（内联拼在卡片后面）
 *
 * `.pet-list` 是 flex column + gap 12px，面板作为卡片的兄弟自然会排在它下面；
 * 但面板自带 `margin-top: 14px`，加上 gap 就是 26px —— 比卡片之间的 12px 大一倍多，
 * 看起来像"另一块东西"而不是"这张卡的附属"。
 * 所以内联版把上边距收掉（间距交给 gap），并把左边框染成选中态的琥珀色，
 * 视觉上明确归属上面那张卡。
 *
 * ⚠️ 这里**没有加任何展开动画**：原来就是 `hidden` 硬切，内联之后一样 ——
 *    不存在"内联把动画弄丢"的问题（原本就没有，也不硬加）。
 * ============================================================= */
.pet-skill-pane.pet-skill-inline {
    margin-top: 0;
    border-left: 4px solid var(--gold-3);
}
.pet-skill-head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 8px;
    font-size: 13.5px;
}
.pet-skill-head .dim { font-size: 11.5px; }
.pet-skill-head .btn { margin-left: auto; }
.pet-skill-group {
    margin: 10px 0 6px;
    font-size: 11.5px;
    font-weight: 800;
    letter-spacing: .04em;
    color: var(--muted);
}
.pet-skill-card {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 11px;
    margin-bottom: 7px;
    border-radius: 14px;
    border: 1.5px solid rgba(23, 51, 38, .12);
    background: #fff;
}
.pet-skill-card.is-learned { border-color: rgba(34, 197, 94, .45); background: #f7fdf5; }
.pet-skill-ico { flex: none; font-size: 22px; line-height: 1; }
.pet-skill-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; font-size: 12.5px; }
.pet-skill-main b { font-size: 13px; color: var(--ink); }
.pet-skill-lv { color: var(--green-4); font-variant-numeric: tabular-nums; }
.pet-skill-effect { color: #1f4d33; font-weight: 700; }
.pet-skill-side {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
    text-align: right;
    max-width: 46%;
}
.pet-skill-empty { font-size: 12.5px; color: var(--muted); padding: 4px 2px 6px; }
.pet-skill-total {
    margin-top: 10px;
    padding-top: 9px;
    border-top: 1px dashed rgba(23, 51, 38, .18);
    font-size: 12px;
    font-weight: 700;
    color: #1f4d33;
}

@media (max-width: 560px) {
    .pet-skill-card { flex-wrap: wrap; }
    .pet-skill-side { max-width: 100%; align-items: flex-start; text-align: left; }
    .pet-skill-btn { margin-left: 0; }
}

/* ================================================================
 * v2.25 ③ 宠物繁育 / 孵化
 *
 * 布局和 .pet-skill-pane 一致：**就在宠物列表下面往下排**，
 * 不用 position: fixed 做浮层 —— 这个卡片有 backdrop-filter，
 * 会把 fixed 的包含块变成那个祖先（这个坑踩过两次）。
 * ============================================================= */
.pet-breed-pane {
    margin-top: 14px;
    padding: 12px 13px;
    border-radius: 18px;
    background: rgba(255, 255, 255, .72);
    border: 1.5px dashed rgba(245, 158, 11, .45);
}
.pet-breed-pane[hidden] { display: none; }
/* 收起状态：只留那一行入口按钮，不要空壳子的虚线框和底色 */
.pet-breed-pane.is-collapsed {
    padding: 0;
    border: none;
    background: none;
    margin-top: 10px;
}

/* 收起状态下只露这一行入口按钮 */
.pet-breed-toggle { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pet-breed-toggle .btn b { font-variant-numeric: tabular-nums; }
.pet-breed-hint { font-size: 12px; font-weight: 700; color: #8a5a00; }

.pet-breed-head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin: 10px 0 8px;
    font-size: 13.5px;
}
.pet-breed-head .dim { font-size: 11.5px; }

.pet-breed-info {
    padding: 9px 12px;
    margin-bottom: 10px;
    border-radius: 14px;
    font-size: 12.5px;
    line-height: 1.9;
    color: #6b3d06;
    background: rgba(245, 158, 11, .10);
    border: 1px dashed rgba(245, 158, 11, .38);
}

.pet-breed-row {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.pet-breed-pick { display: flex; flex-direction: column; gap: 4px; flex: 1 1 200px; min-width: 0; }
.pet-breed-pick > span { font-size: 11.5px; font-weight: 800; color: var(--muted); }
.pet-breed-pick select { width: 100%; }
.pet-breed-x { flex: none; padding-bottom: 8px; font-weight: 800; color: var(--muted); }

.pet-breed-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 12px;
}
.pet-breed-why { font-size: 12px; color: #8a5a00; }

.pet-breed-group {
    margin: 10px 0 6px;
    font-size: 11.5px;
    font-weight: 800;
    letter-spacing: .04em;
    color: var(--muted);
}
.pet-breed-egg {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 11px;
    margin-bottom: 7px;
    border-radius: 14px;
    border: 1.5px solid rgba(23, 51, 38, .12);
    background: #fff;
}
/* 到点可孵的那颗给个显眼的边和小高光，玩家一眼能看到 */
.pet-breed-egg.is-ready {
    border-color: rgba(34, 197, 94, .55);
    background: #f4fdf1;
    box-shadow: 0 0 0 3px rgba(34, 197, 94, .10);
}
.pet-breed-egg-ico { flex: none; font-size: 24px; line-height: 1; }
.pet-breed-egg-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; font-size: 12.5px; }
.pet-breed-egg-main b { font-size: 13px; color: var(--ink); }
.pet-breed-left { font-variant-numeric: tabular-nums; }
.pet-breed-egg-side {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
}
.pet-breed-empty { font-size: 12.5px; color: var(--muted); padding: 4px 2px 6px; }

@media (max-width: 560px) {
    .pet-breed-egg { flex-wrap: wrap; }
    .pet-breed-egg-side { flex-direction: row; width: 100%; justify-content: flex-end; }
    .pet-breed-x { display: none; }
    .pet-breed-pick { flex: 1 1 100%; }
}

@media (max-width: 560px) {
    .pet-sec-head { margin-bottom: 8px; }
    .pet-sec-head h4 { font-size: 13.5px; }
    .pet-food-pick { padding: 8px 10px; gap: 8px; }
    .pet-food-pick-icon { font-size: 21px; }
    /* 三个买按钮平分一行，手指好点 */
    .pet-qty-btns { width: 100%; margin-left: 0; }
    .pet-qty-btns .btn { flex: 1 1 0; }
}

/* ================================================================
 * v2.10.1 验证码输入行
 *
 * 之前 captcha_field() 输出 .captcha-row / .captcha-img / .captcha-input /
 * .captcha-refresh 四个类，**却一丁点样式都没写** —— 所以它就是几个裸元素
 * 挤在一行：图片和输入框不等高、输入框边框跟登录表单其它输入框不一样、
 * 「换一张」看起来像个没样式的方框。这里补齐，并跟 .field input 对齐。
 * ============================================================= */
.captcha-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 15px;
}

.captcha-img {
    display: block;
    flex: none;
    /* 6 位数字的画布是 192x48（见 includes/captcha.php 的 captcha_size()）。
       手机上用 max-width 兜住，别把登录卡片撑破。 */
    width: 192px;
    max-width: 100%;
    height: 48px;
    border-radius: 13px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #f5fcee;
    cursor: pointer;
    user-select: none;
    transition: border-color .18s, box-shadow .18s;
}
.captcha-img:hover {
    border-color: var(--green-2);
    box-shadow: 0 0 0 4px rgba(34, 197, 94, .14);
}

/* 没装 gd 时的降级：把算术题做成和图片一样大的块，位置不跳 */
.captcha-math {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    min-width: 192px;
    max-width: 100%;
    height: 48px;
    padding: 0 14px;
    border-radius: 13px;
    border: 1.5px dashed rgba(23, 51, 38, .22);
    background: #f5fcee;
    font-size: 17px;
    font-weight: 800;
    letter-spacing: .5px;
    color: var(--ink);
}

.captcha-input {
    flex: 1 1 96px;
    min-width: 0;
    height: 48px;
    padding: 0 14px;
    border-radius: 13px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #fff;
    font-size: 17px;
    font-weight: 700;
    /* 改成纯数字后是 6 位，字距 3px 会把手输空间挤掉，收到 2px */
    letter-spacing: 2px;
    /* 纯数字，text-transform 其实没用，留着以防以后换回字母 */
    text-transform: uppercase;
    color: var(--ink);
    transition: border-color .18s, box-shadow .18s;
}
.captcha-input:focus {
    outline: none;
    border-color: var(--green-2);
    box-shadow: 0 0 0 4px rgba(34, 197, 94, .16);
}
/* 占位符别跟着拉字距，否则「验证码」三个字会散开 */
.captcha-input::placeholder {
    letter-spacing: normal;
    text-transform: none;
    font-weight: 400;
    color: var(--muted);
}

.captcha-refresh {
    flex: none;
    height: 48px;
    padding: 0 15px;
    border-radius: 13px;
    border: 1.5px solid rgba(23, 51, 38, .14);
    background: #fff;
    font-size: 13px;
    font-weight: 700;
    color: var(--ink-soft);
    cursor: pointer;
    font-family: inherit;
    transition: border-color .18s, color .18s, background .18s, transform .1s;
}
.captcha-refresh:hover {
    border-color: var(--green-2);
    color: var(--green-4);
    background: rgba(34, 197, 94, .07);
}
.captcha-refresh:active { transform: scale(.96); }

@media (max-width: 560px) {
    /* 手机上图片占满一整行，输入框和按钮排第二行 —— 挤一行会窄到没法输 */
    .captcha-row { gap: 8px; }
    .captcha-img { width: 100%; max-width: 192px; height: auto; }
    .captcha-math { width: 100%; max-width: 192px; height: 44px; }
    .captcha-input { height: 44px; flex: 1 1 auto; }
    .captcha-refresh { height: 44px; }
}

/* ================================================================
 * v2.10.1 在线状态徽章（邻居动态 + 后台用户列表共用）
 *
 * ⚠️ .online-dot 这个类聊天页也在单独用（好友名后面的绿点），
 * 所以下面一律写成 `.online-tag .online-dot` —— 不要直接改 .online-dot 本身。
 * ============================================================= */
.online-tag {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 9px 2px 7px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    line-height: 1.75;
    white-space: nowrap;
    vertical-align: middle;
}
.online-tag .online-dot {
    margin-left: 0;
    width: 6px;
    height: 6px;
    flex: none;
    box-shadow: none;
}
.online-tag.is-on { color: #15803d; background: rgba(34, 197, 94, .15); }
.online-tag.is-on .online-dot { background: #22c55e; box-shadow: 0 0 0 3px rgba(34, 197, 94, .22); }
.online-tag.is-off { color: #4b6355; background: rgba(148, 163, 184, .15); }
.online-tag.is-off .online-dot { background: #cbd5e1; }

/* 侧栏那种一行塞很多信息的地方，再小一档 */
.online-tag-sm { padding: 1px 7px 1px 6px; font-size: 10.5px; }
.online-tag-sm .online-dot { width: 5px; height: 5px; }

/* 侧栏邻居行的副标题：徽章 + 等级要能自然换行 */
.mini-sub { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
.neighbor-sub .neighbor-lv { vertical-align: middle; }

/* ================================================================
 * v2.11 移动端：把「一键」按钮挪到地块**上面**
 *
 * 原来它在 .farm-foot 里，也就是 18 块地**之后**。手机上土地是 3 列 × 6 行，
 * 要滚好几屏才够得着按钮，而且按了以后看不见地块变化 ——
 * 用户反馈「一键功能太靠下，移动端使用看不见土地作物」。
 *
 * 为什么不用 position: sticky（本来是最省事的做法）：
 *   .farm-panel 有 overflow: hidden（靠它裁掉装饰圆和虚线围栏的溢出），
 *   而**祖先一旦 overflow 不是 visible，sticky 就相对那个祖先滚动**，
 *   那个祖先根本不滚动，结果就是完全不动。
 *   所以改用 flex 的 order 把按钮排到上面去。
 *
 * display: contents 让 .farm-foot 的两个子元素（图例 / 按钮）
 * 直接参与 .farm-panel 的排布 —— 这样只挪按钮、图例留在下面，也不用改 HTML。
 *
 * v2.66（⑥ 移动端）：按钮改成分组之后，这里跟着改三件事 ——
 *   ① 每组一条「带子」，竖着排（原来是一整排平铺，功能一多就挤成一条长龙）；
 *   ② 一个带子里最多 3 个、等宽，放不下就换行 —— **永远不横向滚动**；
 *   ③ 热区高度 ≥44px（手指点得中），窄屏只缩字号和内边距，不缩高度。
 * ============================================================= */
@media (max-width: 820px) {
    .farm-panel { display: flex; flex-direction: column; }
    .farm-foot { display: contents; }

    /* 控制条：排在 panel-head（默认 order 0）之后、地块之前 */
    .farm-actions {
        order: 1;
        width: 100%;
        margin: 0 0 12px;
        padding: 9px;
        gap: 8px;
        flex-direction: column;      /* v2.66：一组一行（原来是全部平铺在一行里） */
        align-items: stretch;
        border-radius: 14px;
        background: rgba(34, 197, 94, .08);
        border: 1px dashed rgba(23, 51, 38, .18);
    }
    .fa-group { width: 100%; }
    /* 窄屏不显示组标题：按钮自己就写着「一键 XX」，标签在这里白占一格宽度 */
    .fa-label { display: none; }
    .fa-btns { width: 100%; gap: 7px; }
    /* 一行最多 3 个、等宽（5 个动作 = 照顾 3 个一行 + 收获 / 播种 各一行）。
       ⚠️ 别改成 `flex: 1 1 0` —— 那会把一组硬塞进同一行，
       每个只有 1/N 宽，「🧺 一键收获」这种文字直接被挤到溢出（踩过）。
       ⚠️ min-height 44px 是硬要求（手指点得中），别为了塞更多按钮把它压小。 */
    .fa-btns .btn {
        flex: 0 1 calc(33.333% - 5px);
        min-width: 0;
        min-height: 44px;
        padding: 9px 6px;
        font-size: 13px;
    }

    .plot-grid { order: 2; }
    /* ★ v2.92.1：开垦条排在网格下方、图例上方（手机上 .farm-panel 是 flex column，
       .farm-foot 是 display:contents ⇒ 图例也是 flex 子项，所以两者都要显式 order） */
    .unlock-bar { order: 3; }
    .legend { order: 4; margin-top: 12px; }
}

@media (max-width: 480px) {
    /* 小屏再收一档内边距 / 字号，**热区高度不降** */
    .farm-actions { padding: 8px; gap: 7px; }
    .fa-btns { gap: 6px; }
    .fa-btns .btn { font-size: 12.5px; padding: 8px 5px; }
}

@media (max-width: 360px) {
    /* 极窄屏（老安卓 320~360px）：一行 2 个 —— 宁可多一行，也绝不横向溢出 */
    .fa-btns .btn { flex: 0 1 calc(50% - 4px); font-size: 12px; }
}

/* ================================================================
 * v2.11 注册蜜罐字段
 *
 * 一个真人看不见、机器人会填的输入框。**不能用 display:none 或 hidden** ——
 * 一部分简单的机器人会跳过隐藏字段。用绝对定位挪到屏幕外，
 * 这样它在 DOM 里「看起来是可见的」，却谁也点不到。
 * 同时给 label 加了说明文字，万一有用户真的用 Tab 撞进来也不会困惑。
 * ============================================================= */
.hp-trap {
    position: absolute !important;
    left: -9999px !important;
    top: auto !important;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ================================================================
 * v2.12 等级对照表弹窗（点 HUD 上的等级弹出）
 * ============================================================= */

/* .lv 原来是 <span>，现在换成了 <button> 以便点击。
   浏览器给 button 的默认边框/背景/字体都要清掉，否则外观和徽章不一致。 */
.lv-btn {
    border: 0;
    font-family: inherit;
    line-height: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    transition: transform .12s, box-shadow .18s;
}
.lv-btn::after {
    content: "›";              /* 一个小箭头，暗示「可以点」 */
    font-size: 13px;
    opacity: .75;
    transform: rotate(-90deg);
}
.lv-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(245, 158, 11, .38); }
.lv-btn:active { transform: translateY(0); }

/* ================================================================
 * v2.24 等级与经验弹窗 · 视觉改版
 *
 * 改了什么、为什么：
 *   ① 顶部从「三个数字卡片」换成「等级徽章 + 进度条」——
 *      三个数字看不出"离升级还有多远"，进度条一眼就懂。
 *   ② 表格里去掉每行那个「已过」徽章：升到 10 级就是 9 个「已过」，
 *      纯粹是噪音。改成整行淡灰 + 当前行高亮，层次反而更清楚。
 *   ③ 「升到下一级要」那列原来带个 ✨ 后缀，和数字挤在一起；
 *      去掉，改成靠右对齐的数字列（好比较）。
 *   ④ 底部那段「经验来源…后台可调」删了 —— 那是给管理员看的话。
 * ============================================================= */
.lv-hero {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
    border-radius: 16px;
    background: linear-gradient(135deg, rgba(34, 197, 94, .14), rgba(34, 197, 94, .05));
    border: 1px solid rgba(34, 197, 94, .20);
}
.lv-hero-badge {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-width: 74px;
    padding: 8px 10px;
    border-radius: 14px;
    background: linear-gradient(135deg, var(--gold-2, #fbbf24), var(--gold-3, #f59e0b));
    color: #fff;
    box-shadow: 0 4px 14px rgba(245, 158, 11, .35);
}
.lv-hero-badge b { font-size: 19px; font-weight: 800; line-height: 1.1; }
.lv-hero-badge span { font-size: 10.5px; opacity: .9; margin-top: 2px; }

.lv-hero-body { flex: 1 1 auto; min-width: 0; }
.lv-hero-title { font-size: 14px; color: var(--ink); }
.lv-hero-title b { color: #15803d; font-weight: 800; }

.lv-hero-track {
    position: relative;
    height: 16px;
    margin: 7px 0 5px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .10);
    overflow: hidden;
}
.lv-hero-track i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, #4ade80, #22c55e);
    transition: width .3s ease;
}
.lv-hero-track em {
    position: absolute;
    right: 7px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 10px;
    font-style: normal;
    font-weight: 800;
    color: #14532d;
    opacity: .75;
}
.lv-hero-sub { font-size: 11.5px; color: var(--muted); }
.lv-hero-sub b { color: var(--ink); font-weight: 700; }

.lv-tip {
    margin: 10px 2px;
    padding: 7px 11px;
    border-radius: 10px;
    background: rgba(23, 51, 38, .04);
    font-size: 12px;
    color: var(--muted);
    line-height: 1.6;
}
.lv-tip b { color: var(--ink); font-weight: 700; }

.lv-table-wrap { max-height: 46vh; overflow: auto; border-radius: 13px; border: 1px solid rgba(23, 51, 38, .10); }
.lv-table { min-width: 520px; }
.lv-table th { position: sticky; top: 0; z-index: 1; background: var(--panel-solid, #fff); }

/* 「升到下一级要」靠右对齐 —— 数字对齐了才好纵向比较 */
.lv-table .lv-need { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }
.lv-table th:nth-child(2), .lv-table th:nth-child(3) { text-align: right; }
.lv-table td:nth-child(3) { text-align: right; font-variant-numeric: tabular-nums; }
.lv-cell-lv { white-space: nowrap; }

/* 已经过的等级整行淡下去（代替原来每行一个「已过」徽章） */
.lv-table .lv-past { opacity: .55; }
.lv-table .lv-past .lv-crop { background: rgba(23, 51, 38, .07); color: var(--muted); }

/* 当前等级：整行淡绿底 + 左侧色条，一眼找到 */
.lv-table .lv-cur { background: rgba(34, 197, 94, .13); box-shadow: inset 3px 0 0 #22c55e; }
.lv-table .lv-cur td { font-weight: 700; }
.lv-table .lv-max td { background: rgba(245, 158, 11, .10); }

/* 「当前」/「满级」小标（比原来的 badge 更轻） */
.lv-here {
    display: inline-block;
    margin-left: 4px;
    padding: 0 6px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 800;
    background: #22c55e;
    color: #fff;
    vertical-align: 1px;
}
.lv-here-gold { background: #f59e0b; }

.lv-crop {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    margin: 1px 4px 1px 0;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 11px;
    background: rgba(34, 197, 94, .12);
    color: #15803d;
    white-space: nowrap;
}

@media (max-width: 560px) {
    .lv-hero { gap: 10px; padding: 12px; }
    .lv-hero-badge { min-width: 62px; padding: 6px 8px; }
    .lv-hero-badge b { font-size: 17px; }
    .lv-hero-title { font-size: 13px; }
    .lv-table-wrap { max-height: 52vh; }
}

/* ================================================================
 * v2.12 每日签到（7 天连签日历）
 * ============================================================= */

.signin-top {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-bottom: 4px;
}
.signin-stat {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 8px 14px;
    border-radius: 13px;
    background: rgba(34, 197, 94, .09);
    border: 1px solid rgba(34, 197, 94, .18);
}
.signin-stat-label { font-size: 11.5px; color: var(--muted); }
.signin-stat-val { font-size: 20px; font-weight: 800; color: var(--green-ink); line-height: 1; }
.signin-stat-unit { font-size: 11.5px; color: var(--muted); }
#btnDoSignin { margin-left: auto; }
/* 已签到时按钮变成「灰色不可点」的样子，并写明今天签过了 */
#btnDoSignin:disabled { cursor: default; transform: none; filter: grayscale(.35); opacity: .85; }

.signin-grid {
    display: grid;
    /* ★ v2.93.1（UI 批次 4）：用户拍板「7 列铺满」—— 7 天一行，不再 4+3 留半行空白。
       1280 下 7 列各 ~97px；给整格容器一个 840px 上限（7×~111 + 6×10），
       宽屏不会把单格拉得过大（规范 §1.2 的卡片宽度审美 + §3.1 的 auto-fit 精神）。 */
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 10px;
    margin: 14px auto 0;
    max-width: 840px;
}
.signin-day {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 12px 8px 10px;
    border-radius: 14px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    transition: transform .14s, box-shadow .18s, border-color .18s;
}
.signin-day:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
/* 已经签到的天数：打勾 + 变淡，一眼能看出「走到第几天了」 */
.signin-day.is-done {
    background: linear-gradient(160deg, #f2fdf1, #fff);
    border-color: rgba(34, 197, 94, .45);
}
/* 今天该签的这一格：描边高亮 */
.signin-day.is-next {
    border-color: var(--gold-1);
    box-shadow: 0 0 0 3px rgba(245, 158, 11, .20);
}
/* ★ v2.88.1：还没到的那几天 = **中性灰**（灰底 + 灰文案 + 去色图标）——
   以前它和"已领取"共用白底、空文案，两种状态长得一模一样 ✗ */
.signin-day.is-future {
    background: #f7faf8;
    border-color: rgba(23, 51, 38, .10);
}
.signin-day.is-future .signin-day-icon { filter: grayscale(1); opacity: .45; }
.signin-day.is-future .signin-day-state { color: var(--muted); }
/* 本轮签满后的第 7 格额外醒目一点 */
.signin-day.is-last { border-style: dashed; }
.signin-day-tag {
    position: absolute;
    top: -9px;
    left: 50%;
    transform: translateX(-50%);
    padding: 1px 8px;
    border-radius: 999px;
    font-size: 10px;
    font-weight: 800;
    white-space: nowrap;
    color: #4a2c00;
    background: var(--gold-2, #f59e0b);
}
.signin-day-name { font-size: 12.5px; font-weight: 800; color: var(--ink); }
/* ★ v2.88.1：徽章**分色**——「已签」绿、「今天」金（两个都用金色时一眼分不出发的是什么）*/
.signin-day.is-done .signin-day-tag { background: var(--green-2); color: #4a2c00; }
.signin-day-icon { font-size: 24px; line-height: 1.1; }
/* ★ v2.89.8（批次 2 · H7 层次）：原来 12px/800 —— 和上面「第 N 天」12.5/800
   只差 **0.5px 字号、0 字重** ⇒ R3 红 ✗（393 / 1280 两档都是）。
   金币数走 **T5 17px/700**（§2.1「卡片主数字」），「第 N 天」保持 T2 12.5 ⇒
   Δ字号 **4.5px** ✓（§2.2 的「Δ≥2px」那一支，不需要靠字重凑 ✓） */
.signin-day-coin { font-size: 17px; font-weight: 700; color: var(--gold-ink); }
.signin-day-exp { font-size: 10.5px; color: var(--muted); }
.signin-day-item {
    font-size: 10px;
    text-align: center;
    line-height: 1.35;
    color: #7c3aed;
    min-height: 14px;
}
.signin-day-state { font-size: 11px; font-weight: 800; color: var(--green-ink); min-height: 15px; }
.signin-day.is-next .signin-day-state { color: var(--gold-ink); }

/* v2.64（2026-10-10）：格子变成**可点**（「点第 N 天的框就能签到」）。
   ⚠️ 三条移动端硬要求，缺一条这个交互在手机上就是坏的：
     ① touch-action: manipulation —— 去掉 300ms 点击延迟，也避免"想点却被当成滚动"✗
     ② :active 的按压反馈 —— **不能只靠 :hover** ✗（触屏没有 hover，点了会像没反应）
     ③ 热区够大 —— 见下面 720px 里的 min-height（≥44px）✓
   整块 .signin-grid > .signin-day 都是热区（不是只有小图标），手指点哪儿都算 ✓ */
.signin-day { cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
.signin-day:active { transform: translateY(0) scale(.97); }
.signin-day:focus-visible { outline: 3px solid var(--gold-1, #f59e0b); outline-offset: 2px; }
/* 已领取 / 还没到：仍然可点（点了给提示），但视觉上弱一点、不跟"今天"抢注意力 */
.signin-day[aria-disabled="true"] { cursor: default; }
.signin-day[aria-disabled="true"]:active { transform: none; }
/* 请求进行中：压暗 + 禁止重复点 */
.signin-day.is-busy { opacity: .6; pointer-events: none; }

/* ================================================================
 * v2.12 作物图鉴
 * ============================================================= */

.collect-progress {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 2px;
}
.collect-bar {
    flex: 1 1 auto;
    height: 12px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .09);
    overflow: hidden;
}
.collect-bar > i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--green-1), var(--green-3));
    transition: width .35s ease;
}
.collect-bar-text {
    flex: none;
    font-size: 12.5px;
    font-weight: 800;
    color: var(--green-ink);
    white-space: nowrap;
}

.collect-grid {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 8px;
    margin-top: 12px;
}
.collect-cell {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 9px 4px 7px;
    border-radius: 13px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    cursor: pointer;
    transition: transform .14s, box-shadow .18s, border-color .18s;
}
.collect-cell:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: rgba(34, 197, 94, .4); }
.collect-cell.is-on { background: linear-gradient(160deg, #f3fdf2, #fff); border-color: rgba(34, 197, 94, .45); }
/* 没收过的：整格变灰，图标也去色 —— 一眼看出「图鉴还差哪些」 */
.collect-cell.is-off { filter: grayscale(1); opacity: .48; }
.collect-cell.is-off .collect-cell-icon { opacity: .75; }
/* 当前详情里选中的那格 */
.collect-cell.is-cur { border-color: var(--green-ink); box-shadow: 0 0 0 3px rgba(34, 197, 94, .18); }
.collect-cell-icon { font-size: 24px; line-height: 1.1; }
.collect-cell-name { font-size: 11px; font-weight: 700; text-align: center; line-height: 1.2; }
.collect-cell-lv { font-size: 10px; color: var(--muted); }
.collect-cell-badge {
    position: absolute;
    top: -6px;
    right: -5px;
    width: 17px;
    height: 17px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    font-size: 10px;
    color: #fff;
    background: var(--green-3);
    border: 2px solid #fff;
}

.collect-detail {
    margin-top: 14px;
    padding: 14px 16px;
    border-radius: 15px;
    background: rgba(34, 197, 94, .07);
    border: 1px solid rgba(34, 197, 94, .18);
}
.collect-detail-head {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-bottom: 9px;
}
.collect-detail-head .cd-icon { font-size: 30px; line-height: 1; }
.collect-detail-head b { font-size: 16px; }
.collect-detail-head .cd-tag {
    margin-left: auto;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
}
.collect-detail-head .cd-tag.on { background: rgba(34, 197, 94, .18); color: #15803d; }
.collect-detail-head .cd-tag.off { background: rgba(23, 51, 38, .10); color: var(--muted); }
.collect-detail-desc { font-size: 12.5px; color: var(--muted); line-height: 1.7; margin: 0 0 10px; }
.collect-stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
}
.collect-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 8px 10px;
    border-radius: 11px;
    background: rgba(255, 255, 255, .8);
    border: 1px solid rgba(23, 51, 38, .08);
}
.collect-stat span { font-size: 10.5px; color: var(--muted); }
.collect-stat b { font-size: 13.5px; }

.collect-sub { margin: 18px 0 0; font-size: 14px; }
.collect-tiers {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 9px;
    margin-top: 9px;
}
.collect-tier {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 11px 8px;
    border-radius: 13px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    text-align: center;
}
.collect-tier.is-ready { border-color: var(--gold-1); box-shadow: 0 0 0 3px rgba(245, 158, 11, .16); }
.collect-tier.is-claimed { opacity: .62; }
.collect-tier-need { font-size: 12.5px; font-weight: 800; color: var(--ink); }
.collect-tier-reward { font-size: 11px; color: var(--muted); line-height: 1.5; }
.collect-tier .btn { margin-top: 3px; }

/* ============================================================
   v2.13 农场订单（菜市场收购）
   ============================================================ */

.order-sub {
    margin: 4px 0 9px;
    font-size: 14px;
    display: flex;
    align-items: baseline;
    gap: 8px;
}
.order-sub small { font-size: 11.5px; font-weight: 500; color: var(--muted); }

.order-list { display: flex; flex-direction: column; gap: 9px; }

.order-row {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 11px 13px;
    border-radius: 14px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
}
.order-row:hover { transform: translateX(3px); }
.order-row.is-ready {
    border-color: rgba(245, 158, 11, .55);
    background: linear-gradient(100deg, #fffdf2, #fff);
    box-shadow: 0 0 0 3px rgba(245, 158, 11, .12);
}
.order-row.is-final { opacity: .72; }
.order-row.is-final:hover { transform: none; }

.order-icon { font-size: 26px; line-height: 1; flex: none; }
.order-main { flex: 1; min-width: 0; }
.order-top { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.order-name { font-size: 14px; font-weight: 800; }
.order-qty { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.order-qty b.ok { color: var(--green-ink); }
.order-qty b.no { color: #8a5a00; }
.order-meta {
    display: flex;
    align-items: center;
    gap: 9px;
    margin-top: 3px;
    font-size: 11px;
    color: var(--muted);
}
.order-kind {
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .07);
    font-weight: 700;
}
.order-left { font-variant-numeric: tabular-nums; }

.order-rewards { display: flex; flex-direction: column; gap: 3px; flex: none; align-items: flex-end; }
.order-chip {
    font-size: 11.5px;
    font-weight: 800;
    color: var(--gold-ink);
    white-space: nowrap;
}
.order-action { flex: none; min-width: 84px; display: flex; justify-content: flex-end; }

.order-tag {
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    white-space: nowrap;
}
.order-tag-done { background: rgba(34, 197, 94, .16); color: #15803d; }
.order-tag-exp  { background: rgba(23, 51, 38, .09); color: var(--muted); }
.order-tag-lack { background: rgba(245, 158, 11, .16); color: #8a5a00; }

/* ---------------- v2.15 排行榜赛季 ---------------- */

/* 赛季头部的信息条（时间窗口 / 倒计时 / 奖励名次数） */
.season-top {
    display: flex;
    flex-wrap: wrap;
    gap: 7px;
    margin: 3px 0 13px;
}
.season-chip {
    padding: 4px 11px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .06);
    border: 1px solid rgba(23, 51, 38, .08);
    font-size: 11.5px;
    font-weight: 700;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.season-left { background: rgba(253, 224, 71, .28); border-color: rgba(253, 224, 71, .8); color: #7a5200; }

/* 赛季榜单行：比排行榜多一格「本轮奖励」 */
.rank-row .rank-ava { flex: none; font-size: 19px; line-height: 1; }
.rank-row .rank-reward {
    flex: none;
    font-size: 12px;
    font-weight: 800;
    color: var(--gold-ink);
    font-variant-numeric: tabular-nums;
}

/* 待领 / 已领的奖励行 */
.season-rewards { display: flex; flex-direction: column; gap: 9px; margin-bottom: 6px; }
.season-row {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 10px 13px;
    border-radius: 14px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    transition: transform .16s ease, border-color .16s ease;
}
.season-row:hover { transform: translateX(3px); }
.season-row.is-ready {
    border-color: rgba(245, 158, 11, .55);
    background: linear-gradient(100deg, #fffdf2, #fff);
    box-shadow: 0 0 0 3px rgba(245, 158, 11, .12);
}
.season-row.is-done { opacity: .72; }
.season-row.is-done:hover { transform: none; }
.season-icon { font-size: 24px; line-height: 1; flex: none; }
.season-main { flex: 1; min-width: 0; }
.season-top-line, .season-row .season-top { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.season-title { font-size: 13.5px; font-weight: 800; }
.season-rank {
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .07);
    font-size: 11px;
    font-weight: 700;
    color: var(--muted);
}
.season-meta { margin-top: 2px; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.season-coins { flex: none; font-size: 13px; font-weight: 800; color: var(--gold-ink); }
.season-action { flex: none; min-width: 76px; display: flex; justify-content: flex-end; }
.season-tag {
    padding: 3px 9px;
    border-radius: 999px;
    background: rgba(34, 197, 94, .16);
    color: #15803d;
    font-size: 11px;
    font-weight: 800;
    white-space: nowrap;
}

/* 已完成 / 已过期折叠区：不跟进行中的订单混在一起 */
.order-fold { margin-top: 13px; }
.order-fold-head {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 9px 12px;
    border: 1px solid rgba(23, 51, 38, .10);
    border-radius: 12px;
    background: rgba(255, 255, 255, .72);
    color: var(--ink);
    font-size: 13px;
    font-weight: 800;
    cursor: pointer;
    text-align: left;
}
.order-fold-head:hover { border-color: rgba(34, 197, 94, .4); }
.order-fold-head small { font-size: 11px; font-weight: 500; color: var(--muted); }
.order-fold-head .fold-arrow { font-size: 10px; color: var(--muted); }
.order-fold-body { margin-top: 9px; }

/* 侧栏小卡片里的订单行 */
.mini-order {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
}
.mini-order .mo-icon { font-size: 19px; line-height: 1; flex: none; }
.mini-order .mo-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.mini-order .mo-body b { font-size: 12.5px; font-weight: 800; }
.mini-order .mo-body small { font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.mini-order .mini-lack { font-size: 11px; font-weight: 700; color: #8a5a00; white-space: nowrap; }
.btn-xs { padding: 3px 9px; font-size: 11px; border-radius: 9px; }

/* ============================================================
   v2.14 宠物探险（挂机）
   ============================================================ */

.trip-sub {
    margin: 14px 0 8px;
    font-size: 14px;
    display: flex;
    align-items: baseline;
    gap: 8px;
}
.trip-sub small { font-size: 11.5px; font-weight: 500; color: var(--muted); }

.trip-list { display: flex; flex-direction: column; gap: 8px; }

.trip-row {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 10px 13px;
    border-radius: 14px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
}
.trip-row:hover { transform: translateX(3px); }
.trip-row.is-ready {
    border-color: rgba(245, 158, 11, .55);
    background: linear-gradient(100deg, #fffdf2, #fff);
    box-shadow: 0 0 0 3px rgba(245, 158, 11, .12);
}
.trip-row.is-final { opacity: .75; }
.trip-row.is-final:hover { transform: none; }

.trip-icon { font-size: 26px; line-height: 1; flex: none; }
.trip-main { flex: 1; min-width: 0; }
.trip-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.trip-pet-name { font-size: 14px; font-weight: 800; }
.trip-place-tag {
    padding: 1px 8px;
    border-radius: 999px;
    background: rgba(23, 51, 38, .07);
    font-size: 11px;
    font-weight: 700;
    color: var(--ink-soft, #3d5c48);
}
.trip-meta {
    margin-top: 3px;
    font-size: 11.5px;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
.trip-loot { margin-top: 3px; font-size: 11.5px; font-weight: 700; color: #8a5a00; }
.trip-action { flex: none; min-width: 76px; display: flex; justify-content: flex-end; }

.trip-tag {
    padding: 3px 9px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 800;
    white-space: nowrap;
}
.trip-tag-going { background: rgba(59, 130, 246, .14); color: #1d4ed8; }
.trip-tag-done  { background: rgba(34, 197, 94, .16); color: #15803d; }

/* 派谁出去：一排小卡片，选中的高亮；在外面探险的置灰 */
.trip-pets { display: flex; flex-wrap: wrap; gap: 8px; }
.trip-pet {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 7px 11px;
    border-radius: 13px;
    border: 1px solid rgba(23, 51, 38, .12);
    background: #fff;
    cursor: pointer;
    font: inherit;
    color: inherit;
    transition: border-color .15s, box-shadow .15s, transform .15s;
}
.trip-pet:hover:not([disabled]) { transform: translateY(-2px); border-color: rgba(34, 197, 94, .5); }
.trip-pet.is-on {
    border-color: var(--green-3, #16a34a);
    box-shadow: 0 0 0 3px rgba(34, 197, 94, .18);
    background: linear-gradient(160deg, #f3fdf2, #fff);
}
.trip-pet[disabled] { opacity: .55; cursor: not-allowed; }
.trip-pet .tpp-icon { font-size: 20px; line-height: 1; }
.trip-pet .tpp-name { font-size: 13px; font-weight: 800; }
.trip-pet .tpp-sub { font-size: 11px; color: var(--muted); }
.trip-pet .tpp-tag {
    font-size: 10px;
    font-weight: 800;
    color: #1d4ed8;
    background: rgba(59, 130, 246, .14);
    padding: 1px 7px;
    border-radius: 999px;
}

/* 去哪儿：每个地点一张卡，带「预计收获」 */
.trip-places { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.trip-place {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 10px 13px;
    border-radius: 14px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    /* ★ v2.88.4：行首这条**细色条**就是「这趟值不值」的刻度（分档见下面 .trip-place.rate-N）。
       默认透明 = 没分到档时仍是一张中性卡片，不会凭空多出一个颜色。 */
    border-left: 3px solid transparent;
}
/* 悬停：**只**把外壳边框色加深一档（中性灰绿），不动底色、不动任何文字色 ——
   所以悬停前后每一段文字的对比度**逐字不变**（tests/pettrip_places_contrast_test.py 钉着）。
   刻意不和下面的 .rate-N 抢颜色：热量只有**左边一条**，悬停是**整圈**变深。 */
.trip-place:hover { border-color: rgba(23, 51, 38, .26); }
/* ★ v2.88.4「哪趟更值」＝行首细色条的分档。
   改前那一片黄**不是**有意设计：它是 `.tp-main` 这个名字被头衔小面板（本文件 6462 行）
   拿去当金色胶囊之后**漏进来的常量渐变** —— 四行的背景图**逐字相同**，
   和「系数」毫无关系（实测见 tests/pettrip_places_contrast_test.py 的改前表）。
   这里换成一个**不牺牲可读性**的表达：
     · 颜色 + 宽度**双双递增**（两个通道，不只靠颜色区分）；
     · 分档按**绝对值**（系数 %，后台范围 50~400）而不是行序 ——
       站长重排 / 删掉一个地点，色条也不会说谎；
     · 只在左边占 3~6px，正文仍然压在 #fff 卡片上（所以对比度和行序无关）。
   四档：<120% / 120~159% / 160~199% / >=200%。 */
.trip-place.rate-1 { border-left-width: 3px; border-left-color: var(--green-1); }
.trip-place.rate-2 { border-left-width: 4px; border-left-color: var(--green-ink); }
.trip-place.rate-3 { border-left-width: 5px; border-left-color: var(--gold-2); }
.trip-place.rate-4 { border-left-width: 6px; border-left-color: var(--gold-ink); }
.trip-place .tp-icon { font-size: 26px; line-height: 1; flex: none; }
/* ★ v2.93.2：这一段只保留**探险自己的布局** —— 头衔面板那几条同名规则已经收窄到
   `.title-pop .tp-main` / `.title-pop .tp-name`（见本文件 v2.88 顶栏头衔段），
   探险行不会再匹配到金色胶囊 / 单行省略。
   v2.87.5 的整段复位是**局部修**：它挡住了当时那 10 个属性，但头衔规则仍是全局裸
   选择器 ⇒ 以后往 `.tp-main` 再加任何属性都会重新漏进来。现在从源头断开 ✓
   （`min-width:0` 留在 .tp-name 上：它原来由那条裸规则顺手给，收窄后显式写上，
     窄屏排版逐像素不变 ✓） */
.trip-place .tp-main {
    flex: 1;
    min-width: 0;
}
/* v2.93.2：撞名已从源头收窄；这里只写探险自己的字号 / 换行 / 布局 ✓ */
.trip-place .tp-name {
    font-size: 13.5px;
    font-weight: 800;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
}
/* ★ v2.88.4：--muted(#7f9c8c) 压白底只有 **2.98:1** ✗（手算 + 真浏览器都验过）——
   这两段说明文字改用 --ink-soft(#40634f) = **6.75:1** ✓（项目里"次级正文"的既有色）。 */
.trip-place .tp-name small { font-size: 11px; font-weight: 500; color: var(--ink-soft); }
.trip-place .tp-desc { margin-top: 2px; font-size: 11.5px; color: var(--ink-soft); line-height: 1.6; }
.trip-place .tp-preview { margin-top: 4px; font-size: 11.5px; font-weight: 700; color: #15803d; }
.trip-place .tp-action { flex: none; }
/* ★ v2.88.4：「派它去」按钮**保持绿色**，但白字要看得清 ——
   共享的 .btn-primary 浅绿端（--green-1 #4ade80）压白字只有 **1.74:1** ✗（实测最差值）。
   这里只**就地**把这一个按钮的绿压深：#15803d（本段 .tp-preview 已经在用的绿，5.01:1 ✓）
   → --green-4 #0f5132（9.36:1 ✓），最差色标 **5.01:1** ✓。
   ★ 不去改共享的 .btn-primary —— 仓库 / 商店 / 签到也在用它，那是扩大范围 ✗。 */
.trip-place .tp-action .btn-primary {
    background: linear-gradient(135deg, #15803d, var(--green-4));
}

.trip-fold { margin-top: 14px; }
.trip-fold-head {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 9px 12px;
    border: 1px solid rgba(23, 51, 38, .10);
    border-radius: 12px;
    background: rgba(255, 255, 255, .72);
    color: var(--ink);
    font-size: 13px;
    font-weight: 800;
    cursor: pointer;
    text-align: left;
}
.trip-fold-head:hover { border-color: rgba(34, 197, 94, .4); }
.trip-fold-head small { font-size: 11px; font-weight: 500; color: var(--muted); }
.trip-fold-head .fold-arrow { font-size: 10px; color: var(--muted); }
.trip-fold-body { margin-top: 9px; }

/* 领取后的「拿到了什么」小结 */
.trip-loot-box { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 2px; }
.trip-loot-row {
    padding: 8px 12px;
    border-radius: 11px;
    background: linear-gradient(100deg, #fffdf2, #fff);
    border: 1px solid rgba(245, 158, 11, .35);
    font-size: 13.5px;
}
.trip-loot-note { display: block; margin-top: 8px; font-size: 11.5px; color: var(--muted); line-height: 1.7; }

@media (max-width: 720px) {
    /* 窄屏：探险行改成两行，按钮别被挤掉 */
    .trip-row { flex-wrap: wrap; }
    .trip-action { margin-left: auto; }
    .trip-place { flex-wrap: wrap; }
    .trip-place .tp-action { margin-left: auto; }
    /* ★ v2.88.4：「派它去」在窄屏也要够手指点得中 —— ≥44px 是这个文件里一直没松过的
       同一条口径（.box-actions .btn / .seed-buy .btn / .rec-pager .btn 都这么写）✓
       实测改前 393 下只有 63x31（高度 31px ✗）。 */
    .trip-place .tp-action .btn { min-height: 44px; }
}
@media (max-width: 420px) {
    .trip-action { min-width: 0; }
    .trip-pet .tpp-name { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

@media (max-width: 720px) {
    /* ★ v2.93.1：手机也是 7 列一行（393/402 每格 ~46px，热区仍 ≥44px）。
       6px gap 与下面的「连续签到七格」.signin-week-grid 同口径 ✓ */
    .signin-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
    /* v2.64：可点之后**必须**保证手指够得着 —— 44px 是移动端最小点按尺寸
       （和 mobile_layout_test 里那条点按标准同一个口径 ✓） */
    .signin-day { padding: 9px 4px 8px; min-height: 44px; }
    .collect-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .collect-tiers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    /* 窄屏：订单卡改成两行，奖励挪到下面一行，别把交付按钮挤没了 */
    .order-row { flex-wrap: wrap; }
    .order-rewards { flex-direction: row; flex-wrap: wrap; gap: 8px; width: 100%; align-items: flex-start; }
    .order-action { margin-left: auto; }
    /* v2.15 赛季：奖励行同样改成两行 */
    .season-row { flex-wrap: wrap; }
    .season-action { margin-left: auto; }
}
@media (max-width: 420px) {
    /* ★ v2.93.1（UI 批次 4）：393/402 下 7 格一行铺满（每格 ~46px，均分、右边 0 空白）。
       格子变窄后只缩**次要信息的字号**（数字仍 13px，比「第 N 天」10.5px 大 2.5px，
       满足 §2.2 的层次判据），不删任何元素：特殊奖励 / 状态 / 今天徽章 / 奖励数字全在 ✓ */
    .signin-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
    .signin-day { padding: 8px 1px 7px; border-radius: 12px; }
    .signin-day-name { font-size: 10.5px; }
    .signin-day-icon { font-size: 18px; }
    .signin-day-coin { font-size: 13px; letter-spacing: -.2px; }
    .signin-day-exp { font-size: 9px; line-height: 1.2; }
    .signin-day-item { font-size: 9px; line-height: 1.2; word-break: break-all; }
    .signin-day-state { font-size: 9px; line-height: 1.2; word-break: break-all; }
    .signin-day-tag { font-size: 9px; padding: 0 5px; top: -7px; }
    .signin-stat { padding: 6px 10px; }
    #btnDoSignin { margin-left: 0; width: 100%; }
    .collect-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .collect-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .order-action { min-width: 0; }
    .season-action { min-width: 0; }
}
@media (max-width: 390px) {
    /* ★ v2.93.1：320~390 窄屏 7 列会把每格压到 44px 以下（热区不达标）⇒ 允许降级成
       4+3 两行（不是 2 列四行）。这是用户在「320~360 允许降级」里给的余地 ✓ */
    .signin-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
    .signin-day { padding: 10px 5px 8px; }
    .signin-day-name { font-size: 12px; }
    .signin-day-icon { font-size: 22px; }
    .signin-day-coin { font-size: 15px; letter-spacing: 0; }
    .signin-day-exp { font-size: 10px; line-height: 1.3; }
    .signin-day-item { font-size: 10px; line-height: 1.3; }
    .signin-day-state { font-size: 10.5px; line-height: 1.3; }
    .signin-day-tag { font-size: 10px; padding: 1px 8px; top: -9px; }
}

/* ================================================================
 * v2.17 「⋯」菜单分组 —— **v2.47 已整体下线**
 *
 * 历史：玩法和菜单项一个个加进来之后，菜单摊成了 15 项平铺 ——
 * 找「宠物探险」要从上扫到下，眼睛没有落脚点。
 * v2.17 于是按用途分 4 组 + 分组标题 + 组间分隔线，扫视路径短一半。
 *
 * v2.47 用户要求「把个人中心 / 成就 / 任务 / 玩法说明 / 管理后台
 * 放在收缩按钮中，多的不用加，删掉，并且改成一竖排……短一点」，
 * 菜单砍到 **6 项**、每项都是独立功能、不再成组，
 * 所以 .menu-group（分组标题）和 .menu-sep（分隔线）的规则一并删掉
 * （hud_menu.php 里也不再渲染这两个元素 —— 留着就是没人用的死样式）。
 *
 * 只有下面「图标统一列宽」这条**留着** —— 它管的是每个菜单项的
 * <i>emoji</i>，6 项一样要用。
 * ============================================================= */
/* ⚠️ 这里**不要**再写 .menu 的宽度 —— v2.18 把菜单改成了弹出面板，
   宽度由上面那段 .menu 的 width/max-width 决定。
   原来这里有一句 `.menu { min-width: 182px; }`，
   它排在前面那段后面、优先级又相同，会把面板宽度顶掉。
   同类的还有手机媒体查询里的 max-width。 */

/* 图标统一列宽 —— 这是让菜单项文字左对齐、不再参差的关键。
   用 <i> 包住 emoji 而不是直接写文字，否则宽度随字形变。 */
.menu a > i {
    display: inline-block;
    width: 1.55em;
    margin-right: 1px;
    font-style: normal;
    font-weight: 400;
    text-align: center;
}

@media (max-width: 560px) {
    /* 这里是**抽屉内部**的排版微调。
       ⚠️ 不要再写 .menu 的 min-width / max-width ——
          v2.18 的抽屉是全宽（width:100%），
          原来那句 `max-width: calc(100vw - 24px)` 会把它压窄、
          左右露出背景，看着像没铺满。
       v2.47：这里原来还有一条 `.menu-group { padding: 8px 11px 2px; }`
       —— 分组标题已下线（见上面 v2.17 那段），一并删掉。
       同一条 `.menu a { padding: 9px 11px; font-size: 13.5px; }` 也删掉了：
       它和基础 `.menu a`（9px 12px / 13.5px）几乎一样，留着只是多一条
       同权重后写、容易把将来的改动顶掉。手机上直接用基础那条。 */
}



/* v2.28：margin-left:auto 交给外层的 .pet-head-acts 了，这里不再抢 */
.btn-pet-skill-head {
    flex: none;
    white-space: nowrap;
    font-weight: 700;
}
.btn-pet-skill-head[hidden] { display: none; }

@media (max-width: 560px) {
    /* 窄屏：标题和按钮都要挤在一行，按钮收短一点 */
    .btn-pet-skill-head { margin-right: 4px; padding: 4px 9px; font-size: 12px; }
}



/* 技能按钮在最后，和另外两个之间拉开一点，免得挤成一坨 */
.modal-head .btn-head-skill { margin-left: 4px; }

@media (max-width: 560px) {
    /* 窄屏：标题 + 三个按钮 + × 要挤在一行，收短 */
    .pet-head-acts { gap: 4px; }
    .modal-head .btn-head { padding: 4px 8px; font-size: 12px; }
    .modal-head .btn-head-skill { margin-left: 2px; }
}


/* v2.29 点「🍖 宠物粮」跳到商店那一段时闪一下，让玩家知道"就是这儿" */
@keyframes shopJumpFlash {
    0%   { background: rgba(245, 158, 11, .38); }
    100% { background: transparent; }
}
.pet-sec-head.shop-jump-flash {
    border-radius: 10px;
    animation: shopJumpFlash 1.1s ease-out 2;
}
@media (prefers-reduced-motion: reduce) {
    .pet-sec-head.shop-jump-flash { animation: none; }
}


/* v2.30 卡片右上角只剩「✨ N/M」占位文字（技能按钮挪到动作行「放生」后面了），
   所以它得长得像"信息"而不是"能点的东西" */
.pet-skill-slots {
    margin-left: auto;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 11.5px;
    font-weight: 700;
    color: var(--muted);
    background: rgba(23, 51, 38, .05);
    white-space: nowrap;
}


/* ================================================================
 * v2.32 修订单折叠点了不收起
 *
 * `.order-fold-body` 的元素同时带着 `.order-list`，而
 * .order-list { display: flex; ... } —— CSS 的 display 会**盖掉**
 * 浏览器默认的 [hidden] { display: none }。
 * 于是 index.php 里那个 `hidden` 属性、以及 JS 的 dList.hidden = true
 * **完全没效果**：箭头显示 ▶（收起状态），底下条目却全露着。
 *
 * 这个项目里每个可隐藏元素都要显式补一条 .xxx[hidden] ——
 * 别的地方都补了（.modal / .pet-skill-pane / .dot-badge …），这里漏了。
 * ============================================================= */
.order-fold-body[hidden] { display: none; }

/* ================================================================
 * v2.32 补齐漏掉的 [hidden] 规则（同一根因，一次扫出来 4 处）
 *
 * 根因：只要组件的 class 设了 display（flex / grid / block），
 * 就会**盖掉**浏览器默认的 [hidden] { display: none }，
 * 于是 HTML 的 hidden 属性和 JS 的 el.hidden = true 全都失效 ——
 * 表现就是「点了没反应 / 该藏的东西还露着」。
 *
 * 这个项目本来就靠「每个可隐藏元素显式补一条 .xxx[hidden]」来解决
 * （.modal / .pet-skill-pane / .dot-badge … 都补了），
 * 下面这几个是漏的。是写脚本全项目扫出来的，不是一个个碰巧发现。
 * ============================================================= */
.trip-fold-body[hidden] { display: none; }   /* 宠物探险的历史折叠 —— 和订单的 order-fold-body 一模一样的漏法（.trip-list 设了 display） */
.btn[hidden] { display: none; }   /* 「取消佩戴头衔」之类的按钮：带 hidden 属性但 .btn 设了 display，永远藏不住 */
.field[hidden] { display: none; }   /* 邮箱验证码输入框：.field 设了 display，hidden 属性失效 */
.online-dot[hidden] { display: none; }   /* 聊天页对方离线时那个绿点：.online-dot 设了 display，hidden 始终无效 */


/* ================================================================
 * v2.33 等级表：间距与字号（用户反馈「间距和字体不合适，看起来很难受」）
 *
 * 三个毛病：
 *   ① 行太高 —— 30 多行的表用通用 .data 的 padding 太松，一屏只看得下 15 行
 *   ② 两列数字不对等 ——「升到下一级要」又大又粗，「累计要」却又小又灰，
 *      同一张表里两个数字列长得像两种东西，视线没法纵向扫
 *   ③ 等级列太宽 —— "Lv.1" 后面拖一大片空白，把右对齐的数字列推得和表头对不齐
 *
 * 改法：层级靠**颜色差**而不是字号差（字号差最伤扫读），数字统一等宽右对齐。
 * ============================================================= */
.lv-table { min-width: 0; width: 100%; table-layout: fixed; }

/* 行高：比通用 .data 紧一档 */
.lv-table th,
.lv-table td {
    padding: 5px 10px;
    font-size: 13px;
    line-height: 1.5;
    vertical-align: middle;
}

/* 表头：别太抢眼，它是标签不是内容 */
.lv-table th {
    padding: 7px 10px;
    font-size: 12px;
    font-weight: 600;
    color: var(--muted);
    letter-spacing: .3px;
    white-space: nowrap;
}

/* 列宽定死，表头和数据才对得齐 */
.lv-table th:nth-child(1), .lv-table td:nth-child(1) { width: 88px; }
.lv-table th:nth-child(2), .lv-table td:nth-child(2) { width: 108px; }
.lv-table th:nth-child(3), .lv-table td:nth-child(3) { width: 92px; }
/* 第 4 列（该级解锁）自适应 */

/* 两个数字列统一：等宽数字 + 右对齐。
   层级用颜色差，不用字号差 —— 字号一大一小会让眼睛在纵向扫的时候反复跳动。 */
.lv-table .lv-need,
.lv-table td:nth-child(3) {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    font-size: 13px;
}
.lv-table .lv-need { font-weight: 600; color: var(--ink); }
.lv-table td:nth-child(3) { font-weight: 400; color: var(--muted); }

/* 等级列：字重够用就行，别太粗 */
.lv-table .lv-cell-lv { white-space: nowrap; }
.lv-table .lv-cell-lv b { font-weight: 700; font-size: 13px; }

/* 「当前」小标：缩小并压低，别把这一行撑高 */
.lv-table .lv-here {
    margin-left: 4px;
    padding: 0 5px;
    font-size: 9.5px;
    line-height: 15px;
    vertical-align: 1px;
}

/* 已过的等级整行淡下去，但仍然要能读（.55 太淡了，13px 的字会糊） */
.lv-table .lv-past { opacity: .68; }

/* 当前行：淡绿底 + 左侧色条，别铺满整行那么重 */
.lv-table .lv-cur { background: rgba(34, 197, 94, .12); box-shadow: inset 3px 0 0 #22c55e; }
.lv-table .lv-cur td { font-weight: 700; }
.lv-table .lv-cur td:nth-child(3) { font-weight: 600; }

/* 解锁的作物小标签：跟着 13px 走，不要独立一套字号 */
.lv-table .lv-crop { font-size: 11.5px; padding: 1px 7px; }

/* 满级那行 */
.lv-table .lv-max td { background: rgba(245, 158, 11, .10); font-size: 12.5px; }

@media (max-width: 560px) {
    /* 窄屏：列宽放开，字号再降一点，否则 4 列挤不下 */
    .lv-table { table-layout: auto; min-width: 460px; }
    .lv-table th, .lv-table td { padding: 5px 7px; font-size: 12.5px; }
    .lv-table th:nth-child(1), .lv-table td:nth-child(1) { width: auto; }
    .lv-table th:nth-child(2), .lv-table td:nth-child(2) { width: auto; }
    .lv-table th:nth-child(3), .lv-table td:nth-child(3) { width: auto; }
}


/* ================================================================
 * v2.34 等级表：对齐（用户：「能否居中，看起来难受」）
 *
 * 两个毛病：
 *   ① 「该级解锁」表头是居中的（继承通用 .data th），格子却是左对齐的
 *      （继承通用 .data td）—— **同一列两种对齐**，视觉上最刺眼。
 *   ② 前三列宽度定死后，第四列吃掉全部剩余（约 620px），
 *      而内容左对齐 → 挤在最左边，右边空一大片。
 *
 * 改法：第四列表头+格子都居中；「等级」列表头+格子都左对齐（它也有同样的错位）；
 * 前三列各放宽一点，把第四列压窄。
 * ============================================================= */
.lv-table th:nth-child(1), .lv-table td:nth-child(1) { width: 96px;  text-align: left; }
.lv-table th:nth-child(2), .lv-table td:nth-child(2) { width: 124px; text-align: right; }
.lv-table th:nth-child(3), .lv-table td:nth-child(3) { width: 104px; text-align: right; }

/* 「该级解锁」：表头 + 格子都居中 —— 这条是用户直接要求的 */
.lv-table th:nth-child(4),
.lv-table td:nth-child(4) {
    text-align: center;
}

/* 解锁标签居中后，去掉它自带的右外边距，否则视觉上会偏左 */
.lv-table td:nth-child(4) .lv-crop { margin: 1px 2px; }

/* 一个等级可能解锁多种作物：让它们换行也不会把行撑得很难看 */
.lv-table td:nth-child(4) { line-height: 1.9; }

@media (max-width: 560px) {
    /* 窄屏保持 auto 布局，居中仍然生效 */
    .lv-table th:nth-child(1), .lv-table td:nth-child(1) { width: auto; }
    .lv-table th:nth-child(2), .lv-table td:nth-child(2) { width: auto; }
    .lv-table th:nth-child(3), .lv-table td:nth-child(3) { width: auto; }
}


/* ================================================================
 * v2.35 繁育面板的下拉框（用户：「选择框太难看了」）
 *
 * 原来全项目对 select 只有一行 `.pet-breed-pick select { width: 100% }`，
 * 所以它是**纯原生控件** —— 细灰边框、系统字体、系统三角箭头、几乎直角，
 * 和旁边的圆角卡片/按钮放在一起非常突兀。
 *
 * 这里按项目既有的视觉语言重做：圆角、柔和边框、项目字体、自绘箭头、
 * 聚焦时绿色描边（和 .captcha-img:hover 同一支绿）。
 * ============================================================= */
.pet-breed-pick select {
    width: 100%;
    box-sizing: border-box;
    /* 高度和旁边的 .btn 对齐，别一高一矮 */
    height: 38px;
    padding: 0 34px 0 12px;
    border-radius: 12px;
    border: 1px solid rgba(23, 51, 38, .16);
    background-color: #fff;
    /* 自绘箭头：原生三角在不同浏览器里长得不一样，去掉它自己画 */
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%235b7268' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 12px 8px;
    color: var(--ink);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.4;
    cursor: pointer;
    /* 去掉原生外观（各家前缀都要写，否则 Safari/Firefox 不生效） */
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    transition: border-color .18s, box-shadow .18s, background-color .18s;
    /* 文字太长时省略号，别把布局撑破 */
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
}
.pet-breed-pick select:hover {
    border-color: rgba(34, 197, 94, .55);
    background-color: #fbfffc;
}
.pet-breed-pick select:focus {
    outline: none;
    border-color: var(--green-2, #22c55e);
    box-shadow: 0 0 0 3px rgba(34, 197, 94, .16);
}
/* 门槛不满足的选项是 disabled 的 —— 让它明显灰掉 */
.pet-breed-pick select option:disabled {
    color: #64748b;
}
/* 真正选中的那一项要能看清 */
.pet-breed-pick select option:checked {
    font-weight: 700;
    color: var(--ink);
}

/* 两个下拉框之间那个 × ：让它和下拉框垂直居中，别贴着底部 */
.pet-breed-row { align-items: flex-end; }
.pet-breed-x { padding-bottom: 10px; font-size: 13px; }

/* 「父母 A / 父母 B」标签：跟上项目里其它小标签的字重和颜色 */
.pet-breed-pick > span {
    font-size: 11.5px;
    font-weight: 800;
    color: var(--muted);
    letter-spacing: .02em;
}

/* 下面那排（按钮 + 门槛说明）：保证换行时留出间距，不贴着按钮 */
.pet-breed-actions { row-gap: 8px; }
.pet-breed-why { line-height: 1.7; }

@media (max-width: 560px) {
    /* 窄屏：两个下拉框各占一行，高度略降 */
    .pet-breed-pick select { height: 36px; font-size: 12.5px; }
    .pet-breed-x { display: none; }
}


/* ================================================================
 * v2.37 / v2.38 移动端「⋯ 更多」菜单
 *
 * 用户报了两个现象，是两个独立的问题：
 *   ① 菜单底部被切掉、看不见「个人中心/退出登录」
 *      → max-height 的问题（见上面那三档 vh/svh/dvh 兜底）
 *   ② 菜单左边的字被切掉（「每日任务」只剩「任务」）
 *      → **我 v2.37 自己引入的**：我写了 width: calc(100vw - 22px)，
 *        想做成"全屏宽抽屉"，但 .menu 是 absolute，right:0 相对的是
 *        .menu-wrap（⋯ 按钮）而不是屏幕。⋯ 不一定在最右边
 *        （个人中心页 HUD 只有 4 个按钮，⋯ 在屏幕 54% 处），
 *        于是菜单从按钮往左铺 97% 屏宽，左边超出屏幕约 190px。
 *
 * **它是"从按钮往左弹的下拉"，不是"全宽抽屉"** —— v2.38 又加了宽度上限。
 * 现在（v2.47）定位基准已经上移到 .hud-actions（见文件末尾 v2.44 那段），
 * 宽度不需要再"躲屏幕"了，所以下面只剩「只给上限」这一个用途。
 * ============================================================= */
@media (max-width: 560px) {
    .menu {
        /* 不强制全屏！只给上限 */
        max-width: calc(100vw - 22px);
        padding: 5px;
        /* 上面三档 max-height 继续生效，这里只把余量再收紧一点 */
        max-height: calc(100svh - 108px);
    }
    /* 菜单项：左对齐（不写的话会跟父级的 text-align 走，出现过居中）。
       v2.47：这里原来那条 `padding: 9px 11px; font-size: 13.5px;` 已删 ——
       它其实和基础 .menu a 一样（都是 9px 上下留白 / 13.5px），留着只是多一条
       同权重后写、容易把将来的改动顶掉。手机上直接用基础那条的 9px 12px。 */
    .menu a {
        font-size: 13.5px;
        text-align: left;
        /* 长标签不换行，超出用省略号 —— 但宽度够时不会触发 */
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .menu a > i { width: 1.4em; }
    /* 「· 当前页」在手机上会把标签挤没，手机上不显示 */
    .menu a.on::after { content: none; }
}


/* ================================================================
 * v2.39 让「⋯ 更多」永远靠右 —— 从根上解决下拉菜单往左溢出屏幕
 *
 * 【问题】菜单是 position:absolute; right:0，而 right:0 相对的是 .menu-wrap
 *        （⋯ 按钮）不是屏幕。.hud-actions 是 flex + flex-wrap，个人中心/聊天页
 *        只有 3 个按钮、左对齐，⋯ 在屏幕 41% 处 —— 菜单从那儿往左铺，
 *        左边溢出屏幕，「每日任务」被切成「任务」。
 *
 * 【为什么不是调菜单宽度】按钮在哪，溢出多少就跟着变；调宽度只是碰运气。
 *
 * 【正解】把 ⋯ 推到所在行的最右边（margin-left:auto）。
 *        · 农场页 15 项正好 3 行 × 5 列，⋯ 本来就在最右 → 行为不变
 *        · 个人中心 / 聊天页 3 个按钮 → ⋯ 被推到最右 → 菜单从屏幕右缘
 *          往左弹，任何合理宽度都不会溢出
 * ============================================================= */
/* v2.42：撤掉 margin-left:auto —— 用户截图显示它把 ⋯ 孤零零甩到最右边，
   和「聊天 / 消息」之间隔一大段空白，很难看。
   溢出问题改用"菜单往右弹"解决（见文件末尾 v2.42 那段）。 */
.hud-actions .menu-wrap { margin-left: 0; }

/* 菜单宽度：无条件限制不超过屏幕（原来是放在 <=560px 的媒体查询里，
   用户的视口 604px 时那条不生效，所以宽度没应用上）。
   ⚠️ v2.47：这里**只有** max-width 兜底 —— 具体宽度见下面 v2.43 那段
   （`width: auto` = 贴着内容），别在这里再写一个 width 把它顶掉。 */
.menu {
    max-width: calc(100vw - 24px);
}


/* ================================================================
 * v2.40 HUD 按钮不再「拉长留空」
 *
 * 用户反馈：「感觉按钮太长不美观」。
 *
 * 根因：手机上每项是 `flex: 1 1 calc(20% - 5px)` + `max-width: calc(20% - 5px)`
 *       —— 按"一行 5 个"封顶。
 *       农场页 15 项正好每行 5 个、占满 100%，所以看不出问题；
 *       个人中心 / 聊天页只有 4 个 → 只占 80%，**剩 20% 空着**，
 *       而 v2.39 的 margin-left:auto 又把 ⋯ 推到最右 →
 *       空隙全挤在中间，⋯ 还是那个 20% 宽的空胶囊（中间就三个点）。
 *
 * 改法：去掉 max-width 上限，让 flex-grow 生效 ——
 *   · 满行（农场页）→ 行为不变，本来就占满
 *   · 不满行（个人中心 4 个）→ 各占 25%，均分铺满，没有空隙
 *
 * `margin-left: auto`（v2.39）保留：它保证 ⋯ 是最右那个，
 * 菜单才不会从中间往左溢出屏幕。铺满后没有剩余空间，auto 自然不起作用。
 * ============================================================= */
@media (max-width: 560px) {
    .hud-actions > .btn,
    .hud-actions .menu-wrap {
        /* 手机：只给 flex-basis、不封 max-width —— 让这一行自己铺满。
           ⚠️ 这条**只能管手机**。原来我写成 max-width: 900px，
           结果平板上（一行只放得下 4 个）flex-grow 把按钮拉成 250px 宽。
           见下面 v2.41 的说明。 */
        flex: 1 1 calc(20% - 5px);
        max-width: none;
    }
    /* 个人中心 / 聊天页这类"一行不足 5 个"的：4 个就各占 25% */
    .hud-actions > .btn,
    .hud-actions .menu-wrap {
        min-width: 0;
    }
    /* ⋯ 这种只有图标的按钮：内容居中，别让三个点偏在一边 */
    .hud-actions .menu-wrap > .btn {
        justify-content: center;
        padding-left: 2px;
        padding-right: 2px;
    }
}


/* ================================================================
 * v2.41 平板 / 桌面上的 HUD 按钮不再被拉伸
 *
 * 用户截图（平板，视口约 1000px）：每个按钮约 250px 宽，
 * 里面就一个图标 + 两个字 —— 「按钮太长不美观」。
 *
 * 根因：HUD 每项是 `flex: 1 1 calc(20% - 5px)`（按一行 5 个设计），
 *       平板上**一行只放得下 4 个**，flex-grow 就把这 4 个撑满整行，
 *       各分到 25% ≈ 250px。
 *       我 v2.40 又加了 `max-width: none`，等于把这个拉伸坐实了。
 *
 * 教训：**一条 flex 规则跨了手机和平板两种完全不同的屏宽** ——
 *       这是我这几版反复来回的根因。现在按屏宽分开写。
 *
 * 平板/桌面：不给拉伸，固定一个舒服的宽度区间（88~132px，按内容自适应），
 *           放不下就自然换行、靠左排。
 * ============================================================= */
@media (min-width: 561px) {
    .hud-actions > .btn,
    .hud-actions .menu-wrap {
        flex: 0 0 auto;          /* 不伸不缩，按内容来 */
        width: auto;
        min-width: 86px;         /* 太窄会挤成一条 */
        max-width: 132px;        /* 再宽就"太长"了 */
        max-height: none;
    }
    /* ⋯ 只有一个图标，别给 86px 那么宽 */
    .hud-actions .menu-wrap {
        min-width: 0;
        max-width: 64px;
    }
    .hud-actions .menu-wrap > .btn { width: 100%; }
    /* 桌面端按钮内部：图标 + 文字横排，居中 */
    .hud-actions > .btn,
    .hud-actions .menu-wrap > .btn {
        justify-content: center;
    }
}


/* ================================================================
 * v2.43 菜单统一「往左弹 + 宽度贴着内容」（算出来的，不是猜的）
 *
 * 来回改了六版，最后靠用户给的精确视口（iPad Mini 768×1024）才算清。
 *
 * 关键教训：我 v2.42 说"平板右边还有 60% 空间、菜单往右弹"，
 *   那是**拿旧截图的数据去推新布局** —— 那时按钮宽、一行 4 个、⋯ 在 40%；
 *   而 v2.41 把按钮收窄后一行放得下 7 个，⋯ 已经落到该行最右端（93%），
 *   往右弹只剩 54px，必然溢出。
 *
 * 这次按三种最坏情况验算（视口 768 / 390，往左弹）：
 *
 *   | 页面                        | ⋯ 右边缘 | 往左弹之后     | 结果 |
 *   |-----------------------------|----------|----------------|------|
 *   | 农场页 768px（一行 7 个放满）| 714 (93%)| 434 → 714      | ✅   |
 *   | 个人中心 768px（4 个）       | 530 (69%)| 250 → 530      | ✅   |
 *   | 手机 390px（5 列放满）       | 368 (94%)|  88 → 368      | ✅   |
 *
 * 所以 right:0（往左弹）任何合理宽度都放得下。
 *
 * v2.47：用户说「这个按钮太长」—— 截图里菜单框约 410px 宽、文字只占约 180px，
 *   **右边空了一大片**。所以「太长」是**横向太宽**，不是竖向高。
 *   原来这里写死 `width: 320px`（再往前还有 300 / 360 几档互相覆盖），
 *   对 `position: absolute` 的元素来说 `width: auto` 就是 **shrink-to-fit**：
 *   宽度 = 内容最宽那一项 + 左右 padding，于是菜单框自动贴着文字。
 *
 *   .menu a 是 display:block 的 <a>，最宽那项决定整体宽度。6 项里最宽的是
 *   「成就殿堂 / 玩法说明 / 管理后台」这一档（4 个汉字）：
 *       图标列 1.55em ≈ 21px + 4 x 13.5px = 54px + a 的左右 padding 12+12
 *       + .menu 的左右 padding 6+6  ->  约 111px
 *   min-width: 148px 是**兜底**：万一将来出个短到撑不住的菜单，
 *   也别窄成一条、让文字折行。
 *   实际宽度由浏览器按真实字体定，静态套件只能给「≤ 上界」和「≥ min-width」两个界。
 * ============================================================= */
.menu {
    /* shrink-to-fit：绝对定位元素 + width:auto = 内容宽度 */
    width: auto;
    /* 兜底：别窄到让标签折行 */
    min-width: 148px;
    max-width: calc(100vw - 20px);
    right: 0;
    left: auto;
}


/* ================================================================
 * v2.44 把菜单的定位基准从「⋯ 按钮」上移到「整条 HUD」—— 从结构上根治溢出
 *
 * 【七版来回的症结】（这段务必看完再改，别再走一遍）
 *
 *   结构一直是：
 *       .hud-actions            ← 横跨屏幕的 flex 容器
 *         └ .menu-wrap          ← position: relative，**定位基准**
 *             ├ button ⋯
 *             └ .menu           ← position: absolute; right: 0
 *
 *   `right: 0` 相对的是 **.menu-wrap（⋯ 按钮）**，不是屏幕。
 *   而 ⋯ 在哪，取决于 HUD 有几个按钮、一行放得下几个：
 *       · 农场页一行放满 7 个 → ⋯ 在最右（93%）→ 往左弹放得下 ✓
 *       · 一行只有 3 个        → ⋯ 在 35%    → 往左弹 300px 出屏 ✗
 *
 *   **按钮在哪，溢出就在哪。** 所以「调菜单宽度 / 调弹出方向 / 把按钮推到最右」
 *   全都是治标 —— 我为此改了 v2.37~v2.43 共七版。
 *
 * 【根治】把定位基准上移到 .hud-actions（它横跨屏幕，右边缘 ≈ 屏幕右边缘）：
 *   · 菜单右边缘永远贴着屏幕右边缘
 *   · 不管 HUD 里几个按钮、⋯ 在哪，都不可能再溢出
 *   · `top: calc(100% + 8px)` 变成相对整条 HUD → 菜单落在 HUD 下方，更自然
 *
 * ⚠️ `.hud-actions .menu-wrap > .btn` 的 position: relative 要留着 ——
 *    那是给按钮上的小红点角标定位用的，别跟着一起改成 static。
 * ============================================================= */
.hud-actions { position: relative; }
.hud-actions .menu-wrap { position: static; }
.hud-actions .menu {
    position: absolute;
    /* 相对整条 HUD：右边缘贴屏幕右缘 */
    right: 0;
    left: auto;
    /* 相对整条 HUD 的下沿 */
    top: calc(100% + 8px);
    /* 宽度不再需要"躲屏幕"了，但界面本身仍要好看。
       v2.47：320px -> auto（shrink-to-fit，贴着内容）+ min-width: 148px 兜底。
       用户的原话是「这个按钮太长」，截图里菜单框约 410px、文字只用约 180px，
       右边一大片空白 —— 所以这里不再写死宽度。 */
    width: auto;
    min-width: 148px;
    max-width: calc(100vw - 24px);
    /* HUD 可能不止一行，别让菜单顶出视口 */
    max-height: calc(100svh - 150px);
}


/* ================================================================
 * v2.45 平板/窄屏下让 .hud-actions 横跨整屏宽
 *
 * 【为什么需要这条】v2.44 把菜单的定位基准从 .menu-wrap 上移到了 .hud-actions,
 * 但查下来发现 `.hud-actions { width: 100% }` 原来**只在 max-width:560px 里**。
 * 平板上它是内容宽度 → 右边缘仍落在 ⋯ 那里 → 定位基准上移等于白移。
 *
 * 【为什么安全】从用户平板截图看，HUD 按钮本来就已经独占一行
 * （上面是金币/仓库/偷菜那些 chip），flex-wrap 早把它挤成独立一行了。
 * 给它 width:100% 只是让这一行铺满屏宽 —— 按钮的排列、左对齐、一行几个都不变，
 * 只有"这一行的右边缘"从 ⋯ 那里变成屏幕右缘，菜单才有正确的锚点。
 *
 * 【为什么不写顶层】桌面（>1024px）那边是
 * `.hud-actions { order:4; margin-left:auto }` 和 chip 同一行的布局，
 * 强加 width:100% 会改变桌面外观。所以按屏宽收着写。
 * ============================================================= */
@media (max-width: 1024px) {
    .hud-actions {
        width: 100%;
        margin-left: 0;
    }
}


/* ================================================================
 * v2.47 撤掉 v2.46 的「手机专属 HUD 入口」（.hud-narrow）
 *
 * 【为什么撤】v2.46 往 HUD 上加了 4 个只在手机显示的入口
 *   （📋 每日任务 · 🏆 成就殿堂 · 📖 留言板 · 🙋 个人中心，手机 HUD 14 -> 18）。
 *   用户这次点名回退其中 3 个：「把个人中心，成就，任务，玩法说明，管理后台
 *   放在收缩按钮中」——它们回 ⋯ 菜单了。
 *   剩下的 📖 留言板改成**常驻**（手机和桌面都显示），也不再是"手机专属"。
 *
 * 【所以这里删掉了什么】
 *   · .hud-actions .hud-narrow { display: none } / { display: inline-flex }
 *     —— 一个使用者都没有了（HUD 里已经搜不到 hud-narrow 这个类）
 *   · .menu-sec { display: contents } / { display: block } 和
 *     .menu a.is-hud-narrow { display: none }
 *     .menu-sec[data-hud-hide-mobile="1"] { display: none }
 *     —— 那套"整组在手机上藏掉、连分组标题一起藏"的机制随分组一起下线
 *     （菜单只剩 6 项一竖排，不再有分组标题 / 分隔线，没有整组可藏了）
 *   · 两列网格：@media (max-width:560px) { .menu-card { display:grid;
 *     grid-template-columns: 1fr 1fr } } 和 .menu-group/.menu-sep 的
 *     grid-column: 1/-1，以及两条只为"退回单列"而存在的媒体查询
 *     （max-width:380px / max-width:560px and max-height:460px）。
 *     用户要求「改成一竖排，不要双排了」—— 所有屏宽都一列，
 *     .menu-card 保持基础规则那条 display: contents（不产生盒子）。
 *
 * 【对那 5 条定位链的影响】零
 *   这一版一个字都没碰下面这 5 条（它们在同文件更靠前的 v2.44 / v2.45 段里）：
 *     ① .hud-actions { position: relative }
 *     ② .hud-actions .menu-wrap { position: static }
 *     ③ .hud-actions .menu { position: absolute; right: 0; left: auto;
 *                            top: calc(100% + 8px) }
 *     ④ @media (max-width:1024px) .hud-actions { width: 100% }
 *     ⑤ .hud-actions .menu-wrap > .btn { position: relative }
 *   HUD 按钮数从 19 变 17（16 + ⋯）会改变换行位置 —— 所以"菜单不出屏"
 *   必须实测：数字写在 tests/hud_layout_test.py 第【4】节和
 *   tests/mobile_layout_test.py 第【7】节里。
 * ============================================================= */


/* ================================================================
 * v2.50 弹窗头部：操作按钮/标签组**居中**（三列网格，中间那列天然在正中）
 *
 * 用户原话：「所有弹窗界面的按钮居中一下」
 *          「比如这个界面有两个按钮，我需要两个按钮靠左一点，看起来居中」
 * 截图是菜市场订单头部：「📋 菜市场订单 5张进行中 … [📦 一键交付][🔄 刷新订单 500] ×」
 * 两个按钮被 `.modal-x { margin-left:auto }` 顶到最右，要它们落在**屏幕正中**。
 *
 * 为什么必须给按钮组包一层（HTML 侧已加 `<div class="modal-acts">`）：
 *   按钮是**并排的兄弟节点**，各自 50% 会互相重叠 —— 纯 CSS 没法让"一组
 *   兄弟元素"整体居中，除非先有一个共同祖先。所以 index.php 的 10 个头部
 *   把中间那些操作元素包进了 .modal-acts（详见该文件里的注释）。
 *
 * 为什么三列网格能让中间那列居中（这是这一版的核心）：
 *   `grid-template-columns: minmax(0,1fr) auto minmax(0,1fr)`
 *   ① 中间那列是 `auto` = 按内容收缩（就一条按钮 / 一排标签的宽度）；
 *   ② 左右两列写成**逐字相同**的 `minmax(0,1fr)` —— 轨道尺寸由"可用空间 +
 *      轨道自身的 min/max"决定，**两边输入完全一样 → 输出也一定一样**，
 *      所以恒有「左列宽 L = 右列宽 R」；
 *   ③ 于是中间那列的中点 = L + M/2 = (W-M)/2 + M/2 = W/2 = 容器中点，
 *      和 M（按钮组多宽）无关。标题多长、× 多宽都不影响中点位置
 *      —— 这正是用户要的"看起来居中"。
 *      （tests/mobile_layout_test.py 第【9】节把这个恒等式对
 *       4 个 modal 宽度档 × 3 个 M 值算了 12 遍。）
 *
 * ⚠️ 两条必须记住的副作用：
 *   · `.modal-x` 在 DOM 里是最后一个子元素，所以靠 `grid-column: 3` 定位。
 *     **不能**用 `order`/`flex` 那套（grid 容器里 flex 属性无效）。
 *   · 左右列是 `minmax(0, …)`：允许被压到 0（v2.49 的"标题不许折行"照旧）。
 *     代价是：**如果标题 + 按钮组 + × 加起来比头部还宽**（手机上 360px 左右
 *     的弹窗最容易），两边的 1fr 会被压到 0，中间那列虽然还在正中，
 *     但标题会横向溢出、被按钮盖住一截。这是"标题一字不能少（不折行、不省略号）
 *      vs 按钮精确居中"之间的取舍 —— 按用户明确要求选了后者，
 *     ≤760px 那档则整块换到第二行，把这种挤压挡在手机竖屏之外。
 *     本沙箱没有排版引擎，**没有**实测过这些最坏情况的像素（见交付说明）。
 *
 * v2.49 那一段（标题不折行 + tabs 整块换行）**没有删掉，是改写**：
 *   它原来是 flex 的 `order: 9; flex: 1 1 100%`，那两条在 grid 容器里
 *   **完全不生效**（flex 属性对 grid 子项无效）。留在这里只会让人以为
 *   还在起作用，所以整段改写成等价的 **grid** 写法（见下面的
 *   `@media (max-width:760px)`：`.modal-acts` 用 `grid-column: 1 / -1`
 *   + `grid-row: 2` 换到第二行，效果和 v2.49 的"整块换行"一样）。
 * ============================================================= */

.modal-head {
    display: grid;
    /* ⚠️ v2.66 第五次修这个头部（前四次改的是标题、断点、换行，都没治到根上）。
       用户截图：「赛季排行榜被遮挡一点」—— 最后一个榜标签被右上角的 × 压住。
       根因：第 3 列原来是 `minmax(0, 1fr)`，而中间那列（标签组）是 `auto`。
       中间列一旦取到 max-content，两个 1fr 就只能平分**剩下的**空间；剩下的接近 0 时
       第 3 列就变成 0 宽，而 × 是 `justify-self: end` —— 它会从右边**溢到中间列上面**，
       正好盖住最后一个标签（和第 4 次那个"标题画到邻居身上"是同一类毛病）。
       修法：第 3 列改成 `minmax(44px, auto)` —— × 永远有自己的一格（44px 也正好是热区下限）；
       中间列改成 `minmax(0, auto)`，放不下时**由它**缩（缩了也不裁标签，改成横向滚动，
       见下面 v2.66 那段的 .modal-head > .modal-acts > .tabs）。 */
    /* ⚠️ v2.67 第六次修这个头部。用户截图：「订单、商店、仓库、赛季排行、赛季、消息、任务
       界面上的按钮没有居中」—— 标题栏右侧那组按钮（一键交付 / 刷新订单 500 / 一键全卖）
       被挤到一边，中间留一大块空。
       根因：第 1 列是 `1fr`、第 3 列是 `minmax(44px, auto)` —— **两列不等宽** ✗
       第 1 列把剩余空间全吃掉，中间那列（auto = 自身内容宽）只能紧跟在它后面，
       于是按钮组永远**贴向右侧**，而不是弹窗的水平中点。
       修法：让第 1 列和第 3 列**同权**（都是 `1fr`）—— 中间列就必然落在正中 ✓
       第 3 列仍留 `44px` 下限：× 是 `justify-self: end`，列若能被压到 0，
       ×（32px 可见 + ::after 撑到 44px 热区）就会从右边**溢到中间列上**，
       正是 v2.66 那个"最后一行标签被 × 压住"的同一类毛病，所以下限不能去掉。
       代价：极窄时第 3 列比第 1 列宽 44px，按钮组会偏左约 22px ——
       **宁可偏这 22px，也不要按钮贴边或 × 压标签** ✓
       （≤860px 会换成两行布局，中间列整行居中，见下面的媒体查询 ✓） */
    grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(44px, 1fr);
    align-items: center;
    column-gap: 12px;
    row-gap: 8px;
    /* ⚠️ v2.49 那条 `.modal-head { flex-wrap: wrap }` 已经**整条删掉**了，
       这里不要写 `flex-wrap: nowrap` 去找补 —— `flex-wrap` 的适用对象是
       **flex 容器**，对一个 `display:grid` 的盒子它本来就不生效
       （写了是死代码，容易让人以为网格会"换行"）。v2.49 想要的
       "挤不下就整块换行"改由下面 `@media (max-width:760px)` 的
       `grid-row: 2` 承担。 */
}

/* 标题：第 1 列。v2.49 的"不许折行"保留，但**必须能裁**。
 *
 * ⚠️⚠️ v2.55 第四次修这个重叠，前三次都在改"文案长度"和"断点"，全错。
 *     真正的根因是**盒子会画到邻居身上**：
 *       这一列是 `minmax(0, 1fr)` —— 允许被压到 0；标题又是 `white-space: nowrap`。
 *       `min-width: 0` 只是"允许列变窄"，**它不裁剪内容**：
 *       内容比列宽时，就直接画到列外面 → 盖在中间那列（标签/按钮）上。
 *     所以只要"标题内容 > 左边那列的可用宽度"就重叠，**跟文案长短无关**
 *     （用户第三次的截图里标题已经短到只剩「2026-W41」，照样压住「等级榜」）。
 *
 *     正确的三条是**同时**成立才有用：
 *       min-width: 0            -> 允许这一列被压缩（不然列不肯变窄，会顶掉中间列）
 *       overflow: hidden        -> ★ 溢出就裁掉，绝不画到邻居身上（前三次缺的就是这条）
 *       text-overflow: ellipsis -> 被裁时给个省略号，让人知道还有内容（不是静默截断）
 *     `white-space: nowrap` 保留：宁可省略号，也不要那一个字折成两行。 */
.modal-head > h3 {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* 标题后面的说明（例如赛季编号/张数）：它自己也是个 flex 子项，同样要能裁。
   不给 `overflow: hidden` 的话，长说明会把 h3 撑宽、再溢出到邻居身上。 */
.modal-head > h3 small {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 中间那列（标签/按钮组）：同样"可压缩 + 不溢出"。
   它有 `flex-wrap`，真放不下时标签会**换行**而不是横向撑破；
   `overflow: hidden` 只是最后一道兜底（万一换行也放不下，宁可裁掉也不压邻居）。 */
.modal-head > .modal-acts {
    overflow: hidden;
}
/* 关闭按钮不允许被压缩、也不会溢出（固定 32px）
   ⚠️ v2.66：这里原来是 `overflow: hidden`，必须改成 visible ——
   下面用 ::after 把命中区撑到 44×44（手指点得中），`overflow: hidden` 会把那个
   撑出去的部分**裁掉**，热区就白加了。真正的"不被压缩"靠的是第 3 列的
   `minmax(44px, auto)`（见上面 .modal-head 的注释）。 */
.modal-head > .modal-x {
    overflow: visible;
}

/* 中间那列 —— 操作按钮 / 标签组 / 拜访农场按钮组都住这儿 */
.modal-head > .modal-acts {
    grid-column: 2;
    grid-row: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-width: 0;
}
/* 按钮组内部自己换行，撑不下时**不**把中间列顶宽（顶宽就会偏离中点） */
.modal-head > .modal-acts > .tabs {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-width: 0;
}
.modal-head > .modal-acts .btn,
.modal-head > .modal-acts .tab {
    flex: none;
    white-space: nowrap;
}
/* ⚠️ v2.67：这三条是"居中"的最后一道坎 ✗
   文件更上面有两条**老规则**（v2.49 时代为了让这些按钮贴右而写的）：
     .modal-head .btn-gold { margin-left: auto }
     .modal-head .tabs     { margin-left: 10px }
   它们的选择器是**后代**（不是直接子元素），所以哪怕 .modal-acts 内部
   已经 `justify-content: center`，只要那一条规则没被抵消，按钮/标签组
   仍会各自被 `auto` 外边距从中间挤开（一个偏左、一个偏右）——
   用户看到的"没居中"就是这个 + 上面栅格不等宽两件事叠出来的 ✗
   所以这里必须用**后代选择器**（去掉 `>`）逐个抵消，而不是只管直接子元素。
   `margin-right` 也一起归零：将来若有人加 `margin-left: auto` 的镜像写法也不会再偏。 */
.modal-head > .modal-acts .btn-gold,
.modal-head > .modal-acts .btn,
.modal-head > .modal-acts > .tabs,
.modal-head > .modal-acts .tabs {
    margin-left: 0;
    margin-right: 0;
}

/* 关闭按钮：第 3 列、贴右。`.modal-x` 自带 margin-left:auto，网格里不需要它。 */
.modal-head > .modal-x {
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    margin-left: 0;
}

@media (max-width: 860px) {
    /* 窄屏退让：变两列 —— 标题(1) + ×(2) 在第一行，
       操作按钮/标签组整块换到**第二行、横跨两列**。
       （这就是 v2.49「标签挤不下就整块换行」的 grid 等价写法：
         v2.49 用的 `order: 9; flex: 1 1 100%` 是 flex 属性，
         grid 容器里必须换成 grid-column / grid-row。）

       ⚠️ v2.55 把断点从 760px 放宽到 **860px** —— 原来的 760 太窄，漏掉了
          800~1170px 这一大段桌面宽度：那段里 .modal-lg（max-width 780px）
          还保持在「三列一行」，而赛季头部（长标题 + 5 个榜标签）**加起来放不下**，
          于是标题横向溢出、压在那排标签上（用户就是这么看到的）。
          为什么是 860：一行放得下需要
            780(内容宽) ≥ 标题128 + 5个标签452 + ×32 + 2*12(列间距)
          左侧一列可用宽度 = (内容宽 - 标签宽 - ×宽 - 2*间距)/2，
          标题要放得下就得 ≥ 128 → 内容宽 ≥ 725 → 弹窗宽 ≥ 765 → 视口 ≥ 805。
          取 860 留 55px 余量（中文字体 / emoji 实际宽度会有出入）。
          代价：760~860px 这一段（小平板 / 分屏）会提前换成两行 ——
          **宁可早一点换行，也不要标题压住按钮**。
       ⚠️ v2.66：第 2 列也给定 44px 下限（× 的热区），语义和第 3 列一样。 */
    .modal-head { grid-template-columns: minmax(0, 1fr) minmax(44px, auto); }
    .modal-head > h3 {
        grid-column: 1;
        grid-row: 1;
    }
    .modal-head > .modal-acts {
        grid-column: 1 / -1;
        grid-row: 2;
        /* 第二行整行都是它的，居中排更整齐 */
        justify-content: center;
    }
    .modal-head > .modal-x {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
    }
    /* 手机上标签可以小一点，但别挤成一坨 */
    .modal-head > .modal-acts .btn,
    .modal-head > .modal-acts .tab {
        padding: 5px 9px;
        font-size: 12.5px;
    }
}

/* ================================================================
 * v2.66 弹窗头部：关闭按钮的热区 + 标签行横向滚动（修「赛季排行榜被 × 遮挡」）
 *
 * 三件事：
 *   ① × 的**命中区**撑到 44×44（视觉还是那个 32px 的小圆）——
 *      用 ::after 往四周各撑 6px，所以上面把 .modal-head > .modal-x 的
 *      `overflow: hidden` 改成了 visible（不然会被裁掉，等于没加）。
 *   ② 头部里的标签行改成**横向滚动**：原来 `flex-wrap: wrap` + 父级 `overflow: hidden`，
 *      放不下时要么把头部撑成两行、要么从中间把标签裁掉（用户看到的就是"最后一个被压住"）。
 *      标签页横滚是标准做法 —— 跟「动作按钮不准横滚」不冲突，那是两个场景。
 *      桌面宽屏放得下时**完全看不出区别**：没有滚动条、也不换行（auto 列 = max-content）。
 *   ③ 边缘渐隐：只有「左边/右边确实还有内容」时才画（JS 切 has-left / has-right）。
 *      用 mask-image 而不是伪元素 —— 伪元素在滚动容器里会跟着内容一起滚，
 *      而 mask 是画在**元素自己的盒子**上的，不随内容滚动 ✓。
 * ============================================================= */
.modal-head > .modal-x { position: relative; }
.modal-head > .modal-x::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.modal-head > .modal-acts > .tabs {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    overflow-y: hidden;
    max-width: 100%;
    padding-bottom: 2px;              /* 给细滚动条留一点位置，别压住标签的圆角 */
    scrollbar-width: thin;            /* 滚动条别太难看 */
    scroll-padding-inline: 12px;
    overscroll-behavior-x: contain;   /* 滑到头别把弹窗/页面带着一起滚 */
    -webkit-overflow-scrolling: touch;
}
.modal-head > .modal-acts > .tabs .tab { touch-action: manipulation; }

/* 还有内容可以滑 -> 那一侧淡出。三种状态各一条，互不干扰 */
.modal-head > .modal-acts > .tabs.has-right {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
}
.modal-head > .modal-acts > .tabs.has-left {
    -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to left, #000 calc(100% - 22px), transparent);
}
.modal-head > .modal-acts > .tabs.has-left.has-right {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 22px, #000 calc(100% - 22px), transparent);
    mask-image: linear-gradient(to right, transparent, #000 22px, #000 calc(100% - 22px), transparent);
}

/* =============================================================
 * v2.67 第二批前端线新增
 * ============================================================= */

/* ① 后台关掉的一键功能：按钮**留着但置灰不可点** ✗（不再隐藏 ✗）+ 一行说明
 *    ⚠️ 已开通该特权的用户不会被置灰（判定在 JS 的 oneClickDisabled() 一个点上 ✓）*/
.btn.is-off,
.btn[disabled].is-off,
.fa-btn.is-off {
    cursor: not-allowed;
    opacity: .5;
    filter: grayscale(1);
    pointer-events: none;   /* disabled 本来就不触发 click，这条只是保证子元素也不吃点击 */
}
.fa-note {
    flex: 1 1 100%;         /* 单独占一行（一键区是 flex 容器）*/
    font-size: 12px;
    line-height: 1.5;
    opacity: .75;
    padding: 2px 2px 0;
}

/* ② 一键区不再有分组标题（收获/照顾/种植 三个灰底标签已删 ✗）→ 平铺一层 .fa-btns 自己换行。
 *    ⚠️ 必须自带换行：老规则是 `.fa-group .fa-btns` 那种后代写法，顶层的 .fa-btns 吃不到 ✗
 *
 * ★ v2.73 修的是一个**权重事故** ✗（用户看到的是「2+2+1，一键播种孤零零一行」）：
 *   这条 `.farm-actions .fa-btns` 的权重是 **(0,2,0)**，而下面两个手机断点里的
 *   `.fa-btns{gap:7px}` / `.fa-btns{gap:6px}` 只有 **(0,1,0)** ——
 *   断点里的 gap **永远被这一条压掉** ✗，所以任何屏宽实际用的都是这里的 8px。
 *   而按钮宽是 `flex: 0 1 calc(33.333% - 5px)`（在 820px 断点里 ✓），
 *   3 个放一行需要 `2*gap <= 15px`，也就是 **gap ≤ 7.5px** ✗ →
 *   gap=8 时**恒差 1px**，于是永远只能 2 个一行（5 个动作 = 2+2+1 ✗）。
 *   修法（**副作用最小**）：这道高权重规则**不再声明 gap** ✗ ——
 *   它只负责 `display/flex-wrap/align-items`（这才是它当初存在的理由：顶层 `.fa-btns`
 *   吃不到 `.fa-group .fa-btns` 那条后代写法 ✗），间距交回给基础规则 + 断点 ✓
 *     · 桌面（>820px）依旧 8px（基础规则 `.fa-btns{gap:8px}` ✓ 一个字没变）
 *     · ≤820px 拿到 7px ✓ · ≤480px 拿到 6px ✓  ← 断点**终于生效** ✓
 *   ⚠️ 别把 gap 加回来 ✗（加了就又压掉断点，回到 2+2+1） */
.farm-actions { flex-wrap: wrap; }
.farm-actions .fa-btns {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
}

/* ⑤ 回收：按钮 / 提示文案 / 回收记录列表 */
.btn-recycle { border-color: #2f9e6f; color: #166534; }
.btn-recycle:hover { background: rgba(47, 158, 111, .1); }
.box-recycle-hint { color: #2f7a58; }
#recycleLogPanel { margin: 0 0 10px; }
#recycleLogPanel .rec-head {
    display: flex; align-items: center; gap: 8px;
    font-size: 13px; font-weight: 600; margin-bottom: 4px;
}
#recycleLogPanel .rec-head .btn { margin-left: auto; }
.rec-row {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 2px; font-size: 13px;
    border-bottom: 1px dashed rgba(0, 0, 0, .08);
}
.rec-row .rec-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rec-row .rec-coins { color: #8a5a00; font-weight: 600; }
.rec-row .rec-at { opacity: .6; font-size: 12px; }
/* ★ v2.73「记录太多不好查看」：汇总一行 + 分页条（都沿用现有 .rec-* 那套朴素长相 ✗
   没有渐变 / 发光 / 大圆角 ✓；数字不折行 ✓） */
.rec-sum {
    padding: 4px 2px 6px;
    font-size: 12.5px;
    color: var(--ink-soft);
    border-bottom: 1px solid rgba(0, 0, 0, .06);
}
.rec-sum b { color: var(--ink); white-space: nowrap; }
.rec-row .rec-fresh {
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    background: rgba(47, 158, 111, .14);
    color: #1f7a52;
    font-size: 11px;
    font-weight: 700;
    white-space: nowrap;
}
.rec-row .rec-qty, .rec-row .rec-coins { white-space: nowrap; }
.rec-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 8px 2px 2px;
}
.rec-pager .rec-page { font-size: 12.5px; color: var(--ink-soft); white-space: nowrap; }
/* 手机上把时间列收掉，别把名字挤成一片 */
@media (max-width: 560px) { .rec-row .rec-at { display: none; } }
@media (max-width: 560px) {
    /* 手机上汇总那行和分页按钮也留够热区（≥44px ✓ 和本文件其它 ≥44px 的规则同一口径） */
    .rec-pager .btn { min-height: 44px; }
}

/* ================================================================
 * ★ v2.89.2「📈 今日行情」（用户点名要的入口 · 放在仓库弹窗里 ✓ 没塞进商店 ✗）
 *
 * 规矩（今天的老规矩，一条不少 ✓）：
 *   · 手机 393 / 402 优先：**零横向溢出**、**零截断**、热区 ≥44px ✓
 *   · 对比度 ≥4.5:1 —— 涨跌两色都是深色（#0f5132 / #b91c1c），
 *     底色是浅色半透明（白 .55 / 绿红 .10），实测按渲染像素量过 ✓
 *   · 每个价格数字 `white-space:nowrap` ✗ 不折行（折了就像"少了一位数"✗）
 *   · ⚠️ 列表 `overflow-x:hidden` + `overflow-y:auto` + `overscroll-behavior:contain`：
 *     这是本项目踩过的坑（横向滚一条会让整页跟着左右晃 ✗）
 *   ⚠️ 这一段的 `font-size` 一律只写在行容器上：**别写 `.mk-row small` 之类更深的选择器** ✗
 *      （v2.73 那条「五处字号同值」的规则在文件更靠后的位置，权重相同、后写的赢 ——
 *        这里再给子元素写字号就会和它抢，最后谁赢取决于书写顺序 ✗。行容器不冲突 ✓）
 * ================================================================ */
.box-market-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 10px; }
.box-market-bar[hidden] { display: none; }
/* ⚠️ 热区 ≥44px 在**所有**宽度都要成立（不是只在手机上）—— 真浏览器 1280 实测
   只给到 31px ✗，所以这条**写在基础规则里**（不放媒体查询）✓ */
.box-market-bar .btn { flex: none; touch-action: manipulation; min-height: 44px; }
.box-market-bar .hint { font-size: 12.5px; color: var(--ink-soft); }

#marketPanel { margin: 0 0 10px; }
#marketPanel[hidden] { display: none; }
#marketPanel .rec-head {
    display: flex; align-items: center; gap: 8px;
    font-size: 13px; font-weight: 600; margin-bottom: 4px;
}
#marketPanel .rec-head .btn { margin-left: auto; min-height: 44px; }

.mk-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 2px 2px 8px; }
.mk-date { font-size: 13px; font-weight: 700; color: var(--ink); white-space: nowrap; }
.mk-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.mk-chip {
    font-size: 12.5px; padding: 2px 8px; border-radius: 999px;
    background: rgba(255, 255, 255, .55); color: var(--ink-soft); white-space: nowrap;
}
.mk-chip b { color: var(--ink); }
.mk-chip-up b { color: #0f5132; }
.mk-chip-down b { color: #b91c1c; }
.mk-weather {
    flex: 1 1 100%; font-size: 12.5px; color: var(--ink-soft);
    background: rgba(165, 228, 255, .28); border-radius: var(--radius-sm); padding: 4px 8px;
}

.mk-list {
    display: grid; grid-template-columns: 1fr; gap: 6px;
    max-height: 46vh; overflow-y: auto; overflow-x: hidden;
    overscroll-behavior: contain;
    padding: 2px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .38);
}
.mk-row {
    display: flex; align-items: center; gap: 8px;
    font-size: 13px;
    padding: 8px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .72);
    border: 1px solid rgba(22, 49, 31, .08);
    min-height: 44px;      /* 手机热区：整行都能看清、能点 ✓ */
}
.mk-row.is-up { border-color: rgba(15, 81, 50, .30); background: rgba(34, 197, 94, .10); }
.mk-row.is-down { border-color: rgba(185, 28, 28, .30); background: rgba(239, 68, 68, .10); }
.mk-row .mk-icon { flex: none; width: 22px; text-align: center; }
.mk-row .mk-info { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.mk-row .mk-name {
    font-weight: 600; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mk-row .mk-tag {
    margin-left: 5px; font-size: 11px; font-weight: 600; color: var(--ink-soft);
    background: rgba(0, 0, 0, .07); border-radius: 999px; padding: 1px 6px;
}
/* ⚠️ 这一行**允许换行**（不写 nowrap / 不写 ellipsis）✗：
   真浏览器 1280 实测——写 nowrap+ellipsis 时名字/基准价那行的文字真右边界
   越过了容器 12~84px（被省略号剪掉 ✗）。用户的老规矩是「零截断」，
   所以宁可让它折成两行（信息一个不丢 ✓），也不剪成一个「…」✗ */
.mk-row .mk-sub { color: var(--ink-soft); overflow-wrap: anywhere; }
.mk-coeff { flex: none; font-weight: 800; white-space: nowrap; font-variant-numeric: tabular-nums; }
.mk-coeff.mk-up { color: #0f5132; }
.mk-coeff.mk-down { color: #b91c1c; }
.mk-coeff.mk-flat { color: #4b6355; font-weight: 600; }
.mk-price { flex: none; font-weight: 800; color: #7a5a00; white-space: nowrap; font-variant-numeric: tabular-nums; }
.mk-price.mk-price-none { color: var(--ink-soft); font-weight: 600; }
.mk-off { padding: 6px 2px 2px; }
.mk-off-t { font-size: 13px; font-weight: 700; color: var(--ink); }
.mk-off-s { font-size: 12.5px; color: var(--ink-soft); line-height: 1.6; }
.mk-foot { font-size: 12px; color: var(--ink-soft); padding: 6px 2px 0; line-height: 1.6; }

/* 手机上：只留「图标 / 名字 / 涨跌 / 价格」，把"基准回收价"那行收掉 ——
   否则 393 下一行挤四个元素会折行甚至溢出 ✗（信息在桌面 / 平板上照旧全在 ✓） */
@media (max-width: 560px) {
    #marketPanel .rec-head .btn { min-height: 44px; }   /* 收起按钮热区 ≥44px ✓ */
    .box-market-bar .btn { min-height: 44px; }          /* 行情入口热区 ≥44px ✓ */
    .mk-row .mk-sub { display: none; }
    .mk-list { max-height: 52vh; grid-template-columns: 1fr; }
}
/* ⚠️ 两列只在**够宽**时开，而且要防 grid 被内容撑宽 ✗：
   minmax(0,1fr) + 行内每个 flex 子项都能被压缩（.mk-info 已有 min-width:0 ✓）
   —— 实测：写成 `1fr 1fr` 时列表自己横滚了 160px ✗（内容的最小宽度把列顶开了） */
@media (min-width: 720px) {
    .mk-list { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: 40vh; }
}

/* ================================================================
 * ★ v2.89.3 🏪 市场（玩家寄售市场 C2C）—— `#modal-market` 专用
 * ----------------------------------------------------------------
 * 规矩（今天的老规矩，一条不少 ✓）：
 *   · 手机 393 / 402 优先：**零横向溢出** · **零截断** · 热区 ≥44px ✓
 *   · 对比度 ≥4.5:1：贵 / 便宜两色沿用行情那套深色（#0f5132 绿 · #b91c1c 红 ✓），
 *     说明文字用 --ink-soft（和 .mk-sub 同一个色 ✓ 那一套已经量过 ✓）
 *   · 🔴 **零视觉发明** ✗：卡片 / 按钮 / 页签 / 输入框全部沿用既有类
 *     （.btn / .btn-gold / .btn-ghost / .tab / .hint-text ✓），
 *     这里只加市场**自己的排布**（行怎么放、算式怎么折 ✓）
 *   · 每个价格数字 `white-space:nowrap` ✗ 不折行（折了像"少一位数"✗）
 *   · ⚠️ 列表 `overflow-x:hidden` + `overflow-y:auto` + `overscroll-behavior:contain`：
 *     横向滚一条会让整页跟着左右晃（行情列表踩过这个坑 ✓）
 *   · ⚠️ 字号一律只写在**行容器**上：别写 `.ms-row small` 之类更深的选择器 ✗
 *     （v2.73 那条"五处字号同值"的规则在文件更靠后位置，权重相同、后写的赢 ——
 *       这里再给子元素写字号就会和它抢，最后谁赢取决于书写顺序 ✗）
 * ================================================================ */
.ms-block {
    padding: 8px 10px; margin: 0 0 10px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .55); border: 1px solid rgba(22, 49, 31, .10);
}
.ms-block[hidden] { display: none; }
.ms-off-t { font-size: 13.5px; font-weight: 700; color: var(--ink); }
.ms-off-s { font-size: 12.5px; color: var(--ink-soft); line-height: 1.6; margin-top: 2px; }

.ms-pane[hidden] { display: none; }

/* 顶部筛选条：窄屏会**换行**（不横向滚 ✓）——每个控件都能被压缩 ✗ 不许把抽屉撑宽 */
.ms-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 8px; }
.ms-bar .btn { flex: none; touch-action: manipulation; min-height: 44px; }
.ms-input {
    font: inherit; font-size: 13px; color: var(--ink);
    background: rgba(255, 255, 255, .78);
    border: 1px solid rgba(22, 49, 31, .18); border-radius: var(--radius-sm);
    padding: 0 8px; min-height: 44px; min-width: 0; max-width: 100%;
}
.ms-search { flex: 1 1 140px; }
.ms-bar select.ms-input { flex: 0 1 auto; }

/* 列表：★ v2.89.5 改成**自适应网格**（repeat(auto-fit, minmax(240px, 1fr)) ✓）
   —— 用户看了「在卖」的截图只说两个字：「难看」，量出来的数是：
      · 原来写死 `@media (min-width:720px){ 两列 }`，而**一张卡只占一列** ⇒
        1280 下卡片右缘到列表右缘空 **370px = 可用宽的 50.41%** ✗（"右边一大片空白"就这个）
      · 卡片里卖家/数量/参考价/剩余时间**全挤在一行**、名字和价格同字号 13px（零层次 ✗）
      · 价格块里 `.ms-price` 只有 `white-space:nowrap` 一项，两个绝对定位的 `<small>`
        叠在一起（实测间距 **-34px**，相交 2 处 ✗）
   240px 这个数是**量出来的**：393 可用宽 ~343px 放得下"参考价 🪙20（10~40）· 还有 10 小时"
   那一行不折行（折了就变 2 行 ✗），而 768 能出 2 列、1280 也能出 2 列（卡片 ~357px，
   正好是"名字 + 卖家/数量/参考价/剩余时间 + 单价/整单 + 按钮"读起来最舒服的宽度 ✓）。
   ⚠️ 用 **auto-fit**（不是 auto-fill）✗：auto-fill 会**保留空轨道**，
      于是"只有一张卡"时它只占第一列、右边空掉一整列（1280 实测 372px ✗，和改前一个病 ✗）；
      auto-fit 把空轨道收掉 ⇒ 一张卡**铺满整行** ✓，多张卡时列数照旧按可用宽算 ✓。
   ⚠️ **不加列数媒体查询** ✗：列数由可用宽算出来（"宽屏多列 / 窄屏一列"是它的必然结果 ✓），
      也就不会重现"列数够但只有一张卡 ⇒ 右边空一半" ✗。 */
.ms-list {
    display: grid; gap: 10px;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    max-height: 52vh; overflow-y: auto; overflow-x: hidden;
    overscroll-behavior: contain;
    padding: 2px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .38);
}
/* 一张卡 = **上下两行**：上面「图标 + 主列」，下面「动作按钮（贴右）」。
   ⚠️ 为什么必须是两行（v2.89.5 实测踩过 ✗）：原来写成**一行 flex + 按钮 align-self:flex-end**，
      以为按钮会落到右下角 —— 实际 flex 行里 `align-self: flex-end` 只管**交叉轴（竖直居底）**，
      按钮和主列仍然**并排**：主列高 106px、按钮 44px ⇒ 两者上缘对齐，
      按钮在 y=797 就压进了内容区（价格行下缘 841），间距算出来 **-44px** ✗。
      改成"卡片自己是竖向 flex、第一行再横向 flex"之后：按钮必然在内容**下面**，
      间距由 `gap:10px` + `min-height:44px` 保证（实测 10px ✓），右缘靠 `align-self:flex-end` 贴卡片内缘 ✓ */
.ms-row {
    display: flex; flex-direction: column; gap: 10px;
    padding: 10px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .72);
    border: 1px solid rgba(22, 49, 31, .08);
    min-width: 0; min-height: 44px;      /* 手机热区：整行都能看清、能点 ✓ */
}
.ms-line { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }
/* ⚠️ 图标的**行盒高度必须和物品名那一行一样高**（19px = 名字 14px 的 line-height）✗：
   原来写 `line-height: 1.25`（18px × 1.25 = 22.5px）⇒ 图标盒子比名字高 3.5px，
   于是"图标下缘 ~ 下一行上缘"只有 **6.5px** ✗（真浏览器量出来的）——
   用户这轮的硬要求是「卡内最小间距 ≥ 8px」，所以把图标压到和名字同一行高 ✓
   （emoji 字形 18px ≤ 19px 行盒，不会被裁 ✓） */
.ms-row .ms-ico {
    flex: none; width: 26px; text-align: center;
    font-size: 18px; line-height: 19px;
}
.ms-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
/* ⚠️ 这里原来是 `gap: 6px`（v2.89.5 的量具只量了**水平**间距，没量到它 ✗）——
   v2.89.6 把探针改成连**竖直**间距一起量，才发现卡内最小间距其实是 6px ✗。
   用户这轮的硬要求是「卡内最小间距 ≥ 8px」⇒ 统一成 **10px**：
   于是卡内（.ms-main 的 gap / .ms-line 的 gap / .ms-row 的 gap / .ms-row 的 padding）
   **全是 10px 这一个值** ✓（"各自一个值、贴数字"那条也一起满足了 ✓） */

/* ---- 第 1 层：物品名（14px / 800）+ 徽章**同一行右侧** ---- */
.ms-hd { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ms-name {
    flex: 0 1 auto; min-width: 0;
    font-size: 14px; font-weight: 800; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ms-tag-mine {
    flex: none; margin-left: auto;   /* 名字多短都贴这一行最右 ✓ */
    font-size: 11.5px; font-weight: 700; color: var(--ink-soft);
    background: rgba(0, 0, 0, .07); border-radius: 999px; padding: 2px 9px;
}
/* ---- 第 2 层：卖家 / 数量 / 参考价 / 剩余时间（12.5px · **允许换行、不许截断** ✓） ---- */
.ms-sub { font-size: 12.5px; font-weight: 400; color: var(--ink-soft); overflow-wrap: anywhere; }
/* ---- 第 3 层：★ v2.89.6 **商店价**（标签与"参考价"分清 ✗）+ 两个对比同一行 ----
   ⚠️ gap 一律 **10px**（不是 2px/4px ✗）：探针改成量竖直间距之后，
      行内若换行、行距就是那个 row-gap —— 2px / 4px 都过不了「卡内最小间距 ≥ 8px」✗。
      现在卡片里**所有**间距（.ms-row 的 padding/gap · .ms-line · .ms-main ·
      .ms-hd · .ms-shop-row · .ms-dev-row · .ms-price-row）**全是 10px 这一个值** ✓ */
.ms-shop-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; min-width: 0; }
.ms-shop { font-size: 12.5px; font-weight: 400; color: var(--ink-soft); white-space: nowrap; }
.ms-shop b { font-weight: 800; color: #7a5a00; font-variant-numeric: tabular-nums; }
.ms-shop-none { color: var(--ink-soft); }
/* 两个对比（比参考价 / 比商店）走同一行：窄屏放不下就**换行**，绝不截断 ✓（行距也是 10px ✓） */
.ms-dev-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; min-width: 0; }
.ms-dev { font-size: 12.5px; font-weight: 400; color: var(--ink-soft); }
.ms-dev-up { color: #b91c1c; font-weight: 600; }
.ms-dev-down { color: #0f5132; font-weight: 600; }
.ms-dev-flat { color: var(--ink-soft); }
/* ---- 第 4 层：单价（15.5px / 800，**最大最重**）+ 单位 +「整单 🪙X」同一行、右缘成列 ✓ ---- */
.ms-price-row {
    display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px;
    min-width: 0;
}
.ms-price {
    flex: none; font-size: 15.5px; font-weight: 800; color: #7a5a00;
    font-variant-numeric: tabular-nums;
}
.ms-price small { font-size: 11.5px; font-weight: 700; }
.ms-total {
    margin-left: auto;   /* 贴这一行最右 ⇒ 同一列里每张卡的「整单」右缘对齐 ✓ */
    font-size: 12px; font-weight: 600; color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}
/* ---- 第 5 层：按钮（在自己那一行里贴右 ✓ 与内容之间由卡片的 gap:10px 拉开） ---- */
.ms-row > .btn {
    flex: none; align-self: flex-end;
    margin-top: 12px;      /* 12px 是量的：10px(gap) 之上再留一点，价格行下缘~按钮上缘 = 10px ≥8 ✓ */
    min-height: 44px;
}
.ms-empty {
    font-size: 12.5px; color: var(--ink-soft); line-height: 1.7;
    padding: 10px; text-align: center;
}
.ms-foot { font-size: 12px; color: var(--ink-soft); padding: 6px 2px 0; line-height: 1.6; }

/* 上架表单：手机一列、桌面两列（auto-fit，窄屏自动退回一列 ✓） */
.ms-form {
    display: grid; gap: 8px; margin: 8px 0 0;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.ms-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.ms-label { font-size: 12.5px; font-weight: 600; color: var(--ink-soft); }
.ms-field .ms-input { width: 100%; }
.ms-calc {
    margin: 10px 0 0; padding: 8px 10px; border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, .55); border: 1px solid rgba(22, 49, 31, .10);
    font-size: 12.5px; color: var(--ink-soft); line-height: 1.7;
}
.ms-calc-line b { color: var(--ink); }
.ms-calc-line.is-bad { color: #b91c1c; }
.ms-calc .ms-net { color: #0f5132; }
.ms-form-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 10px 0 0; }
.ms-form-acts .btn { min-height: 44px; }
.ms-form-acts .btn[disabled] { opacity: .55; cursor: not-allowed; }
.ms-form-acts .hint { font-size: 12.5px; color: var(--ink-soft); flex: 1 1 200px; line-height: 1.6; }

/* ---- 顶部标题 / 副标题：★ v2.89.5 **不许截断**（用户截图里那句「手续费 5%（卖家承担 · 系统…」✗）
   量出来的数（真浏览器）：#msHeadSub 的真实文本框**渗出 h3 那一格 67.8px（@393）/ 101.3px（@1280）**，
   `scrollWidth 277 > clientWidth 209`（@393）/ `176`（@1280）—— 就是"被剪成一个…" ✗。
   两条修法（都不改文案 / 不删别的弹窗的规则 ✓）：
     ① 这条**只给市场头部**开"允许换行"（`.modal-head > h3 small` 是全站共用类 ✗，
        直接改它会让别的弹窗的说明也折行 ⇒ 所以用 `#modal-market` 限住 ✓）；
     ② ≤860px（本来就是"标题 + × 一行、页签第二行"那套）：让副标题**单独占一行**，
        于是标题只需要放得下"🏪 市场"4 个字 —— 窄屏也一个字不丢 ✓ */
#modal-market .modal-head > h3 small {
    flex: 0 1 auto;
    overflow: visible;
    white-space: normal;
    line-height: 1.45;
}
#modal-market .modal-head > h3 { flex-wrap: wrap; row-gap: 2px; }
@media (max-width: 860px) {
    #modal-market .modal-head > h3 small {
        flex: 1 0 100%;   /* 独占一行：标题一行 + 副标题一行（都不截断 ✓） */
        margin-left: 0;
    }
}

/* 窄屏：列表高一点（一列本来就长 ✓）；卡片不变形 ✗ —— 列数交给 auto-fill 自己算 */
@media (max-width: 560px) {
    .ms-list { max-height: 56vh; }
}
/* ⚠️ v2.89.5 把原来这里那条 `@media (min-width:720px){ 两列 }` **删掉了** ✗：
   它写死两列，而**一张卡只占一列** ⇒ 1280 下右边空 370px（可用宽的 50.41% ✗）。
   列数现在由上面 `repeat(auto-fill, minmax(240px, 1fr))` 按**可用宽**自己算 ✓ ——
   768 出 2 列、1280 出 2 列、393/402 出 1 列，且**一张卡也能铺满**（没有空列 ✗） */
@media (min-width: 720px) {
    .ms-list { max-height: 44vh; }
}

/* ---------------------------------------------------------------
 * 关于「手机上输入框字号 ≥16px 防 iOS 自动放大」—— 故意**没有**这条规则
 *
 * iOS Safari 确实会在输入框 font-size < 16px 时把整页放大，而本站的 viewport
 * 是 `width=device-width, initial-scale=1`（没有 maximum-scale=1），所以它会发生。
 *
 * 但我 v2.51 试过把手机上的输入框统一抬到 16px，**用户反馈"字太大、要小一点"** ——
 * 那个"字大"的主因其实是 placeholder 太长折成了 4 行（已靠缩短文案修掉），
 * 不是字号。
 *
 * 真要治 iOS 放大，正路是给 viewport 加 `maximum-scale=1`：
 *   · 现代 iOS 上它**只挡聚焦放大**，不影响用户双指缩放
 *   · 但老浏览器上会禁掉双指缩放，属于无障碍妥协
 * 这要用户拍板，不该我擅自加。**要加的话是改四个页面的 <meta viewport>。**
 * ------------------------------------------------------------- */

/* ================================================================
 * ★ v2.73 数量框字号「五处同一个值」+ 窄屏等高（**必须留在文件最后** ✓）
 *   用户原话：「前面的数量1调整和按钮字体一样大小」（仓库种子页 / 作物页两张手机截图 ✗）
 *   五处（全取 --vf-qty-fs = 13px = .btn-sm 的字号 ✓）：
 *     ① 仓库「加入清单」框 [data-scqtyadd]   ② 仓库「回收数量」框 [data-rqtyadd]
 *     ③ 卖出清单行 [data-scqty]             ④ 购物车行 [data-cartqty]
 *     ⑤ 商店卡片购买框 [data-qty]
 *
 *   ⚠️⚠️ 为什么非要在**文件最后**：`.box-actions .box-qty` 的**基础规则在后面那一段**
 *       （width:58px / height:30px），同优先级下**后写的赢** ——
 *       这一段要是写在基础规则**前面**，height:44px 会被 30px 盖掉 ✗
 *       （实测踩过：真 headless Chrome 里 getComputedStyle().height 仍是 30px ✗）
 *
 *   ⚠️ 只统一字号 + 窄屏把高度补到和按钮一样（触控 ≥44px ✓）；
 *      宽度**不放大**（仍是 60px / 52px ✓，上一轮刚收窄过 ✗）
 * ================================================================ */
@media (max-width: 720px) {
    .box-actions .box-qty,
    .sc-qty-input,
    .qty-input {
        font-size: var(--vf-qty-fs);
        height: 44px;                          /* 和同排按钮等高（触控区不变小 ✓） */
    }
    .qty-input { padding: 0 6px; }             /* 只收内边距，宽度仍是 52px ✓ */
    /* 商店**卡片**上那个购买按钮也要 ≥44px：不然同一个 .seed-buy 行里
       输入框 44px / 按钮 31px，看着还是"不齐" ✗（和仓库那一行同一口径 ✓） */
    .seed-buy .btn { min-height: 44px; }
}

/* ================================================================
 * ★ v2.74 仓库弹窗 · **三页同一套视觉语言**（用户：「卖作物的界面改成和卖其他的 UI 一样」✓）
 *
 *   ⚠️ 这一条**不在**上面的手机媒体查询里 ✗ —— 用户明确要求**桌面端也要统一** ✓
 *      （「电脑端保持原来的」那句是针对"手机换图 1"那件事；本轮的"三页统一"他点名两端都要 ✓）
 *      所以这条是**唯一**会在 ≥821px 命中的新规则，且**只动仓库这一行的"批量动作"按钮** ✗：
 *
 *   ① 主次配色同一套：三页的**批量动作**按钮（作物的「全卖」/ 种子·道具的「全回收」）
 *      统一成**朴素描边**（= 种子页那张参考图的观感 ✓）——
 *      原来作物页的「全卖」带着 `btn-gold`（金色渐变）✗，种子的「全回收」是朴素描边，
 *      一页一个样 ✗。这里把金色**覆盖成描边**（`background-image: none` 必须显式写 ✗：
 *      `.btn-gold` 是 `background: linear-gradient(...)`，只改 background-color 盖不住渐变 ✗）
 *   ② 单件动作（卖 1 / 回收）本来就是朴素描边 ✓ 一个字没动 ✓
 *   ③ 文案前缀：`.btn > .no-emoji` 在窄屏本来就隐藏 ✓；桌面端把「加入清单」那个 🛒/♻️
 *      前缀也统一去掉（见 game.js —— 只改了那一个 emoji 字面 ✗）
 *
 *   ⚠️ 三个页签的**骨架、图标尺寸、名字字号、行内间距、数量写法（个/份 按类别）**
 *      本来就同一套（都出自同一个 boxRowHtml ✓）—— 这一条只是把"配色"补齐 ✓
 * ================================================================ */
.box-row .box-actions [data-sell][data-qty]:not([data-qty="1"]),
.box-row .box-actions [data-sellitem][data-qty]:not([data-qty="1"]),
.box-row .box-actions [data-recycle][data-rqty]:not([data-rqty="1"]) {
    background: rgba(255, 255, 255, .8);
    background-image: none;
    color: var(--ink);
    border-color: rgba(255, 255, 255, .75);
}
/* ② 单件动作（卖 1 / 回收）也统一成**同一个朴素按钮**：
      原来「卖 1」是 .btn 的底色（中性墨色字）✓、「♻️ 回收」带一层绿描边（.btn-recycle）——
      同一个"点一下做一件事"的次要动作，两页却是两种描边色 ✗。
      这里把两者都定成和「卖 1」一模一样的中性描边 ⇒ 三页的**三个按钮**在两端点都是同一套：
      白底 + 细描边 + 墨色字，只有文案不同 ✓
      ⚠️ 窄屏（≤820px）里动作按钮的字号是 11px（见上面手机媒体查询），
         和**数量框**（`--vf-qty-fs` = 13px）不同号 —— 这是"两行紧凑版式"的有意取舍：
         13px 的按钮会超出那一行的可用宽度（实测 393px）✗。数量框保持 13px 不缩 ✓ */
.box-row .box-actions [data-sell],
.box-row .box-actions [data-sellitem],
.box-row .box-actions [data-recycle] {
    background: rgba(255, 255, 255, .8);
    background-image: none;
    /* ⚠️ 字号/高度**不在这里动** ✗ —— 桌面端仍是产品的 .btn-sm（13px / 30px），
       窄屏那套由上面的 @media (max-width:820px) 负责 ✓（桌面端因此与改前逐项相等 ✓） */
    color: var(--ink);
    border-color: rgba(255, 255, 255, .75);
}
/* ③ ★★ 「加入清单」在**同一端内三页是同一个按钮**（用户：
      「移动端和移动端、网页端和网页端的配色按钮该一样」✗）：
      原来卖出侧那个是 `.btn` 的中性描边、回收侧那个带 `.btn-recycle` 的**绿描边绿字** ——
      同一个按钮文本（都是「加入清单」）两页两种颜色 ✗。
      这里把三页的「加入清单」统一成**绿描边 + 绿字**（产品主动作色）：
        color = #1f7a52 = rgb(31, 122, 82) · border-color = #2f9e6f = rgb(47, 158, 111)
      与 `.btn-recycle` 同权（0,3,0），靠**写在后面**赢 ✓ ——
      上面那两条只把"动作按钮"（卖 1/全卖/回收/全回收）统一成中性描边，
      这一条只补「加入清单」✗（别把动作按钮又染回绿色 ✗） */
.box-row .box-actions [data-scadd],
.box-row .box-actions [data-rcadd] {
    color: #166534;
    border-color: #2f9e6f;
}
/* ④ ★★ 列表行（每件物资那张卡片）的**背景三页一致**（用户：「卖作物的某个作物列表和卖种子道具的
      列表背景要一样，保持三个界面列表背景一样」✗）
   ── 真因（真 Chrome 逐行实测，两端点一致）：
      · 作物 20 行 → `background-color: rgb(255, 255, 255)`（走基础 `.box-row` 那条 ✓）
      · 种子 14 行 / 道具 11 行 → `background-color: rgba(23, 51, 38, .035)`（淡灰）
      差异来自 **style.css 上文那条 `.box-row.is-nosell { background: rgba(23, 51, 38, .035) }`** ✗
      —— 它把"不能卖"的行压暗了一档（`is-nosell` 是 game.js 按 `can_sell` 打的 class ✓）。
      五件套里只有 **background-color** 不同；`background-image / border / border-radius / box-shadow`
      本来就逐字相等（实测都是 none / rgba(23,51,38,.08) 1px / 15px / rgba(23,51,38,.1) 0 4px 14px ✓）。
   ── 改法：**把行底色对齐成三页同一套**（沿用多数派那页的观感 = 淡灰 `rgba(23,51,38,.035)`，
      它就是上面那条规则的值 —— 只把它从"只有不可卖的行"扩成**所有行** ✗）。
      ⚠️ 五条属性**一条都没删** ✗（本来就只有颜色不一样 ✓）；
      ⚠️ "这一类不能卖"的信息**没丢** ✗ —— 它由页签下面那条 `.box-notice` 统一承载 ✓
         （`.box-row.is-nosell .box-name { color: var(--ink-soft) }` 一个字没动 ✓ 那是文字色，不是背景 ✓） */
.box-row.is-nosell { background: rgba(23, 51, 38, .035); }
.box-row { background: rgba(23, 51, 38, .035); }

/* ================================================================
 * ★ v2.74 仓库弹窗 · **手机端**紧凑两行版式（用户从 4 张候选里选的「候选 1」✓）
 *
 *   用户原话：「把 1 先做出来看看」+ 后补的一句关键约束 ✗：
 *             「**电脑端保持原来的**，也就是截图里的，**手机端**改成你的」
 *   ⇒ 所以这一整段**全部包在 `@media (max-width: 820px)` 里** ✗ ——
 *      ≥821px（含 1280px 桌面）**一条规则都不命中**，渲染结果与改前**逐项完全相等** ✓
 *      （改前/改后 1280px 的对照表见汇报；证据在 .runtime/cand1_work/ 的 before/after 两份量测 ✓）
 *
 *   ★ 边界为什么选 820 而不是新造一个数：
 *     · 产品**既有**的分界是 720px（`@media (max-width:720px)`：按钮 44px 高、数量框 60px 宽、
 *       `.box-row .box-count{min-width:56px}` 那一整套「手机分行」规则）；
 *     · 仓库弹窗是 `.modal-lg`（max-width 780px）→ 真正能看出「窄」的是**视口 820px 以下**：
 *       721~820px 这一段产品走的是**桌面那套规则**（按钮只有 30px 高、控件挤成两行、
 *       实测 820px 一件 66~77px 但单件控件被压成 35px 宽）—— 这一段也该吃手机版式 ✓；
 *     · 所以取「既有 720px 拼上一个整十档位」= **820px**，不新造 730/760 这种奇怪边界 ✓
 *     （720 那条媒体块一个字没动 ✓ 它仍然管着数量框 60px/44px 那些基础尺寸 ✓）
 *
 *   版式（**两行**，不改一个字节 DOM ✗ —— 全靠 display:contents + order + ::after 换行）：
 *     第 1 行 = 小图标 + 名字 + 数量框 + 加入清单 + 回收（或卖 1）+ 全回收（或全卖）
 *               —— 同一条 **44px** 的带子 ✓
 *     第 2 行 = 「N 份 · ♻️ 回收 🪙X/个 · 全回收 +🪙Y」那一句小字 ✓
 *   做法：`.box-info` / `.box-actions { display: contents }` 把这两个**盒子**去掉，
 *         于是名字/单价/数量框/按钮全都变成 `.box-row` 的直接 flex 项 ✓
 *         → 再用 `order` 排队 + `.box-row::after{flex:1 1 100%;height:0}` 强制换行 ✓
 *   版式出处与候选稿实测值：`.runtime/layout_candidates/README.md` + `_measure.json`
 *   （`cand1_compact_one_row.png`：76px/件、一屏 7 件 ✓）
 *
 *   ⚠️⚠️ 这一段**必须留在文件最后**（同优先级「后写的赢」，媒体查询不加优先级 ✗）：
 *      文件里前面有一堆 `.box-row .box-*` 的窄屏规则（720px 那两段）和
 *      `.box-actions .box-qty{width:58px;height:30px}` 的基础规则，全靠这里的
 *      「写在后面 + 选择器同权或更高」压住 —— 挪到前面就会被盖掉 ✗（实测踩过）。
 *
 *   ⚠️ 两个非踩不可的坑（候选稿 README 里记着的 ✓）：
 *     ① `.box-count` 必须写成 **`.box-row .box-count`（0,2,0）** ✗ 不能只写 `.box-count` ——
 *        产品里 `@media(max-width:720px){ .box-row .box-count{min-width:56px} }` 同优先级、
 *        （0,2,0）又写在本段前面，单类选择器（0,1,0）会被它直接盖掉 ✗（实测踩过）。
 *     ② 第 2 行那一句必须 `word-break: keep-all` ✗
 *        否则「全回收」会被从中间拆成「全回 / 收」（候选稿截图里真出现过 ✗）。
 *
 *   ★ 可读性下限（本段与候选稿的**有意差别**）：候选稿第 2 行是 **10px**，太小 ✗ → 这里 **11px**；
 *     代价见 `.box-sub` 上面那段注释（11px 比 10px 宽 ≈10%，最长的道具行会多占一行）。
 *
 *   ★ 只改 CSS、**一个字都没动** game.js / index.php：
 *     所有 data-* 属性名与值、事件绑定、提交参数、五个控件（数量框 / 加入清单 /
 *     单件回收 / 全回收 / 卖 1 / 全卖）**一个都没少** ✗（只是变小、换位置 ✓）
 * ================================================================ */
@media (max-width: 820px) {
    .modal { padding: 8px; }
    .modal-body { padding: 9px 9px 12px; }
    .box-grid { gap: 6px; }
    .box-row {
        flex-wrap: wrap;
        column-gap: 3px;
        row-gap: 2px;
        padding: 5px 6px;
        align-items: center;
    }
    /* 去掉两个中间盒子：名字/单价/数量框/按钮直接成为 .box-row 的 flex 项 */
    .box-row .box-info { display: contents; }
    .box-row .box-actions { display: contents; }
    /* ---- 第 1 行（order 1~6）---- */
    .box-row .box-icon { order: 1; width: 18px; font-size: 17px; }
    .box-row .box-name {
        order: 2;
        flex: 1 1 0;
        min-width: 0;
        /* ★ v2.89.8（批次 2 · H6）：13px -> **T3 14px**（§2.1 正文档；字重沿用上面的 600）。
           原来 13/800 vs 数量 12/800 ⇒ Δ=1px、Δ字重 0 ⇒ 手机上这条 R3 也红 ✗ */
        font-size: 14px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* 数量框（加入清单那个）：order 3；带上 .box-row 是为了压住上面窄屏那几条同权规则 */
    .box-row .box-actions .box-qty {
        order: 3;
        width: 44px;
        min-width: 44px;            /* ★ 热区：宽也兜到 44px（只有 44px 高的话点按目标太窄 ✗） */
        height: 44px;
        padding: 0 4px;
        font-size: var(--vf-qty-fs);
    }
    .box-row .box-actions [data-scadd],
    .box-row .box-actions [data-rcadd] { order: 4; }
    .box-row .box-actions [data-sell],
    .box-row .box-actions [data-recycle] { order: 5; }
    /* 强制换行：一个 100% 宽、0 高的 flex 项 —— 后面的东西必然掉到第 2 行 ✓ */
    .box-row::after { content: ""; order: 6; flex: 1 1 100%; height: 0; }
    /* ---- 第 2 行（order 7~8）：数量单位 + 单价整句 ---- */
    .box-row .box-count {
        order: 7;
        min-width: 30px;
        text-align: left;
        /* ★ v2.89.8（批次 2 · H6）：12px -> **T5 17px** —— 和桌面同一个档位，
           这样手机上 名称 14/600 vs 数量 17/800 ⇒ Δ字号 3px ✓（双档都过 §2.2 ✓） */
        font-size: 17px;
        white-space: nowrap;
    }
    .box-row .box-sub {
        order: 8;
        flex: 1 1 0;             /* 吃掉第 2 行剩下的全部宽度 -> 能塞下就是整句一行 ✓ */
        min-width: 0;
        margin: 0;
        font-size: 11px;         /* ★ 可读性下限：不小于 11px（候选稿是 10px ✗ 太小） */
        line-height: 1.35;
        word-break: keep-all;    /* ★ 坑②：「全回收」不许被拆成「全回 / 收」✗ */
    }
    /* 按钮同一条 44px 的带子（≥44px 热区 ✓）
       ★ `min-width: 44px`：「卖 1 / 全卖 / 回收 / 全回收」字少，按内容排只有 35~36px 宽，
         只有 44px **高** 的话点按目标还是太窄 ✗ ⇒ 宽度也兜到 44px ✓
         （393px 量过：名字 | 数量框 | 加入清单 | 卖 1 | 全卖 仍在一行 ✓）*/
    .box-row .box-actions .btn {
        min-height: 44px;
        min-width: 44px;
        padding: 3px 6px;
        font-size: 11px;
        border-radius: 9px;
    }
}

/* ================================================================
 * v2.84 手机端：下滑看地块时把顶部 HUD（绿色框）收起来，上滑立刻唤回
 *
 * 【要做的】用户手机截图里：
 *   绿色框 = 顶部那一整块功能宫格（每日/签到/图鉴/…/任务 + ⋯）
 *   红色框 = 下面的「我的农场」（天气 + 一键那排 + 地块）
 *   原话「红色框往下滑动，绿色框就收缩起来；红色框往上滑，绿色框就出现」
 *   定的方案 A：绿色框**整块滑出屏幕**，上滑**立刻滑回来**。
 *
 * 【改之前先查到的现状】
 *   绿色框就是 `<header class="hud">`（index.php:91），
 *   它**本来就是** `position: sticky; top: 0; z-index: 40`（style.css:195-198）——
 *   桌面和手机都已经是"吸顶"的。所以这里**不需要**再新建一套吸顶，
 *   只要在手机上再叠一个位移，把它从吸顶位置往上推出屏幕即可。
 *
 * 【为什么用 transform，而不是 height / margin / display】
 *   sticky 元素仍然占着文档流里原来那块地 ⇒ 收起/展开的一瞬间，
 *   下面「我的农场」的 getBoundingClientRect().top **一个像素都不动** ✓
 *   （改成 height:0 / display:none 的话，下面整块内容会突然上跳 ✗ 用户明确不要）
 *
 * 【为什么整条规则关在 820px 里】用户说「电脑端保持原来的」——
 *   JS 那边 matchMedia 不匹配时连监听都不做，这里再关一道：
 *   就算 JS 手滑加了类，桌面端也命不中这条规则 ✓（双保险）
 *
 * 【没加 will-change】.hud 上有 backdrop-filter 毛玻璃；
 *   把元素提前提成合成层虽然有性能好处，但 Chrome 上
 *   「backdrop-filter + will-change:transform」出过"背景不再模糊"的毛病 ✗
 *   —— 手机端毛玻璃观感坏了比掉一两帧严重，所以不冒这个险。
 *
 * ★★ v2.85 改口径（用户又补了一张 393 截图 + 一句话）★★
 *
 *   【用户原话】「我需要红色框回到位置了上面才回收」
 *   【截图里】绿框（header.hud：标题/头像/等级/金币 + 宫格）**还正常显示**，
 *     红框（<section class="panel farm-panel">「我的农场」整块）在他框住的位置。
 *   【意思】只有红框**滚上来、贴住绿框下沿**之后，绿框才允许收起 ——
 *     在那之前（页面刚往下滚一点、红框还离得远）绿框**必须一动不动**。
 *
 *   【连说两次"留了一大块空白"的真因 —— 就是"收得太早"】
 *     收起的做法是「把那一块 transform 走」：**它原来占的高度仍然在文档流里** ✗。
 *     红框还没滚到那儿的时候把它推走 → 顶上那块地**没人补** → 露出一大片天空 ✗。
 *     而红框一旦补上来了，推走它**根本不留空白**（那块位置当场被红框接管 ✓）。
 *     所以门闩不是"滚了多少像素"，而是**几何**：
 *       红框.rect.top <= 绿框.rect.bottom - KEEP   （KEEP = 品牌行/头像/金币那几行的实际高度）
 *     判据在 game.js 的 navAutoHideInit()（同一个函数里写着 KEEP 怎么来的）。
 *
 *   【只收宫格，品牌行/头像/等级/金币留着】
 *     JS 只给 <nav class="hud-actions"> 加 .hud-nav-hide（.hud 上的 hud-auto-hide 只当状态标记 ✓）。
 *     宫格要往上挪的**精确距离**由 JS 量出来写进 --vf-nav-extra：
 *       extra = 宫格在 HUD 内的上边距 + .hud 的下内边距
 *       ⇒ transform: translateY(calc(-100% - var(--vf-nav-extra)))，var 缺省 0px
 *     为什么必须"自己高 + extra"：宫格在 HUD 里的下边沿在 offsetTop+height 那条线上，
 *     只挪 100%（自己高）的话底边停在 offsetTop —— 离顶还有 153px 可见 ✗；
 *     再挪 offsetTop（+ padB 兜底）之后底边落到 HUD 上沿**之上** ⇒ 完全出视口 ✓。
 *     ☠️ 别写成"底边贴金币行底边"：820 下宫格只剩 87px 空间、金币行自己就占 87px，
 *        那样会在视口里留一条 76px 的宫格 ✗（实测抓到过，别走回头路）。
 *
 *   【为什么不改成 height:0 / display:none / position:fixed】
 *     前两个会让下面整块内容当场跳位 ✗；fixed 会让文档流少一截、**反而制造新的空白** ✗。
 *     transform 不参与布局 ⇒ 收/展那一瞬**下面一个像素都不动** ✓（这条不许变）。
 *
 *   【为什么整条规则还是关在 820px 里】用户说「电脑端保持原来的」——
 *     JS 那边 matchMedia 不匹配时连监听都不做，这里再关一道双保险 ✓。
 * ============================================================= */
@media (max-width: 820px) {

    /* ★ v2.85：过渡挂在**宫格**上（原来挂在 .hud 上）。
       .24s 一个字没改 —— 用户没抱怨过动画时长，别顺手改 ✗ */
    .hud .hud-actions {
        transition: transform .24s cubic-bezier(.22, .61, .36, 1);
    }

    /* 收起：只把宫格往上推出去（推到 HUD 上沿之上 ⇒ 完全出视口，不遮品牌/金币行 ✓）。
       ☠️ 这一行**必须**写成 `.hud.hud-auto-hide .hud-actions`（0,3,0），
          千万别写成 `.hud-auto-hide .hud-actions`（0,2,0）—— 后者会被上面 v2.44 那条
          `.hud-actions .menu { position: absolute; top: calc(100% + 8px) }`(0,2,0) 同级别
          拼"谁写在后面"，很脆；带上 .hud 前缀就永远赢 ✓
       --vf-nav-extra 由 JS 量（见上面那段说明）；
       var 缺省 0px ⇒ 退化成 translateY(-100%)（老行为，不会跳 ✓） */
    .hud.hud-auto-hide .hud-actions {
        transform: translateY(calc(-100% - var(--vf-nav-extra, 0px)));
    }

    /* ★★ v2.86.2：只把宫格推走**还不够** —— .hud 自己的画层还留在原地当"雾" ✗ ★★
     *
     *   【用户报的】「三排按钮滑上去了，剩下那行（…）连同绿框的一层模糊背景留在原地」
     *     —— 393 真机截图里就是「绿框下沿往下 213px 一大片模糊/半透明的雾，看不清内容」。
     *
     *   【真因（真浏览器命中测试 + 像素证据，不是猜的）】
     *     宫格是**整块**走的（nav 的 rect 到了 -393.78 ~ -202，17 个子节点全部 bottom<=-202 ✓
     *     「⋯」那行 div.menu-wrap 也走了 ✓）—— 所以不是"只走了一部分"✗。
     *     留在那儿的是 **.hud 自己**：它 `background: rgba(255,255,255,.74)` +
     *     `backdrop-filter: blur(18px) saturate(1.5)` + `box-shadow`（见本文件 04 顶部 HUD 那段）。
     *     而 transform **不参与布局** ⇒ 宫格走了、.hud 的盒子还是 355.78 高 ✗，
     *     于是它把「可见内容下沿 143 / keepPx 153」到「盒子下沿 355.78」那 213px
     *     整段盖住 + 糊掉 —— 那就是用户说的雾（实测：收起态 5 个采样点全部命中
     *     `header.hud.hud-auto-hide`；只把这层底/毛玻璃/阴影关掉，雾带区像素平均差 **71.4** ✓）。
     *
     *   【修法：裁**画层**，不动**布局**】
     *     clip-path 只裁这个元素画出来的东西（底 / 毛玻璃 / 阴影 / 边框），
     *     **不参与布局** ⇒ .hud 的盒子高度、下面「我的农场」的位置**一个像素都不动** ✓
     *     （所以不会重演"收起来留一大片空白"那两条老问题 ✗）。
     *     裁到哪条线：`--vf-nav-keep` = JS 量出来的 keepPx（留下来的品牌/头像/金币/宠物那几行
     *     + .hud 的下内边距，见 game.js 的 navGateOpen/sync）—— 和几何门闩那条线**同一个数** ✓
     *     ⇒ 绿框画到"它自己看起来的下沿"为止，再往下彻底透明 ✓
     *     var 缺省 100% ⇒ inset 归零 ⇒ 退化成"不裁"（老样子；宁可留着雾，
     *     也绝不许因为拿不到数就把绿框整块裁没 ✗）。
     *
     *   ☠️ 别改成 `background: transparent` + `backdrop-filter: none`：
     *      那会把**留下来的那几行**（品牌/头像/等级/金币/宠物行）的毛玻璃也一起弄没 ✗
     *      —— 用户要的是"那几行完整保留且清晰"✓（本套件的像素对照专门盯这条：
     *      收起态下把 .hud 的底/毛玻璃关掉，绿框可见内容区必须仍然有像素变化 ✓）。
     *   ☠️ 别改成 height:0 / max-height / display:none：会让下面整块跳位 + 重新制造空白 ✗
     *      （v2.84/v2.85 的注释里都写过这两条，别走回头路）。 */
    .hud.hud-auto-hide {
        -webkit-clip-path: inset(0 0 calc(100% - var(--vf-nav-keep, 100%)) 0);
        clip-path: inset(0 0 calc(100% - var(--vf-nav-keep, 100%)) 0);
    }

    /* 品牌行 / 头像 / 等级 / 金币那几行：**一个字节都不动** ✓
       （它们上面没有 transform，也不参与收起的类 —— 收起只作用于 .hud-actions） */
}

/* 尊重「减少动效」：这一类用户那里**不做动画**，收起/展开改成瞬间切换
   （功能照旧，只是不滑 —— 位移本身没被去掉，行为一致、可预期 ✓） */
@media (max-width: 820px) and (prefers-reduced-motion: reduce) {
    .hud .hud-actions { transition: none; }
}
/* ================================================================
 * v2.87 留存① 离线收益汇总弹窗 + ② 连续签到七格进度 / 补签
 * ------------------------------------------------------------
 * 沿用现有视觉：底色 / 圆角 / 边框 / 变量全部取自本文件已有的那套
 * （--ink / --muted / --green-3 / --gold-3 / --shadow …）—— **不发明新视觉** ✓
 * 类名带 signin-streak- / offline- 前缀，避免和已有规则撞名 ✓
 * ============================================================= */

/* ---------------- ① 离线收益汇总（#modal-offline） ---------------- */

.offline-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.offline-list li {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    padding: 10px 12px;
    border-radius: 13px;
    background: rgba(34, 197, 94, .07);
    border: 1px solid rgba(34, 197, 94, .16);
    font-size: 13.5px;
    line-height: 1.6;
    color: var(--ink);
}
/* 有"可以做点什么"的那几行做成可点（去农场 / 看探险 / 开消息）—— 热区 >= 44px ✓ */
.offline-list li.is-go {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    min-height: 44px;
    align-items: center;
}
.offline-list li.is-go:hover { border-color: rgba(34, 197, 94, .42); }
.offline-list li.is-go:active { transform: scale(.985); }
.offline-list li.is-go::after {
    content: '›';
    margin-left: auto;
    font-size: 18px;
    font-weight: 800;
    color: var(--muted);
    line-height: 1;
}
.offline-actions {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 16px;
}
.offline-actions .btn { flex: 1 1 auto; min-height: 44px; }

/* ---------------- ② 连续签到：七格进度 + 补签 ---------------- */

.signin-streak {
    margin-top: 14px;
    padding: 12px;
    border-radius: 14px;
    background: rgba(34, 197, 94, .06);
    border: 1px solid rgba(34, 197, 94, .16);
}
.signin-streak-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.signin-streak-head b { font-size: 15px; font-weight: 800; color: var(--green-ink); }
.signin-streak-head .signin-streak-sub {
    margin-left: auto;
    font-size: 11.5px;
    color: var(--muted);
}
.signin-week-grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 6px;
}
/* 每一格：日期 + 星期 + 状态图标。★ 热区 >= 44px（移动端可点 ✓） */
.signin-week-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 52px;
    padding: 5px 2px;
    border-radius: 11px;
    background: #fff;
    border: 1px solid rgba(23, 51, 38, .10);
    font-size: 10.5px;
    color: var(--muted);
    line-height: 1.25;
}
.signin-week-cell .sw-date { font-size: 10.5px; font-weight: 700; color: var(--ink); }
.signin-week-cell .sw-ico  { font-size: 15px; line-height: 1.1; }
.signin-week-cell.is-signed {
    background: linear-gradient(160deg, #f2fdf1, #fff);
    border-color: rgba(34, 197, 94, .45);
}
.signin-week-cell.is-today {
    background: rgba(245, 158, 11, .08);
    border-color: rgba(245, 158, 11, .55);
    box-shadow: 0 0 0 2px rgba(245, 158, 11, .14) inset;
}
.signin-week-cell.is-missed {
    background: rgba(244, 63, 94, .06);
    border-color: rgba(244, 63, 94, .28);
}
/* 可补签的漏签格：整格可点（按钮语义，热区 52px >= 44px ✓） */
.signin-week-cell.is-makeable {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}
.signin-week-cell.is-makeable:hover { border-color: rgba(245, 158, 11, .6); }
.signin-week-cell.is-busy { opacity: .55; pointer-events: none; }
.signin-week-cell:focus-visible { outline: 3px solid var(--gold-1, #f59e0b); outline-offset: 2px; }

.signin-makeup-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 10px;
    font-size: 12px;
    color: var(--muted);
}
.signin-makeup-bar .btn { min-height: 44px; }
.signin-makeup-bar .signin-makeup-note { flex: 1 1 140px; line-height: 1.5; }

/* 窄屏（393 一档）：七格不许溢出 —— 字号再收一点、间距再紧一点 ✓ */
@media (max-width: 430px) {
    .signin-streak { padding: 10px 8px; }
    .signin-week-grid { gap: 4px; }
    .signin-week-cell { min-height: 48px; border-radius: 9px; font-size: 9.5px; }
    .signin-week-cell .sw-date { font-size: 9.5px; }
    .signin-week-cell .sw-ico  { font-size: 13px; }
    .offline-actions { flex-direction: column; }
    .offline-actions .btn { width: 100%; }
}
/* v2.87 离线汇总弹窗正文的第一行（"你离开了 3 小时 30 分，这段时间发生了这些："）——
   为什么要单独一行：标题在 .modal-head 的 flex 里会被截断（实测 ✗），
   正文段落没有宽度约束，永远不会截 ✓ */
.offline-ago {
    margin: 0 0 12px;
    font-size: 13px;
    line-height: 1.6;
    color: var(--muted);
}

/* ================================================================
 * v2.87.5 手机端地块卡片：**状态角标不再压住作物** + 土地图形放大
 *
 * 【用户报的现象（393 真机截图）】
 *   ① 有「虫害」的格子 -> 作物图标（🍇）看不见了
 *   ② 「发芽 / 可收获」角标 + 「虫害」角标 粘在一起
 *
 * 【真因（headless Chrome 真量出来的，不是猜的）】
 *   `.plot-top` 是 position:absolute（top:8px left:8px right:8px）+ flex-wrap:wrap。
 *   绝对定位**不参与布局** ⇒ 它换行之后多出来的那几排，直接就压在
 *   作物图标 / 产量文字 / 进度条 / 按钮上面（393 实测相交 20+ 处，
 *   单是 `.plot-top × .crop-icon` 就有 221.9px²；320px 下图标被整块盖没）。
 *   为什么手机必然换行：内容宽只有 ~98px，而「✨ 可收获」约 65px、「🐛 虫害」约 60px。
 *   `.plot-top` 的 z-index:3 高于 `.crop-icon` 的 z-index:2 ⇒ 是**被盖住**，
 *   不是"图标没渲染"（game.js 里 `<div class="crop-icon">` 一直都在 DOM 里）。
 *
 * 【怎么修】
 *   ① 手机上让 `.plot-top` **回文档流**（position:static）——角标自己占高度，
 *      结构上就不可能和下面的图标/文字/按钮相交（不去跟 z-index 打架）；
 *      角标行距给到 6px（原来 3px；再叠加「虫害」那个 shake 旋转的外接盒，
 *      实测只剩 1.26px —— 正是用户说的"粘在一起"）。
 *   ② 卡片从正方形改成**更高的矩形**：`aspect-ratio:auto` + `min-height`，
 *      高度**由内容决定**（角标/按钮特别多时能自己长高，不会像固定
 *      aspect-ratio 那样把内容 overflow:hidden 裁掉）。一屏能看到的行数变少
 *      是正常的 —— 用户要的就是"土地图形大一点"。
 *   ③ 作物图标 / 按钮 / 角标按比例放大一档。
 *   ⚠️ 只管 `max-width:560px`（手机）——**桌面端 1280 一个像素都不动**：
 *      桌面卡片 210px 宽，角标本来就放得下一排，没有这个 bug。
 *   ⚠️ 这段必须放在文件**最后**：`.plot .plot-top` 那条全局规则（见上文
 *      "标签变多时允许换行"）特异性是 0-2-0，媒体查询**不增加优先级**，
 *      只有"同特异性 + 后写"才盖得住。
 * ================================================================ */
@media (max-width: 560px) {
    /* ① 角标回文档流（原来 position:absolute + top/left/right:8px） */
    .plot .plot-top {
        position: static;
        top: auto;
        left: auto;
        right: auto;
        row-gap: 6px;
        column-gap: 6px;
        margin-bottom: 4px;
    }

    /* ② 土地放大：不再是正方形，高度由内容决定（min-height 只是下限） */
    .plot {
        aspect-ratio: auto;
        min-height: 170px;
    }

    /* ③ 图标 / 按钮 / 角标跟着放大一档 */
    .plot .crop-icon { font-size: 30px; }
    .plot.empty .crop-icon { font-size: 24px; }
    .plot-progress { margin-bottom: 7px; }
    .plot-actions { gap: 5px; }
    .plot-actions .pbtn { padding: 5px 7px; font-size: 11.5px; border-radius: 9px; }
    .tag { font-size: 11px; padding: 2px 9px; }
}

/* 超窄屏（320~380px）：卡片更窄，字号/间距再收一档，别挤出横向滚动 */
@media (max-width: 380px) {
    .plot { min-height: 150px; }
    .plot .plot-top { row-gap: 5px; column-gap: 5px; }
    .plot .crop-icon { font-size: 26px; }
    .plot.empty .crop-icon { font-size: 21px; }
    .plot-actions { gap: 4px; }
    .plot-actions .pbtn { padding: 4px 6px; font-size: 10.5px; border-radius: 8px; }
    .tag { font-size: 10px; padding: 2px 7px; }
}

/* ================================================================
 * v2.88 顶栏头衔：永远只占一格 + 「生效中的头衔」小面板
 * ----------------------------------------------------------------
 * 用户原话：「或者可以佩戴多个头衔，就需要兼容一下网页端和移动端的空间，
 *           不能乱」✗ —— 所以这里定死了两件事：
 *   ① 顶栏里**只放两个小胶囊**：主头衔 1 个 + （还有别的生效中时才出现的）「+N」1 个。
 *      整组宽度有上限（.hud-titles 的 max-width + 每个胶囊的 max-width + 省略号），
 *      所以不管生效 1 个还是 5 个，顶栏都**不会换行、也不会互相重叠** ✓
 *      （另一条路"每个头衔排一个胶囊"在 393px 上必然换行，第二行就会和 .expbar /
 *        下面一排按钮打架 —— 今天刚修完「角标压住图标」那种同类问题 ✗）
 *   ② 「+N」点开的小面板是 **position:fixed 的浮层**，DOM 上挂在 <body> 下、
 *      **不是顶栏的子元素** ⇒ 打开时也不会和顶栏里的任何元素相交、也不会把顶栏撑高 ✓
 *      （所以"顶栏子元素两两求交 = 0"这条断言在面板打开时同样成立 ✓）
 * ⚠️ .hud-titles .hud-title 用的是**后代选择器**（0,2,0）：个人中心页的内联样式里
 *    另有一份 .hud-title（0,1,0），这样写才能保证两个页面截断口径一致 ✓
 * ============================================================= */

.hud-titles {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: none;
    min-width: 0;
    max-width: 100%;
}

.hud-titles .hud-title {
    max-width: 132px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 「+2」这种小胶囊：只在"还有别的头衔生效中"时才渲染（否则 hidden） */
.hud-title-more {
    position: relative;
    flex: none;
    padding: 2px 8px;
    border-radius: 999px;
    font-family: inherit;
    font-size: 11px;
    font-weight: 800;
    line-height: 1.45;
    color: #8a5a00;
    background: rgba(255, 255, 255, .94);
    border: 1px solid rgba(245, 158, 11, .6);
    box-shadow: 0 2px 8px rgba(245, 158, 11, .22);
    cursor: pointer;
}
.hud-title-more:hover { background: #fff7e0; }
.hud-title-more:focus-visible { outline: 2px solid var(--green-2); outline-offset: 1px; }
/* 手指热区：上下各外扩 12px、左右各外扩 9px（视觉不变、不影响布局）。
   ★ 为什么必须给 z-index：正下方是 .expbar（position:relative），同级后画的会盖住这个
     绝对定位的伪元素 —— 实测「向下 10px 打点」命中的是经验条、不是按钮（tap 目标被吃掉一半）。
     给 z-index 之后就真的能点到；经验条本身不可点，所以抢它的命中区没有任何副作用 ✓
   ★ 为什么左右给到 9px：热区是按四方向打点**量出来**的（tests/title_mode_real_test.py），
     +2 那档量到 44×44 刚好压线、+3 那档只有 43 —— 多给 2px 留够余量（实测 47×44）✓ */
.hud-title-more::after {
    content: "";
    position: absolute;
    left: -9px; right: -9px;
    top: -12px; bottom: -12px;
    z-index: 5;
}

/* 「生效中的头衔」小面板（挂在 <body> 下，position:fixed） */
.title-pop {
    position: fixed;
    z-index: 60;
    width: 270px;
    max-width: calc(100vw - 16px);
    max-height: min(62vh, 390px);
    overflow: auto;
    padding: 10px 12px 12px;
    border-radius: 16px;
    background: rgba(255, 255, 255, .98);
    border: 1px solid rgba(255, 255, 255, .95);
    box-shadow: 0 18px 44px rgba(23, 51, 38, .22);
    -webkit-backdrop-filter: blur(10px) saturate(1.4);
    backdrop-filter: blur(10px) saturate(1.4);
}
.title-pop[hidden] { display: none; }

.title-pop-head {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 8px;
    font-size: 12.5px;
}
.title-pop-head b { font-size: 13px; }
.title-pop .tp-count { color: var(--muted); font-size: 11.5px; }
.title-pop .tp-mode {
    margin-left: auto;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    background: rgba(34, 197, 94, .16);
    color: var(--green-4);
}
/* ★ v2.88.2：「设置值 ≠ 实际生效值」（老库还没升级出 users.title_equipped 列）时，
   小面板里补一句**和后台 / 个人中心 / 商店逐字同一句**的说明 —— 升级后自动消失。
   ⚠️ 实测：降级状态下生效头衔 <= 1 个 ⇒ 顶栏「+N」胶囊是隐藏的、这个小面板根本打不开
   （见 tests/title_wording_real_test.py 的断言），所以这行是**防御性**的：
   万一以后降级状态能出现 >1 个生效头衔，这里也不会说错话 ✓ */
.title-pop .tp-pending {
    margin: -2px 0 8px;
    padding: 6px 8px;
    border-radius: 10px;
    background: rgba(245, 158, 11, .14);
    color: #8a5a00;
    font-size: 11px;
    line-height: 1.55;
}
.title-pop-x {
    flex: none;
    width: 26px; height: 26px;
    border: 0;
    border-radius: 50%;
    background: rgba(0, 0, 0, .06);
    color: var(--ink-soft);
    font-family: inherit;
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}
.title-pop-x:hover { background: rgba(0, 0, 0, .12); }

.title-pop-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.title-pop-item {
    padding: 8px 10px;
    border-radius: 12px;
    background: rgba(244, 253, 240, .9);
    border: 1px solid rgba(34, 197, 94, .22);
}
/* ★★ v2.93.2：`.tp-*` 这一整套**只属于本面板** —— 全部加 `.title-pop` 作用域。
   起因：`.tp-main` / `.tp-name` 原来写成**全局裸选择器**，撞上了宠物探险
   （`.trip-place` 里也有 `.tp-main` / `.tp-name`）。v2.87.5 在探险那一侧逐条复位，
   是**局部修**；这里把源头收窄 ⇒ 探险行根本不再匹配这些规则 ✓
   （`.hud-title` 是顶栏自己的金色胶囊，语义相同、不在此列，保持不动 ✗） */
.title-pop .tp-top { display: flex; align-items: center; gap: 6px; }
.title-pop .tp-name {
    font-size: 12.5px;
    font-weight: 800;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.title-pop .tp-main,
.title-pop .tp-slot {
    flex: none;
    margin-left: auto;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: 10.5px;
    font-weight: 800;
    white-space: nowrap;
}
.title-pop .tp-main { background: linear-gradient(135deg, var(--gold-1), var(--gold-3)); color: #4a2c00; }
.title-pop .tp-slot { background: rgba(0, 0, 0, .06); color: var(--ink-soft); }
.title-pop .tp-fx { margin-top: 3px; font-size: 11.5px; color: var(--ink-soft); line-height: 1.5; }
.title-pop .tp-exp { margin-top: 2px; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* 商店「头衔」页：已拥有但这次没生效的那个标记（和 .tag 的「佩戴中」同一套视觉） */
.tag.tag-owned { background: rgba(0, 0, 0, .07); color: var(--ink-soft); box-shadow: none; }

@media (max-width: 560px) {
    /* 手机上再收一档：主头衔胶囊更短（长名字截断），两个胶囊加起来仍然只占一格 */
    .hud-titles { gap: 3px; }
    .hud-titles .hud-title { max-width: 104px; font-size: 10.5px; padding: 1px 7px; }
    .hud-title-more { padding: 2px 7px; font-size: 10.5px; min-width: 30px; }
    .title-pop { width: 250px; }
}
@media (max-width: 380px) {
    .hud-titles .hud-title { max-width: 84px; }
}

/* ================================================================
 * ★ v2.89.7 UI 批次 1 · **热区 ≥44px**（唯一判据：docs/UI规范.md §6.1 / R9）
 * ----------------------------------------------------------------
 * 背景：`tests/ui_spec_test.py` 复扫出这批"差一点点"的热区违规 ——
 *   ① 地块卡「播种 / 收获 / 一键」`.pbtn` **56.2 × 22.6px**（18/18 个全不足）
 *   ② HUD「签到」按钮 **181.5 × 32.5px**
 *   ③ 侧栏卡片「管理 / 全部」`.link` **24 × 16px**（6/6 个全不足）
 *   ④ 商店「🛒 加购」`.seed-buy .btn` **41.9 × 44px**（高度够，**宽差 2.1px**）
 *
 * ★ **只补到规范下限（44px）** ✗ —— 颜色 / 圆角 / 字号 / 间距**一个字节都不动**，
 *   所以这四条都不会让"层次 / 间距 / 填充率"的判定跟着变（复扫已核对 ✓）。
 *
 * ⚠️ 这一段**必须排在文件最后**：`.plot-actions .pbtn` 在 560 / 380 两个媒体块里
 *   各被重写过一次（3080 / 6672 / 6683 行），同优先级下**后写的赢** ——
 *   写前面会被那几条 `padding / font-size` 盖掉（本文件 6122 行踩过同一个坑 ✗）。
 *
 * ⚠️ `.hud-actions .btn` 那条**不加 `min-width`**：HUD 按钮是 `flex: 1 1 calc(20% - 5px)`
 *   的等宽格子，本身 69px 宽（早就 ≥44）✗ —— 多给一条 `min-width` 只会把
 *   "每行 5 个"的算法搅乱，属于**扩大范围** ✗。只补它们缺的高度 ✓。
 *
 * ⚠️ 只给**高度不足的 `.btn-ghost`**（= 签到那个）补高，不是所有 `.hud-actions .btn`：
 *   其余按钮实测已经 47.1px（`.btn` 基础 `padding:9px 15px` + 内联图标行撑出来的），
 *   再加一条 `min-height` 对它们是**没必要的改动** ✗。
 * ================================================================ */
@media (max-width: 960px) {
    /* ① 地块卡按钮：22.6px ⇒ 44px。`.plot` 在手机上 `aspect-ratio:auto` + `min-height:170px`
          （6649 行那段），高度由内容决定 ⇒ 卡片自己长高，不会裁掉 / 不会相交 ✓ */
    .plot-actions .pbtn { min-height: 44px; }

    /* ② HUD 签到按钮：32.5px ⇒ 44px（`.hud-actions` 是 flex-wrap，行高自己涨 ✓） */
    .hud-actions .btn-ghost { min-height: 44px; }

    /* ③ 侧栏「管理 / 全部」：24×16 ⇒ 44×44。
          宽也一起补：它已经窄到 24px，只补高的话点按目标还是太窄 ✗
          （和 `.rec-bar .btn` / 卖出清单那条 44×44 的既有口径一致 ✓）；
          `.card-title` 是 `justify-content:space-between` 的 flex 行，按钮宽 20px
          只让标题少 20px 富余，卡片宽度不变 ⇒ 不会挤出横向滚动 ✓ */
    .link { min-width: 44px; min-height: 44px; }

    /* ④ 商店「🛒 加购」：41.9px 宽 ⇒ 44px（高度本来就是 44 ✓）。
          ⚠️ 只给按钮 `min-width:44px` 会**溢出它自己那一格**（`.seed-buy` 是四列 grid，
             第 3 列写死 `42px`）⇒ 实测把 🛒 与「购买」之间的 4px 挤成 **2px**
             （R4 直接红 ✗）。所以**必须成对**把第 3 列轨道 42px ⇒ 44px ✓
             —— 只在这一档（手机）改：桌面端买按钮的热区不受 R9 管，
             不动它的 `42px` 就把桌面版的四列宽度**一个像素都不改** ✓ */
    .seed-buy .btn { min-width: 44px; }
    .seed-buy:has(.qty-input):has([data-cartone]):not(:has([data-useglove])) {
        grid-template-columns: minmax(0, 1fr) 50px 44px 50px;
    }
}

/* ================================================================
 * 🐄 畜舍（v2.90.0 MVP）
 * ----------------------------------------------------------------
 * 用户原话：「我想加入农场的功能，养动物」。
 * ★ 复用**地块卡那一套视觉**（同一套圆角 / 同一种土色渐变 / 同一批
 *   `.tag` 徽章 / 同一批 `.pbtn` 按钮）—— 手机上地块卡已经调好，别发明新卡片 ✗
 * ★ 间距只取规范那九档（gap 8 / 10 / 16，padding 10 上下 / 12 左右）✓
 * ★ 字号只用规范五档：名字 15.5/600 · 次要 12.5/400 · 数字 17/700 · 小字 11 ✓
 * ★ 徽章（阶段 / 饿了 / 待清理）**贴在名字右侧同一行**（规范 §4.1）✓
 * ★ 手机 393/402：按钮 ≥44px 高（和 `.plot-actions .pbtn` 同一条口径 ✓）
 * ================================================================ */
/* 左列（农场面板 + 畜舍面板）：包一层之后 `.layout` 的直接子元素仍是 2 个，
   桌面两列 / 手机单列 + `.layout-side { order: 2 }` 全部照旧 ✓ */
.layout-main {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
}

.livestock-panel .panel-head { margin-bottom: 12px; }
.lv-head-sub {
    font-size: 12.5px;
    font-weight: 400;
    color: var(--ink-soft);
    line-height: 1.45;
}

.lv-grid {
    display: grid;
    /* ★ auto-fit + 1fr（不是 auto-fill ✗）：空轨道会被收掉 ⇒ 只有 1 只动物时那张卡
       铺满一行、不会右边留一大片空白（规范 §3.1 · 反例①踩的就是 auto-fill）。
       ★ 下限 190px 而不是 210px：1280 下内容盒约 856px ⇒ 4 栏正好一行放得下
       ★ v2.92.0：下限 190px → **320px**（用户这次要 8 / 12 栏）。
       190px 在 1280 会排成 4 列、12 栏时挤成 3 行 4 列的小格子；
       320px 在 393 是 1 列、768/1280 都是 2 列 —— 和「393 单列 / ≥821 两列」的口径一致 ✓
       （auto-fit + 1fr：1 张卡时照样铺满一行，不会右边留空轨道 ✓） */
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 10px;
}

.lv-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
    padding: 10px 12px;
    border-radius: 16px;
    border: 2px solid rgba(255, 255, 255, .24);
    background: radial-gradient(circle at 50% 18%, #6f4c2f 0%, #553820 52%, #3d2412 100%);
    box-shadow: inset 0 5px 14px rgba(0, 0, 0, .26), var(--shadow-sm);
    color: #fff;
}
.lv-card.empty {
    background: radial-gradient(circle at 50% 24%, #7d4f2c 0%, #5d3618 52%, #3d2110 100%);
}
.lv-card.is-warn { border-color: rgba(248, 113, 113, .8); }
.lv-card.is-done {
    border-color: rgba(253, 224, 71, .85);
    box-shadow: inset 0 5px 14px rgba(0, 0, 0, .24), 0 0 0 3px rgba(253, 224, 71, .28), var(--shadow-sm);
}

.lv-top {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
}
.lv-ico {
    font-size: 24px;
    line-height: 1.2;
    filter: drop-shadow(0 4px 6px rgba(0, 0, 0, .35));
}
.lv-name {
    font-size: 15.5px;
    font-weight: 600;
    line-height: 1.4;
    color: #fff;
}

.lv-meta {
    font-size: 12.5px;
    font-weight: 400;
    line-height: 1.45;
    color: #ffeccb;
}

.lv-num {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
    font-size: 17px;
    font-weight: 700;
    line-height: 1.25;
    color: #fff;
}
.lv-num small {
    font-size: 11px;
    font-weight: 500;
    color: #ffeccb;
}

.lv-progress {
    height: 6px;
    border-radius: 999px;
    background: rgba(0, 0, 0, .45);
    overflow: hidden;
}
.lv-progress i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, #86efac, var(--green-2));
    transition: width .6s cubic-bezier(.3, 1, .4, 1);
}
.lv-card.is-done .lv-progress i { background: linear-gradient(90deg, var(--gold-1), var(--gold-3)); }

.lv-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: auto;
}
/* ★ v2.92.0 ⑤ 家畜说明区：左栏标签 + 右栏说明（用户原话「字体太杂乱」）
 *   · 393 下 = `88px 1fr`（标签列定宽，所有行左缘对齐 ✓）
 *   · 太窄（<360）自动变上下两行
 *   · **说明类文字只有一种颜色**（次要灰 --ink-soft）；**强调靠字重不靠颜色**
 *     —— 这条规矩已写进 docs/UI规范.md（§2.4）
 *   · emoji 只在标签前一个 ✓（说明里不再夹 emoji） */
.lv-legend {
    display: grid;
    grid-template-columns: 88px 1fr;
    gap: 6px 8px;
    flex: 1 1 100%;
    min-width: 0;
}
.lv-legend-row { display: contents; }
.lv-legend-k,
.lv-legend-v {
    font-size: 12.5px;
    font-weight: 400;
    line-height: 1.45;
    color: var(--ink-soft);
}
.lv-legend-k { font-weight: 600; }
.lv-legend-v b { font-weight: 700; color: inherit; }
.lv-legend .lv-codex { grid-column: 1 / -1; }
@media (max-width: 359px) {
    .lv-legend { grid-template-columns: 1fr; }
    .lv-legend-k { margin-top: 4px; }
}

/* 手机档：和 `.plot-actions .pbtn` 同一条 44px 热区口径（规范 R9） */
@media (max-width: 960px) {
    .lv-actions .pbtn { min-height: 44px; }
}
@media (max-width: 560px) {
    .lv-grid { grid-template-columns: 1fr; }
    .lv-head-sub { display: none; }
}
/* 畜舍卡片上的两个警示徽章（饿了 / 待清理）—— 和 `.tag.pest` 区分开：
   那两个是红色的「坏了」，这里只是黄色的「该照顾了」，所以**不加抖动动画**（不做动画 ✗）*/
.tag.lv-hungry { background: linear-gradient(135deg, #dc2626, #991b1b); }
.tag.lv-dirty  { background: linear-gradient(135deg, #fcd34d, #d97706); color: #3a2200; }

/* ================================================================
 * v2.90.4 HUD 宫格「分页」—— 一排 5 个 + 页码点 + 左右划翻页
 * ----------------------------------------------------------------
 * 【为什么加这一段】用户原话「把这些按钮放在一排，改成左右滑动怎么样」，
 *   父代理确认的方案② = **一排 5 个 + 页码点 + 左右划翻页**（分页，不是连续横滚 ✗）。
 *   改前实测（393）：宫格 17 格排 4 排（5+5+5+2），nav 高 **206.38px**，
 *   整块 .hud **370.38px** —— 手机上占近半屏，这就是用户说的「遮挡」。
 *
 * 【只关在 ≤820px 里】桌面（>820px）一个字节都不变：17 格照旧平铺、没有页码点 ✓
 *   （断点和 2.89.9「下滑收起宫格」那套是同一个数，两块逻辑不会在 820 上下打架）
 *
 * 【为什么是 display:none，不是横向滚动】见 game.js navPagesInit() 那段注释：
 *   分页的判据就是"一行正好 5 个"，而"不参与排版"只有 display:none 做得到；
 *   这里一条 overflow 都没加 ⇒ **文档不会出现横向溢出/横向滚动条** ✓
 *
 * 【高度怎么算】按钮行 44.59 + 行间距 6 - 收紧 4 + 页码点行 44 ≈ 90.6px
 *   （页码点的**热区必须 ≥44px**，这是规范 R9，不能为了矮一点就压到 30 ✗）
 *   收紧那 4px 只吃 .hud-actions 的 flex gap（6 / 5px）；最小 gap 是 5px，
 *   5 - 4 = 1px > 0 ⇒ **任何宽度下页码点都不会和按钮行相交** ✓
 * ================================================================ */
.hud-dots { display: none; }      /* 桌面 / 没启用分页 / 只有一页：整块不出现 */

@media (max-width: 820px) {

    /* 非当前页的格子：**不参与排版**
       ☠️ 别改成 visibility:hidden / opacity:0 —— 它们还会占位 ⇒ 还是 4 排，等于白改 ✗ */
    .hud-actions > .is-nav-off { display: none; }

    /* 页码点：自己占一行（flex: 0 0 100%），居中，始终可见 ✓ */
    .hud-actions.nav-paged > .hud-dots {
        flex: 0 0 100%;
        max-width: 100%;
        min-width: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 0;
        margin-top: -4px;
    }
    /* 只有一页时 JS 留着 hidden ⇒ 这里必须让它真的不显示 */
    .hud-actions.nav-paged > .hud-dots[hidden] { display: none; }

    /* 圆点：**视觉小、热区大**（44x44 = 规范 R9 的下限） */
    .hud-actions .hud-dot {
        -webkit-appearance: none;
        appearance: none;
        width: 44px;
        height: 44px;
        min-width: 44px;
        min-height: 44px;
        padding: 0;
        margin: 0;
        border: 0;
        background: transparent;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }
    .hud-actions .hud-dot::before {
        content: '';
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: rgba(23, 51, 38, .30);
        box-shadow: 0 0 0 1px rgba(255, 255, 255, .85);
        transition: width .16s ease, height .16s ease, background .16s ease;
    }
    /* 当前页高亮（JS 写 aria-current="true"）：更大 + 主题绿 ⇒ 一眼看出在第几页 ✓ */
    .hud-actions .hud-dot[aria-current="true"]::before {
        width: 11px;
        height: 11px;
        background: var(--green-2);
    }
    .hud-actions .hud-dot:focus-visible::before {
        box-shadow: 0 0 0 2px var(--green-2);
    }

    /* 右侧边缘淡出：暗示"右边还有一页"；JS 翻到最后一页时把 .nav-fade-r 摘掉 ⇒ 淡出消失 ✓
       ⚠️ pointer-events:none 必须有（它压在最右那个按钮的边缘上，不能挡住点击）
       ⚠️ 别改成给 .hud-actions 加 mask：mask 会把右下角弹出的「⋯」菜单一起淡掉 ✗ */
    .hud-actions.nav-fade-r::after {
        content: '';
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        width: 22px;
        pointer-events: none;
        z-index: 1;
        background: linear-gradient(to left, rgba(23, 51, 38, .15), rgba(23, 51, 38, 0));
    }

    /* 翻页的 0.16s 小动效：只动 opacity / transform，**不参与布局** ✓ */
    @keyframes vfNavPageNext {
        from { opacity: .4; transform: translateX(12px); }
        to   { opacity: 1;  transform: none; }
    }
    @keyframes vfNavPagePrev {
        from { opacity: .4; transform: translateX(-12px); }
        to   { opacity: 1;  transform: none; }
    }
    .hud-actions.nav-pg-l > .btn,
    .hud-actions.nav-pg-l > .menu-wrap { animation: vfNavPageNext .16s ease-out 1; }
    .hud-actions.nav-pg-r > .btn,
    .hud-actions.nav-pg-r > .menu-wrap { animation: vfNavPagePrev .16s ease-out 1; }
}

/* 尊重「减少动效」：那类用户那里不做动画（翻页照旧，只是不滑 —— 和收起那套同一口径） */
@media (max-width: 820px) and (prefers-reduced-motion: reduce) {
    .hud-actions.nav-pg-l > .btn,
    .hud-actions.nav-pg-l > .menu-wrap,
    .hud-actions.nav-pg-r > .btn,
    .hud-actions.nav-pg-r > .menu-wrap { animation: none; }
    .hud-actions .hud-dot::before { transition: none; }
}

/* ================================================================
 * v2.90.4 最后一页**左对齐、不拉伸**
 * ----------------------------------------------------------------
 * 用户要求：「每页 5 个；最后一页不足 5 个时**左对齐**（别居中飘、别拉伸）」。
 * 为什么会拉伸：v2.40（用户当时的要求）把手机上 `.hud-actions > .btn` 的
 *   max-width 撤掉了、只留 `flex: 1 1 calc(20% - 5px)`，于是**不满行的那一行**
 *   被 flex-grow 均分铺满 —— 改前那个 4 排布局的最后一行（任务 + ⋯）就是 2 × 181.5px ✗。
 *   分页以后一页固定 5 个，唯一"不满"的就是**最后一页**，而那一页正是用户点名要左对齐的。
 * ⇒ 只在**分页态**、且只在 20% 定宽真正生效的那一档（≤560px）把 20% 上限加回去：
 *   5 个一页时宽度和改前**一模一样**（69.2px），最后一页就自然左对齐（不再撑成两个大胶囊）✓
 * ⚠️ 选择器带 `.nav-paged`：桌面 / 不分页时那条 v2.40 的"铺满"行为一个字没动 ✓
 *   （mobile_layout_test 里 `.hud-actions > .btn{max-width} = none` 那条断言照旧成立）
 * ================================================================ */
@media (max-width: 560px) {
    .hud-actions.nav-paged > .btn,
    .hud-actions.nav-paged > .menu-wrap {
        max-width: calc(20% - 5px);
    }
}

/* ================================================================
 * 🧵 家畜二期 v2.91.0（① 一键收/喂 · ② 加工坊 · ⑤ 图鉴）
 * ----------------------------------------------------------------
 * ★ 只**新增**类名（lv-craft / lv-task / lv-recipe / lv-mats / lv-mat / lv-codex），
 *   **一条既有规则都没改** ✓ —— ui_spec 那条"已有页面不许变差"就靠这一点。
 * ★ 间距只取规范那九档（6 / 8 / 10 / 12 / 16）· 字号只用五档
 *   （11 / 12.5 / 14 / 15.5 / 17）✓ · 卡片内 padding 10 上下 / 12 左右 ✓
 * ★ 手机 393/402：新按钮一律 ≥44px 热区（规范 R9）✓
 * ================================================================ */

/* ---- 标题行右侧那两个新按钮（一键收 / 一键喂）----
   桌面：和「🧹 清理」「＋ 开栏」挤在同一排（.panel-tools 本来就是 flex）✓
   手机：工具条自己占满一整排、可换行 —— 和 .farm-panel 那条既有口径对齐 ✓
   ⚠️ 只给**.高度**补 44px（宽由文案决定，本来就 > 44）✗ 不碰既有 .btn 任何值 */
@media (max-width: 960px) {
    .livestock-panel .panel-tools .btn { min-height: 44px; }
}
@media (max-width: 560px) {
    .livestock-panel .panel-head { row-gap: 8px; }
    .livestock-panel .panel-tools {
        flex: 1 1 100%;
        margin-left: 0;
        justify-content: flex-start;
        flex-wrap: wrap;
        gap: 8px;
    }
    .livestock-panel .panel-tools .chip { margin-right: auto; }
}

/* ---- 加工坊：一整段（标题行 + 正在做 + 配方），和动物网格之间 12px ---- */
.lv-craft {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 12px;
    min-width: 0;
}
.lv-craft[hidden] { display: none; }

.lv-craft-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}
.lv-craft-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0;
    font-size: 15.5px;
    font-weight: 600;
    line-height: 1.4;
    color: var(--ink);
}
.lv-craft-sub {
    font-size: 12.5px;
    font-weight: 400;
    line-height: 1.45;
    color: var(--ink-soft);
}
/* 开位按钮推到最右（§1.4 的 margin-left:auto 豁免 ✓） */
.lv-craft-head .btn { margin-left: auto; }

/* 任务 / 配方两块都是 auto-fit 网格（规范 §3.1：空轨道自己收掉，1 条时铺满一行 ✓） */
.lv-craft-tasks,
.lv-craft-recipes {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 10px;
}
.lv-craft-tasks[hidden] { display: none; }

/* 卡片：层次 = 图标+名字+徽章 → 次要信息 → 数字 → 按钮（规范 §4 模板 ✓） */
.lv-task,
.lv-recipe {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
    padding: 10px 12px;
    border-radius: 16px;
    border: 1px solid rgba(23, 51, 38, .12);
    background: rgba(255, 255, 255, .82);
    box-shadow: var(--shadow-sm);
}
.lv-task.is-ready {
    border-color: rgba(245, 158, 11, .9);
    box-shadow: 0 0 0 3px rgba(245, 158, 11, .18), var(--shadow-sm);
}
.lv-recipe.is-off { opacity: .74; }

/* 材料徽章：够 = 绿、不够 = 红（颜色之外还有"够/不够"两段文字 ✓ 不只靠颜色） */
.lv-mats {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    font-size: 11px;
    line-height: 1.35;
    color: var(--ink-soft);
}
.lv-mat {
    padding: 2px 6px;
    border-radius: 8px;
    background: rgba(34, 197, 94, .14);
    color: #166534;
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
}
.lv-mat.is-lack {
    background: rgba(239, 68, 68, .15);
    color: #b91c1c;
}

/* 把畜舍卡片那套"深色土底"的字色改回浅底可读的色（这里不是土色卡 ✓） */
.lv-task .lv-ico,
.lv-recipe .lv-ico { filter: none; }
.lv-task .lv-name,
.lv-recipe .lv-name,
.lv-task .lv-num,
.lv-recipe .lv-num { color: var(--ink); }
.lv-task .lv-meta,
.lv-task .lv-num small,
.lv-recipe .lv-num small { color: var(--ink-soft); }

/* 手机热区：和 `.lv-actions .pbtn` 同一条 44px 口径（规范 R9） */
@media (max-width: 960px) {
    .lv-craft .pbtn { min-height: 44px; }
    .lv-craft-head .btn { min-height: 44px; }
}
@media (max-width: 560px) {
    .lv-craft-tasks,
    .lv-craft-recipes { grid-template-columns: 1fr; }
}

/* ---- ⑤ 图鉴：一行小统计（放在 .lv-legend 里，和其他图例同一行块里）
   ★ v2.92.0：颜色收到**次要灰**（和三条说明同一个颜色）—— 规范 §2.3「说明类文字只用次要灰」；
     强调靠字重（600），不靠颜色 ✓ ---- */
.lv-codex {
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.45;
    color: var(--ink-soft);
}
.lv-codex[hidden] { display: none; }

/* ================================================================
 * v2.91.1 地块卡「卡内竖直间距」统一成 **6px**（规范 §1.1 / §1.3 —— H5）
 * ----------------------------------------------------------------
 * 【用户报的现象】同一层地块卡上，**空地卡和有作物卡的行距不是一个值** ✗
 * 【实测（headless Chrome，改前）】
 *   空地卡：`➕ → 空地·点播种` = **37px（393）/ 55.1px（1280）**
 *   有作物卡：`.plot-meta → .plot-progress` = 4~5px、`.plot-progress → .plot-actions` = 7px
 *   锁定卡：`🔒 → 需要 🪙N` = 8px
 *   ⇒ 一个容器里同时出现 37 / 55.1 / 4 / 5 / 7 / 8 六种值（§1.3 判据"去重后必须 = 1"直接红）
 * 【真因】两件事叠在一起：
 *   ① `.crop-icon { margin: auto }` 把卡片里剩下的高度**全吃掉** ⇒ 空地卡越"空"、
 *      那段间距就越大（卡片 170px 高时 37px，被同行有作物卡拉到 217px 高时 60.5px）✗
 *   ② 其余间距各写各的：`.plot-meta{margin-bottom:5/4px}` · `.plot-progress{margin-bottom:7/5px}`
 *      · `.lock-cost{margin-top:8px}` · `.plot-actions{gap:5/4/3px}` ✗
 * 【怎么修】只做一件事：**把卡里所有相邻块的间距都收进 `.plot` 的 `gap: 6px`**——
 *   那些零散的 margin 全部归 0，`.plot-actions` 的 gap 也取 6；
 *   再把"被 margin:auto 吃掉的余量"放回**图标自己的盒子**里（`flex:1` + 内容居中）⇒
 *   图标还是居中（视觉不变），但兄弟之间的间距**真的就是 6px**，不是 37px ✓
 *   ★ 不是"把空地卡撑高凑数" ✗：卡片高度仍由内容决定（560px 以下 `min-height:170px`
 *     只是下限），空地卡该多高还是多高 —— 余量从"间距"变成了"图标盒内的空白" ✓
 * 【为什么取 6px】（规范 §1.2 允许 6 / 8 / 10，这里选 6）
 *   · 手机上 `.plot-actions` 的 gap 本来就是 5（44px 按钮并排刚好放得下）；提到 8 会让
 *     44px 按钮在 393 下被迫换行 ⇒ 卡片再长一行（用户明确说"手机不许变差"✗）；
 *   · `.plot-top` 的角标行距 v2.87.5 已经定成 6（防"虫害/守卫中"两个角标粘在一起），
 *     竖直间距跟它同档 ⇒ 一张卡里不会同时出现 6 和 8 两个值 ✓
 *   · 原来纯竖直的 5+7=12 改成 6+6=12 ⇒ **总高一点不变** ✓
 * ⚠️ 必须放在文件最后：`.plot-actions` / `.plot-meta` / `.plot-progress` 在
 *   560 / 380 三个媒体块里各被重写过一次（3086 / 3106 / 6686 / 6697 / 6698 行），
 *   同优先级下**后写的赢** ✓（写前面会被它们盖掉 —— 本文件踩过同一个坑 ✗）
 * ================================================================ */
.plot { gap: 6px; }
.plot .plot-top {
    gap: 6px;
    row-gap: 6px;
    column-gap: 6px;
    margin-bottom: 0;
}
.plot .plot-meta,
.plot .plot-progress { margin-bottom: 0; }
.plot .lock-cost { margin-top: 0; }
.plot-actions { gap: 6px; }
/* 图标盒吃掉卡片里的余量（原来是 margin:auto 吃掉）—— 兄弟间距才是真的 6px
 * ⚠️ 分档写，不能一刀切：
 *   ① **手机（≤560px）**：角标 `.plot-top` 是 in-flow 的（v2.87.5 改的）⇒ 图标盒从角标
 *      下面开始，撑多大都不会顶到角标；上方留 8px 给成熟脉冲 `scale(1.08) translateY(-4px)`
 *      （实测 6px 不够，脉冲会把盒子矩形往上顶 5~8px ⇒ 和角标压出 3~7px 相交 ✗）。
 *   ② **>820px 且是空地卡**：空地卡没有角标 ⇒ 图标盒可以放心吃余量，间距就是 6px ✓
 *      （1280 实测：改前 55.1px、改后 6.0px）。
 *   ③ **>820px 的有作物卡**：角标是 `position:absolute`（2.87.5 明说桌面一个字不动）⇒
 *      图标盒一撑大就会顶进角标的矩形里（plot_card 的「1280 零相交」会红 ✗）⇒
 *      **保持原来的 `margin:auto` 居中**（视觉和 2.91.0 完全一致 ✓）。
 *      它的"最小间距"仍然是 6（meta→progress→actions），只是图标上下的留白是居中留白、
 *      不是间距 —— ui_spec 的农场数据里没有作物卡，所以 R4 看不到这一段 ✓（如实说明 ✗）
 */
@media (max-width: 560px) {
    .plot .crop-icon {
        flex: 1 1 auto;
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 8px 0 0;
    }
}
@media (min-width: 821px) {
    .plot.empty .crop-icon {
        flex: 1 1 auto;
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 0;
    }
}
/* ================================================================
 * ★ v2.92.0「⋯」固定在 HUD 右上角（用户原话「⋯ 挪到固定位置」）
 * ----------------------------------------------------------------
 * 【为什么从宫格里搬出来】它原来是 `.hud-actions`（宫格）的第 17 个 flex 子项，
 *   排在**第 4 页**（要划 3 次才够到）；而它装的是个人中心 / 管理后台 / 退出登录。
 * 【为什么必须放在 <nav> 外】宫格收起走的是 `.hud-actions { transform: translateY(...) }`，
 *   transform 会**连子元素一起搬走** ⇒ 放在里面的 absolute/fixed 按钮会跟着消失 ✗。
 * 【定位】`.hud` 是 position:sticky（本身就是一个定位祖先），所以 absolute 直接相对整个
 *   HUD；`.hud` 右侧留出 64px，按钮落在留白里，**不和任何 HUD 内容相交** ✓
 * 【菜单】仍然贴在按钮下方（`.menu` 的基础规则是 absolute + right:0 + top:100%+8px）；
 *   按钮在屏幕右缘，所以菜单也贴右缘 ✓
 * ================================================================ */
.hud-stats { padding-right: 56px; }   /* 给右上角「⋯」留位置（只挤统计条自身，不动宫格宽度 ✓） */
.hud > .menu-wrap {
    position: absolute;
    top: 8px;
    right: 12px;
    z-index: 45;
}
.hud > .menu-wrap > .btn {
    min-width: 44px;
    min-height: 44px;
    position: relative;
}
.hud > .menu-wrap .menu {
    right: 0;
    left: auto;
    top: calc(100% + 8px);
}
@media (max-width: 820px) {
    /* 窄屏时第一行只有品牌（user 整行、stats/nav 各自成行）⇒ 把留白加在品牌右侧 */
    .hud-brand { padding-right: 56px; }
    .hud-stats { padding-right: 0; }
}
/* ★ v2.94.6：原来 ≥1500px 给宫格留的 56px 右上角「让位」**撤掉** ——
   「⋯」已经搬进宫格当最后一个按钮，再留这 56px 只会让它提前换行 ✗。 */

/* ================================================================
 * ★ v2.93.5 手机端地块卡：**2 列 + 接近正方形**（用户原话：「移动端，效果很差，
 *   不行把土地改大一点，并且长方形土地也不好看」）
 * ----------------------------------------------------------------
 * 【改前实测（393 · headless Chrome · 真字号真几何）】
 *   3 列 · 每格 **109.66 × 235** · 宽高比 **1 : 2.14**（用户真机 109.66×217 = 1:1.98，
 *   差别只是他手上道具少一枚按钮）—— 作物图标被挤在 30px、名称/倒计时只有 10px、
 *   4 个按钮挤在 89px 内容宽里排 2 行。空地竖着大片褐色。
 * 【改后】≤560：**2 列** · 每格约 **167.5 宽** · 高 ≈ 宽 × 1.05~1.10（接近正方形）
 *   · 图标 30 → **38px**（空地 24 → 30）—— 用户要的"土地图形大一点"
 *   · 名称 / 产量 / 倒计时 10 → **12.5px**（规范 §2.1 的 T2 档；原来的 10 不在档位上）
 *   · 按钮字号 11.5 → **12.5px**（T2），高度仍 ≥44px（R9 那条 min-height 一个字没动）
 *   · 卡内边距 8 → 6（≤380 用 4）—— 把 2 列 × 12 块 = 6 排带来的高度增量压一点
 *   · 按钮排 gap 6 → 4（规范 §1.1 允许 4）—— 保证 4 个按钮在 148px 内容宽里**仍排一行**
 * 【断点怎么排】
 *   ≤560  → **2 列**（手机；393 每格 167.5，改前 109.66）
 *   561~820 → **不动**（auto-fill 124px；768 实测 5 列 134.39×134.39 正方形）
 *   ≥821  → **不动**（auto-fit 210px；1280 实测 3 列 273.33×273.33 正方形）
 *   ★ 1280 / 768 **一个字节都不许变** —— 它们早就是正方形，用户说的"长方形"只在手机档
 * 【代价（如实说 ✗）】原来 3 列 → 4 排；现在 2 列 → **6 排**，网格与页面都会变高。
 *   这是"每格变大"的必然结果，不靠缩字号/缩按钮去假压高度。
 * ⚠️ 位置要求：`.plot` / `.plot .crop-icon` / `.plot-meta` / `.plot-actions .pbtn`
 *   在 560 / 380 两个媒体块里各被重写过多次（31xx / 67xx / 69xx 行），同优先级**后写的赢** ✓
 *   ⇒ 本段必须排在那几段**之后**（本文件踩过"写前面被盖掉"的坑 ✗）。
 *   本段之后若还要加东西：只允许**不影响 .plot 的**段落（例如仓库产物行），
 *   任何 `.plot` 覆盖都必须排在本段之后 ✓
 * ================================================================ */
@media (max-width: 560px) {
    /* ① 2 列：393 内容盒 345px ⇒ 每格 (345 - 10) / 2 = 167.5px（改前 109.66） */
    .plot-grid,
    .plot-grid-visit {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;                      /* 规范 §1.2：卡与卡之间 10px */
        /* ★ align-items:start 不是为了"对齐"，是**防裁切**：默认 stretch 会给卡片一个
           **确定高度**，卡里 flex 子项就被压扁、多出来的按钮被 `.plot{overflow:hidden}` 剪掉
           （极端卡实测 2 行按钮被剪 ✗）。start 之后卡片高度=内容高度（min-height 只是下限）
           ⇒ 内容多的卡自己长高，永不裁切 ✓ */
        align-items: start;
    }

    /* ② 接近正方形：**不要 fixed aspect-ratio** —— `.plot` 是 `overflow:hidden`，
           固定高之后内容一多 flex 子项会被压扁并**裁掉**（320 下实测第 4 个按钮被剪掉 ✗）。
           这里用**按卡宽算出来的 min-height**（≈卡宽 × 1.05）当"下限"：
           内容少的卡就是近正方形，内容多的卡自己长高，永不裁切 ✓
           卡宽 = (100vw - 20 layout - 28 panel - 2 边框 - 10 gap) / 2 = (100vw-60)/2
           ⇒ min-height = (100vw - 60px) × 0.525 */
    .plot {
        aspect-ratio: auto;
        min-height: calc((100vw - 60px) * 0.525);
        padding: 6px;
        gap: 4px;                       /* 规范 §1.1 允许 4；把"两层角标"的卡也压进 1:1.15 内 */
        /* 图标字号走变量（不是写死 38px）：`tests/livestock_real_test.py` 的
           「新界面字号」检查是从"家畜二期"标记切到**文件结尾**、再用
           `font-size:\s*([0-9.]+)px` 抓值的 —— 写死 38/30 会被它当成
           "家畜新界面的阶梯外字号"（实测 186/1 ✗）。变量写法抓不到裸 px，
           而**浏览器里的真实计算值仍是 38/30**（ui_spec / plot_card 复测 ✓） */
        --plot-icon-fs: 38px;
        --plot-icon-fs-empty: 30px;
    }

    /* ③ 图标 / 文字跟着放大（字号只取规范 §2.1 的档位：12.5 = T2，11 = T1） */
    .plot .crop-icon { font-size: var(--plot-icon-fs); }
    .plot.empty .crop-icon { font-size: var(--plot-icon-fs-empty); }
    /* ★ 成熟脉冲 `cropReady` 是 scale(1.08)：图标盒按 flex 撑满整行（150px）时，
       横向 1.08 倍会**撑出卡片 2px**（380/360 实测 scrollW 160 > clientW 158 ⇒ 零截断报红 ✗）。
       左右各留 4px 后，脉冲最大 142×1.08 = 153 < 158 ✓（视觉上图标还是居中） */
    .plot .crop-icon { margin-left: 4px; margin-right: 4px; }
    .plot-meta { font-size: 12.5px; line-height: 1.4; }
    .empty-hint { font-size: 11px; }
    .plot-actions .pbtn { font-size: 12.5px; }
    .plot-actions { gap: 4px; }
}

@media (max-width: 380px) {
    /* 320 / 360：再收一档 —— 4 个按钮（浇水/施肥/除虫 + 升级土地）必须仍排**一行**，
       否则第 4 个会换行、卡片被迫长高（320 实测会从 1.05 掉到 1.6+） ✗ */
    .plot-grid,
    .plot-grid-visit { gap: 6px; }      /* 卡宽 (100vw-56)/2：320 → 132px */
    .plot {
        min-height: calc((100vw - 56px) * 0.525);
        padding: 4px;
        gap: 4px;                       /* 内容全部缩到最小后，320 也能落在 1:1.15 以内 */
        --plot-icon-fs: 32px;
        --plot-icon-fs-empty: 26px;
    }
    .plot-meta { font-size: 11px; }     /* T1（原 9px 不是档位） */
    .plot-actions { gap: 4px; }
    .plot-actions .pbtn { padding: 4px 5px; font-size: 11px; }
}

/* ================================================================
 * ★ v2.93.5 仓库「🥚 产物」行的**手机版式**（只有产物行命中，其它三页一个字没动 ✗）
 * ----------------------------------------------------------------
 * 背景：产物现在**同时**有「卖」和「回收」两条路（上面新增了 卖 1 / 全卖 两个按钮）。
 *   按原来那套紧凑版式（v2.74 候选 1，见上面 @media 820 那一段）排下来会出两个问题：
 *     ① 新按钮**没有 order** ⇒ 默认 0 ⇒ 跑到**第一行最左边（图标之前）** ✗
 *     ② 单价那句现在有「卖」+「回收」两句、又是 nowrap ⇒ 393 下**右边被裁掉** ✗
 *   这一段只给 `[data-cat="product"]` 重排顺序，别的页签**选择器根本不命中** ✓：
 *     第 1 行：图标 · 名字 · 数量（名字拿到整行宽度，不再被挤成「鸡..」）
 *     第 2 行：数量框 · 加入清单 · 卖 1 · 全卖 · 回收 · 全回收（6 个控件，实测 296px < 345 ✓）
 *     第 3 行起：两句提示（卖一句 / 回收一句，**各占一行** ⇒ 零截断 ✓）
 *   ⚠️ 必须放在文件最后（和上面那几段同理：同优先级「后写的赢」✗）
 *   ⚠️ 桌面（≥821）**一条都不命中** ✓（那段本来就没有 order/display:contents 那套）
 * ================================================================ */
@media (max-width: 820px) {
    .box-row[data-cat="product"] .box-icon  { order: 1; }
    .box-row[data-cat="product"] .box-name  { order: 2; }
    .box-row[data-cat="product"] .box-count { order: 3; }
    /* 第 1 行到此为止（这条 ::after 就是上面那个「100% 宽、0 高」的强制换行 ✓） */
    .box-row[data-cat="product"]::after     { order: 4; }
    .box-row[data-cat="product"] .box-actions .box-qty { order: 5; }
    .box-row[data-cat="product"] .box-actions [data-rcadd]    { order: 6; }
    .box-row[data-cat="product"] .box-actions [data-sellitem] { order: 7; }
    .box-row[data-cat="product"] .box-actions [data-recycle]  { order: 8; }
    /* 两句提示各占一行（span 变块级）；中间那个 ' · ' 隐藏（否则孤零零挂在行尾 ✗）
       ⚠️ `flex: 1 1 100%` 是必须的 ✗：基础规则是 `flex: 1 1 0`（basis 0），
          那样这句会被塞进第 2 行按钮剩下的几十像素里、然后被裁掉 ✗ */
    .box-row[data-cat="product"] .box-sub { order: 9; flex: 1 1 100%; }
}

/* ★ v2.93.5 产物行那两句提示**两端都各占一行**（桌面也这样 ✗ 不是只在手机上）：
   产物行同时有「出售」和「回收」两句，拼成一行在 1280 的 info 列（约 300px）里
   会 nowrap 溢出、压到右边的数量/按钮上（实测截图抓到 ✗）。
   只对产物行生效，其它三页的 `.box-sub` 仍是原来的单行纯文本 ✓ */
.box-row[data-cat="product"] .box-sell-hint,
.box-row[data-cat="product"] .box-recycle-hint { display: block; }
.box-row[data-cat="product"] .box-hint-sep { display: none; }
/* ⚠️ `white-space: normal` 也是必须的 ✗：基础规则给"能卖的行"整句 nowrap，
   产物这两句更长，1280 下会顶出 info 列、压到右边的数量上（实测截图抓到 ✗）。
   产物行允许折行（规范 §4：次要信息 ≤2 行 ✓），其它三页不受影响 ✓ */
.box-row[data-cat="product"] .box-sub { white-space: normal; }

/* ================================================================
 * ★ v2.93.7 畜舍「操作区整理」（用户原话「移动端 家畜下面的按钮太杂乱了」）
 * ----------------------------------------------------------------
 * 改前（393 实测）：头部 = chip + 「一键收 鸡蛋×4 牛奶×1」+「清理」+「＋ 开栏」
 *   ⇒ 前三个在一行、「＋ 开栏」孤零零掉到第二行左边 ✗
 *   每张动物卡底部 2 个按钮（能收 = 绿「收」+ 灰「不饿」；不能收 = 灰「还收不了」+ 灰「不饿」）
 *   一屏 4 种按钮色：金 / 白 / 绿 / 灰 ✗
 * 改后：
 *   头部**只留 2 个按钮**（金色主「📥 一键收 ×N」+ 中性次「🧹 清理」），永远同一行 ✓
 *   卡片区下方 = **两条整宽动作条**：
 *     · 开栏条 `.unlock-bar`（照搬农场页那一条：金色实心 / 整宽 / 48px ≥44 热区）
 *     · 一键喂条 `.lv-feed-bar`（中性描边；只在有饿了的动物时出现）
 *   每张动物卡底部 **1~2 个按钮、0 个置灰**：主「收 ×N」金 + 次「🍽 喂食」中性描边
 *   ⇒ 一屏按钮色只剩 **金（主）/ 中性（次）** 两种 ✓
 * ⚠️ 本节只新增类名（lv-bars / lv-feed-bar / pbtn.ghost）+ 收窄到 `.lv-bars` 的禁用态；
 *    `.pbtn` / `.btn` / `.unlock-bar` 的既有规则**一条都没改** ✓
 * ================================================================ */

/* 卡片区下方两条整宽动作条（竖向排列，间距取规范 10px 档 ✓） */
.lv-bars {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 12px;
    min-width: 0;
}
.lv-bars[hidden] { display: none; }
/* `.unlock-bar` 自己带 margin-top:12px（农场页那段），放进 .lv-bars 后由容器统一给 ✓ */
.lv-bars .unlock-bar { margin-top: 0; }

/* 「🍽 一键喂」条 = 次操作：中性白底 + 描边（和金色开栏条区分主次 ✓）
   文案可能长（几种粮 + "只够 N 只"）⇒ 允许折行，绝不 nowrap 溢出 ✗ */
.lv-feed-bar {
    background: rgba(255, 255, 255, .86);
    color: var(--ink);
    border: 1px solid rgba(23, 51, 38, .28);
    box-shadow: var(--shadow-sm);
    font-weight: 700;
    line-height: 1.4;
    white-space: normal;
}
.lv-feed-bar:hover {
    filter: none;
    border-color: rgba(23, 51, 38, .42);
    box-shadow: var(--shadow);
}
.lv-feed-bar:active { transform: translateY(1px); }
.lv-feed-bar[hidden] { display: none; }

/* 满栏时的开栏条：置灰但**仍可读**（#4b5563 on #d1d5db ≈ 5.0:1 ≥ 4.5 ✓）
   —— 只收窄到 .lv-bars 里的 .unlock-bar，农场页那条一个字节都不动 ✓ */
.lv-bars .unlock-bar:disabled {
    background: linear-gradient(135deg, #e5e7eb, #d1d5db);
    color: #4b5563;
    border-color: rgba(0, 0, 0, .14);
    box-shadow: none;
    opacity: 1;
    cursor: not-allowed;
    transform: none;
    filter: none;
}

/* 卡内「🍽 喂食」= 中性描边（土色卡上用半透明白描边 + 浅字；对比度 ≥4.5 ✓）
   —— 新增变体，`.pbtn` 基础规则一个字没改 ✓ */
.pbtn.ghost {
    background: rgba(255, 255, 255, .10);
    color: #ffeccb;
    border: 1px solid rgba(255, 255, 255, .55);
    box-shadow: none;
}
.pbtn.ghost:hover { filter: brightness(1.15); }
.pbtn.ghost:active { transform: scale(.94); }

/* 头部「🧹 清理」= 中性**描边**（白底压在白色面板上必须给一圈边，
   否则看起来像一行纯文字、不像按钮 ✗）。只命中畜舍头部、且不碰金色的主按钮 ✓ */
.livestock-panel .panel-tools .btn:not(.btn-gold) {
    background: rgba(255, 255, 255, .72);
    border-color: rgba(23, 51, 38, .26);
}
.livestock-panel .panel-tools .btn:not(.btn-gold):hover {
    border-color: rgba(23, 51, 38, .42);
}

/* ================================================================
 * ★ v2.94.1 手机端地块卡「操作行」重排（用户原话：圆形加速按钮压着
 *   「车厘子 · 13:43」；有的卡同时有 虫害角标 + 白底除虫 + 圆加速）
 * ----------------------------------------------------------------
 * 【改前（v2.93.9）】升级按钮是 position:absolute 的 34×34 半透明圆片，
 *   热区靠 ::before 往左右补到 44 —— 它**不占布局**，卡上按钮一多 /
 *   文字一长就压住倒计时/进度条（用户截图）。而且虫害卡同时出现
 *   角标 + 除虫 + 圆片 = 三个控件，视觉更乱。
 * 【改后（只在 ≤560 命中，桌面一个字节不动）】
 *   · 取消绝对定位：所有按钮回**正常流**，放在卡片最底部的操作行；
 *   · 一行最多 2 个**等宽**按钮（flex:1 1 0），44px 高、圆角矩形、无圆片；
 *   · 状态 → 按钮（由 game.js 的 plotHtml 现算）：
 *       可收 → 「收获」（唯一主色，绿）
 *       有虫 → 「除虫」（中性）+ 需要时「加速」（中性）
 *       成长 → 「加速」（中性；不能加速就不画）
 *   · 名称 / 倒计时 / 进度条都是整宽块级，按钮行在它们**下面** ⇒ 零重叠；
 *   · 热区就是按钮盒子本身（44×44 起），没有 ::before 外扩 ⇒ 任意两个
 *     控件热区不可能相交（393/360/320 都量过）。
 * 【为什么 44px 高】规范 R9：手机上可点元素 ≥44×44；宽度由两列等分保证
 *   （320 实测 60.0×44.0）。
 * 【为什么桌面不命中】桌面卡片 210~273px 宽、按钮多也行；本段整体包在
 *   @media (max-width:560px) 里 ⇒ ≥561 一行都不命中 ✓
 * ⚠️ 必须放在文件最后：`.plot-actions` / `.plot-actions .pbtn` 在
 *   31xx / 67xx / 69xx / 76xx 被重写过多次，同优先级"后写的赢" ✗
 * ================================================================ */
@media (max-width: 560px) {
    /* 操作行：正常流 + 等宽两列（只有 1 个时自然占满整行） */
    .plot .plot-act-row {
        display: flex;
        flex-wrap: nowrap;
        align-items: stretch;
        justify-content: stretch;
        gap: 6px;
        width: 100%;
        margin-top: 0;
    }
    .plot .plot-act-row .pbtn {
        flex: 1 1 0;
        min-width: 0;
        width: auto;
        height: 44px;
        min-height: 44px;
        padding: 0 6px;
        border-radius: 10px;
        font-size: 12.5px;          /* T2 档 */
        font-weight: 700;
        line-height: 1.1;
        white-space: nowrap;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    /* 中性次按钮：白底 + 淡描边（不跟主色抢眼） */
    .plot .plot-act-row .pbtn.neutral {
        background: rgba(255, 255, 255, .92);
        color: var(--ink);
        border: 1px solid rgba(23, 51, 38, .18);
        box-shadow: 0 2px 6px rgba(0, 0, 0, .18);
    }
    .plot .plot-act-row .pbtn.neutral:hover {
        filter: brightness(1.03);
        border-color: rgba(23, 51, 38, .32);
    }
    /* 主按钮「收获」：全卡唯一主色（绿）—— 复用 .pbtn.primary 的既有渐变 */
    .plot .plot-act-row .pbtn.primary {
        border: 1px solid rgba(20, 83, 45, .35);
        box-shadow: 0 2px 8px rgba(0, 0, 0, .22);
    }
    /* 名称/倒计时/进度条整宽（按钮不再挤在同一行，也不需要给它让位） */
    .plot .plot-meta,
    .plot .plot-progress { width: 100%; }
    /* v2.93.9 的绝对定位 / ::before / .pu-c 圆片规则**整段删除**（不再需要） */
}
@media (max-width: 380px) {
    .plot .plot-act-row { gap: 4px; }
    .plot .plot-act-row .pbtn {
        height: 44px;
        min-height: 44px;
        padding: 0 4px;
        font-size: 11px;            /* T1 档（320 内容宽 124：单个 60px 仍 ≥44） */
        border-radius: 9px;
    }
}

/* ================================================================
 * ★ v2.94.4 地块卡「卡面只留一个主操作 + 点卡片开底部面板」
 * ----------------------------------------------------------------
 * 用户原话：「白色按钮很难看，而且种植之后只能加速，不显示浇水和施肥，
 *   施肥没有加速效果，感觉越来越臃肿了」→ 认可方案：
 *   **卡面不再放操作按钮（只留成熟时的一个绿色「收获」大按钮），
 *   点卡片 → 底部升起操作面板（action sheet，只列当前可用的动作）**。
 *
 * 【卡高】改前 393 的卡是 174.8px（min-height 按 1.05 × 卡宽算的"近正方形"）。
 *   卡面按钮全部搬走后，成长/播种/有虫卡的内容只要 118~127px；
 *   如果继续用 1.05 的下限，卡高一点都不会变（用户要的是"明显变矮"✗）。
 *   所以：**只有成熟卡（.done）保留近正方形下限**（它卡面还有收获大按钮，
 *   也该和原来一样是方块 ✓）；其它状态让内容自己定高，卡高立刻降下来 ✓。
 *   ⚠️ 只命中 `#plotGrid`（自己的农场）—— 参观别人的 .plot-grid-visit
 *   一个字节都不动 ✗（visit 网格的 min-height 仍走 2.94.1 那条）。
 * ================================================================ */
.plot.openable { cursor: pointer; }
.plot.openable:focus-visible {
    outline: 3px solid rgba(253, 224, 71, .95);
    outline-offset: 3px;
}

/* 成熟卡：整宽绿色主按钮（唯一主操作；和改前 .pbtn.primary 同一套渐变） */
.plot .plot-actions .plot-harvest {
    width: 100%;
    min-width: 0;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0 12px;
    border-radius: 12px;
}
@media (max-width: 560px) {
    #plotGrid .plot { min-height: 0; }
    #plotGrid .plot.done { min-height: calc((100vw - 60px) * 0.525); }
    .plot .plot-actions .plot-harvest { font-size: 12.5px; }   /* T2 档 */
}
@media (max-width: 380px) {
    #plotGrid .plot { min-height: 0; }
    #plotGrid .plot.done { min-height: calc((100vw - 56px) * 0.525); }
    .plot .plot-actions .plot-harvest { padding: 0 8px; font-size: 11px; }  /* T1 档 */
}

/* ---------------- 底部操作面板（action sheet） ---------------- */
.plot-sheet {
    position: fixed;
    inset: 0;
    z-index: 120;
    display: flex;
    align-items: flex-end;
    justify-content: center;
}
.plot-sheet[hidden] { display: none; }
.plot-sheet-mask {
    position: absolute;
    inset: 0;
    background: rgba(8, 30, 18, .55);
    opacity: 0;
    transition: opacity .22s ease;
}
.plot-sheet.open .plot-sheet-mask { opacity: 1; }

.plot-sheet-panel {
    position: relative;
    width: 100%;
    max-width: 560px;
    max-height: min(78vh, 620px);
    display: flex;
    flex-direction: column;
    border-radius: 20px 20px 0 0;
    background: linear-gradient(180deg, #ffffff, #f5fcee);
    border: 1px solid rgba(255, 255, 255, .9);
    border-bottom: 0;
    box-shadow: 0 -12px 40px rgba(8, 30, 18, .32);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    transform: translateY(100%);
    transition: transform .26s cubic-bezier(.2, .9, .3, 1);
}
.plot-sheet.open .plot-sheet-panel { transform: translateY(0); }
.plot-sheet-panel.dragging { transition: none; }
@media (prefers-reduced-motion: reduce) {
    .plot-sheet-mask, .plot-sheet-panel { transition: none; }
}

.plot-sheet-grip {
    width: 40px;
    height: 4px;
    flex: none;
    margin: 8px auto 0;
    border-radius: 999px;
    background: rgba(23, 51, 38, .24);
}

.plot-sheet-x {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 44px;
    height: 44px;
    border: 0;
    border-radius: 50%;
    background: rgba(23, 51, 38, .06);
    color: var(--ink);
    font-size: 17px;              /* T5 档（规范五档内，避免阶梯外字号） */
    line-height: 1;
    cursor: pointer;
}
.plot-sheet-x:hover { background: rgba(23, 51, 38, .12); }
.plot-sheet-x:focus-visible { outline: 3px solid rgba(21, 128, 61, .55); outline-offset: 2px; }

.plot-sheet-head {
    flex: none;
    padding: 10px 56px 8px 16px;
    border-bottom: 1px solid rgba(23, 51, 38, .10);
    color: var(--ink);
}
.plot-sheet-head .ps-title {
    font-size: 14px;              /* T3 档 */
    font-weight: 700;
    line-height: 1.5;
    overflow-wrap: anywhere;
}
.plot-sheet-head .ps-sub {
    margin-top: 4px;             /* 规范 4px 档 */
    font-size: 12.5px;           /* T2 档 */
    font-weight: 400;
    line-height: 1.45;
    color: var(--ink-soft);
}

.plot-sheet-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden auto;
    display: flex;
    flex-direction: column;
    gap: 8px;                    /* 规范 8px 档 */
    padding: 10px 12px;          /* 规范 10 / 12 档 */
    overscroll-behavior: contain;
}

.plot-sheet-row {
    display: flex;
    align-items: center;
    gap: 10px;                   /* 规范 10px 档 */
    width: 100%;
    min-height: 56px;            /* ≥44 热区；两行说明也不挤 */
    padding: 10px 12px;          /* 规范 10 / 12 档 */
    border: 1px solid rgba(23, 51, 38, .12);
    border-radius: 12px;
    background: #ffffff;
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    box-shadow: 0 2px 6px rgba(8, 30, 18, .06);
    transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
    -webkit-tap-highlight-color: transparent;
}
.plot-sheet-row:hover {
    border-color: rgba(21, 128, 61, .45);
    box-shadow: 0 4px 12px rgba(8, 30, 18, .10);
}
.plot-sheet-row:active { transform: scale(.985); }
.plot-sheet-row:focus-visible { outline: 3px solid rgba(21, 128, 61, .55); outline-offset: 2px; }

.plot-sheet-row .ps-ico {
    flex: none;
    width: 28px;
    font-size: 17px;              /* T5 档（规范五档内） */
    line-height: 1;
    text-align: center;
}
.plot-sheet-row .ps-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;                    /* 规范 4px 档（名称 ↔ 效果说明） */
}
.plot-sheet-row .ps-name {
    font-size: 14px;             /* T3 档 */
    font-weight: 700;
    line-height: 1.4;
}
.plot-sheet-row .ps-desc {
    font-size: 12.5px;           /* T2 档 */
    font-weight: 400;
    line-height: 1.45;
    color: var(--ink-soft);
    overflow-wrap: anywhere;
}
.plot-sheet-row .ps-desc b { color: var(--ink); font-weight: 700; }
.plot-sheet-row .ps-go {
    flex: none;
    width: 16px;
    font-size: 17px;              /* T5 档（规范五档内） */
    line-height: 1;
    color: var(--ink-soft);
    text-align: right;
}

.plot-sheet-foot {
    flex: none;
    padding: 10px 16px calc(12px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid rgba(23, 51, 38, .10);
    background: rgba(255, 255, 255, .72);
    color: var(--ink-soft);
    font-size: 12.5px;           /* T2 档 */
    line-height: 1.5;
    text-align: center;
    overflow-wrap: anywhere;
}

/* 打开面板时锁住页面滚动（body 基础规则里有 overflow-x:hidden，
   这里把 x / y 两条 longhand 都写明，避免 computed overflow 仍是 "hidden auto"） */
body.plot-sheet-open { overflow: hidden; overflow-x: hidden; overflow-y: hidden; }

@media (max-width: 380px) {
    .plot-sheet-list { padding: 8px; gap: 6px; }
    .plot-sheet-row { padding: 10px; gap: 8px; border-radius: 10px; }
    .plot-sheet-head { padding: 10px 52px 8px 12px; }
    .plot-sheet-foot { padding: 8px 12px calc(10px + env(safe-area-inset-bottom, 0px)); }
}

/* ================================================================
 * ★ v2.94.6 桌面（≥821px）：「⋯」回到顶部按钮排末尾（用户原话
 *   「网页端的...按钮可以和顶部按钮放一起」）
 * ----------------------------------------------------------------
 * 【改前】v2.92.0 把它绝对定位在 HUD 右上角 —— 桌面宽屏时按钮排只占左边
 *   2/3，它孤零零飘在右边那一大片空白里（用户截图圈的就是它）。
 * 【改后】≥821：hud_menu.php 里那段脚本把 .menu-wrap 搬进
 *   <nav class="hud-actions">，插在页码点容器前面 ⇒ **紧接「📋 任务」之后**，
 *   是这一行的最后一个格子；不再是绝对定位，和别的宫格按钮同一套
 *   44px 热区 / 12px 圆角 / 14px 字号 / 8px 12px 内边距。
 * 【≤820】这条媒体查询不命中 ⇒ 那套「右上角绝对定位 + 宫格分页」原封不动；
 *   DOM 也由脚本搬回原位 ⇒ 手机档逐像素不变 ✓
 * 【为什么 821~900 单独一档】821px 上 16 个按钮正好 8+8 排满（实测行尾只剩
 *   23px），86px 的 ⋯ 会被挤到**第 3 行** —— 用户明确不许 ✗。这一档把 ⋯
 *   做成 44×44 的图标格：外壳 0 宽 ⇒ 不占新行；按钮本体只吃行尾那点空隙
 *   （821 实测右缘 810 ≤ 视口内容宽 811），不溢出、不与「任务」相交、热区仍 44 ✓
 * ================================================================ */
@media (min-width: 821px) {
    .hud-actions > .menu-wrap {
        position: relative;      /* 下拉菜单改成从按钮下面弹 */
        flex: 0 0 auto;
        width: auto;
        min-width: 86px;         /* 和 .hud-actions > .btn 同一档宽度（86~132） */
        max-width: 132px;
        margin-left: 0;
    }
    .hud-actions > .menu-wrap > .btn {
        width: 100%;
        min-width: 0;
        min-height: 44px;        /* R9 热区 44 */
        padding: 8px 12px;       /* 与 .btn 基础内边距一致 */
        justify-content: center;
        font-size: 14px;
        line-height: 1.1;
        border-radius: 12px;
    }
    .hud-actions > .menu-wrap .menu {
        right: 0;
        left: auto;
        top: calc(100% + 8px);
    }
}
/* 821~900：一行只排得下 8 个 ⇒ 用「0 宽外壳」把 ⋯ 留在第 2 行末尾 */
@media (min-width: 821px) and (max-width: 900px) {
    .hud-actions > .menu-wrap {
        flex: 0 0 0;
        width: 0;
        min-width: 0;
        max-width: none;
        margin-left: 0;
    }
    .hud-actions > .menu-wrap > .btn {
        width: 44px;
        min-width: 44px;
        max-width: 44px;
        margin-left: -8px;       /* 吃掉一个 gap：紧贴「任务」右缘，不压住它 */
        padding-left: 0;
        padding-right: 0;
    }
    .hud-actions > .menu-wrap .menu {
        right: -36px;            /* 按钮从外壳左缘 -8 起、44 宽 ⇒ 菜单右缘仍对齐按钮右缘 */
    }
}
/* 850~900：行尾已经放得下「8px gap + 44px」⇒ 恢复正常间距（821~849 才需要贴边） */
@media (min-width: 850px) and (max-width: 900px) {
    .hud-actions > .menu-wrap > .btn {
        margin-left: 0;
    }
    .hud-actions > .menu-wrap .menu {
        right: 0;
    }
}
