/* ============================================================================
   Rally · base.css —— 唯一样式文件（设计系统全文）

   依据：docs/UI.md（v2，iOS 原生感）。
   页面作者不写 <style>、不写内联样式；改这个文件 = 改全站，先看 UI.md。

   结构：
     1. 设计 token（深/浅两套）
     2. 重置
     3. 图标（内联 SVG 尺寸）
     4. 骨架：顶栏（毛玻璃）/ 内容区 / 底部 tabbar（毛玻璃）
     5. 卡片
     6. 按钮 / 图标按钮
     7. 表单
     8. 列表
     9. 标签 / 空态 / 骨架屏 / 错误态
    10. 统计 / 进度条
    11. 应用网格
    12. 热力图（12 周）
    13. 月历（网球页主视觉）
    14. 球拍
    15. tabs / seg
    16. fab / sheet / toast
    17. 登录页
    18. 工具类
    19. 无障碍 / 动效偏好

   毛玻璃注意（iOS Safari）：backdrop-filter 放在「伪元素」上而不是 chrome 本身，
   这样 ① fixed 定位的 tabbar 不会被 filter 的包含块劫持（否则会产生堆叠上下文错位），
   ② 祖先里出现 transform 时不至于整块失效。必须有 @supports 降级。
   ========================================================================== */

/* ---------------------------------------------------------------- 1. 设计 token */
:root{
  /* 表面层次：三档必须肉眼可分（iOS 深色 = 纯黑底 + 抬升面） */
  --bg:#0e1116;              /* 页面底（保留偏蓝黑调性） */
  --bg-elev:#1a2029;         /* 卡片 / 弹层 */
  --bg-elev2:#262f3b;        /* 卡片内抬升（输入框、stat、fg 的分隔） */
  --bg-sunken:#0a0d12;       /* 下沉（分段控件底） */

  --fg:#e6edf3;              /* 正文 400 */
  --fg-dim:#9aa7b4;          /* 辅助 400，降透明度 */
  --fg-faint:#6b7885;        /* 更弱：占位、标签 */

  --line:rgba(255,255,255,.075);      /* 极淡分隔线 */
  --line-soft:rgba(255,255,255,.05);
  --line-strong:rgba(255,255,255,.13);

  --accent:#3ddc84; --accent-fg:#08210f;
  --warn:#ffb02e; --danger:#ff5d5d; --info:#4aa8ff;

  /* 毛玻璃 chrome */
  --chrome-bg:rgba(14,17,22,.72);
  --chrome-bg-solid:#0e1116;          /* 无 backdrop-filter 时的降级 */
  --chrome-tab-solid:#161b23;

  /* 圆角：iOS 更圆润 */
  --radius:16px;      /* 卡片 */
  --radius-sm:12px;   /* 按钮 / 输入框 */
  --radius-lg:20px;   /* 弹层顶部 */
  --radius-pill:999px;

  --gap:12px; --pad:14px;
  --nav-h:60px;

  --safe-t:env(safe-area-inset-top,0px); --safe-b:env(safe-area-inset-bottom,0px);
  --safe-l:env(safe-area-inset-left,0px);  --safe-r:env(safe-area-inset-right,0px);

  /* 字号阶梯（移动端为主，中文最小 13px —— 再小就糊）：
     xs 13 微字（域名、时间、角标）
     sm 14 次要文字（标签、说明）
     md 16 正文（中文在手机上 16 才不费眼）
     h2 18 卡片/区块标题
     lg 20 页面标题、弹层标题
     xl 22 大数字（百分比、比分）
     2xl 30 登录品牌 */
  --fs-2xs:12px;                     /* 比 xs 再小一档：仪表盘标签、底部脚注（仅限紧凑读数） */
  --fs-xs:13px; --fs-sm:14px; --fs-md:16px;
  --fs-h2:18px; --fs-lg:20px; --fs-xl:22px; --fs-2xl:30px;
  --fs-input:16px;
  --fs-tab:12px;                     /* 底栏标签：iOS 原生 UITabBar 是 10pt≈13px，中文压到 12 才不挤 */

  --tap:44px;                        /* M6：所有可点区下限 */
  --t:160ms;                         /* 统一过渡时长（120–180ms） */
  --ease:cubic-bezier(.2,.7,.3,1);

  --shadow:0 1px 2px rgba(0,0,0,.40);
  --shadow-card:0 1px 2px rgba(0,0,0,.32), 0 8px 24px rgba(0,0,0,.18);
  --shadow-pop:0 16px 48px rgba(0,0,0,.55);

  /* 首页「重心卡」：比普通卡多一层近距离投影 + 一层远距离扩散，
     深色下对比度有限，所以第二层更淡；hover 时抬高一档。 */
  --shadow-hero:0 1px 2px rgba(0,0,0,.22), 0 10px 28px -14px rgba(0,0,0,.55);
  --shadow-hero-hover:0 1px 3px rgba(0,0,0,.28), 0 16px 36px -14px rgba(0,0,0,.62);
  /* 重心卡自带极淡的「上亮下暗」渐变：模拟光从上方来，卡片才有厚度感。
     深色下顶面比 --bg-elev 亮一档、底面暗一档，但都不脱离深色调性。 */
  --hero-top:#1f2734;
  --hero-bottom:#171d26;

  --app-bg:radial-gradient(1100px 520px at 50% -8%, rgba(61,220,132,.055), transparent 62%), var(--bg);
}

/* ============================================================ 浅色主题
   两种来源要同时支持：
     a) 系统是浅色，用户没强制 → @media 生效
     b) 用户在设置页强制浅色      → :root[data-theme="light"] 生效
   CSS 没法把「选择器」和「媒体查询」写进同一条规则，直接抄两遍变量块会让
   以后改一个颜色要改两处（典型的漂移来源）。
   所以这里把**浅色值只定义一份**（--light-* 挂在 :root 上，永远存在但默认没人用），
   下面两个「激活」块只做 `--bg:var(--light-bg)` 这种别名搬运 —— 值只有一处可改。
   dark 是基础值（上面 :root 里那份），所以「强制深色」不需要任何规则，
   只要 data-theme="dark" 不命中浅色激活块即可。 */

:root{
  /* --- 浅色调色板：iOS = #f2f2f7 底 + 纯白卡片 + 极淡阴影 --- */
  --light-bg:#f2f2f7; --light-bg-elev:#ffffff; --light-bg-elev2:#ececf1; --light-bg-sunken:#e6e6ec;

  --light-fg:#1c1c1e; --light-fg-dim:#6b6b70; --light-fg-faint:#8e8e93;

  --light-line:rgba(60,60,67,.12);
  --light-line-soft:rgba(60,60,67,.08);
  --light-line-strong:rgba(60,60,67,.20);

  --light-accent:#16a34a; --light-accent-fg:#ffffff;
  --light-warn:#b45309; --light-danger:#dc2626; --light-info:#2563eb;

  --light-chrome-bg:rgba(255,255,255,.72);
  --light-chrome-bg-solid:#f2f2f7;
  --light-chrome-tab-solid:#ffffff;

  --light-shadow:0 1px 2px rgba(16,32,48,.05);
  --light-shadow-card:0 1px 2px rgba(16,32,48,.05), 0 8px 24px rgba(16,32,48,.05);
  --light-shadow-pop:0 16px 48px rgba(16,32,48,.20);

  /* 浅色下阴影是唯一能拉开层次的线索，比深色更明显一点 */
  --light-shadow-hero:0 1px 2px rgba(16,32,48,.06), 0 10px 28px -14px rgba(16,32,48,.20);
  --light-shadow-hero-hover:0 2px 4px rgba(16,32,48,.08), 0 18px 40px -16px rgba(16,32,48,.26);
  --light-hero-top:#ffffff;
  --light-hero-bottom:#fbfbfd;
}

/* 浅色生效时把 --light-* 搬到正式变量名上（两个来源共用这一段声明） */
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]){
    --bg:var(--light-bg); --bg-elev:var(--light-bg-elev);
    --bg-elev2:var(--light-bg-elev2); --bg-sunken:var(--light-bg-sunken);

    --fg:var(--light-fg); --fg-dim:var(--light-fg-dim); --fg-faint:var(--light-fg-faint);

    --line:var(--light-line); --line-soft:var(--light-line-soft); --line-strong:var(--light-line-strong);

    --accent:var(--light-accent); --accent-fg:var(--light-accent-fg);
    --warn:var(--light-warn); --danger:var(--light-danger); --info:var(--light-info);

    --chrome-bg:var(--light-chrome-bg);
    --chrome-bg-solid:var(--light-chrome-bg-solid);
    --chrome-tab-solid:var(--light-chrome-tab-solid);

    --shadow:var(--light-shadow); --shadow-card:var(--light-shadow-card);
    --shadow-pop:var(--light-shadow-pop);
    --shadow-hero:var(--light-shadow-hero); --shadow-hero-hover:var(--light-shadow-hero-hover);
    --hero-top:var(--light-hero-top); --hero-bottom:var(--light-hero-bottom);

    --app-bg:radial-gradient(1100px 520px at 50% -8%, rgba(22,163,74,.05), transparent 62%), var(--bg);
  }
}
/* 用户强制浅色：与上面同一份搬运，只是选择器换成了属性（不依赖系统偏好） */
:root[data-theme="light"]{
  --bg:var(--light-bg); --bg-elev:var(--light-bg-elev);
  --bg-elev2:var(--light-bg-elev2); --bg-sunken:var(--light-bg-sunken);

  --fg:var(--light-fg); --fg-dim:var(--light-fg-dim); --fg-faint:var(--light-fg-faint);

  --line:var(--light-line); --line-soft:var(--light-line-soft); --line-strong:var(--light-line-strong);

  --accent:var(--light-accent); --accent-fg:var(--light-accent-fg);
  --warn:var(--light-warn); --danger:var(--light-danger); --info:var(--light-info);

  --chrome-bg:var(--light-chrome-bg);
  --chrome-bg-solid:var(--light-chrome-bg-solid);
  --chrome-tab-solid:var(--light-chrome-tab-solid);

  --shadow:var(--light-shadow); --shadow-card:var(--light-shadow-card);
  --shadow-pop:var(--light-shadow-pop);
  --shadow-hero:var(--light-shadow-hero); --shadow-hero-hover:var(--light-shadow-hero-hover);
  --hero-top:var(--light-hero-top); --hero-bottom:var(--light-hero-bottom);

  --app-bg:radial-gradient(1100px 520px at 50% -8%, rgba(22,163,74,.05), transparent 62%), var(--bg);
}

/* color-scheme：让原生控件（日期/时间选择器、滚动条、输入框自动填充）也跟着变，
   否则浅色主题下 `<input type=date>` 图标还是白的、看不见。 */
:root{ color-scheme:dark; }
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]){ color-scheme:light; }
}
:root[data-theme="light"]{ color-scheme:light; }

/* ---------------------------------------------------------------- 2. 重置 */
*,*::before,*::after{ box-sizing:border-box; }

html{
  -webkit-text-size-adjust:100%;
  text-size-adjust:100%;
  height:100%;
}

body{
  margin:0;
  min-height:100dvh;
  background:var(--app-bg);
  background-attachment:fixed;
  color:var(--fg);
  font-family:-apple-system,"PingFang SC","Noto Sans SC",system-ui,sans-serif;
  font-size:var(--fs-md);
  line-height:1.5;
  /* M13：横屏/长内容都不许横向滚动。
     用 clip 而不是 hidden：hidden 会让 body 变成滚动容器、破坏 sticky 顶栏和毛玻璃。 */
  overflow-x:clip;
  -webkit-font-smoothing:antialiased;
  -webkit-tap-highlight-color:transparent;
  overscroll-behavior-y:contain;
  font-variant-numeric:tabular-nums;   /* 数字刷新不跳动 */
}
@supports not (overflow:clip){
  body{ overflow-x:hidden; }
}

h1,h2,h3,h4,p,ul,ol,figure{ margin:0; }
ul,ol{ padding:0; list-style:none; }
img{ display:block; max-width:100%; }
a{ color:var(--info); text-decoration:none; }
button,input,textarea,select{ font:inherit; color:inherit; }
button{ cursor:pointer; background:none; border:0; padding:0; }
svg{ display:block; }
[hidden]{ display:none !important; }

:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:2px;
  border-radius:6px;
}
/* 触屏点按不要出现焦点框（键盘操作仍保留 :focus-visible） */
:focus:not(:focus-visible){ outline:none; }

.noscript{ padding:24px; color:var(--fg-dim); }

/* ---------------------------------------------------------------- 3. 图标（内联 SVG） */
.ico{
  width:24px; height:24px;
  display:block;
  flex:0 0 auto;
  stroke:currentColor;
  pointer-events:none;
}
.ico-sm{ width:18px; height:18px; }
.ico-lg{ width:28px; height:28px; }
/* 行内图标：默认 .ico 是 block（图标按钮里需要），但塞进一行文字里会换行
   —— 例如应用名后面的收藏星标会掉到名字下一行。用它跟文字排在同一基线上。 */
.ico-inline{ display:inline-block; vertical-align:-.18em; }

/* ---------------------------------------------------------------- 4. 布局骨架（UI.md §3） */

/* --- 顶栏：品牌 + 导航（桌面）+ 操作按钮。毛玻璃由 ::before 承担 --- */
.topbar{
  position:sticky; top:0; z-index:30;
  display:flex; align-items:center; gap:10px;
  min-height:calc(52px + var(--safe-t));
  padding:var(--safe-t) calc(var(--pad) + var(--safe-r)) 0 calc(var(--pad) + var(--safe-l));
  /* 注意：这里不要 background —— 玻璃层是 ::before（z-index:-1） */
}
.topbar::before{
  content:"";
  position:absolute; inset:0; z-index:-1;
  background:var(--chrome-bg);
  -webkit-backdrop-filter:blur(20px) saturate(180%);
  backdrop-filter:blur(20px) saturate(180%);
}
.topbar::after{
  content:"";
  position:absolute; left:0; right:0; bottom:0; height:1px;
  background:var(--line);
  pointer-events:none;
}

.topbar-brand{ display:flex; align-items:center; min-width:0; flex:0 1 auto; }
.topbar h1{
  font-size:var(--fs-lg);
  font-weight:700;
  letter-spacing:-.2px;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
  min-width:0;
}
.topbar-action{ margin-left:auto; color:var(--fg-dim); }
.topbar-action:active{ background:var(--bg-elev2); color:var(--fg); }
.topbar-action .ico{ width:22px; height:22px; }
.topbar-action.spinning .ico{ animation:ico-spin .55s linear; }
@keyframes ico-spin{ from{ transform:rotate(0); } to{ transform:rotate(360deg); } }
@media (hover:hover){
  .topbar-action:hover{ background:var(--bg-elev2); color:var(--fg); }
}

/* --- 桌面侧边栏专用部件：手机/平板端完全不参与布局 ---
   .topbar-foot 用 display:contents，让里面的按钮仍直接是 .topbar 的 flex 子项，
   与加这一层之前完全一致（手机端零像素位移）；.brand / .view-hd 同理不占位。
   真正启用它们的是文件末尾 §20 的 @media (min-width:1024px)。 */
.topbar-foot{ display:contents; }
/* 设置按钮：手机/平板也显示（它是唯一进设置页的入口）。
   DOM 顺序是「设置 → 刷新 → 退出」，让 .topbar-action 继续吃 margin-left:auto，
   这样刷新按钮的像素位置和加设置按钮之前**完全一样**（零位移），设置只是挤在它左边。 */
.topbar-settings{ color:var(--fg-dim); }
.topbar-settings .ico{ width:22px; height:22px; }
.topbar-settings:active{ background:var(--bg-elev2); color:var(--fg); }
@media (hover:hover){
  .topbar-settings:hover{ background:var(--bg-elev2); color:var(--fg); }
}
/* 手机（<641px）上把「设置 + 刷新」并成一组靠右：
   手机上 .tabbar 是底部固定条（不在顶栏 flex 流里），如果只有 .topbar-action 吃 margin-left:auto，
   设置按钮就会紧贴标题右边，看起来像标题上的装饰图标（431 轮截图确认很怪）。
   让设置按钮吃剩余空间、刷新紧随其后，两个操作按钮并排落在右侧。
   641px 以上 .tabbar 回到顶栏里并自带 margin-left:auto，不需要这条。 */
@media (max-width:640px){
  .topbar-settings{ margin-left:auto; }
  .topbar-action{ margin-left:0; }
}
/* 用 .topbar .topbar-logout 提一级选择器：.icon-btn 自己的 display:inline-flex
   在文件后面（优先级相同但更靠后），只写 .topbar-logout 会被它盖住。 */
.topbar .topbar-logout{ display:none; }
.brand{ display:none; }
.view-hd{ display:none; }

/* --- 内容区 --- */
#view{
  display:block;
  max-width:960px;
  margin:0 auto;
  padding:var(--gap) calc(var(--pad) + var(--safe-r)) calc(var(--nav-h) + var(--safe-b) + 20px) calc(var(--pad) + var(--safe-l));
  outline:none;
}
/* 含 FAB 的页面：底部再补出悬浮球的高度（56 + 16 间距），
   否则滚到最底时最后一行内容会永久被球盖住、滚不动。
   .has-fab 由 core.js 在渲染后按 host.querySelector('.fab') 切换；
   :has() 是不支持 JS 挂类时的兜底（iOS Safari 15.4+ / Chrome 105+）。
   fab[hidden]（日程页切到「本周」时会把球藏起来）不算，别留白。 */
#view.has-fab,
#view:has(.fab:not([hidden])){
  padding-bottom:calc(var(--nav-h) + var(--safe-b) + 20px + 72px);
}

/* --- 底部 tabbar（手机固定；桌面变成顶部一行） --- */
.tabbar{
  position:fixed; left:0; right:0; bottom:0; z-index:40;
  margin:0;
  display:grid;
  grid-template-columns:repeat(5,1fr);
  align-items:stretch;
  height:calc(var(--nav-h) + var(--safe-b));
  padding-bottom:var(--safe-b);
  padding-left:var(--safe-l);
  padding-right:var(--safe-r);
}
.tabbar::before{
  content:"";
  position:absolute; inset:0; z-index:-1;
  background:var(--chrome-bg);
  border-top:1px solid var(--line);
  -webkit-backdrop-filter:blur(20px) saturate(180%);
  backdrop-filter:blur(20px) saturate(180%);
}
.tabbar a{
  position:relative;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:3px;
  min-width:var(--tap); min-height:var(--tap);   /* M6 */
  color:var(--fg-faint);
  font-size:11px;
  line-height:1.2;
  touch-action:manipulation;
  user-select:none;
  -webkit-user-select:none;
  transition:color var(--t) var(--ease);
}
/* 图标背后 32×24 胶囊（选中态） */
.tabbar a .tab-icon{
  position:relative;
  display:flex; align-items:center; justify-content:center;
  width:32px; height:24px;
  transition:transform 140ms var(--ease);
}
.tabbar a .tab-icon::before{
  content:"";
  position:absolute; inset:0;
  border-radius:var(--radius-pill);
  background:color-mix(in srgb, var(--accent) 16%, transparent);
  opacity:0;
  transform:scale(.82);
  transition:opacity var(--t) var(--ease), transform var(--t) var(--ease);
}
.tabbar a .tab-icon .ico{ width:22px; height:22px; }
.tabbar a .tab-label{ font-size:var(--fs-tab); font-weight:500; letter-spacing:.2px; }
.tabbar a.active{ color:var(--accent); }
.tabbar a.active .tab-icon::before{ opacity:1; transform:scale(1); }
.tabbar a.active .tab-label{ font-weight:600; }
.tabbar a:active .tab-icon{ transform:scale(.9); }

/* 桌面（>=641px）：顶栏一行 = 品牌 + 胶囊导航 + 操作 */
@media (min-width:641px){
  .topbar{
    min-height:60px;
    padding:calc(8px + var(--safe-t)) max(calc(var(--pad) + var(--safe-r)), calc((100vw - 960px) / 2 + var(--pad))) 8px max(calc(var(--pad) + var(--safe-l)), calc((100vw - 960px) / 2 + var(--pad)));
    gap:16px;
  }
  .topbar h1{ font-size:var(--fs-xl); letter-spacing:-.4px; }
  .tabbar{
    position:static;
    height:auto;
    display:flex; align-items:center; gap:2px;
    margin-left:auto;
    padding:0;
  }
  .tabbar::before{ display:none; }
  .tabbar a{
    flex-direction:row;
    gap:7px;
    padding:0 14px;
    min-height:var(--tap);
    border-radius:var(--radius-pill);
    font-size:var(--fs-sm);
    color:var(--fg-dim);
    transition:background var(--t) var(--ease), color var(--t) var(--ease);
  }
  .tabbar a .tab-icon{ width:auto; height:auto; }
  .tabbar a .tab-icon::before{ display:none; }
  .tabbar a .tab-icon .ico{ width:18px; height:18px; }
  .tabbar a .tab-label{ font-size:var(--fs-sm); }
  .tabbar a.active{ background:var(--bg-elev2); color:var(--accent); }
  .tabbar a.active .tab-label{ font-weight:600; }
  .tabbar a:active{ background:color-mix(in srgb, var(--fg) 8%, var(--bg-elev2)); }
  .topbar-action{ margin-left:0; }
  #view{ padding-top:20px; padding-bottom:40px; }
  /* 桌面没有 tabbar，FAB 的 bottom 是 safe-b+24、高 56 → 占视口底部 24~80px；
     含 FAB 的页面同样补留白，避免 641–960 区间压在卡片上。 */
  #view.has-fab,
  #view:has(.fab:not([hidden])){
    padding-bottom:calc(40px + 72px);
  }
}

/* 横屏矮屏（≤420px 高）：tabbar 压到 52px，别挡住内容 */
@media (max-height:420px){
  :root{ --nav-h:52px; }
  .tabbar a{ gap:1px; font-size:10px; }
  .tabbar a .tab-icon{ height:20px; }
  .tabbar a .tab-icon .ico{ width:19px; height:19px; }
}

/* ---------------------------------------------------------------- 5. 卡片 */
.card{
  background:var(--bg-elev);
  border:1px solid var(--line);
  border-radius:var(--radius);
  padding:var(--pad);
  box-shadow:var(--shadow-card);
}
.card + .card{ margin-top:var(--gap); }

/* 网格里的卡片是"格子"不是"堆叠卡片"：禁止继承上面的 margin，
   否则 .card + .card 的 12px 外边距会从第 2 张起把它往下推、高度还被压掉 12px，
   表现为同一网格里卡片高度不齐（首页「常用应用」就是这个 bug）。 */
.grid > .card{ margin-top:0; }
.section{ margin-bottom:var(--gap); }
.section:last-child{ margin-bottom:0; }

.card-hd{
  display:flex; justify-content:space-between; align-items:center;
  gap:var(--gap);
  margin-bottom:10px;
  min-height:28px;
}
.card-hd h2,.card-hd h3{
  font-size:var(--fs-h2);
  font-weight:650;
  letter-spacing:-.1px;
  display:flex; align-items:center; gap:7px;
  min-width:0;
}
.card-hd .ico{ color:var(--fg-dim); }
.card-hd .hd-action{
  display:flex; align-items:center; gap:5px;
  font-size:var(--fs-sm); color:var(--fg-dim);
  flex:0 0 auto;
}
a.card-hd-cta, .card-cta{
  color:var(--fg-dim);
  font-size:var(--fs-xs);             /* 13：不再和卡片标题同级抢戏 */
  font-weight:500;
  letter-spacing:.1px;
  min-height:var(--tap);              /* M6：高度 44（视觉小、点击区不小） */
  min-width:var(--tap);               /* M6：宽度也 44 */
  padding:0 2px;
  display:inline-flex; align-items:center; justify-content:flex-end; gap:2px;
  transition:color var(--t) var(--ease);
}
.card-cta .ico{ width:15px; height:15px; color:var(--fg-faint); transition:color var(--t) var(--ease); }
.card-cta:active{ color:var(--accent); }
.card-cta:active .ico{ color:var(--accent); }
@media (hover:hover){
  .card-cta:hover{ color:var(--accent); }
  .card-cta:hover .ico{ color:var(--accent); }
}

/* 整卡可点进 */
.card.tapable{ transition:background var(--t) var(--ease), border-color var(--t) var(--ease); }
.card.tapable:active{ background:var(--bg-elev2); }
@media (hover:hover){
  .card.tapable:hover{ border-color:var(--line-strong); background:var(--bg-elev2); }
}

/* --- 重心卡（首页「服务器」「网球」）：比普通卡片多一层投影 + 极淡的上亮下暗渐变 --- */
.card-hero{
  background:linear-gradient(180deg, var(--hero-top) 0%, var(--hero-bottom) 100%);
  box-shadow:var(--shadow-hero);
  transition:transform var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
/* 只有真指针设备才做位移，触屏的 sticky hover 会卡住（M11） */
@media (hover:hover) and (pointer:fine){
  .card-hero:hover{ transform:translateY(-1px); box-shadow:var(--shadow-hero-hover); }
}
@media (prefers-reduced-motion:reduce){
  .card-hero{ transition:none; }
  .card-hero:hover{ transform:none; }
}

/* ---------------------------------------------------------------- 6. 按钮 */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:6px;
  flex:0 0 auto;                         /* 在 flex 头部里不许被压窄，否则内边距被吃掉、文字被裁 */
  min-height:var(--tap);                 /* M6 */
  padding:0 18px;
  border-radius:var(--radius-sm);
  border:1px solid transparent;
  background:var(--accent);
  color:var(--accent-fg);
  font-size:var(--fs-md);
  font-weight:600;
  letter-spacing:.1px;
  text-align:center;
  touch-action:manipulation;
  transition:transform 120ms var(--ease), filter var(--t) var(--ease),
             background var(--t) var(--ease), border-color var(--t) var(--ease);
}
.btn .ico{ width:18px; height:18px; }
.btn:active{ transform:scale(.975); filter:brightness(.95); }
.btn:disabled{ opacity:.45; cursor:default; }
.btn:disabled:active{ transform:none; filter:none; }
@media (hover:hover){
  .btn:hover:not(:disabled){ filter:brightness(1.06); }
}
.btn.ghost{
  background:color-mix(in srgb, var(--fg) 6%, transparent);
  color:var(--fg);
  border-color:var(--line);
}
.btn.ghost:active{ background:var(--bg-elev2); }
@media (hover:hover){
  .btn.ghost:hover:not(:disabled){ background:var(--bg-elev2); filter:none; }
}
.btn.danger{
  background:var(--danger);
  color:#fff;
}
/* M6：小按钮的点击区也必须是 44×44 —— 高度直接给足，宽度用 ::after 左右各扩 6px */
.btn.sm{
  position:relative;
  min-height:var(--tap);
  padding:0 14px;
  font-size:var(--fs-sm);
  font-weight:500;
}
.btn.sm::after{ content:""; position:absolute; inset:0 -6px; }
.btn.block{ width:100%; }
.btn-row{ display:flex; gap:10px; }
.btn-row .btn{ flex:1; }

.icon-btn{
  display:inline-flex; align-items:center; justify-content:center;
  width:var(--tap); height:var(--tap);        /* M6 */
  border-radius:var(--radius-pill);
  color:var(--fg-dim);
  touch-action:manipulation;
  flex:0 0 auto;
  transition:background var(--t) var(--ease), color var(--t) var(--ease), transform 120ms var(--ease);
}
.icon-btn .ico{ width:22px; height:22px; }
.icon-btn:active{ background:var(--bg-elev2); color:var(--fg); transform:scale(.94); }
.icon-btn.danger{ color:var(--danger); }
@media (hover:hover){
  .icon-btn:hover{ background:var(--bg-elev2); color:var(--fg); }
}

/* ---------------------------------------------------------------- 7. 表单（M8/M9/G3） */
.field{
  display:flex; flex-direction:column; gap:6px;
  margin-bottom:var(--gap);
  min-width:0;
}
.field > span{
  font-size:var(--fs-sm);
  color:var(--fg-dim);
  font-weight:500;
}
.input,.textarea,.select{
  width:100%;
  min-height:46px;
  padding:11px 13px;
  border-radius:var(--radius-sm);
  border:1px solid var(--line);
  background:var(--bg-elev2);
  color:var(--fg);
  font-size:var(--fs-input);      /* M8：必须 16px，否则 iOS 聚焦会放大 */
  line-height:1.35;
  appearance:none;
  -webkit-appearance:none;
  transition:border-color var(--t) var(--ease), background var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.textarea{ min-height:88px; resize:vertical; }
.select{
  padding-right:34px;
  background-image:linear-gradient(45deg,transparent 50%,var(--fg-dim) 50%),linear-gradient(135deg,var(--fg-dim) 50%,transparent 50%);
  background-position:calc(100% - 17px) 20px,calc(100% - 12px) 20px;
  background-size:5px 5px,5px 5px;
  background-repeat:no-repeat;
}
.input:focus,.textarea:focus,.select:focus{
  outline:none;
  border-color:color-mix(in srgb, var(--accent) 70%, var(--line));
  background:var(--bg-elev);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
::placeholder{ color:var(--fg-faint); opacity:1; }

/* 校验就地标红（G3） */
.field.invalid .input,.field.invalid .textarea,.field.invalid .select{
  border-color:var(--danger);
  background:color-mix(in srgb, var(--danger) 8%, var(--bg-elev2));
  box-shadow:0 0 0 3px color-mix(in srgb, var(--danger) 12%, transparent);
}
.field-err{
  display:block;
  color:var(--danger);
  font-size:var(--fs-xs);
  min-height:0;
}
.field-hint{
  display:block;
  color:var(--fg-faint);
  font-size:var(--fs-xs);
}

/* 两列字段（桌面并排，手机堆叠） */
.field-row{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:var(--gap);
  min-width:0;
}
.field-row > .field{ margin-bottom:var(--gap); }
@media (max-width:400px){
  .field-row{ grid-template-columns:1fr; }
}

.form-actions{ display:flex; gap:10px; margin-top:4px; }
.form-actions .btn{ flex:1; }

/* ---------------------------------------------------------------- 8. 列表（.row / .list） */
.list{ display:block; }
.list > .row{ border-bottom:1px solid var(--line-soft); }
.list > .row:last-child{ border-bottom:0; }

.row{
  display:flex; align-items:center; gap:var(--gap);
  min-height:var(--tap);              /* M6 */
  padding:9px 0;
  color:inherit;
  min-width:0;
}
.row-main{ flex:1 1 auto; min-width:0; color:inherit; }
/* 应用行：图标 + 名称/域名，不折行；高度补到 44（M6） */
.row-main.inline{ flex-wrap:nowrap; gap:10px; min-height:var(--tap); align-items:center; }
.row .app-icon{
  flex:0 0 auto;
  width:26px;
  text-align:center;
  font-size:21px;
  line-height:1.2;
}
.row-ico{
  flex:0 0 auto;
  display:inline-flex; align-items:center; justify-content:center;
  width:24px;
  color:var(--fg-dim);
}
.row-ico .ico{ width:20px; height:20px; }
.row-main .t{
  display:block;
  font-size:var(--fs-md);
  font-weight:500;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.row-main .s{
  display:block;
  font-size:var(--fs-sm);
  color:var(--fg-dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.row-side{
  flex:0 0 auto;
  display:flex; align-items:center; gap:6px;
  font-size:var(--fs-sm);
  color:var(--fg-dim);
  white-space:nowrap;
}
.row-side .ico{ width:18px; height:18px; color:var(--fg-faint); }
a.row,a.row-main{ touch-action:manipulation; }
a.row:active{ background:var(--bg-elev2); }
@media (hover:hover){
  a.row:hover{ background:var(--bg-elev2); }
}

/* 行也能当按钮用（操作菜单里的每一项） */
button.row{
  width:100%;
  background:none;
  border:0;
  color:inherit;
  font:inherit;
  text-align:left;
  cursor:pointer;
  touch-action:manipulation;
  border-radius:var(--radius-sm);
  transition:background 120ms var(--ease);
}
button.row:active{ background:var(--bg-elev2); }
button.row:disabled{ cursor:default; background:none; }
button.row .row-side{ color:var(--fg-faint); }
/* 多选态（球友面板等）：只靠 aria-pressed 表达，不需要额外类名 */
button.row[aria-pressed="true"]{
  background:color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--accent) 34%, transparent);
}
button.row[aria-pressed="true"] .row-side{ color:var(--accent); }
button.row[aria-pressed="true"]:active{
  background:color-mix(in srgb, var(--accent) 18%, transparent);
}
@media (hover:hover){
  button.row:hover:not(:disabled){ background:var(--bg-elev2); }
  button.row[aria-pressed="true"]:hover:not(:disabled){
    background:color-mix(in srgb, var(--accent) 18%, transparent);
  }
}

/* --- 多选格子（球友面板）------------------------------------------------
   原来是整行列表，一行只放一个人、左边贴边右边留白，很浪费界面。
   改成 2 列网格卡片：名字 + 水平在上，频次在下，选中整格变绿。
   行高固定 58px，390 宽一屏能看到 8 个人。 */
.picker-grid{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  gap:8px;
}
@media (min-width:641px){
  .picker-grid{ grid-template-columns:repeat(3,minmax(0,1fr)); }
}
.picker-cell{
  display:flex; flex-direction:column; gap:3px;
  min-height:58px;
  padding:9px 11px;
  background:var(--bg-elev2);
  border:1px solid transparent;
  border-radius:var(--radius-sm);
  color:inherit;
  font:inherit;
  text-align:left;
  cursor:pointer;
  touch-action:manipulation;
  min-width:0;
  transition:background 120ms var(--ease), border-color 120ms var(--ease);
}
.picker-cell .pc-n{
  display:flex; align-items:center; gap:5px;
  font-size:var(--fs-md); font-weight:500;
  min-width:0;
}
.picker-cell .pc-n .t{
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  min-width:0;
}
.picker-cell .pc-n .ord{
  flex:0 0 auto;
  font-size:var(--fs-xs); font-weight:600;
  color:var(--accent);
}
.picker-cell .pc-m{
  font-size:var(--fs-xs);
  color:var(--fg-dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.picker-cell:active{ background:color-mix(in srgb, var(--fg) 10%, var(--bg-elev2)); }
.picker-cell[aria-pressed="true"]{
  background:color-mix(in srgb, var(--accent) 14%, transparent);
  border-color:color-mix(in srgb, var(--accent) 45%, transparent);
}
.picker-cell[aria-pressed="true"] .pc-m{ color:var(--accent); }
@media (hover:hover){
  .picker-cell:hover{ background:color-mix(in srgb, var(--fg) 6%, var(--bg-elev2)); }
  .picker-cell[aria-pressed="true"]:hover{
    background:color-mix(in srgb, var(--accent) 20%, transparent);
  }
}

/* 新球友输入区：和格子之间留出呼吸，不要贴着 */
.picker-new{ margin-top:16px; }
.picker-new .input{ margin-top:6px; }
.picker-new .btn{ margin-top:10px; }
/* sheet 里直接放网格时，底部留白不要靠 .sheet-bd 的 padding 撑 */
.picker-wrap{ display:block; }

.danger-text{ color:var(--danger); }

/* ---------------------------------------------------------------- 9. 标签 / 空态 / 骨架 / 错误 */
.badge{
  display:inline-flex; align-items:center; gap:4px;
  padding:2px 9px;
  border-radius:var(--radius-pill);
  font-size:var(--fs-xs);
  font-weight:600;
  line-height:1.6;
  background:var(--bg-elev2);
  color:var(--fg-dim);
  white-space:nowrap;
}
.badge.ok{ background:color-mix(in srgb,var(--accent) 16%,transparent); color:var(--accent); }
.badge.warn{ background:color-mix(in srgb,var(--warn) 18%,transparent); color:var(--warn); }
.badge.red{ background:color-mix(in srgb,var(--danger) 18%,transparent); color:var(--danger); }
.badge.info{ background:color-mix(in srgb,var(--info) 18%,transparent); color:var(--info); }

/* 空状态（G1）：图标 + 主文案 + 辅助文案 + 动作，层级分明 */
.empty{
  display:flex; flex-direction:column; align-items:center; gap:10px;
  padding:26px 12px;
  text-align:center;
  color:var(--fg-dim);
  font-size:var(--fs-sm);
}
.empty p{ max-width:36ch; }
.empty .empty-icon{
  color:var(--fg-faint);
  line-height:1;
  font-size:30px;          /* 兜底：还没换成 SVG 的页面（emoji）也保持原大小 */
  opacity:.9;
  margin-bottom:2px;
}
.empty .empty-icon .ico{ width:34px; height:34px; }

/* 骨架屏（加载态） */
@keyframes sk-shimmer{
  0%{ background-position:-320px 0; }
  100%{ background-position:320px 0; }
}
.skeleton{
  background:linear-gradient(90deg,var(--bg-elev2) 25%,color-mix(in srgb,var(--fg) 8%,var(--bg-elev2)) 37%,var(--bg-elev2) 63%);
  background-size:640px 100%;
  animation:sk-shimmer 1.3s linear infinite;
  border-radius:8px;
  min-height:14px;
}
.sk-line{ height:14px; margin:8px 0; }
.sk-line.w40{ width:40%; }
.sk-line.w60{ width:60%; }
.sk-line.w80{ width:80%; }
.sk-block{ height:56px; margin:10px 0; border-radius:var(--radius-sm); }
.sk-circle{ width:44px; height:44px; border-radius:50%; }

/* 错误态（G5） */
.load-error{
  display:flex; flex-direction:column; align-items:center; gap:10px;
  padding:22px 12px;
  text-align:center;
  font-size:var(--fs-sm);
  color:var(--fg-dim);
}
.load-error .le-msg{ color:var(--danger); font-weight:500; }
.load-error .le-meta{ font-size:var(--fs-xs); color:var(--fg-faint); }

/* ---------------------------------------------------------------- 10. 统计 / 进度条 */
.stats{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(96px,1fr));
  gap:10px;
}
/* 三格统计强制一行（网球概览）：数字都只有一两位，塌成「2 + 1」很难看。
   配合更短的标签（「近 30 天打卡」）在 320 宽也能三格并排。 */
.stats-3{ grid-template-columns:repeat(3,minmax(0,1fr)); }
@media (max-width:374px){
  .stats.stats-3 > .stat:last-child:nth-child(odd){ grid-column:auto; }
}
.stats-3 > .stat{ padding:9px 10px; }
/* 窄屏（≤374px，如 iPhone SE 320）统计格只能排 2 列，3 个格子会剩一个
   孤零零的半格 —— 让最后一个独占整行，看起来是"有意为之"而不是"没排好"。
   只在小屏生效：375 以上三格并排本来就够宽，拉满反而更丑。 */
@media (max-width:374px){
  .stats > .stat:last-child:nth-child(odd){ grid-column:1 / -1; }
}
.stat{
  display:flex; flex-direction:column; gap:2px;
  min-width:0;
  padding:10px 12px;
  border-radius:var(--radius-sm);
  background:var(--bg-elev2);
}
.stat-v{
  font-size:var(--fs-xl);
  font-weight:650;
  line-height:1.15;
  font-variant-numeric:tabular-nums;
  letter-spacing:-.4px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  display:flex; align-items:center; gap:4px;
}
.stat-v .ico{ width:17px; height:17px; color:var(--fg-faint); }
.stat-v.warn{ color:var(--warn); }
.stat-v.red{ color:var(--danger); }
.stat-k{
  font-size:var(--fs-xs);
  color:var(--fg-dim);
  line-height:1.25;
  /* 说明文字允许折行：96px 宽的中文标签在手机上不折行会被截成「近 12 周打…」 */
  overflow:hidden; overflow-wrap:anywhere;
}

/* 进度条 .meter/.meter-bar 已随服务器页重做删除（第 16 轮：服务器页改用 .sv-bar）。
   首页用的是 .gauge / .spark，其它页面都没有进度条，属确认过的死代码。 */

/* ---------------------------------------------------------------- 10b. 仪表盘组件
   首页「服务器 / 网球」两张重心卡用的通用件：环形仪表 / 迷你折线 / 分段读数 /
   等级徽章 / 状态 pill / 产品行。都是通用类（不绑首页），其它页面可直接复用。 */

/* --- 环形仪表 .gauge：SVG 双层 circle，进度弧靠 stroke-dashoffset（不用 conic-gradient） --- */
.gauges{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:6px;
  margin:2px 0 14px;
}
.gauge{
  display:flex; flex-direction:column; align-items:center; gap:5px;
  min-width:0;
}
.gauge-ring{
  position:relative;
  width:64px; height:64px;      /* 外径 64（规格要求） */
  flex:0 0 auto;
}
.gauge-ring svg{ width:100%; height:100%; }
/* 轨道 + 进度弧。stroke-width 5、半径 27（周长 2πr ≈ 169.646） */
.gauge-track{ stroke:var(--bg-elev2); }
.gauge-arc{
  stroke:var(--accent);
  stroke-linecap:round;
  /* 弧从 12 点开始顺时针 */
  transform:rotate(-90deg);
  transform-origin:50% 50%;
  transition:stroke-dashoffset .45s var(--ease), stroke .3s var(--ease);
}
.gauge-arc.warn{ stroke:var(--warn); }
.gauge-arc.red{ stroke:var(--danger); }
.gauge-val{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  font-size:var(--fs-lg);
  font-weight:600;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
  letter-spacing:-.6px;
  color:var(--fg);
}
.gauge-val.warn{ color:var(--warn); }
.gauge-val.red{ color:var(--danger); }
.gauge-k{
  font-size:var(--fs-2xs);
  color:var(--fg-dim);          /* 标签用 dim（不是 faint），形成三档灰阶 */
  line-height:1.2;
  text-align:center;
  max-width:100%;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* 三个环下面那一行极小的硬件说明原本是服务器页概览卡用的（.gauges-note）；
   第 16 轮服务器页改成 .sv-* 独立设计后已无引用，故删除。 */

/* --- 迷你折线 .spark：viewBox 100×28 + preserveAspectRatio:none，
       线宽用 vector-effect 保持 2px 不被横向拉伸 --- */
.spark{
  position:relative;
  min-width:0;
  height:28px;
  color:var(--info);            /* 折线/面积都吃 currentColor */
}
.spark.down{ color:var(--accent); }
.spark svg{ width:100%; height:28px; display:block; overflow:visible; }
.spark-line{
  fill:none;
  stroke:currentColor;
  stroke-width:2;
  stroke-linecap:round;
  stroke-linejoin:round;
  vector-effect:non-scaling-stroke;
}
/* 面积用同色 12% 透明填充（不用渐变 defs，避免每个实例都要唯一 id） */
.spark-area{ stroke:none; fill:currentColor; opacity:.12; }
/* 末端圆点：零长线段 + 圆头线帽 + 非缩放描边，
   这样在 preserveAspectRatio:none 的拉伸坐标里也不会被拉成椭圆。 */
.spark-dot{
  fill:none;
  stroke:currentColor;
  stroke-width:5;
  stroke-linecap:round;
  vector-effect:non-scaling-stroke;
}
/* 只有一个点时也画一条水平线（首点不留白），线稍淡表示"还没有趋势" */
.spark.single .spark-line{ opacity:.55; }

/* --- 网络读数：上行/下行各一块，当前值是主信息，曲线在下面做趋势 --- */
.net{
  display:flex; flex-direction:column; gap:10px;
  margin-top:12px; padding-top:10px;
  border-top:1px solid var(--line-soft);
}
.net-hero{ gap:12px; }
.net-row{
  display:flex; flex-direction:column; gap:3px;
  min-width:0;
}
.net-top{
  display:flex; align-items:baseline; justify-content:space-between; gap:10px;
  min-width:0;
}
.net-k{
  display:inline-flex; align-items:center; gap:4px;
  font-size:var(--fs-2xs);
  color:var(--fg-dim);
  white-space:nowrap;
}
.net-k .ico{ width:14px; height:14px; color:var(--fg-faint); }
/* 当前速率是主信息：手机上先看到"现在多快"，曲线只做趋势参考 */
.net-v{
  font-size:var(--fs-lg);
  font-weight:650;
  line-height:1.15;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
  letter-spacing:-.45px;
  white-space:nowrap;
  text-align:right;
}
.net-row .spark{ margin-top:1px; }
.net-row.up .spark, .net-row.up .net-v{ color:var(--info); }
.net-row.down .spark, .net-row.down .net-v{ color:var(--accent); }
/* 服务器页的网络卡原本复用首页 .net + .net-tall（52px 高折线）；
   第 16 轮改成 .sv-net / .sv-chart 面积图，.net-tall 已无引用，故删除。
   .net / .net-row / .net-v 仍由首页「服务器」卡使用，保留。
   窄屏：主数字略收，避免 320 宽下和标签挤在一起 */
@media (max-width:359px){
  .net-v{ font-size:var(--fs-md); }
}

/* --- 分段读数 .metrics：三段横向、竖线分隔、上大数字下小标签 --- */
.metrics{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  margin:14px 0 2px;
}
.metrics-cell{
  position:relative;
  display:flex; flex-direction:column; align-items:center; gap:1px;
  min-width:0;
  padding:0 4px;
}
.metrics-cell + .metrics-cell::before{
  content:"";
  position:absolute; left:0; top:15%; bottom:15%; width:1px;
  background:var(--line);
}
.metrics-v{
  font-size:18px;
  font-weight:600;
  line-height:1.2;
  letter-spacing:-.4px;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
  white-space:nowrap;
}
.metrics-k{
  font-size:var(--fs-2xs);
  color:var(--fg-dim);
  text-align:center;
  line-height:1.25;
}
.metrics-k .dim{ color:var(--fg-faint); font-weight:400; }

/* --- 等级徽章 .level-badge（超椭圆：圆角给到 38%） --- */
.level-badge{
  display:inline-flex; align-items:center; justify-content:center;
  width:60px; height:60px;
  flex:0 0 auto;
  border-radius:38%;
  background:color-mix(in srgb, var(--accent) 13%, transparent);
  border:1.5px solid color-mix(in srgb, var(--accent) 32%, transparent);
  color:var(--accent);
  font-size:var(--fs-xl);
  font-weight:600;
  letter-spacing:-.8px;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}
.level-badge.empty{
  color:var(--fg-faint);
  background:var(--bg-elev2);
  border-color:var(--line);
}

/* --- 状态 pill（比 .badge 更明确：red 实底白字，warn/ok 浅底彩字） --- */
.pill{
  display:inline-flex; align-items:center; gap:4px;
  padding:3px 10px;
  border-radius:var(--radius-pill);
  font-size:var(--fs-2xs);
  font-weight:600;
  line-height:1.5;
  white-space:nowrap;
  background:var(--bg-elev2);
  color:var(--fg-dim);
}
.pill.ok{ background:color-mix(in srgb, var(--accent) 15%, transparent); color:var(--accent); }
.pill.warn{ background:color-mix(in srgb, var(--warn) 17%, transparent); color:var(--warn); }
.pill.red{ background:var(--danger); color:#fff; }
.pill.info{ background:color-mix(in srgb, var(--info) 17%, transparent); color:var(--info); }

/* --- 产品行（网球卡「最该换线的拍」）：缩略图 + 两行文字 + 状态 pill --- */
.product{
  display:flex; align-items:center; gap:10px;
  margin-top:12px; padding-top:11px;
  border-top:1px solid var(--line-soft);
  min-width:0;
  color:inherit;          /* 它是 <a>，别继承全局链接蓝 */
}
.product:active{ background:var(--bg-elev2); border-radius:var(--radius-sm); }
@media (hover:hover){
  .product:hover .product-name{ color:var(--accent); }
}
.product-thumb{
  display:inline-flex; align-items:center; justify-content:center;
  /* 竖版 3:4 —— 球拍是竖着的，塞进 36×36 方块只剩中间一条拍框，看不清（用户反馈）。
     44×58 在手机上行高仍然 ≤ 66，4 行 + 「还有 N 把」在 390 宽不会溢出。 */
  width:44px; height:58px;
  flex:0 0 auto;
  border-radius:10px;
  background:var(--bg-elev2);
  color:var(--fg-faint);
  overflow:hidden;
}
.product-thumb img{ width:100%; height:100%; object-fit:contain; }
.product-thumb .ico{ width:20px; height:20px; }
.product-body{ flex:1 1 auto; min-width:0; }
.product-name{
  display:block;
  font-size:15px;
  font-weight:600;
  letter-spacing:-.1px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.product-sub{
  display:block;
  margin-top:1px;
  font-size:var(--fs-2xs);
  color:var(--fg-dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.product-side{ flex:0 0 auto; }

/* 首页网球卡「所有在用球拍」：每行一条，行间用极淡分隔线（第一行不留顶线） */
.product-list{
  display:flex; flex-direction:column;
  margin-top:12px;
  border-top:1px solid var(--line-soft);
}
.product-list .product{ margin-top:0; padding-top:8px; padding-bottom:8px; border-top:0; }
.product-list .product + .product{ border-top:1px solid var(--line-soft); }
/* 超过 4 把时的「还有 N 把 ›」：一行小字，不抢行 */
.product-more{
  display:inline-flex; align-items:center; justify-content:flex-end; gap:2px;
  align-self:flex-end;
  margin-top:2px;
  min-height:var(--tap);
  padding:0 2px;
  font-size:var(--fs-xs);
  color:var(--fg-faint);
  transition:color var(--t) var(--ease);
}
.product-more .ico{ width:14px; height:14px; }
.product-more:active{ color:var(--accent); }
@media (hover:hover){
  .product-more:hover{ color:var(--accent); }
}

/* --- 重心卡内部的轻量页脚：12px 极淡、右对齐、不占一整行 44px --- */
.hero-foot{
  display:flex; align-items:center; justify-content:flex-end; gap:4px;
  margin-top:10px;
  font-size:var(--fs-2xs);
  line-height:1.3;
  color:var(--fg-faint);
  font-variant-numeric:tabular-nums;
  min-width:0;
}
.hero-foot .ico{ width:13px; height:13px; }
.hero-foot a{
  position:relative;
  display:inline-flex; align-items:center; gap:2px;
  color:var(--fg-faint);
  transition:color var(--t) var(--ease);
}
/* 视觉 12px，但点击区按 M6 补到 ≥44×44（::after 扩热区，不改布局高度）。
   M6 判定口径：可点区 = 元素盒 + ::after 的负 inset 扩张。 */
.hero-foot a::after{ content:""; position:absolute; inset:-15px -12px; }
.hero-foot a:active{ color:var(--accent); }
@media (hover:hover){
  .hero-foot a:hover{ color:var(--accent); }
}

/* --- 网球卡头部：徽章 + 两行说明 --- */
.hero-head{
  display:flex; align-items:center; gap:12px;
  min-width:0;
}
.hero-head-text{ min-width:0; }
.hero-head-k{
  font-size:var(--fs-2xs);
  color:var(--fg-dim);
  letter-spacing:.2px;
}
.hero-head-s{
  margin-top:2px;
  font-size:var(--fs-xs);
  color:var(--fg);
  font-weight:500;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.hero-head-s .dim{ color:var(--fg-faint); font-weight:400; }

/* ---------------------------------------------------------------- 11. 应用卡片网格（P1/P2） */
.grid{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
  gap:10px;
  /* 所有行等高、格子撑满：同一网格里的卡片高度必须一致，
     否则图标/域名长短不同就会参差不齐（需求 3.4：一眼扫过去要整齐）。 */
  align-items:stretch;
  grid-auto-rows:1fr;
}
.grid > .app{ height:100%; }
.app{
  display:flex; flex-direction:column; gap:6px;
  min-height:calc(var(--tap) * 2);
  padding:12px;
  color:inherit;
  position:relative;
  touch-action:manipulation;
  transition:background var(--t) var(--ease), border-color var(--t) var(--ease);
}
.app .app-icon{ font-size:26px; line-height:1.1; }
.app .app-icon .ico{ width:26px; height:26px; color:var(--fg-dim); }
.app .app-name{
  font-size:var(--fs-md);
  font-weight:600;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.app .app-host{
  font-size:var(--fs-xs);
  color:var(--fg-faint);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.app .app-fav{
  position:absolute; top:6px; right:6px;
  color:var(--warn);
  padding:4px;
  line-height:1;
}
.app .app-fav .ico{ width:13px; height:13px; }
.app:active{ background:var(--bg-elev2); }
@media (hover:hover){
  .app:hover{ border-color:var(--line-strong); }
}

/* ---------------------------------------------------------------- 12. （原 12 周热力图已删除）
   2026-11 第 15 轮：网球页概览与打球日历均改为 30 天窗口 + 月历（.month.compact），
   旧的 84 格热力图 .heat / .heat-cell / .heat-legend 在前端已无任何引用，整段移除。 */

/* ---------------------------------------------------------------- 13. 月历（网球页主视觉）
   类名与 tennis.js 约定：
     .month > .month-hd( .month-title + .month-nav-wrap > button.month-nav-btn ×2 )
            + .month-grid( .month-wd ×7 + .day-blank* + button.day > span.day-num )
   .day 的尺寸只由 .month-grid > .day 决定；.day 自己不带 width/aspect-ratio，
   这样图例里裸 <span class="day day-lv3 day-match"> 也能用（见 .month-legend）。 */
.month{ display:block; }
.month-hd{
  display:flex; align-items:center; justify-content:space-between;
  gap:10px;
  margin-bottom:10px;
  min-height:var(--tap);
}
.month-title{
  font-size:var(--fs-lg);
  font-weight:700;
  letter-spacing:-.2px;
  font-variant-numeric:tabular-nums;
}
.month-nav-wrap{ display:flex; align-items:center; gap:6px; }
.month-nav-btn{
  width:var(--tap); height:var(--tap);
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:var(--radius-pill);
  background:var(--bg-elev2);
  color:var(--fg-dim);
  font-size:20px;
  line-height:1;
  touch-action:manipulation;
  transition:background var(--t) var(--ease), color var(--t) var(--ease), opacity var(--t) var(--ease);
}
.month-nav-btn .ico{ width:20px; height:20px; }
.month-nav-btn:active{ background:color-mix(in srgb,var(--fg) 12%,var(--bg-elev2)); color:var(--fg); }
.month-nav-btn:disabled{ opacity:.35; }
@media (hover:hover){
  .month-nav-btn:hover:not(:disabled){ color:var(--fg); background:color-mix(in srgb,var(--fg) 9%,var(--bg-elev2)); }
}

.month-grid{
  display:grid;
  grid-template-columns:repeat(7,1fr);
  gap:2px;
  width:100%;
}
/* 格子尺寸的唯一来源：网格列宽 + 正方形（390 宽 ≈ 46×46，375 宽 ≈ 44×44 → M6 达标） */
.month-grid > .day{
  min-height:44px;
  aspect-ratio:1 / 1;
  width:100%;
}
/* 宽屏上限制月历宽度：否则 960 内容区里 7 列会变成 127px 见方的大方块，一屏放不下。
   手机上不受影响（<641px 走上面的规则）。整块（标题 + 网格 + 图例）一起限宽并居中。 */
@media (min-width:641px){
  .month{ max-width:480px; margin-inline:auto; }
}
.month-wd{
  font-size:var(--fs-xs);
  font-weight:500;
  color:var(--fg-faint);
  text-align:center;
  padding-bottom:4px;
  line-height:1.4;
}
.day-blank{
  min-width:0;
  aspect-ratio:1 / 1;
}

/* 单日格：颜色由 --day-hue（种类）× --day-tint（强度）两个变量驱动 */
.day{
  --day-hue:var(--fg);
  --day-tint:5%;
  display:flex; align-items:center; justify-content:center;
  min-width:0;
  padding:0;
  border:0;
  border-radius:10px;
  background:color-mix(in srgb, var(--day-hue) var(--day-tint), transparent);
  color:var(--fg-dim);
  font:inherit;
  font-variant-numeric:tabular-nums;
  touch-action:manipulation;
  transition:background var(--t) var(--ease), color var(--t) var(--ease), transform 120ms var(--ease);
}
.month-grid > .day:active{ transform:scale(.94); }

/* 强度 5 档（只改 tint，不换色相） */
.day-lv1{ --day-tint:16%; }
.day-lv2{ --day-tint:28%; }
.day-lv3{ --day-tint:42%; }
.day-lv4{ --day-tint:58%; }
.day-lv5{ --day-tint:76%; }
.day-lv3,.day-lv4,.day-lv5{ color:var(--fg); }

/* 种类：比赛=绿 / 拉球=蓝 / 都有=双色对角渐变 */
.day-match{ --day-hue:var(--accent); }
.day-practice{ --day-hue:var(--info); }
.day-mixed{
  --day-hue:var(--accent);
  background-image:linear-gradient(135deg,
    color-mix(in srgb, var(--accent) var(--day-tint), transparent) 0 48%,
    color-mix(in srgb, var(--info) var(--day-tint), transparent) 52% 100%);
}

.day-num{ font-size:13px; font-weight:500; line-height:1; }

/* 今天（且未被选中日占用 .day-today 时）：数字直接标成 accent。
   13px 字号下单纯加重 font-weight 辨识度不够，实测用户分不出「今天是几号」。
   注意不要动 .day-today —— 它在日程页是「选中日」、在网球页是「今天」，两边共用。 */
.day-num.strong{ font-weight:700; color:var(--accent); }

/* 今天：内描边环（不改底色，和强度叠加也不打架） */
.day-today{
  box-shadow:inset 0 0 0 1.5px var(--accent);
  color:var(--fg);
  font-weight:650;
}
/* 未来日期：降透明度（仍可点）。
   浅色底上 .42 的灰几乎读不出数字 → 浅色改成实色 --fg-faint（比正常日浅一档、但可读）。
   和顶部变量块一样，浅色有两个来源（系统偏好 / 用户强制），所以这段也写两份。 */
.day-future{ opacity:.42; }
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]) .day-future{ opacity:1; color:var(--fg-faint); }
}
:root[data-theme="light"] .day-future{ opacity:1; color:var(--fg-faint); }

/* 图例：容器 + 裸 .day 小色块（14×14） */
.month-legend{
  display:flex; flex-wrap:wrap; align-items:center;
  gap:8px 14px;
  margin-top:10px;
  font-size:var(--fs-xs);
  color:var(--fg-faint);
}
.month-legend .inline{ gap:6px; flex-wrap:nowrap; }
/* 强度 5 档独占一行：否则「少 ▢▢▢▢▢ 多」这一串会和右下角 FAB 叠在一起
   （FAB 固定在视口右下，图例在文档流里，两者宽度都很吃紧）。 */
.month-legend > .inline:last-child{ flex-basis:100%; }
.month-legend .day{
  width:14px; height:14px;
  min-height:0;
  aspect-ratio:auto;
  border-radius:4px;
  flex:0 0 auto;
}

/* 点某天后的详情（弹层里） */
.day-details{
  display:flex; flex-direction:column; gap:10px;
  font-size:var(--fs-md);
}

/* --- 紧凑版月历 .month.compact -------------------------------------------
   用户反馈「两个月视图有点大，手机几乎占一整屏」。
   加上这个修饰类后，标题+网格+图例三块在 390 宽下总高 ≤ 400px：
     标题行 44 + 6  ≈ 50
     星期行 18 + 2  ≈ 20
     6 周 ×44 + 5×3 = 279
     图例 16 + 6    ≈ 22   → 合计 ≈ 371
   做法：格子取消 aspect-ratio 改固定 44px 高（M6 的触控高度不变），
   gap 收到 3px，图例压成一行 nowrap + 横向滚动（滚动条隐藏）。 */
.month.compact .month-hd{
  margin-bottom:6px;
  gap:8px;
}
.month.compact .month-title{ font-size:var(--fs-md); }
.month.compact .month-grid{ gap:3px; }
.month.compact .month-grid > .day{
  height:44px;
  min-height:44px;
  aspect-ratio:auto;
}
.month.compact .month-wd{
  padding-bottom:2px;
  font-size:var(--fs-2xs);
}
/* 空白格也跟着固定 44 高，否则 aspect-ratio:1/1 会把行高撑到列宽（≈46） */
.month.compact .day-blank{
  height:44px;
  min-height:44px;
  aspect-ratio:auto;
}
.month.compact .month-legend{
  flex-wrap:nowrap;
  gap:0 10px;
  margin-top:6px;
  padding-bottom:1px;
  overflow-x:auto;
  overflow-y:hidden;
  white-space:nowrap;
  /* 隐藏横向滚动条（三套前缀都要写，Firefox 用 scrollbar-width） */
  scrollbar-width:none;
  -ms-overflow-style:none;
}
.month.compact .month-legend::-webkit-scrollbar{ width:0; height:0; }
/* 紧凑版里不再强制换行：图例是一整条可横向滚动的行 */
.month.compact .month-legend > .inline:last-child{ flex-basis:auto; }
.month.compact .month-legend > .inline{ flex:0 0 auto; }

/* --- 网球页月历的手机端瘦身 -----------------------------------------------
   网球页的图例/小结不在 .month 里面（tennis.js 挂在 [data-calendar] 下），
   所以 .month.compact 那一套选择器够不到。这里按同一套尺寸直接作用在网球页，
   不改 tennis.js 一行：格子 44 高、gap 3px、图例压成一行可横滑。 */
@media (max-width:640px){
  #view[data-page="tennis"] [data-calendar] .month-grid{ gap:3px; }
  #view[data-page="tennis"] [data-calendar] .month-grid > .day{
    height:44px;
    min-height:44px;
    aspect-ratio:auto;
  }
  #view[data-page="tennis"] [data-calendar] .day-blank{
    height:44px;
    min-height:44px;
    aspect-ratio:auto;
  }
  #view[data-page="tennis"] [data-calendar] .month-legend{
    flex-wrap:nowrap;
    gap:0 10px;
    padding-bottom:1px;
    overflow-x:auto;
    overflow-y:hidden;
    white-space:nowrap;
    scrollbar-width:none;
    -ms-overflow-style:none;
  }
  #view[data-page="tennis"] [data-calendar] .month-legend::-webkit-scrollbar{ width:0; height:0; }
  #view[data-page="tennis"] [data-calendar] .month-legend > .inline{ flex:0 0 auto; }
}

/* --- 日程页格子配色 .day-sched -------------------------------------------
   日程页与网球页共用 .month/.day 结构，但语义不同：
     网球页：.day-match/.day-practice/.day-mixed 表示打球类型（绿/蓝/双色）
     日程页：只关心"这天有几件事"，用 accent 单色由浅到深表示 1 件 → 多件
   所以单开 .day-sched，不碰上面那三个网球专用类。 */
.day-sched{
  --day-hue:var(--accent);
  --day-tint:20%;
  color:var(--fg);
}
.day-sched.many{ --day-tint:34%; }

/* ---------------------------------------------------------------- 14. 球拍（平面图 3:4，不拉伸） */
.racket{
  display:flex; align-items:stretch; gap:var(--gap);
  min-width:0;
}
.racket-img{
  flex:0 0 auto;
  width:96px;
  aspect-ratio:3 / 4;
  border-radius:var(--radius-sm);
  background:var(--bg-elev2);
  display:flex; align-items:center; justify-content:center;
  overflow:hidden;
}
.racket-img img{
  width:100%; height:100%;
  object-fit:contain;       /* 不拉伸 */
}
.racket-img.empty::after{
  content:"🎾";
  font-size:30px;
  opacity:.4;
}
.racket-info{
  flex:1 1 auto;
  min-width:0;
  display:flex; flex-direction:column; gap:4px;
  justify-content:center;
}
.racket-info .rn{ font-weight:600; }
.racket-info .rs{ font-size:var(--fs-sm); color:var(--fg-dim); }

/* 球拍详情弹层顶部大图（tennis.js 用，role=button 可点放大 → core.js ctx.viewer）。
   竖图 3:4 用 contain 完整显示，不裁切；点按有轻微反馈，键盘 focus 有描边。 */
.racket-figure{
  display:flex;
  align-items:center;
  justify-content:center;
  height:200px;
  margin-bottom:var(--gap);
  border-radius:var(--radius-sm);
  background:var(--bg-elev2);
  overflow:hidden;
  cursor:zoom-in;
  touch-action:manipulation;
  transition:background var(--t) var(--ease), transform 120ms var(--ease);
}
.racket-figure picture{ display:contents; }
.racket-figure img{
  max-width:100%;
  max-height:100%;
  width:auto; height:auto;
  object-fit:contain;       /* 竖图完整显示，不裁切 */
}
.racket-figure:active{ transform:scale(.985); }
.racket-figure:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
@media (hover:hover){
  .racket-figure:hover{ background:color-mix(in srgb, var(--fg) 9%, var(--bg-elev2)); }
}
@media (max-width:359px){
  .racket-figure{ height:180px; }
}

/* ---------------------------------------------------------------- 15. 页内切换（tabs / seg） */
.tabs{
  display:flex; gap:4px;
  padding:4px;
  background:var(--bg-sunken);
  border-radius:var(--radius-sm);
  min-width:0;
}
.tab{
  flex:1 1 0;
  min-width:0;
  min-height:var(--tap);            /* M6 */
  padding:0 10px;
  border-radius:9px;
  color:var(--fg-dim);
  font-size:var(--fs-sm);
  font-weight:600;
  display:flex; align-items:center; justify-content:center;
  touch-action:manipulation;
  transition:background var(--t) var(--ease), color var(--t) var(--ease);
}
.tab.active{ background:var(--bg-elev); color:var(--fg); box-shadow:var(--shadow); }
.tab:active{ color:var(--fg); }

.seg{
  display:inline-flex; gap:4px;
  padding:3px;
  background:var(--bg-sunken);
  border-radius:var(--radius-sm);
  max-width:100%;
}
.seg-item{
  min-height:var(--tap);
  min-width:var(--tap);
  padding:0 14px;
  border-radius:9px;
  color:var(--fg-dim);
  font-size:var(--fs-sm);
  font-weight:600;
  display:inline-flex; align-items:center; justify-content:center;
  touch-action:manipulation;
  transition:background var(--t) var(--ease), color var(--t) var(--ease);
}
.seg-item.active{ background:var(--bg-elev); color:var(--fg); box-shadow:var(--shadow); }

/* 整行等分的分段控件（设置页「外观」三选一用）：每格均分、图标 + 文字横排 */
.seg-block{
  display:flex;
  width:100%;
}
.seg-block .seg-item{
  flex:1 1 0;
  min-width:0;
  padding:0 6px;
  gap:5px;
  white-space:nowrap;
  overflow:hidden;
}
.seg-block .seg-item .ico{ width:17px; height:17px; }
/* 窄屏（320 宽）三个都带图标会挤，图标藏起来只留文字 */
@media (max-width:359px){
  .seg-block .seg-item .ico{ display:none; }
  .seg-block .seg-item{ font-size:var(--fs-xs); padding:0 4px; }
}

/* ---------------------------------------------------------------- 16. fab / sheet / toast */
.fab{
  position:fixed;
  right:calc(var(--pad) + var(--safe-r));
  bottom:calc(var(--nav-h) + var(--safe-b) + 16px);
  z-index:35;
  width:56px; height:56px;
  border-radius:50%;
  background:var(--accent);
  color:var(--accent-fg);
  display:flex; align-items:center; justify-content:center;
  box-shadow:var(--shadow-pop);
  font-size:26px;                /* 兜底：页面里用「＋」字形当 fab 图标时大小合适 */
  font-weight:300;
  line-height:1;
  touch-action:manipulation;
  transition:transform 120ms var(--ease), filter var(--t) var(--ease);
}
.fab .ico{ width:26px; height:26px; stroke-width:2.2; }
.fab:active{ transform:scale(.94); filter:brightness(.96); }
@media (min-width:641px){
  .fab{ bottom:calc(var(--safe-b) + 24px); }
}

/* 底部弹层 */
.sheet-backdrop{
  position:fixed; inset:0; z-index:60;
  display:none;
  align-items:flex-end; justify-content:center;
  background:rgba(0,0,0,.5);
  -webkit-backdrop-filter:blur(8px);
  backdrop-filter:blur(8px);
  padding:0;
  overscroll-behavior:contain;
}
.sheet-backdrop.open{ display:flex; }
@media (min-width:641px){
  .sheet-backdrop{ align-items:center; padding:24px; }
}

@keyframes sheet-up{ from{ transform:translateY(28px); opacity:.65; } to{ transform:translateY(0); opacity:1; } }

.sheet{
  display:flex; flex-direction:column;
  width:100%;
  max-height:88dvh;
  background:var(--bg-elev);
  border:1px solid var(--line);
  border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  padding-bottom:var(--safe-b);
  padding-left:var(--safe-l);
  padding-right:var(--safe-r);
  animation:sheet-up 180ms var(--ease);
  overflow:hidden;
  box-shadow:var(--shadow-pop);
}
@media (min-width:641px){
  .sheet{
    max-width:480px;
    max-height:82dvh;
    border-radius:var(--radius-lg);
  }
}
.sheet-hd{
  display:flex; align-items:center; justify-content:space-between; gap:var(--gap);
  padding:14px var(--pad) 10px;
  border-bottom:1px solid var(--line-soft);
  flex:0 0 auto;
}
.sheet-hd h2{
  font-size:var(--fs-lg);
  font-weight:650;
  letter-spacing:-.2px;
  min-width:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.sheet-bd{
  padding:var(--pad);
  overflow-y:auto;
  -webkit-overflow-scrolling:touch;
  flex:1 1 auto;
  min-height:0;
  overscroll-behavior:contain;
}
.sheet-bd:last-child{ padding-bottom:calc(var(--pad) + 4px); }
.sheet-ft{
  display:flex; gap:10px;
  padding:10px var(--pad) calc(10px + var(--safe-b));
  border-top:1px solid var(--line-soft);
  flex:0 0 auto;
}
.sheet-ft .btn{ flex:1; }
/* 有些页面仍把按钮包在 .btn-row 里再塞进 .sheet-ft（多一层 flex 子元素）。
   那层容器默认宽度只有内容宽，会把按钮压窄导致「记下来」折成两行 —— 让它撑满。 */
.sheet-ft > .btn-row{ flex:1; min-width:0; }
.sheet-ft > .btn-row .btn{ flex:1; min-width:0; white-space:nowrap; }
/* 任何弹层底部按钮都不折行（中文两个字在 72px 宽、16px 内边距下会断行） */
.sheet-ft .btn{ white-space:nowrap; }
.sheet-msg{ font-size:var(--fs-md); color:var(--fg); }
.sheet-msg.muted{ color:var(--fg-dim); }

/* toast */
.toasts{
  position:fixed;
  left:50%;
  bottom:calc(var(--nav-h) + var(--safe-b) + 20px);
  transform:translateX(-50%);
  z-index:80;
  display:flex; flex-direction:column; align-items:center; gap:8px;
  width:max-content;
  max-width:min(92vw, 420px);
  pointer-events:none;
}
@media (min-width:641px){
  .toasts{ bottom:calc(var(--safe-b) + 28px); }
}
@keyframes toast-in{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:translateY(0); } }
.toast{
  width:auto;
  padding:10px 16px;
  border-radius:var(--radius-pill);
  background:color-mix(in srgb, var(--bg-elev2) 92%, transparent);
  -webkit-backdrop-filter:blur(12px);
  backdrop-filter:blur(12px);
  color:var(--fg);
  border:1px solid var(--line);
  box-shadow:var(--shadow-pop);
  font-size:var(--fs-sm);
  line-height:1.4;
  text-align:center;
  animation:toast-in 160ms var(--ease);
  word-break:break-word;
}
.toast.ok{ border-color:color-mix(in srgb,var(--accent) 50%,var(--line)); }
.toast.error{ border-color:color-mix(in srgb,var(--danger) 60%,var(--line)); color:var(--danger); }
.toast.info{ border-color:color-mix(in srgb,var(--info) 45%,var(--line)); }
.toast.hide{ opacity:0; transition:opacity .2s var(--ease); }

/* ---------------------------------------------------------------- 17. 登录页 */
.login-wrap{
  max-width:380px;
  margin:0 auto;
  padding-top:min(11vh, 88px);
  display:flex; flex-direction:column; gap:22px;
}
.login-brand{ text-align:center; }
.login-brand .login-logo{
  width:74px; height:74px;
  margin:0 auto 14px;
  border-radius:24px;
  display:flex; align-items:center; justify-content:center;
  color:var(--accent);
  background:color-mix(in srgb, var(--accent) 13%, var(--bg-elev));
  border:1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow:var(--shadow-card);
}
.login-brand .login-logo .ico{ width:38px; height:38px; }
.login-brand h2{
  font-size:var(--fs-2xl);
  font-weight:700;
  letter-spacing:.2px;
}
.login-brand p{ margin-top:4px; }
.login-card{ display:block; }
.login-lock{
  margin-top:10px;
  text-align:center;
  color:var(--warn);
  font-size:var(--fs-sm);
}
.login-wrap .card{ padding:calc(var(--pad) + 4px); }

/* 登录页：不收进 tabbar 的下边距，也隐藏底部导航与顶栏操作 */
body.route-login .tabbar{ display:none; }
body.route-login .topbar-action{ display:none; }
body.route-login .topbar-settings{ display:none; }
body.route-login #view{ padding-bottom:24px; }
/* 未登录/登录中也别显示顶栏操作（core.js 会切 body.authed 类）
   设置页要登录才能开，所以设置按钮和刷新按钮一起藏。 */
body:not(.authed) .topbar-action{ display:none; }
body:not(.authed) .topbar-settings{ display:none; }

/* 弹层打开时锁住页面滚动（防背景跟着滚） */
body.sheet-open{ overflow:hidden; }
body.viewer-open{ overflow:hidden; }

/* ---------------------------------------------------------------- 17b. 全屏看图（core.js ctx.viewer）
   结构固定：.viewer-backdrop > .viewer > .viewer-img + .viewer-x + .viewer-cap
   深色遮罩 + 图片 contain，点遮罩 / 关闭按钮 / Esc 都能关（逻辑在 core.js）。 */
.viewer-backdrop{
  position:fixed;
  inset:0;
  z-index:70;                       /* 盖住 sheet(60)，低于 toast(80) */
  display:flex;
  align-items:center;
  justify-content:center;
  padding:calc(16px + var(--safe-t)) calc(16px + var(--safe-r))
          calc(16px + var(--safe-b)) calc(16px + var(--safe-l));
  background:rgba(0,0,0,.82);
  -webkit-backdrop-filter:blur(6px);
  backdrop-filter:blur(6px);
  opacity:0;
  transition:opacity var(--t) var(--ease);
}
.viewer-backdrop.open{ opacity:1; }
.viewer{
  position:relative;
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:10px;
  max-width:100%;
  min-width:0;
}
.viewer-img{
  display:block;
  width:auto;
  height:auto;
  max-width:min(92vw,520px);
  max-height:82vh;
  max-height:min(82vh,82dvh);
  object-fit:contain;
  border-radius:var(--radius-sm);
  background:transparent;
  /* 图片本身不响应点击，点空白处才关 */
  touch-action:manipulation;
}
.viewer-x{
  position:absolute;
  top:-6px; right:-6px;
  width:var(--tap); height:var(--tap);
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:var(--radius-pill);
  /* 深色底 + 白图标：产品图常常是白底，白底白按钮会看不见 */
  background:rgba(0,0,0,.55);
  color:#fff;
  box-shadow:0 2px 10px rgba(0,0,0,.35);
  touch-action:manipulation;
  transition:background var(--t) var(--ease), transform 120ms var(--ease);
}
.viewer-x .ico{ width:22px; height:22px; }
.viewer-x:active{ background:rgba(0,0,0,.72); transform:scale(.94); }
@media (hover:hover){
  .viewer-x:hover{ background:rgba(0,0,0,.72); }
}
.viewer-cap{
  max-width:min(92vw,520px);
  font-size:var(--fs-sm);
  line-height:1.4;
  color:rgba(255,255,255,.78);
  text-align:center;
  overflow-wrap:anywhere;
}
/* 关闭按钮在窄屏上要能落在图片内（负偏移会出屏） */
@media (max-width:420px){
  .viewer-x{ top:6px; right:6px; }
}

.form{ display:block; }
.form .field:last-of-type{ margin-bottom:4px; }

/* ---------------------------------------------------------------- 18. 文本 / 工具类 */
.muted{ color:var(--fg-dim); }
.dim{ color:var(--fg-dim); opacity:.62; }   /* 已过去的日程（C 系列） */
.faint{ color:var(--fg-faint); }
.small{ font-size:var(--fs-sm); }
.xs{ font-size:var(--fs-xs); }
.mono{ font-variant-numeric:tabular-nums; }
.strong{ font-weight:600; }
.center{ text-align:center; }
.nowrap{ white-space:nowrap; }
.ellipsis{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
.stack{ display:flex; flex-direction:column; gap:var(--gap); }
.stack-sm{ display:flex; flex-direction:column; gap:6px; }
.inline{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.spread{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap); }
.grow{ flex:1 1 auto; min-width:0; }
.mt{ margin-top:var(--gap); }
.mb{ margin-bottom:var(--gap); }
.sr-only{
  position:absolute; width:1px; height:1px;
  padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); white-space:nowrap; border:0;
}

/* 分割线 */
.hr{ height:1px; background:var(--line); margin:var(--gap) 0; border:0; }

/* ---------------------------------------------------------------- 18b. 页面组件（网球 / 日程共用）
   这三个组件由 tennis.js / schedule.js 产出 DOM，类名已和 Lead 钉死：
   `.col` / `.col-a` / `.col-b`、`.pager` / `.pager-info`、
   `.rec` / `.rec-when` / `.rec-body` / `.rec-main` / `.rec-sub` / `.rec-side`。
   手机 / 平板：`.col` 是 display:contents（子元素直接摊进 .page 的流里，和没有它一样）；
   桌面 ≥1024px：才变成真正的两列容器（见 §20.9）。 */

/* 两列纵向流容器：默认摊平，桌面才成列 */
.col{ display:contents; }

/* 分页条：左「共 N 条」右「上一页/下一页」 */
.pager{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  margin-top:14px; padding-top:12px;
  border-top:1px solid var(--line-soft);
}
.pager .btn{ min-height:var(--tap); }
.pager .btn[disabled]{ opacity:.45; }
.pager-info{
  font-size:var(--fs-xs);
  color:var(--fg-dim);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
  min-width:0;
}

/* 明细行：左「日期块」+ 右「内容」，整行可点（<button> 或 <a>） */
.rec{
  display:flex; gap:12px; align-items:flex-start;
  width:100%; min-height:56px;
  padding:10px 2px;
  text-align:left;
  background:none;
  border:0;
  border-bottom:1px solid var(--line-soft);
  color:inherit;
  cursor:pointer;
  transition:background var(--t) var(--ease);
}
.rec:last-child{ border-bottom:0; }
.rec:active{ background:var(--bg-elev2); }
@media (hover:hover){
  .rec:hover{ background:color-mix(in srgb, var(--fg) 4%, transparent); }
}
.rec-when{
  flex:0 0 52px;
  display:flex; flex-direction:column; align-items:center; gap:2px;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}
.rec-when .d{
  font-size:var(--fs-md);
  font-weight:650;
  letter-spacing:-.3px;
  line-height:1.2;
}
.rec-when .w{
  font-size:var(--fs-2xs);
  color:var(--fg-dim);
  line-height:1.2;
}
.rec-body{
  display:flex; flex-direction:column; gap:3px;
  min-width:0; flex:1 1 auto;
}
.rec-main{
  font-size:var(--fs-md);
  font-weight:600;
  line-height:1.3;
  overflow-wrap:anywhere;
}
.rec-sub{
  font-size:var(--fs-xs);
  color:var(--fg-dim);
  line-height:1.35;
  overflow-wrap:anywhere;
}
.rec-side{
  flex:0 0 auto;
  display:flex; align-items:center; gap:6px;
  color:var(--fg-faint);
}
.rec-side .ico{ width:16px; height:16px; }

/* ---------------------------------------------------------------- 18c. 日程列表（按天分组）
   契约（名字钉死，由 schedule.js 使用，见 task-23 §D）：
     .sched-day > .sched-day-hd( .d + .w + .n ) + .sched-item…
       .sched-item > .st + .sb( .ti + .lo ) + .sa
     状态类：.sched-day.is-today / .sched-item.is-past / .is-allday / .is-recurring
   设计意图：不要「卡片里再套一堆卡片」的观感 —— 用极淡底 + 竖向节奏 + 时间列对齐，
   让一天里多条日程像 iOS 日历的「日程清单」那样连成一片。 */
.sched-day{
  position:relative;
  padding:10px 0 12px;
}
.sched-day + .sched-day{
  border-top:1px solid var(--line-soft);
}
/* 今天：左侧 3px accent 竖条 + 抬头变 accent，不整块上底色（避免和卡片、图表打架） */
.sched-day.is-today::before{
  content:"";
  position:absolute;
  left:-12px;
  top:14px;
  bottom:14px;
  width:3px;
  border-radius:0 3px 3px 0;
  background:var(--accent);
}
.sched-day-hd{
  display:flex;
  align-items:baseline;
  gap:6px;
  min-height:24px;
  margin-bottom:4px;
  padding:0 2px;
}
.sched-day-hd .d{
  font-size:var(--fs-md);
  font-weight:650;
  letter-spacing:-.2px;
  color:var(--fg);
}
.sched-day-hd .w{
  font-size:var(--fs-xs);
  color:var(--fg-dim);
}
/* 条数推到最右，用 faint 一档，不抢日期 */
.sched-day-hd .n{
  margin-left:auto;
  font-size:var(--fs-2xs);
  color:var(--fg-faint);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}
.sched-day.is-today .sched-day-hd .d{ color:var(--accent); }
.sched-day.is-today .sched-day-hd .w{ color:var(--accent); opacity:.72; }

/* 一条日程：左时间列（固定宽，上下两行）/ 中内容 / 右箭头。
   行本身可以是 button / a / div，三种都保持同一套外观。 */
.sched-item{
  display:flex;
  align-items:flex-start;
  gap:12px;
  width:100%;
  min-height:var(--tap);          /* M6：整行可点且 ≥44px */
  padding:9px 2px;
  text-align:left;
  color:inherit;
  background:none;
  border:0;
  border-radius:var(--radius-sm);
  transition:background var(--t) var(--ease);
}
button.sched-item,
a.sched-item{ cursor:pointer; }
.sched-item:active{ background:var(--bg-elev2); }
@media (hover:hover){
  .sched-item:hover{ background:color-mix(in srgb, var(--fg) 4%, transparent); }
}

/* 时间列：上下两行（开始 / 结束）。定宽 52px 让所有标题左边缘对齐。 */
.sched-item .st{
  flex:0 0 52px;
  display:flex;
  flex-direction:column;
  align-items:flex-start;
  gap:1px;
  padding-top:1px;
  font-size:var(--fs-xs);
  line-height:1.35;
  color:var(--fg-dim);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}
.sched-item .st .t2{ color:var(--fg-faint); }

/* 内容列：标题 + 地点（地点没有就不占位） */
.sched-item .sb{
  display:flex;
  flex-direction:column;
  gap:2px;
  min-width:0;
  flex:1 1 auto;
}
.sched-item .sb .ti{
  font-size:var(--fs-md);
  font-weight:600;
  line-height:1.35;
  color:var(--fg);
  overflow-wrap:anywhere;
}
.sched-item .sb .lo{
  font-size:var(--fs-2xs);
  line-height:1.35;
  color:var(--fg-faint);
  overflow-wrap:anywhere;
}
/* 重复日程：标题后面跟一个极小 badge，不另起一行 */
.sched-item.is-recurring .sb .ti{
  display:flex;
  align-items:center;
  gap:6px;
  flex-wrap:wrap;
}
.sched-item.is-recurring .sb .ti .badge{
  font-size:var(--fs-2xs);
  font-weight:500;
}

/* 尾部：箭头 / 可选 badge，垂直居中偏上，和标题第一行对齐 */
.sched-item .sa{
  flex:0 0 auto;
  display:flex;
  align-items:center;
  gap:6px;
  align-self:center;
  color:var(--fg-faint);
}
.sched-item .sa .ico{ width:16px; height:16px; }

/* 已过去：整行降透明，一眼能分出「今天剩下的」 */
.sched-item.is-past{ opacity:.55; }

/* 全天事件：时间列只显示「全天」一行，不再上下两行 */
.sched-item.is-allday{ align-items:center; }
.sched-item.is-allday .st{
  flex-direction:row;
  align-items:center;
  padding-top:0;
  color:var(--fg-dim);
}

/* 当天没有日程 / 整段为空：居中一句 + 可选 44px 按钮 */
.sched-empty{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:12px;
  padding:24px 12px;
  text-align:center;
  color:var(--fg-faint);
  font-size:var(--fs-sm);
}
.sched-empty .btn{ min-height:var(--tap); }

/* ---------------------------------------------------------------- 18d. 服务器页（.sv-*）
   用户第 16 轮：「服务器页不要复用首页的样式」。所以这套是独立设计，和首页的
   .gauge（三环）/ .spark（迷你折线）/ .card-hero（重心卡）/ .pill 完全不共享：
     · 结构：行式布局 —— 左「名称 + 说明」，右「大数字 + 单位」，下面一条细横条
     · 网络：面积图（不是首页那种 28px 的迷你折线），首屏就有线
     · 卡片：普通 .card（不用 .card-hero 的渐变和悬浮位移），只在卡片内部做分区
   全部用 --accent / --warn / --danger 三档语义色，阈值与首页一致（CPU/内存 90、磁盘 85）。 */

/* --- 行式指标 --- */
.sv-rows{
  display:flex;
  flex-direction:column;
}
/* 行与行之间用极淡分隔线，不靠卡片套卡片 */
.sv-row + .sv-row{
  border-top:1px solid var(--line-soft);
}
.sv-row{
  padding:12px 0;
  min-width:0;
}
.sv-row:first-child{ padding-top:2px; }
.sv-row:last-child{ padding-bottom:2px; }
/* 紧凑版（磁盘分区卡里，分区多时不要每行都那么高） */
.sv-rows-compact .sv-row{ padding:9px 0; }

.sv-hd{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:12px;
  min-width:0;
}
.sv-k{
  display:flex;
  align-items:baseline;
  gap:8px;
  min-width:0;
}
.sv-name{
  font-size:var(--fs-sm);
  font-weight:600;
  color:var(--fg);
  white-space:nowrap;
  flex:0 0 auto;
}
.sv-sub{
  font-size:var(--fs-2xs);
  color:var(--fg-faint);
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
/* 右端数值：大号、等宽数字，占视觉重心 */
.sv-v{
  display:inline-flex;
  align-items:baseline;
  gap:2px;
  flex:0 0 auto;
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
  line-height:1;
}
.sv-num{
  font-size:var(--fs-2xl);
  font-weight:650;
  letter-spacing:-1px;
  color:var(--fg);
  transition:color .3s var(--ease);
}
.sv-u{
  font-size:var(--fs-xs);
  font-weight:500;
  color:var(--fg-faint);
  letter-spacing:0;
}
.sv-v.warn .sv-num{ color:var(--warn); }
.sv-v.red .sv-num{ color:var(--danger); }

/* 细横条：高 6px、圆角、底色 --bg-elev2，填充宽度由 --pct 驱动 */
.sv-bar{
  position:relative;
  margin-top:8px;
  height:6px;
  border-radius:var(--radius-pill);
  background:var(--bg-elev2);
  overflow:hidden;
}
.sv-fill{
  display:block;
  height:100%;
  width:var(--pct, 0%);
  border-radius:inherit;
  background:var(--accent);
  transition:width .45s var(--ease), background .3s var(--ease);
}
.sv-fill.warn{ background:var(--warn); }
.sv-fill.red{ background:var(--danger); }
@media (max-width:359px){
  .sv-num{ font-size:var(--fs-xl); }
  .sv-name{ font-size:var(--fs-xs); }
}

/* --- 网络：两张面积图（上下行各一），首屏就画（n===1 时画一条水平基线） --- */
.sv-net{
  display:flex;
  flex-direction:column;
  gap:16px;
}
.sv-net-row .sv-hd{ margin-bottom:6px; }
.sv-net-row .sv-k .ico{ width:16px; height:16px; color:var(--fg-faint); }
.sv-rate{
  font-size:var(--fs-lg);
  font-weight:650;
  letter-spacing:-.5px;
  color:var(--info);
  white-space:nowrap;
}
.sv-net-row.down .sv-rate{ color:var(--accent); }

.sv-chart{
  position:relative;
  height:64px;
  color:var(--info);
}
.sv-net-row.down .sv-chart{ color:var(--accent); }
.sv-chart svg{
  width:100%;
  height:64px;
  display:block;
  overflow:visible;
}
.sv-line{
  fill:none;
  stroke:currentColor;
  stroke-width:2;
  stroke-linecap:round;
  stroke-linejoin:round;
  /* viewBox 100×40 被横向/纵向拉伸，用 non-scaling-stroke 保住 2px */
  vector-effect:non-scaling-stroke;
}
.sv-area{
  stroke:none;
  fill:currentColor;
  opacity:.13;
}
/* 末端点：零长线段 + 圆头，在非等比坐标里也不会被拉成椭圆 */
.sv-dot{
  fill:none;
  stroke:currentColor;
  stroke-width:5;
  stroke-linecap:round;
  vector-effect:non-scaling-stroke;
}
/* 一个点都没有（首次打开且没历史）时给一条极淡的底线，别留一块空白 */
.sv-chart.is-empty::after{
  content:"";
  position:absolute;
  left:0; right:0; bottom:4px;
  height:1px;
  background:var(--line-soft);
}
@media (max-width:359px){
  .sv-chart,
  .sv-chart svg{ height:52px; }
  .sv-rate{ font-size:var(--fs-md); }
}

/* --- 服务器页的卡片：普通卡片即可，不加首页 .card-hero 的渐变/悬浮 --- */
.sv-card{ /* 目前只是语义标记，方便以后单独调服务器页卡片 */ }

/* ---------------------------------------------------------------- 19. 动效偏好 / 降级 */

/* 无 backdrop-filter（老浏览器 / 部分安卓 WebView）：降级成不透明底色，绝不白屏 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
  .topbar::before{ background:var(--chrome-bg-solid); }
  .tabbar::before{ background:var(--chrome-tab-solid); }
  .sheet-backdrop{ background:rgba(0,0,0,.62); }
  .toast{ background:var(--bg-elev2); }
}

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
  .skeleton{ animation:none; }
  .sheet{ animation:none !important; }
}

/* ================================================================ 20. 桌面三档（≥1024px）
   三档策略（本文件唯一的响应式分水岭，基础值一个都不改）：
     < 641px   手机：底部 tabbar + 顶栏标题，本段完全不生效（零像素变化）
     641–1023  平板：顶部横排胶囊导航 + 960 内容宽（沿用 §8 的规则，不新增）
     ≥ 1024px  桌面：左侧 224px 固定侧边栏 + 右侧 1100px 多栏内容
   实现方式：顶部那套 header.topbar 在桌面直接改造成左侧栏（品牌 / 纵向导航 / 底部按钮），
   不新增第二套导航 DOM —— active 态仍由 core.js 的 highlightNav() 切 .active，逻辑零改动。
   每页多栏靠 core.js 写在 #view 上的 data-page 属性做纯 CSS 栅格，
   schedule.js / tennis.js 一行 JS 都不用改就能吃上桌面布局。
   ---------------------------------------------------------------- */
@media (min-width:1024px){

  /* ---- 20.1 侧边栏（把 .topbar 立起来放左边） ---- */
  .topbar{
    position:fixed;
    top:0; left:0; bottom:0;
    width:224px;
    min-height:0;
    flex-direction:column;
    align-items:stretch;
    gap:0;
    padding:calc(16px + var(--safe-t)) 12px calc(12px + var(--safe-b));
    z-index:45;
  }
  /* 玻璃层：右侧 1px 分隔线代替原来的底部线 */
  .topbar::before{
    border-right:1px solid var(--line);
    border-bottom:0;
  }
  .topbar::after{ display:none; }

  /* 品牌区：.brand 只在桌面出现；h1#page-title 让位给内容区的 .view-hd */
  .topbar-brand{
    padding:0 10px 16px;
    flex:0 0 auto;
  }
  .brand{
    display:inline-block;
    font-size:var(--fs-lg);
    font-weight:700;
    letter-spacing:-.3px;
    color:var(--fg);
  }
  .topbar h1{ display:none; }

  /* ---- 20.2 纵向导航 ---- */
  .tabbar{
    position:static;
    display:flex;
    flex-direction:column;
    align-items:stretch;
    gap:2px;
    height:auto;
    flex:1 1 auto;
    min-height:0;
    margin:0;
    padding:0;
    overflow-y:auto;
    overflow-x:hidden;
  }
  .tabbar::before{ display:none; }
  .tabbar a{
    flex-direction:row;
    justify-content:flex-start;
    gap:10px;
    height:44px;
    min-height:var(--tap);
    padding:0 12px;
    border-radius:10px;
    font-size:var(--fs-sm);
    color:var(--fg-dim);
  }
  .tabbar a .tab-icon{ width:auto; height:auto; }
  .tabbar a .tab-icon::before{ display:none; }
  .tabbar a .tab-icon .ico{ width:20px; height:20px; }
  .tabbar a .tab-label{
    font-size:var(--fs-sm);
    font-weight:500;
    letter-spacing:.1px;
  }
  .tabbar a.active{
    background:color-mix(in srgb, var(--accent) 14%, transparent);
    color:var(--accent);
  }
  .tabbar a.active .tab-label{ font-weight:600; }
  /* 选中项左侧 3px 竖条（::before 在 .tabbar a 上没被占用） */
  .tabbar a.active::before{
    content:"";
    position:absolute;
    left:0; top:50%;
    transform:translateY(-50%);
    width:3px; height:18px;
    border-radius:0 3px 3px 0;
    background:var(--accent);
  }
  .tabbar a:active{ background:var(--bg-elev2); }

  /* ---- 20.3 侧边栏底部按钮（刷新 / 退出） ---- */
  .topbar-foot{
    display:flex;
    flex-direction:row;
    align-items:center;
    gap:4px;
    flex:0 0 auto;
    margin-top:12px;
    padding-top:10px;
    border-top:1px solid var(--line-soft);
  }
  .topbar-action{ margin-left:0; }
  .topbar .topbar-logout{ display:inline-flex; }

  /* ---- 20.4 内容区：右侧 1100px，页面头 + 12 栏栅格 ---- */
  #view{
    max-width:none;
    margin-left:224px;
    padding:26px 32px 48px;
  }
  #view > .view-hd,
  #view > .page{
    max-width:1100px;
    margin-left:auto;
    margin-right:auto;
  }
  #view.has-fab,
  #view:has(.fab:not([hidden])){
    padding-bottom:calc(48px + 72px);
  }

  .view-hd{
    display:flex;
    align-items:baseline;
    gap:10px;
    margin-bottom:18px;
  }
  .view-hd h2{
    font-size:24px;
    font-weight:700;
    letter-spacing:-.4px;
    line-height:1.2;
  }
  .view-hd-sub{
    font-size:var(--fs-sm);
    color:var(--fg-dim);
  }

  #view > .page{
    display:grid;
    grid-template-columns:repeat(12, minmax(0,1fr));
    gap:16px;
    align-items:start;
  }
  #view > .page > *{ grid-column:span 12; min-width:0; }
  #view > .page > .section{ margin-bottom:0; }

  /* ---- 20.5 首页：日程独占一行，服务器 / 网球 并排，应用独占一行 ---- */
  #view[data-page="home"] > .page > [data-block="server"],
  #view[data-page="home"] > .page > [data-block="tennis"]{ grid-column:span 6; }

  /* ---- 20.6 服务器：概览(5) + 网络(7) 并排；磁盘(5) + 备份(7) 并排
         第 15 轮重做后 DOM 是 4 个 section，各自带 data-card 标记：
           [data-card="overview"] 三环 + 一行硬件小字（+ 单分区时的磁盘进度条）
           [data-card="network"]  上下行两张 52px 高的折线
           [data-card="disks"]    多分区才出现，单分区时 hidden
           [data-card="backup"]   数据备份四态
         两行都用 5/7：上下两行的竖向分界线必须落在同一列，
         否则左边窄右边宽、再下一行反过来，桌面看着像两块错开的拼图（第 16 轮实测截图确认）。
         兜底：分区卡 hidden 时（只有 1 个分区）备份卡自动吃满 12 列。 */
  #view[data-page="server"] > .page > .section[data-card="overview"]{ grid-column:1 / span 5; grid-row:1; }
  #view[data-page="server"] > .page > .section[data-card="network"]{ grid-column:6 / span 7; grid-row:1; }
  #view[data-page="server"] > .page > .section[data-card="disks"]{ grid-column:1 / span 5; grid-row:2; }
  #view[data-page="server"] > .page > .section[data-card="backup"]{ grid-column:6 / span 7; grid-row:2; }
  /* 分区卡隐藏（单分区）时：备份卡独占第二行 */
  #view[data-page="server"] > .page > .section[data-card="disks"][hidden] + .section[data-card="backup"]{
    grid-column:1 / -1;
  }
  /* 同一行的两张卡高度对齐：grid 默认 align-items:start，两张卡会一高一低留出锯齿空白；
     这里让 server 页的行拉伸，卡片 height:100% 撑满（内容仍在顶部，不会变形）。 */
  #view[data-page="server"] > .page{ align-items:stretch; }
  #view[data-page="server"] > .page > .section > .card{ height:100%; }

  /* ---- 20.7 应用：分组按 3 栏铺开（每栏约 330px，接近手机行宽） ---- */
  #view[data-page="apps"] [data-body]{
    display:grid;
    grid-template-columns:repeat(auto-fill, minmax(300px, 1fr));
    gap:4px 20px;
    align-items:start;
  }
  #view[data-page="apps"] [data-body] > .section{ margin-bottom:0; }

  /* ---- 20.8 日程：月历卡(5) + 列表卡(7) 并排
         schedule.js 把 [data-month-card] / [data-list-card] **同时**挂在 section 和内层 card 上
         （双挂）。所以这里直接命中 section 自身，不用 :has() ——
         `:has()` 只匹配后代，属性挂在自己身上时匹配不到（caldav 实测 matchHas:0）。
         用 `> .page > .section[data-month-card]` 只会命中 section 那一层，
         内层的 .card[data-month-card] 不在 `> .page >` 直接子级，不会被重复匹配。 */
  #view[data-page="schedule"] > .page > .section[data-month-card]{ grid-column:1 / span 5; grid-row:1; }
  #view[data-page="schedule"] > .page > .section[data-list-card]{ grid-column:6 / span 7; grid-row:1; }
  /* 两列布局里月历不再需要"整块居中限宽"，卡片本身已经够窄；但 7 列超过 480 会被
     拉成大方块，所以仍给上限（不是居中，靠左和卡片内容对齐）。 */
  #view[data-page="schedule"] .month{ max-width:480px; margin-inline:0; }
  #view[data-page="schedule"] .month.compact{ max-width:480px; }
  #view[data-page="schedule"] [data-day-list]{ max-width:none; margin-inline:0; }

  /* ---- 20.9 网球：两列纵向流（左 5 / 右 7）
         第 15 轮起 tennis.js 自己把卡片包进 .col.col-a / .col.col-b，
         DOM 就是"左列一组、右列一组、明细独占一行"，
         所以这里不再需要旧版的 grid-row 重排（已删）。
         .col 在手机/平板上是 display:contents（摊平），只有桌面才变成真正的列容器。 */
  #view[data-page="tennis"] > .page > .col-a{
    grid-column:1 / span 5;
    grid-row:1;
    display:flex;
    flex-direction:column;
    gap:16px;
    min-width:0;
  }
  #view[data-page="tennis"] > .page > .col-b{
    grid-column:6 / span 7;
    grid-row:1;
    display:flex;
    flex-direction:column;
    gap:16px;
    min-width:0;
  }
  #view[data-page="tennis"] > .page > .section:has([data-list]){ grid-column:1 / -1; grid-row:2; }
  /* 列内卡片之间已经有 16px gap，section 自己的下边距必须清零，否则间距翻倍 */
  #view[data-page="tennis"] .col > .section{ margin-bottom:0; }
  /* 网球页日历在右列（约 620 宽），480 是"一眼看全 7 列又不显空"的宽度 */
  #view[data-page="tennis"] .month{ max-width:480px; margin-inline:0; }

  /* ---- 20.10 明细行：桌面行宽更大，左右留白放宽一档 ---- */
  .rec{ padding:12px 4px; }

  /* ---- 20.10b 404：窄卡居中 ---- */
  #view[data-page="notfound"] > .page{ max-width:560px; }

  /* ---- 20.10c 设置页：表单类内容不铺满 1100，居中 720 更好读 ---- */
  #view[data-page="settings"] > .page{ max-width:720px; }

  /* ---- 20.11 悬浮球 / 提示条：避开侧边栏 ---- */
  .fab{ right:32px; }
  .toasts{ left:calc(224px + (100vw - 224px) / 2); }

  /* ---- 20.12 缩略图：桌面有空间，放大一档（仍是 3:4 竖版） ---- */
  .product-thumb{ width:48px; height:64px; border-radius:12px; }
  .product-thumb.has-img{ background:color-mix(in srgb, var(--fg) 6%, transparent); }

  /* ---- 20.13 登录页：整屏居中，侧边栏不出现 ---- */
  body.route-login .topbar{ display:none; }
  body.route-login #view{ margin-left:0; padding-top:min(14vh, 120px); }
  body.route-login .view-hd{ display:none; }
  /* 登录卡在栅格里是 grid item，靠 margin:auto 居中时会被收缩成内容宽（≈244px）；
     显式给 100% 宽 + max-width 才能拿到和手机一致的 380 上限。 */
  body.route-login .login-wrap{ width:100%; max-width:380px; }

  /* 桌面悬停反馈（只在有鼠标的设备上） */
  @media (hover:hover){
    .tabbar a:hover{ background:var(--bg-elev2); color:var(--fg); }
    .tabbar a.active:hover{ background:color-mix(in srgb, var(--accent) 18%, transparent); color:var(--accent); }
  }
}
