@charset "UTF-8";
/* ==========================================================================
   慈利文華太極会馆 · 太和（方案 E）
   黑白为主、淡雅留白。全站只用一个中性灰阶，不引入品牌彩色，
   深浅仅靠 墨/纸/线 三级层次与大量留白表达。
   ========================================================================== */

:root {
  /* 墨阶（纯灰，不带任何色相偏移，避免屏幕偏色）
     拆成「承载文字的墨」与「只用于线条/装饰的墨」两组：
     前者必须满足 WCAG AA（小字对比度 >= 4.5），后者不承载信息。 */
  --ink-0: #0E0E0E;
  --ink-1: #1C1C1C;
  --ink-2: #3A3A3A;
  --ink-3: #4A4A4A;
  --ink-4: #5A5A5A;   /* 次级文字  对白底 ≈ 6.7:1 */
  --ink-5: #6E6E6E;   /* 三级文字  对白底 ≈ 5.0:1 */
  --ink-6: #9A9A9A;   /* 仅边框/装饰，不承载文字 */
  --ink-7: #C4C4C4;   /* 仅分割线 */

  /* 深色底上的文字（后台侧栏）。浅底用的 --ink-5 放到深底上会不合格，
     所以深底单独一组，不要混用。 */
  --on-dark-1: #C9C9C6;  /* 深底主文字  ≈ 11.7:1 */
  --on-dark-2: #8C8C8C;  /* 深底次要文字 ≈ 5.7:1 */

  /* 纸阶 */
  --paper-0: #FFFFFF;
  --paper-1: #FCFCFB;
  --paper-2: #F7F7F5;
  --paper-3: #F1F1EE;

  /* 线
     三档轻重，全部按「贴白底/纸底的对比度」定，而不是随手取灰：
       line-1     ≈1.49:1  表格行、最轻的分隔
       line-2     ≈1.71:1  卡片内部分隔、标签描边、结构线
       line-card  ≈2.28:1  卡片外框
     早先 line-1/line-2 分别是 #E6E6E3(1.25:1) / #D8D8D4(1.42:1)，
     贴在白卡片上几乎看不见，用户反馈「看不太清」，故整体上调一档。 */
  --line-1: #D4D4D0;
  --line-2: #C6C6C2;
  --line-card: #ACACA8;

  /* 语义色（*唯一的彩色*，不得用于装饰）
     2026-10-20 用户明确要求提醒中心按红绿灯分级：
       紧急 / 已逾期 / 不可用 → 红     警告 / 待处理 / 有变数 → 橙     提醒 / 已处理 / 正常 → 绿
     原先这里是三个灰阶别名（warn=ink-3、danger=ink-0、ok=ink-5），
     状态全靠线宽与填充表达；改成色相后仍然遵守「低饱和 + 淡底 + 同色描边」，
     在整页灰阶里只占一枚标签的面积，不破坏「淡雅」。
     三组都是「深色字 + 极淡底」，文字对各自底的对比度均 ≥ 5.5:1（AA 正文标准是 4.5）。
     2026-10-21 用户反馈「红色、橙色、绿色的颜色不对，感觉看不清」：
     原来三支都压了灰（饱和 55% / 83% / 31%），红像砖红、橙像枯草黄、绿像灰绿，
     在 12px 小标签上分不出「这是红还是褐」。现在只提高色浓度、不动明度档：
       红 色相 4°、饱和 83%   橙 色相 28°、饱和 94%   绿 色相 153°、饱和 77%
     第一版换完色，12px 的橙还是被看成「橄榄色」——因为小字细笔画和浅底混在一起。
     于是把三支的淡底各加浓一档（红 #FCE4E1、橙 #FFEBCB、绿 #E4F4EA）：
     胶囊里面积最大的是底色，底更「有色」比把字提亮更管用（提亮字会掉对比度）。
     文字对各自新底的对比度：红 6.14、橙 5.22、绿 5.76，仍在 5:1 以上。
     2026-10-22 第三次：12px 的橙仍被视觉复核判「偏浅偏黄」，于是把橙底再深一档
     #FFEBCB→#FFE3C0；底变深会吃掉对比度，橙字同步压深 #9C4C05→#964A04，
     文字对底 5.19:1，仍守 5:1。红 6.14、绿 5.76 不动。
     换色时同步 `_verify_notification_colors.js` 里的期望值。 */
  --state-danger: #A81A10;  --state-danger-bg: #FCE4E1;  --state-danger-line: #F0BFB8;
  --state-warn:   #964A04;  --state-warn-bg:   #FFE3C0;  --state-warn-line:   #E6BE8B;
  --state-ok:     #0E6B41;  --state-ok-bg:     #E4F4EA;  --state-ok-line:     #B7E2C9;

  /* 排版
     2026-10-22 用户要求「全部字体都修改为黑体（无衬线）」：
     原来标题、印章、落款用宋体（--font-song），编号与金额用等宽（--font-num），
     现在三支令牌都指向同一套无衬线黑体，全站只剩一个字体族。
     数字列不会因此跳字：Microsoft YaHei 的数字本身等宽（实测 1111 / 8888 / 0000
     在 20px 下都是 46.91px），所以把 --font-num 从等宽换成黑体，金额与日期照样对齐。
     --font-song / --font-num 这两个名字保留（视图与样式里仍有引用），值指向 --font-hei，
     以后要恢复衬线只改这两行。 */
  --font-hei: "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "SimHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif;
  --font-song: var(--font-hei);
  --font-num: var(--font-hei);

  /* 尺度：8px 基准 */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;

  --r1: 2px; --r2: 4px; --r3: 6px;

  /* 卡片投影：只用极淡的中性黑，靠「大羽化 + 低透明度」做纸片抬起来的感觉。
     不画硬边、不用彩色投影 —— 阴影重了就不「淡雅」了。
     三层：贴边 1px（锐利边界）→ 5px 接触阴影（让卡片真的离开纸面）→ 18px 大羽化（远一点的柔和过渡）。
     原来只有第一层和第三层，中间是空的，浅色底上看着还是「一张纸贴在纸上」。 */
  --shadow-card:
    0 1px 2px rgba(20, 20, 20, .05),
    0 5px 12px -2px rgba(20, 20, 20, .07),
    0 18px 36px -18px rgba(20, 20, 20, .22);
  --shadow-card-hover:
    0 1px 2px rgba(20, 20, 20, .06),
    0 8px 18px -2px rgba(20, 20, 20, .09),
    0 26px 50px -22px rgba(20, 20, 20, .28);

  --maxw: 1180px;
  --header-h: 68px;

  /* 后台：无圆角、无阴影，靠细线分区 */
  --pg-r: 2px;
  --pg-shadow: none;
  --pg-muted: var(--ink-4);
}

* { box-sizing: border-box; }

/* 表单控件不继承 body 的字体（Chrome/Windows 上按钮默认是 Arial），
   全站统一黑体后这会导致页面上出现第二种字体 —— 显式让它继承。
   只继承族名，字号/行高仍由下面各自的规则决定。 */
button, input, select, textarea, optgroup { font-family: inherit; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-hei);
  font-size: 15px;
  line-height: 1.75;
  color: var(--ink-1);
  background: var(--paper-1);
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }

/* 选中文字：系统默认那个蓝色在一片黑白里很扎眼，反白更贴合全站的调子。
   （编辑器 iframe 里另有一份，见 assets/css/editor-content.css —— iframe 拿不到外层样式） */
::selection { background: var(--ink-0); color: #fff; }

a { color: var(--ink-1); text-decoration: none; }
a:hover { color: var(--ink-0); }

h1, h2, h3, h4 {
  font-family: var(--font-song);
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink-0);
  margin: 0;
  letter-spacing: .01em;
}

p { margin: 0 0 var(--s3); }

hr { border: 0; border-top: 1px solid var(--line-1); margin: var(--s5) 0; }

.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 var(--s5); }
.wrap-narrow { max-width: 760px; }

/* ---------------------------------------------------------------- 网站头部 */
.site-header {
  position: sticky; top: 0; z-index: 50;
  height: var(--header-h);
  display: flex; align-items: center;
  background: rgba(252, 252, 251, .94);
  backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--line-1);
}
.site-header .wrap { display: flex; align-items: center; gap: var(--s5); }
.brand { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.brand-mark {
  font-family: var(--font-song); font-size: 21px; font-weight: 600;
  color: var(--ink-0); letter-spacing: .16em;
}
.brand-sub { font-size: 11px; color: var(--ink-5); letter-spacing: .22em; text-transform: uppercase; }

/* ---------------------------------------------------------------- 会馆徽标
   徽标与照片一律保留原色。
   注意：本项目**不要**给人像照片加 grayscale —— 中文语境里黑白人像会被读成遗像，
   这是踩过的坑。黑白只用在版式、文字、线条、按钮这些 UI 元素上。
*/
.brand-logo {
  width: 44px; height: 44px; flex: 0 0 44px;
  object-fit: contain; display: block;
}
.foot-logo { width: 46px; height: 46px; object-fit: contain; display: block; }
.auth-logo { width: 76px; height: 76px; object-fit: contain; display: block; margin: 0 auto var(--s4); }
/* 登录页的两个身份切换（2026-10-23 加会员入口）
   刻意做成「两个并排的标签页」而不是下拉或单选：登录页只该有一次点击的犹豫空间，
   而这两个身份的分界线对来人说得很清楚 —— 有账号的员工 / 只有手机号的会员。 */
.login-tabs {
  display: flex; gap: 0; margin-bottom: -1px; position: relative; z-index: 1;
}
.login-tab {
  flex: 1 1 0; text-align: center; padding: 11px 8px;
  font-size: 13.5px; letter-spacing: .12em; color: var(--ink-4);
  background: var(--paper-2); border: 1px solid var(--line-2);
  border-bottom-color: var(--line-2);
}
.login-tab:first-child { border-radius: var(--r2) 0 0 0; }
.login-tab:last-child { border-radius: 0 var(--r2) 0 0; border-left: 0; }
.login-tab:hover { color: var(--ink-1); background: var(--paper-3); }
.login-tab:focus-visible { outline: 2px solid var(--ink-1); outline-offset: -2px; }
.login-tab.is-on {
  background: var(--paper-0); color: var(--ink-0); font-weight: 500;
  border-bottom-color: var(--paper-0);
}
/* ------------------------------------------------------------------ 滑块验证
   题面是服务端现画的图：缺口在图上、答案在 session 里，页面拿不到。
   这一层只管画和拖 —— 判定一律回服务端。
   颜色沿用全站灰阶，只有「对 / 错」两种状态借用语义色。 */
.captcha { margin-bottom: var(--s4); }
/* 联系我们的留言表单里，右边已经有一整块表单，滑块再占满整列就太高了 ——
   2026-10-25 按用户要求收窄到 340px（题面 340×170、拖动条同宽），
   后台登录页仍然占满整列（那里是窄卡片，本来就小）。 */
.captcha[data-purpose="contact"] { max-width: 340px; margin-left: auto; margin-right: auto; }
.captcha-stage {
  position: relative; width: 100%; aspect-ratio: 2 / 1;
  overflow: hidden; background: var(--paper-2);
  border: 1px solid var(--line-2); border-radius: var(--r2);
}
.captcha-bg { display: block; width: 100%; height: 100%; object-fit: cover; }
/* 拼图块只以百分比定位（宽高与 top 都由 PHP 按缺口算），容器缩放时跟着缩放 */
.captcha-piece {
  position: absolute; left: 0; top: 0; height: auto;
  pointer-events: none; will-change: left;
  /* 两道影子：贴边的一道定轮廓，外扩的一道把它从照片上「抬起来」。
     只留一道淡影时，拼图块压在亮处会和背景糊在一起，分不清哪是块、哪是坑。 */
  filter: drop-shadow(0 0 1px rgba(20, 20, 20, .55)) drop-shadow(0 2px 4px rgba(20, 20, 20, .42));
}
/* 「换一张」：压在题面右上角，半透明白底 —— 题面是照片，纯描边按钮会糊进去。
   不另起一行是刻意的：登录卡片窄，多一行就多 30px，容易把「登录」按钮顶出首屏。 */
.captcha-refresh {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  display: inline-flex; align-items: center; gap: 4px;
  height: 26px; padding: 0 9px 0 7px;
  font-size: 12px; line-height: 1; letter-spacing: .04em; color: var(--ink-2);
  background: rgba(255, 255, 255, .86);
  border: 1px solid var(--line-2); border-radius: 999px;
  cursor: pointer; backdrop-filter: blur(2px);
  transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}
.captcha-refresh:hover { background: #fff; border-color: var(--line-1); color: var(--ink-0); }
.captcha-refresh:focus-visible { outline: 2px solid var(--ink-1); outline-offset: 2px; }
.captcha-refresh svg { transition: transform .45s ease; }
.captcha-refresh:hover svg { transform: rotate(180deg); }
/* 换题那一瞬间图标转起来 —— 网络慢的时候，没有这个动作就像「点了没反应」 */
.captcha-refresh.is-busy { color: var(--ink-4); cursor: progress; }
.captcha-refresh.is-busy svg { animation: captcha-spin .7s linear infinite; }
@keyframes captcha-spin { to { transform: rotate(360deg); } }
.captcha-track {
  position: relative; height: 44px; margin-top: 10px;
  border: 1px solid var(--line-2); border-radius: var(--r2);
  background: var(--paper-1); overflow: hidden;
  user-select: none; -webkit-user-select: none;
  transition: border-color .15s ease, background-color .15s ease;
}
.captcha-fill {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: var(--paper-3); border-right: 1px solid var(--line-1);
}
.captcha-text {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 12.5px; color: var(--ink-4); letter-spacing: .06em;
  pointer-events: none; transition: color .15s ease;
}
.captcha-handle {
  position: absolute; left: 0; top: -1px; width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--paper-0); border: 1px solid var(--line-card); border-radius: var(--r2);
  color: var(--ink-2); cursor: grab; touch-action: none;
  transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}
.captcha-handle:hover, .captcha-handle.is-drag {
  background: var(--ink-0); border-color: var(--ink-0); color: var(--paper-0);
}
.captcha-handle.is-drag { cursor: grabbing; }
.captcha-handle:focus-visible { outline: 2px solid var(--ink-1); outline-offset: 2px; }
.captcha.is-ok .captcha-track {
  border-color: var(--state-ok-line); background: var(--state-ok-bg);
}
.captcha.is-ok .captcha-fill { background: var(--state-ok-bg); border-right-color: var(--state-ok-line); }
.captcha.is-ok .captcha-text { color: var(--state-ok); }
.captcha.is-ok .captcha-handle {
  background: var(--state-ok); border-color: var(--state-ok); color: #fff; cursor: default;
}
.captcha.is-err .captcha-track { border-color: var(--state-danger-line); }
.captcha.is-err .captcha-text { color: var(--state-danger); }
.captcha-tip {
  min-height: 18px; margin: 6px 0 0; font-size: 12px; line-height: 1.5; color: var(--ink-4);
}
.captcha-tip.is-err { color: var(--state-danger); }
.captcha-tip.is-ok { color: var(--state-ok); }
@media (prefers-reduced-motion: reduce) {
  .captcha-track, .captcha-text, .captcha-handle { transition: none; }
  .captcha-refresh, .captcha-refresh svg { transition: none; }
  .captcha-refresh.is-busy svg { animation: none; }
}
/* 深色底（后台侧栏）上徽标偏暗看不清，垫一层纸色圆底当「印章」 */
.logo-plate {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: 0 0 40px;
  background: var(--paper-0); border-radius: 50%; padding: 3px;
}
.logo-plate img { width: 100%; height: 100%; object-fit: contain; display: block; }
.site-nav { display: flex; gap: var(--s5); margin-left: auto; align-items: center; }
.site-nav a {
  font-size: 14px; color: var(--ink-3); padding: 6px 0;
  border-bottom: 1px solid transparent; transition: color .15s, border-color .15s;
}
.site-nav a:hover { color: var(--ink-0); border-bottom-color: var(--ink-0); }
.site-nav a.is-active { color: var(--ink-0); border-bottom-color: var(--ink-0); }

.nav-toggle {
  display: none; margin-left: auto; background: none; border: 1px solid var(--line-2);
  border-radius: var(--r2); padding: 6px 10px; font-size: 13px; color: var(--ink-2); cursor: pointer;
}

/* ---------------------------------------------------------------- 首页 Banner
   左文右图。左列做成**上下对称的框**，首尾与右列齐平：
     顶部  一行引导语（带细横线）  ↔ 图片上沿
     中间  标题 / 副题 / 按钮，紧凑一组
     底部  一行落款（带细横线）    ↔ 控件带下沿

   这一版是踩过两次坑之后的结论：
     · 一开始用 align-items:center —— 两边都不对齐，看着散
     · 后来用 space-between 但只分「头/尾」两块 —— 左列内容不够高，
       中间被撑出 135px 空白，标题和副题被拉开，更难看
     · 现在把落款移到底部当第三个锚点，空隙被分到上下两条「框边」上，
       文字组内部保持紧凑 —— 既对称又不漏风
*/
.banner {
  background: var(--paper-1);
  border-bottom: 1px solid var(--line-1);
}
.banner-inner {
  display: grid;
  /* 左列刻意收窄：标题「慢慢来，才最快」只有 7 个字，文字宽度撑不满太宽的列，
     多出来的部分会变成文字与图片之间的一条空带（实测曾有 145px）。
     收窄 + 加大标题字号（见下）之后，这条缝降到 ~70px。 */
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  gap: var(--s7);
  align-items: stretch;          /* 两列等高，首尾才可能都齐平 */
  padding-top: var(--s6);
  padding-bottom: var(--s6);
}
/* 不要加 max-width 卡宽度：左列填不满时多出来的空间会跑到列间距里，
   实测间距从 48 变成 82，看着像一条莫名其妙的宽缝。 */
.banner-copy {
  display: flex;
  flex-direction: column;
  justify-content: space-between;   /* 引导语贴上、落款贴下、正文居中 */
  min-width: 0;
}

.banner-eyebrow {
  display: flex; align-items: center; gap: 10px;
  font-size: 12px; letter-spacing: .26em; color: var(--ink-4);
  margin: 0;
}
.banner-eyebrow-rule {
  display: block; width: 26px; height: 1px; flex: 0 0 26px;
  background: var(--ink-3);
}
/* 底部落款：与顶部引导语同款细横线，形成对称 */
.banner-sign {
  display: flex; align-items: center; gap: 10px;
  font-size: 12px; letter-spacing: .3em; color: var(--ink-5);
  margin: 0;
}
.banner-sign-rule {
  display: block; width: 26px; height: 1px; flex: 0 0 26px;
  background: var(--ink-3);
}

.banner-core {
  min-width: 0;
  /* 切图换文案时 app.js 的 applyCopy 会临时把 opacity 压到 0 再换字 */
  transition: opacity .16s ease;
}
.banner-title {
  /* 62px 不是随意定的：它让「慢慢来，才最快」的宽度接近左列宽，
     从而消掉文字右缘与图片之间的空带；
     同时字大了左列也更高，上下两条框边也不至于空太多。
     5.05vw 是为了在容器到达最大宽度（1228px 视口）时正好顶到 62px。 */
  font-size: clamp(30px, 5.05vw, 62px);
  /* 1.32：两行标题本来就靠得近，行距再大就散了 */
  line-height: 1.32; letter-spacing: .08em;
  color: var(--ink-0);
  margin: 0 0 var(--s4);          /* 16px：副题要跟标题贴住 */
}
/* 每个 span 是一行：只在作者写的全角空格 / 中文标点处断行，绝不把词劈开 */
.banner-line { display: block; }
/* 两行标题时，第二行贴右对齐：第一行左、第二行右，错开之后标题在版心里
   形成一个对角的两拍节奏（原来两行都顶左，第二行只有四个字，右侧空出一大块）。
   只对「恰好两行」生效，三行以上仍然统一左对齐，免得排成楼梯。 */
.banner-line-alt {
  text-align: right;
  /* letter-spacing 会在末字之后多留 .08em，贴右时把这点空隙补掉 */
  padding-right: .08em;
}
.banner-desc {
  /* 副题是小字：加一点字距、用浅一档的墨色、再把字重压细，
     跟 62px 的标题拉开层次，不抢主标题。 */
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 0 .85em;
  font-size: 15.5px; line-height: 1.9; color: var(--ink-3);
  letter-spacing: .18em;
  font-weight: 300;
  max-width: 34ch;
  margin: 0 0 var(--s5);          /* 24px：与按钮拉开一档 */
}
/* 两个短句之间的细竖线：1px、行高八分，比圆点克制 */
.banner-desc-sep {
  flex: 0 0 auto;
  width: 1px; height: .95em;
  background: var(--line-2);
}
.banner-actions { display: flex; gap: var(--s3); flex-wrap: wrap; }

/* 黑白照片的暖调补偿
   会馆现有素材的源文件本身就是黑白的。冷灰人像在中文语境里会被读成遗像，
   所以对「本身是黑白」的照片补一层暖调，让它读起来是老照片、年代感。
   这个类由 App\Core\Media::warmClass() 决定：**彩色照片不会加**，
   换成彩色照后页面自动恢复原色，无需改代码。
*/
.photo-warm {
  filter: sepia(.32) saturate(1.35) contrast(1.03) brightness(1.02);
}

/* 可点击放大的实景图 */
.banner-figure { margin: 0; min-width: 0; }

/* ---------------------------------------------------------------- 首页图片轮播
   每 5 秒自动切换，鼠标移入 / 键盘聚焦时暂停，也能点数字或箭头手动切换。
   两张图用交叉淡入（opacity）过渡，不做位移动画 —— 呼应「淡雅」基调。
*/
.banner-carousel { position: relative; }
.banner-track {
  position: relative;
  width: 100%;
  /* 14:9（≈1.556）比原来的 3:2 略宽一点点：右列变宽后靠它把高度压住，
     这样左列不用被撑得太高、上下框边也不会空。相比原图 4:3 只多裁 1.6%，
     头部与横幅字都还在。 */
  aspect-ratio: 14 / 9;
  background: var(--paper-3);
  border-radius: var(--r2);
}
.banner-slide {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  padding: 0; margin: 0; display: block;
  border: 0; border-radius: var(--r2);
  background: var(--paper-3); overflow: hidden;
  cursor: zoom-in; font: inherit; line-height: 0;
  opacity: 0; visibility: hidden; z-index: 0;
  transition: opacity .55s ease, visibility .55s;
}
/* visibility:hidden 会让内部元素不可聚焦，所以非当前张也不会被 Tab 选中 */
.banner-slide.is-active { opacity: 1; visibility: visible; z-index: 1; }
.banner-slide img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: center 46%;
  transition: transform .5s ease;
}
.banner-slide:hover img, .banner-slide:focus-visible img { transform: scale(1.015); }
.banner-slide:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 3px; }

/* 「点击放大」：贴在图片右下角内，不拦点击（让点击穿透到当前图） */
.banner-zoom {
  position: absolute; right: 12px; bottom: 12px; z-index: 3;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; letter-spacing: .06em; line-height: 1;
  color: var(--ink-1);
  background: rgba(252, 252, 251, .94);
  border: 1px solid var(--line-2); border-radius: 999px;
  padding: 7px 11px;
  pointer-events: none;
}
.banner-zoom svg { display: block; }

/* 图片下方控件带：左边序号，右边翻页 —— 与图片同宽，把右下角「托住」 */
.banner-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4);
  margin-top: 12px;
}
.banner-count { display: flex; align-items: center; gap: 2px; }
.banner-num {
  /* 视觉只有数字大小，但触控区撑到 44×44（移动端可点） */
  min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 6px; margin: 0;
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-num); font-size: 13px; letter-spacing: .06em;
  color: var(--ink-5);
  transition: color .15s;
}
.banner-num:hover { color: var(--ink-1); }
.banner-num.is-on { color: var(--ink-0); position: relative; }
/* 当前序号下方一条短横线（比圆点更克制） */
.banner-num.is-on::after {
  content: ""; position: absolute; left: 50%; bottom: 9px;
  width: 16px; height: 1px; background: var(--ink-0);
  transform: translateX(-50%);
}
.banner-sep {
  width: 10px; height: 1px; background: var(--line-2); flex: 0 0 10px;
}
.banner-arrows { display: flex; align-items: center; gap: 6px; }
.banner-arrow {
  width: 44px; height: 44px;            /* 满足 44×44 触控目标 */
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; margin: 0;
  background: transparent; border: 1px solid var(--line-2); border-radius: var(--r2);
  color: var(--ink-2); cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
}
.banner-arrow:hover, .banner-arrow:focus-visible {
  border-color: var(--ink-3); color: var(--ink-0); background: var(--paper-2);
}
.banner-arrow:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 2px; }
.banner-arrow svg { display: block; }
/* 没有实景图时的墨色占位 */
.banner-placeholder {
  aspect-ratio: 4 / 3; border: 1px solid var(--line-1); border-radius: var(--r2);
  background: radial-gradient(120% 90% at 78% 24%, #E9E9E5 0%, #F4F4F1 46%, #FCFCFB 100%);
  display: flex; align-items: center; justify-content: center;
}

/* ---------------------------------------------------------------- 灯箱（点击放大）
   点图打开、点任意处关闭、Esc 关闭。全站唯一的浮层控件。
*/
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  padding: 4vmin;
  background: rgba(14, 14, 14, .93);
  cursor: zoom-out;
  animation: lb-fade .16s ease-out;
}
.lightbox[hidden] { display: none; }
.lightbox-img {
  max-width: 94vw; max-height: 88vh; width: auto; height: auto;
  object-fit: contain; display: block;
  border: 1px solid rgba(255, 255, 255, .16);
  box-shadow: 0 24px 70px rgba(0, 0, 0, .55);
}
.lightbox-close {
  position: absolute; top: 18px; right: 20px;
  width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .38); background: transparent;
  color: #fff; font-size: 22px; line-height: 1; cursor: pointer;
  transition: background .15s, border-color .15s;
}
.lightbox-close:hover, .lightbox-close:focus-visible { background: rgba(255, 255, 255, .14); border-color: #fff; }
.lightbox-hint {
  position: absolute; left: 0; right: 0; bottom: 16px; margin: 0;
  text-align: center; font-size: 12px; letter-spacing: .18em;
  color: rgba(255, 255, 255, .55);
}
body.no-scroll { overflow: hidden; }
@keyframes lb-fade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .lightbox { animation: none; }
  .banner-photo img { transition: none; }
}

/* ---------------------------------------------------------------- 按钮 */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 10px 22px; font-size: 14px; font-family: var(--font-hei);
  min-height: 44px;                     /* 触控目标不小于 44px */
  border-radius: var(--r2); border: 1px solid var(--ink-1);
  background: var(--ink-1); color: var(--paper-0);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
  line-height: 1.4; white-space: nowrap;
}
.btn:hover { background: var(--ink-0); border-color: var(--ink-0); color: var(--paper-0); }
.btn-ghost { background: transparent; color: var(--ink-1); border-color: var(--line-2); }
.btn-ghost:hover { background: var(--paper-3); color: var(--ink-0); border-color: var(--ink-4); }
.btn-sm { padding: 6px 14px; font-size: 13px; }
.btn-block { width: 100%; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* 表格行内用的「文字按钮」：外观像链接，但本体是 button
   （删除这类动作必须走 POST 表单，不能做成 <a>，否则会被预取/误点） */
.btn-link {
  display: inline; margin: 0; border: 0; background: none;
  font: inherit; color: var(--ink-2); cursor: pointer; text-decoration: none;
  /* 触控目标：行内文字按钮靠上下内边距补足，避免在手机上变成 12px 高的靶子 */
  padding: 6px 2px;
}
.btn-link:hover, .btn-link:focus-visible { color: var(--ink-0); text-decoration: underline; }
.btn-link:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 2px; }
/* 同一列里既有「能点的文字按钮」又有「点不了的」，不能一个画成文字、一个画成灰方块：
   长得不一样，人就会以为后者是另一种控件。都用文字，只靠颜色和光标区分。 */
.btn-link.is-disabled {
  color: var(--ink-5);
  cursor: not-allowed;
  text-decoration: none;
}
.btn-link.is-disabled:hover { color: var(--ink-5); text-decoration: none; }

/* ---------------------------------------------------------------- 区块 */
.section { padding: var(--s9) 0; }
.section-sm { padding: var(--s7) 0; }
.section-alt { background: var(--paper-1); border-top: 1px solid var(--line-1); border-bottom: 1px solid var(--line-1); }
.section-head { margin-bottom: var(--s6); }
.section-head h2 { font-size: 28px; letter-spacing: .12em; }
.section-head p { color: var(--ink-4); margin: var(--s2) 0 0; font-size: 14px; }

/* 区块右上角的「更多」入口：文字 + 圆圈箭头。
   原来是一个描边小按钮（btn-ghost btn-sm），和页面里其它细线语言不搭，
   看着像工具栏里的一个工具。改成「文字＋圆箭头」，与课程卡的 .course-go
   同一套：悬停时圆填墨、箭头右移 2px —— 全站只有这一种「更多」动作。 */
.link-more {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 13px; letter-spacing: .04em;
  color: var(--ink-2); white-space: nowrap;
  transition: color .18s ease;
}
.link-more .go {
  width: 28px; height: 28px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-card); border-radius: 50%;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.link-more svg { display: block; transition: transform .18s ease; }
.link-more:hover { color: var(--ink-0); }
.link-more:hover .go {
  background: var(--ink-0); border-color: var(--ink-0); color: var(--paper-0);
}
.link-more:hover svg { transform: translateX(2px); }
.link-more:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 3px; }
.sec-label {
  font-size: 11px; letter-spacing: .3em; color: var(--ink-5);
  text-transform: uppercase; margin-bottom: var(--s3);
}

.grid { display: grid; gap: var(--s5); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/* ---------------------------------------------------------------- 卡片 */
.card {
  background: var(--paper-0); border: 1px solid var(--line-card);
  border-radius: var(--r2); padding: var(--s5);
}
/* 官网首页三类卡（课程 / 教练 / 动态）统一「纸片抬起」投影，
   鼠标移上去只加深投影、不上移也不换色 —— 动效克制，画面才稳。 */
.course-card, .coach-card, .news-card {
  box-shadow: var(--shadow-card);
  transition: border-color .18s ease, box-shadow .22s ease;
}
.course-card:hover, .coach-card:hover, .news-card:hover {
  border-color: var(--ink-6);
  box-shadow: var(--shadow-card-hover);
}
.card-flat { background: transparent; border: 0; border-top: 1px solid var(--line-1); padding: var(--s5) 0 0; border-radius: 0; }
.card h3 { font-size: 17px; margin-bottom: var(--s2); }
.card p { color: var(--ink-3); font-size: 14px; margin: 0; }
.card-meta { font-size: 12px; color: var(--ink-5); }

/* ---------------------------------------------------------------- 课程卡片
   结构：纸色抬头（序号 · 班型 / 价格）/ 标题 / 简介 /（阶梯价表）/ 收尾条
   为什么是「票据」两面：
     1. 原来整张卡只有一根底部分隔线，从上到下全是同一片白，四张并排像四块白纸，
        用户反馈「太单调」。抬头压一层纸色 + 一条结构线，卡片立刻有
        「抬头 — 正文 — 落款」三段节奏；纸色用的是全站已有的 --paper-2，
        和提示条、表头同一档灰度，不引入新颜色。
     2. 序号与班型之间加一根竖细线，把它读成「编号｜班型」而不是一句小字；
        价格仍然右对齐，四张卡的价格基线一条线。
     3. 收尾条左侧把时长做成细边签条、右侧箭头套一枚圆形描边，
        鼠标移上去描边填墨、箭头右移 —— 动效克制，不加位移不换底色。
   内边距从 .card 的 padding 挪到了 head / body 各自身上：抬头要通栏。
*/
.course-card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
  transition: border-color .18s ease, box-shadow .22s ease;
}
.course-card:hover { border-color: var(--ink-6); }
.course-card:focus-within { border-color: var(--ink-4); }

.course-head {
  display: flex; align-items: baseline; gap: 10px;
  padding: 13px var(--s5) 12px;
  background: var(--paper-2);
  border-bottom: 1px solid var(--line-2);
  transition: background-color .18s ease;
}
.course-card:hover .course-head { background: var(--paper-3); }
.course-num {
  font-family: var(--font-num);
  font-size: 11px; letter-spacing: .22em;
  color: var(--ink-5);
}
/* 编号与班型之间的竖细线：只是分隔，不承载文字，所以用装饰级的灰 */
.course-type {
  font-size: 11px; letter-spacing: .18em;
  color: var(--ink-4);
  padding-left: 10px;
  border-left: 1px solid var(--line-card);
  line-height: 1;
}
.course-price {
  font-family: var(--font-num);
  font-size: 15px; color: var(--ink-0);
  white-space: nowrap; margin-left: auto;
}
.course-price small { font-size: 12px; color: var(--ink-5); }
/* 「10节课以上，」这种前置说明：字号小一档、颜色浅一档，把数字让出来 */
.price-pre { font-family: var(--font-hei); font-size: 12px; color: var(--ink-5); }

.course-body {
  display: flex; flex-direction: column;
  gap: var(--s3);
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--s5) var(--s5) calc(var(--s5) - 2px);
}
/* .card h3 / .card p 的默认外边距会被这类工具类选择器里的 margin:0 覆盖，
   这里连同上下 margin 一起写死，间距只由 .course-body 的 gap 负责，避免叠成双倍。 */
.course-body .course-title {
  font-size: 20px; letter-spacing: .03em;
  line-height: 1.45;
  margin: 0;
}
.course-title a { color: inherit; }
.course-body .course-desc {
  font-size: 14.5px; line-height: 1.85;
  color: var(--ink-3);
  margin: 0;
}
.course-tier { margin-top: var(--s1); }

/* 收尾条：一条细线把「信息」和「动作」分开，箭头在悬停时右移，不做位移抖动 */
.course-foot {
  margin-top: auto;
  padding-top: var(--s4);
  border-top: 1px solid var(--line-2);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3);
}
/* 时长做成签条：细边 + 全圆角，比一行灰色小字多一层「被设计过」的手感 */
.course-meta {
  font-size: 11px; letter-spacing: .1em;
  color: var(--ink-4);
  border: 1px solid var(--line-2); border-radius: 999px;
  padding: 3px 10px; white-space: nowrap;
}
.course-more {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--ink-2);
  white-space: nowrap;
  transition: color .18s ease;
}
.course-go {
  width: 26px; height: 26px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-card); border-radius: 50%;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
.course-more svg { transition: transform .18s ease; }
.course-card:hover .course-more { color: var(--ink-0); }
.course-card:hover .course-go {
  background: var(--ink-0); border-color: var(--ink-0); color: var(--paper-0);
}
.course-card:hover .course-more svg { transform: translateX(2px); }
.course-card:focus-within .course-more { color: var(--ink-0); }

/* 整卡可点：把链接的伪元素铺满卡片，同时保留标题是真 <a>（键盘与读屏都友好） */
.course-link { text-decoration: none; }
.course-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
.course-card:focus-within { outline: 2px solid var(--ink-0); outline-offset: 2px; }

/* ---------------------------------------------------------------- 教练卡片
   原来整张卡是「居中排版」：圆形首字、姓名、段位、两行事实、居中标签。
   四张并排时每张都是一枚同心的圆，没有一条共同的左轴，视线只能上下跳；
   用户看了直接说「太丑」。现在改成名片式，和课程卡共用同一套语言：

     ┌ 纸色抬头 ───────────────┐
     │ [印] 周敏                 │   ← 印章 + 姓名/段位
     │      中国武术六段          │
     ├─────────────────────────┤
     │ 习练 28 年                │   ← 左对齐的事实
     │ 陈式太极拳第十二代传人      │
     ├─────────────────────────┤
     │ 擅长                      │   ← 落款：签条
     │ [陈式老架一路][太极剑]      │
     └─────────────────────────┘

   对齐仍然靠「事实两行 + 落款预留两行」保证（原来踩过的坑：师承名长短会让
   标签整体错位），只是现在全部左对齐，四张卡的左边缘、分隔线、标签起点是一条线。
*/
.coach-card {
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
}

/* 抬头：纸色带。和课程卡的 .course-head 同一档灰度，不引入新颜色 */
.coach-top {
  display: flex; align-items: center; gap: 14px;
  padding: 18px 20px;
  background: var(--paper-2);
  border-bottom: 1px solid var(--line-2);
  transition: background-color .18s ease;
}
.coach-card:hover .coach-top { background: var(--paper-3); }

.coach-avatar { flex: 0 0 auto; margin: 0; }

/* 没照片时的占位：方形「名章」—— 墨底、白姓、宋体。
   原来是 96px 的浅灰圆，四张并排像四个没填色的洞；改成 60px 的深色方章，
   体量小了一半、边界清楚了，反而成了每张卡的视觉落点。
   内阴影用 1px 亮线 + 4px 墨框，做出「印泥压进纸里」的一圈浅痕。 */
.coach-initial {
  width: 60px; height: 60px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--r1);
  background: var(--ink-0);
  color: var(--paper-0);
  font-family: var(--font-song);
  font-size: 27px; line-height: 1;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .16), inset 0 0 0 4px var(--ink-0);
  transition: background-color .2s ease, color .2s ease, box-shadow .2s ease;
}
/* 悬停时印章「翻」成白底墨字，和课程卡的圆箭头填墨是同一个动作 */
.coach-card:hover .coach-initial {
  background: var(--paper-0);
  color: var(--ink-0);
  box-shadow: inset 0 0 0 1px var(--ink-0);
}
.coach-avatar img {
  display: block;
  width: 60px; height: 60px;
  object-fit: cover;
  border-radius: var(--r1);
  border: 1px solid var(--line-1);
}

.coach-id { min-width: 0; }
/* .card h3 的 17px / margin-bottom 要压掉，所以这里带 .coach-id 提一级 */
.coach-id .coach-name {
  font-size: 19px;
  letter-spacing: .1em;
  margin: 0;
}
.coach-id .coach-rank {
  font-size: 11.5px;
  letter-spacing: .14em;
  color: var(--ink-4);
  margin: 6px 0 0;
}

.coach-body {
  display: flex; flex-direction: column;
  flex: 1 1 auto;
  padding: 16px 20px 20px;
}

/* 事实两行固定：第一行「习练 N 年」，第二行师承。
   固定两行是硬约束 —— 少了它，师承名较长的卡会折成三行，把标签推下去。 */
.coach-facts {
  display: flex; flex-direction: column; gap: 6px;
  font-size: 13px; line-height: 1.7;
  color: var(--ink-3);
  min-height: 46px;
}
.coach-k {
  font-size: 10.5px; letter-spacing: .18em;
  color: var(--ink-5);
}
.coach-facts b {
  font-family: var(--font-num); font-weight: 400;
  font-size: 17px; color: var(--ink-0);
  margin: 0 4px 0 7px;
  letter-spacing: .02em;
}

/* 落款：一条细线 + 「擅长」标签 + 签条。
   flex:1 是这里的第二个坑：四张卡由 grid 拉成等高，但卡内只有 .coach-body
   在伸展；如果落款块不跟着伸展，标签只有一行的卡就会在底部留一截空白，
   分隔线的位置也就跟着错。让落款吃掉剩余高度，标签始终从同一条线开始、
   卡片底边也始终齐平。min-height 只是兜底（一行标签时的最低高度）。 */
.coach-courses {
  flex: 1 1 auto;
  padding-top: var(--s4);
  border-top: 1px solid var(--line-2);
  display: flex; flex-direction: column; gap: 8px;
  min-height: 78px;
}
.coach-tags {
  display: flex; flex-wrap: wrap; gap: 6px;
}

/* ---------------------------------------------------------------- 会馆动态卡片
   原本是「分类 · 日期 / 标题 / 摘要」三行裸文字，三张并排毫无层次。
   现在：日期—细横线—分类 组成一条可伸缩的编排头；
   摘要限制三行；底部「阅读全文 →」与课程卡片同一套交互语言。
   若文章有封面图则自动变成媒体卡（图片在上），没有封面就纯文字 —— 不会出现破图。
*/
.news-card {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
  overflow: hidden;
  transition: border-color .18s ease;
}
.news-card:hover { border-color: var(--ink-6); }
.news-card:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 2px; }

.news-cover {
  display: block;
  position: relative;
  aspect-ratio: 8 / 5;
  overflow: hidden;
  background: var(--paper-3);
}
/* 没有封面时的占位块：位置照旧占住，画两道细线表示「这里本该有图」。
   不这么做的话，同一行里「有图 / 没图」两张卡的编排头会错开一整张图的高度
   （2026-10-24 端到端验证就是这么抓到第 2 行错位 225px 的）。 */
.news-cover.is-empty {
  background: var(--paper-3) url("../img/news-blank.svg") center / cover no-repeat;
  border-bottom: 1px solid var(--line-1);
}
.news-cover img {
  width: 100%; height: 100%;
  object-fit: cover; display: block;
  transition: transform .6s ease;
}
.news-card:hover .news-cover img { transform: scale(1.03); }

.news-body {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  padding: var(--s5);
  flex: 1 1 auto;
}

/* 编排头：日期 + 可伸缩细线 + 分类 */
.news-head { display: flex; align-items: center; gap: 10px; }
.news-date {
  font-family: var(--font-num);
  font-size: 11.5px; letter-spacing: .08em;
  color: var(--ink-4);
  white-space: nowrap;
}
.news-rule { flex: 1 1 auto; height: 1px; background: var(--line-2); }
.news-cat {
  font-size: 11.5px; letter-spacing: .2em;
  color: var(--ink-4);
  white-space: nowrap;
}
/* 作者（2026-10-30 新增）：跟在分类后面，和日期/分类同字号，但不要字距，
   名字是两三字中文，拉开字距会显得散 */
.news-author {
  font-size: 11.5px;
  color: var(--ink-4);
  white-space: nowrap;
}
.news-author::before { content: '· '; }

.news-title {
  font-size: 17px; line-height: 1.5; letter-spacing: .02em;
  margin: 0;
  color: var(--ink-0);
}
/* 摘要最多三行，避免长短不一 */
.news-summary {
  font-size: 13.5px; line-height: 1.8;
  color: var(--ink-3);
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.news-more {
  margin-top: auto;
  padding-top: var(--s2);
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--ink-4);
  transition: color .18s ease;
}
.news-more svg { transition: transform .18s ease; }
.news-card:hover .news-more { color: var(--ink-0); }
.news-card:hover .news-more svg { transform: translateX(3px); }

/* 列表页的底部行：左侧阅读数 + 右侧「阅读全文」 */
.news-foot {
  margin-top: auto;
  padding-top: var(--s2);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3);
  font-size: 12.5px; color: var(--ink-5);
}
.news-foot .news-more { margin-top: 0; padding-top: 0; }
.news-views { white-space: nowrap; }
/* 置顶：跟在分类后面用一条竖线隔开，不用彩色标签 */
.news-pin {
  font-size: 11px; letter-spacing: .18em;
  color: var(--ink-1);
  padding-left: 9px;
  border-left: 1px solid var(--line-2);
  white-space: nowrap;
}

/* 2026-10-25：首页动态 3 篇 → 9 篇（3 列 × 3 行），下面跟一枚「查看更多动态」。
   直接用 .btn.btn-ghost，只补三件事：居中、和上面卡片隔开、把「共 N 篇」压小。
   不用 .link-more 那种右侧小箭头 —— 那行末尾已经有一个了，两处同款会让人以为重复。 */
.news-all { margin-top: var(--s6); display: flex; justify-content: center; }
.news-all .btn { gap: 8px; }
.news-all-n {
  font-family: var(--font-num);
  font-size: 12px; letter-spacing: .04em;
  color: var(--ink-5); padding-left: 8px;
  border-left: 1px solid var(--line-2);
}
.news-all svg { transition: transform .18s ease; }
.news-all .btn:hover svg { transform: translateX(3px); }

/* 文章详情页的封面大图 */
.article-cover {
  margin: 0 0 var(--s5);
  border-radius: var(--r2);
  overflow: hidden;
  background: var(--paper-3);
}
.article-cover img {
  display: block;
  width: 100%; height: auto;
  max-height: 460px;
  object-fit: cover;
}

/* 官网正文：后台编辑器存下来的 HTML。
   和 assets/css/editor-content.css 是同一套尺度 —— 那边管编辑器 iframe 里的编辑视图，
   这边管官网上的成品。改一边记得对一下另一边，否则编辑时和发布后长得不一样。 */
.rich-text {
  font-size: 16px;
  line-height: 1.95;
  color: var(--ink-2);
}
.rich-text > *:first-child { margin-top: 0; }
.rich-text > *:last-child { margin-bottom: 0; }

/* 正文里的地图：库里存的是 .map-embed 占位块（只有坐标和一句话），
   渲染时由 Html::expandMaps() 换成和联系页同一份 .map 标记。
   386px 的默认高度在正文里太高，压到 360；手机上再压到 240。 */
.rich-text .map-embed { margin: 1.8em 0; }
.rich-text .map-embed .map { height: 360px; }
@media (max-width: 720px) {
  .rich-text .map-embed .map { height: 240px; }
}
.rich-text p { margin: 0 0 1.1em; }
.rich-text h1, .rich-text h2, .rich-text h3, .rich-text h4 {
  margin: 1.7em 0 .6em;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: .02em;
  color: var(--ink-1);
}
.rich-text h1 { font-size: 26px; }
.rich-text h2 { font-size: 22px; }
.rich-text h3 { font-size: 19px; }
.rich-text h4 { font-size: 17px; }
.rich-text blockquote {
  margin: 1.3em 0;
  padding: 2px 0 2px 16px;
  border-left: 2px solid var(--line-2);
  color: var(--ink-3);
}
.rich-text ul, .rich-text ol { margin: 0 0 1.1em; padding-left: 1.6em; }
.rich-text li { margin: .35em 0; }
.rich-text img { max-width: 100%; height: auto; }
.rich-text figure { margin: 1.4em 0; }
.rich-text figcaption {
  margin-top: .5em;
  color: var(--ink-4);
  font-size: 13px;
  text-align: center;
}
.rich-text a {
  color: var(--ink-1);
  border-bottom: 1px solid var(--line-2);
}
.rich-text a:hover { border-bottom-color: var(--ink-3); }
.rich-text hr {
  margin: 2.2em 0;
  border: 0;
  border-top: 1px solid var(--line-1);
}
.rich-text table {
  width: 100%;
  margin: 1.4em 0;
  border-collapse: collapse;
  font-size: 14.5px;
}
.rich-text th, .rich-text td {
  border: 1px solid var(--line-1);
  padding: 9px 11px;
  text-align: left;
}
.rich-text th { background: var(--paper-2); font-weight: 600; color: var(--ink-1); }
.rich-text code, .rich-text pre {
  background: var(--paper-2);
  font-family: Consolas, "Courier New", monospace;
  font-size: 13.5px;
}
.rich-text pre { padding: 12px 14px; overflow-x: auto; }

/* UEditor 的图标是一张彩色雪碧图（表格、图片、涂鸦三处带品牌色）。
   CSS 里的颜色已经在 bin/ueditor-mono.php 里全部转灰，位图转不了，只能用滤镜兜住。
   只作用在图标上 —— 整个工具栏加滤镜会让 iframe 里的正文发虚。 */
.editor-wrap .edui-icon {
  filter: grayscale(1);
}

/* 后台正文编辑器外壳：UEditor 的皮肤已经全灰（见 bin/ueditor-mono.php），
   这里只压掉它的圆角、阴影和蓝底，让它跟表单长在一起。 */
.editor-wrap .edui-editor,
.editor-wrap .edui-editor-toolbarboxouter {
  border-color: var(--line-2);
  border-radius: 0;
  box-shadow: none;
}
.editor-wrap .edui-editor-toolbarboxouter {
  border-bottom: 1px solid var(--line-1);
  background: var(--paper-1);
}
.editor-wrap .edui-editor-iframeholder { border-radius: 0; }
.editor-wrap .edui-combox-body,
.editor-wrap .edui-button-body { border-radius: 0; }

/* 编辑器没加载出来时的兜底说明：框还是能写的 */
.editor-fallback {
  margin-top: 6px;
  padding: 8px 12px;
  border-left: 2px solid var(--line-2);
  background: var(--paper-1);
  color: var(--ink-3);
}

.hero-sm { padding: var(--s8) 0 var(--s6); border-bottom: 1px solid var(--line-1); background: var(--paper-0); }
.hero-sm h1 { font-size: 34px; letter-spacing: .14em; }
.hero-sm p { color: var(--ink-4); margin: var(--s3) 0 0; }

/* 数字看板 */
.stats { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 1px; background: var(--line-1); border: 1px solid var(--line-1); }
.stat { background: var(--paper-0); padding: var(--s5); }
.stat-num { font-family: var(--font-num); font-size: 30px; color: var(--ink-0); line-height: 1.2; }
.stat-num small { font-size: 14px; color: var(--ink-4); margin-left: 4px; }
.stat-label { font-size: 13px; color: var(--ink-4); margin-top: var(--s1); }
/* 会员首页「已上 N 节」那张卡是链接：整格可点，标签后面跟一个「明细 →」提示 */
a.stat { position: relative; display: block; color: inherit; text-decoration: none; transition: background-color .16s; }
a.stat:hover { background: var(--paper-1); }
a.stat:focus-visible { outline: 2px solid var(--ink-2); outline-offset: -3px; }
.stat-go { font-size: 12px; color: var(--ink-5); margin-left: 6px; }
a.stat:hover .stat-go { color: var(--ink-2); }

/* 会员首页「上课签到」卡：左边说明、右边按钮，窄屏自动上下排 */
.checkin-card { display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap; }
.checkin-main { flex: 1 1 320px; min-width: 0; }
.checkin-main h3 { margin-bottom: var(--s2); }
.checkin-side { flex: 0 0 auto; }
.checkin-form { margin: 0; }
.btn.is-off { color: var(--ink-5); background: var(--paper-1); border-color: var(--line-1); cursor: default; }

/* 官网首页底部 CTA 的会徽与标题：尺寸由后台参数给（内联 width/height 与 font-size），
   这里只兜住两件事 —— 会徽不撑破窄屏、标题字距与原设计一致。 */
.cta-logo { display: block; margin: 0 auto 24px; object-fit: contain; max-width: 60vw; height: auto; }
.cta-title { letter-spacing: .14em; }

/* 官网首页底部的二维码：一张窄竖条，左边图右边两行字，窄屏自动居中 */
.qr-box {
  display: inline-flex; align-items: center; gap: var(--s4);
  margin-top: var(--s6); padding: 14px 18px 14px 14px;
  border: 1px solid var(--line-1); border-radius: var(--r3); background: var(--paper-1);
  text-align: left;
}
.qr-img {
  display: block; width: 92px; height: 92px; flex: none;
  border: 1px solid var(--line-2); border-radius: var(--r2); background: #fff;
  image-rendering: pixelated; /* 二维码是由方点组成的，缩放时不要被抹成灰边 */
}
.qr-title { font-size: 14px; color: var(--ink-1); margin: 0; letter-spacing: .04em; }
.qr-sub { font-size: 12.5px; color: var(--ink-4); margin: 6px 0 0; line-height: 1.6; max-width: 260px; }

/* 会员名单里名字后面的「手动签到」：不抢名字的注意力，但要看得出是个按钮 */
.signin-form { display: inline; margin: 0 0 0 8px; }
/* 手动签到：原来是一条带下划线的 12px 灰字，挤在姓名下面，看着像名字的一部分
   （用户原话：「这个手动签到的按钮太丑了」）。改成一枚小号描边按钮 ——
   圆角、白底、字色提一档、前面加个对勾，一眼能看出「这是可以按的东西」；
   仍在姓名那一格的下方，不额外占列宽，所以列表行高不变。 */
.signin-btn {
  display: inline-flex; align-items: center; gap: 4px;
  margin: 3px 0 0 8px; padding: 1px 9px 1px 7px;
  border: 1px solid var(--line-2); border-radius: 999px;
  background: var(--paper-0); cursor: pointer;
  font-size: 12px; line-height: 1.7; color: var(--ink-2);
  text-decoration: none; white-space: nowrap;
  transition: background .15s, border-color .15s, color .15s;
}
.signin-btn::before { content: "✓"; font-size: 11px; line-height: 1; color: var(--state-ok); }
.signin-btn:hover { background: var(--paper-2); border-color: var(--ink-4); color: var(--ink-0); text-decoration: none; }
.signin-btn:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 2px; }
@media (max-width: 560px) {
  .qr-box { flex-direction: column; text-align: center; gap: var(--s3); }
  .qr-sub { max-width: none; }
}

/* ---------------------------------------------------------------- 会馆理念（三条）
   原本是三段并排的裸文字，没有边界、没有层次，看着很单调。
   这里给整组加一条上边线 + 条目之间的竖分隔线，把「三段文字」变成一个整体；
   每条再配上序号与刻度线，形成可数的节奏 —— 与轮播的 01/02 编号呼应。
*/
.credos {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s6);
  border-top: 1px solid var(--line-2);
}
.credo {
  position: relative;
  padding-top: var(--s6);
}
/* 分隔线画在「列间距的正中间」，而不是用 border-left。
   用 border-left 的话，第一条左边没线、右边有线，三条的正文可用宽度就不一样
   （实测 345 / 313 / 345），换行位置会不一致。 */
.credo + .credo::before {
  content: "";
  position: absolute;
  left: calc(var(--s6) / -2);
  top: var(--s6);
  bottom: 4px;
  width: 1px;
  background: var(--line-2);
}

.credo-num {
  display: block;
  font-family: var(--font-num);
  font-size: 12px; letter-spacing: .18em;
  color: var(--ink-5);
  line-height: 1;
}
/* 序号下面一小截刻度线，像标尺，也把序号和下面的字分开 */
.credo-num::after {
  content: "";
  display: block;
  width: 20px; height: 1px;
  margin-top: 12px;
  background: var(--ink-3);
  transform-origin: left center;
  transition: transform .25s ease, background-color .25s ease;
}
.credo-label {
  display: block;
  margin-top: var(--s4);
  font-size: 12px; letter-spacing: .26em;
  color: var(--ink-4);
}
.credo-title {
  font-size: 20px; letter-spacing: .04em;
  color: var(--ink-0);
  margin: var(--s2) 0 var(--s3);
}
.credo-text {
  font-size: 14.5px; line-height: 1.9;
  color: var(--ink-3);
  margin: 0;
}
/* 轻微呼应：鼠标划到某一条时，只有那条的刻度线伸长一点点。
   刻意不做位移、不换色，避免暗示「这一块能点」。 */
.credo:hover .credo-num::after { transform: scaleX(1.7); background: var(--ink-1); }

/* 太极图（纯 CSS，无图片依赖） */
.taiji {
  width: 96px; height: 96px; border-radius: 50%;
  background: linear-gradient(90deg, var(--ink-0) 50%, var(--paper-0) 50%);
  position: relative; border: 1px solid var(--ink-0);
}
.taiji::before, .taiji::after {
  content: ""; position: absolute; width: 48px; height: 48px; border-radius: 50%; left: 24px;
}
.taiji::before { top: 0; background: var(--paper-0); box-shadow: inset 0 0 0 1px var(--ink-0); }
.taiji::after { bottom: 0; background: var(--ink-0); box-shadow: inset 0 0 0 1px var(--ink-0); }

/* ---------------------------------------------------------------- 表格 */
.table-wrap { overflow-x: auto; border: 1px solid var(--line-1); border-radius: var(--r2); background: var(--paper-0); }
table.tb { width: 100%; border-collapse: collapse; font-size: 14px; }
table.tb th, table.tb td { padding: 11px 14px; text-align: left; border-bottom: 1px solid var(--line-1); vertical-align: middle; }
table.tb th {
  background: var(--paper-2); font-weight: 500; font-size: 12.5px;
  color: var(--ink-4); letter-spacing: .06em; white-space: nowrap;
}
table.tb tbody tr:last-child td { border-bottom: 0; }
table.tb tbody tr:hover { background: var(--paper-1); }
table.tb .num { font-family: var(--font-num); text-align: right; white-space: nowrap; }
table.tb .nowrap { white-space: nowrap; }

/* ---------------------------------------------------------------- 徽标/状态
   状态用「语义色 + 文字」双通道表达，不单靠颜色（色盲用户也能读字）。
   三档色相：红（紧急 / 已逾期 / 不可用）、橙（警告 / 待处理）、绿（提醒 / 已处理 / 正常）。
   每一档都是「深色字 + 极淡底 + 同色描边 + 同色圆点」，字号字重不变，
   所以彩色只作为一层次要提示，版面仍是黑白为主。
   提醒中心之外的地方（会员在册/停卡、排课已完成/已取消…）复用同一套，
   含义一致：红＝要处理或已坏，橙＝有变数，绿＝正常。

   2026-10-21 重画了这枚标签（用户：「图例里那四个小标签不好看，设计得好看些」）：
   原来 25px 高、`border-radius: 999px` 的全胶囊 + 5px 圆点，四个并排像一串贴纸，
   和系统里 2/4/6px 的圆角体系也对不上。现在：
     · 定高 24px、左右 9px，四个标签高度绝对一致（之前靠 padding + 行高撑，实测 25px）；
     · 圆角降到 6px（`--r3`），从「贴纸」变成「系统里的一枚状态」；
     · 圆点 5px→6px：在 12px 字旁边不再像一粒灰尘；
     · 字重 500、字距 .02em：两三个汉字的标签不再糊成一团，也更压得住淡底。
   颜色一个都没动（红/橙/绿是语义色白名单，深浅由对比度断言钉着）。 */
.tag {
  display: inline-flex; align-items: center; gap: 6px;
  height: 24px; padding: 0 9px;         /* 定高不靠 padding 撑：四个标签和表格行里的标签才对得齐 */
  font-size: 12px; line-height: 1; font-weight: 500; letter-spacing: .02em;
  border: 1px solid var(--line-2);
  border-radius: var(--r3);             /* 6px：和卡片/输入框的 2–6px 圆角同一族，不再用 999px 的胶囊 */
  color: var(--ink-3); background: var(--paper-1); white-space: nowrap;
}
.tag::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--ink-5); }
.tag-ok { border-color: var(--state-ok-line); color: var(--state-ok); background: var(--state-ok-bg); }
.tag-ok::before { background: var(--state-ok); }
.tag-warn { border-color: var(--state-warn-line); color: var(--state-warn); background: var(--state-warn-bg); }
.tag-warn::before { background: var(--state-warn); }
.tag-danger {
  border-color: var(--state-danger-line); color: var(--state-danger); background: var(--state-danger-bg);
  font-weight: 600;
}
.tag-danger::before { background: var(--state-danger); }
.tag-plain::before { display: none; }
/* 官网卡片里的标签（可授课程、班型、课时…）不是「状态」，是内容本身。
   它们原来就是胶囊，2026-10-21 重画后台状态标签时加了这个修饰类把它们留住，
   免得顺手把官网卡片的观感也改了。后台的状态标签不用它。 */
.tag-pill {
  height: auto; padding: 1px 8px; gap: 5px;
  font-weight: 400; letter-spacing: normal; line-height: inherit;
  border-radius: 999px;
}
.tag-solid { background: var(--ink-1); color: var(--paper-0); border-color: var(--ink-1); }
.tag-solid::before { background: var(--paper-0); }

/* 状态色图例：一行「标签 + 一句话」，贴着表格上边。
   做过一版「左侧竖标签 + 四列等宽格子 + 短竖线」的说明带，看着更规整，
   但它把卡片标题和表头之间顶出一条独立的灰带，用户要的是「恢复原来那种」，
   于是退回下面这个朴素版本：一行、可换行、不抢表头的戏。
   颜色只出现在标签上（红/橙/绿是语义色白名单），底色、细线、文字全是灰阶。 */
.tag-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s5);
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--line-1);
  font-size: 12px;
  color: var(--ink-4);
}
.tag-legend-item { display: inline-flex; align-items: center; gap: 6px; }

/* ---------------------------------------------------------------- 筛选筹码
   用于「一排链接当筛选器」的场景（提醒中心按类型、线索按状态）。
   关键点：**必须能看出当前选中哪一个**。
   以前这些链接直接套 .tag / $levelTag(等级)，那套配色表达的是紧急程度，
   跟「选中」无关，所以点来点去颜色都不变 —— 用户反馈的正是这个问题。

   选中态用反色（黑底白字），因为全站无彩色，反色是权重最高的表达；
   非选中态保持描边 + 纸底 + 悬停加深。
*/
.chip-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; min-height: 32px;
  border: 1px solid var(--line-2); border-radius: 999px;
  background: var(--paper-0); color: var(--ink-2);
  font-size: 12.5px; line-height: 1.4; white-space: nowrap;
  text-decoration: none;
  transition: background-color .15s, border-color .15s, color .15s;
}
.chip:hover { border-color: var(--ink-4); color: var(--ink-0); background: var(--paper-2); }
.chip:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 2px; }
/* 计数：与文字同一行，字号略小、颜色浅一档 */
.chip-n { font-family: var(--font-num); font-size: 11.5px; color: var(--ink-4); }
/* 等级小圆点（紧急/警告/提醒），用当前文字色 + 透明度，选中反色后自动跟着变 */
.chip-dot {
  width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; opacity: .45; flex: 0 0 5px;
}
.chip-dot.lv-urgent { opacity: .95; }
.chip-dot.lv-warn { opacity: .70; }

/* 选中：反色。这条是整个组件存在的理由，别删 */
.chip.is-on {
  background: var(--ink-0); border-color: var(--ink-0); color: var(--paper-0);
}
.chip.is-on:hover { background: var(--ink-1); border-color: var(--ink-1); color: var(--paper-0); }
.chip.is-on .chip-n { color: var(--paper-0); opacity: .78; }
.chip.is-on .chip-dot { background: var(--paper-0); opacity: .9; }

/* 说明：提醒中心的「到期时间」已并入「筛选」卡，改成一个下拉（#f-due）。
   原先那一排 .due-chips 筹码、以及配套的 .due-chips-label / .chip-clear
   因此没有视图引用了，一并删掉 —— 留着就是死样式，以后改配色还要白扫一遍。 */

/* ---------------------------------------------------------------- 表单 */
.field { margin-bottom: var(--s4); }
.field > label, .label {
  display: block; font-size: 13px; color: var(--ink-3); margin-bottom: 6px;
}
.label .req { color: var(--ink-0); font-weight: 600; margin-left: 2px; }
.input, .select, .textarea {
  width: 100%; padding: 9px 12px; font-size: 14px; font-family: var(--font-hei);
  color: var(--ink-1); background: var(--paper-0);
  border: 1px solid var(--line-2); border-radius: var(--r2); line-height: 1.6;
  transition: border-color .15s, box-shadow .15s;
  caret-color: var(--ink-0);
}
/* 聚焦：边框压到墨色 + 一圈 3px 淡环，光标是墨色的。
   原来只把 border-color 提到 --ink-2，在浅灰底上几乎看不出来；加环之后
   「点进哪个框」一眼可见，失焦自动退回普通边框（transition 里带着 box-shadow）。 */
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--ink-1);
  box-shadow: 0 0 0 3px rgba(20, 20, 20, .10);
}
.input[readonly], .input:disabled { background: var(--paper-2); color: var(--ink-4); }
.textarea { min-height: 88px; resize: vertical; }
.hint { font-size: 12px; color: var(--ink-5); margin-top: 4px; }
.err { font-size: 12px; color: var(--ink-0); font-weight: 500; margin-top: 4px; }
/* 空的 .err 必须整块消失：::before 的「！」是内容，不是背景，
   元素就算没字也会把这个符号画出来 —— 曾因此在每个输入框下面挂一排感叹号。
   视图那边也改成了「有错才渲染」，这里再兜一层，免得下次谁又写漏。 */
.err:empty { display: none; }
.err::before { content: "！"; margin-right: 2px; }
.input.is-err, .select.is-err, .textarea.is-err { border-color: var(--ink-0); }
.input.is-err:focus, .select.is-err:focus, .textarea.is-err:focus {
  box-shadow: 0 0 0 3px rgba(20, 20, 20, .16);
}

/* ---------------- 输入框右侧的两个小工具
   一键清空（×）与密码显隐（眼睛）都绝对定位在输入框右端内侧，不参与文档流：
   输入框宽度、行高、表格列宽一律不变，所以既不会把窄格撑开，也不会让
   校验脚本量到的尺寸对不上。按钮 26×26，输入框右侧补同宽的内边距让文字让位。
*/
/* 浏览器自带的密码显隐眼睛（Edge 的 ::-ms-reveal / 旧版 ::-ms-clear）
   会和右下角那枚眼睛并排成「两只眼睛」，用户反馈过一次。统一关掉，只留我们自己的。 */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear { display: none; }
input[type="password"]::-webkit-credentials-auto-fill-button,
input[type="password"]::-webkit-textfield-decoration-container { visibility: hidden; margin-right: 0; }
.inp-host { position: relative; }
.input.has-tools { padding-right: 38px; }
.input.has-two { padding-right: 66px; }
.inp-tools {
  position: absolute; display: flex; align-items: center; gap: 2px;
  z-index: 2; pointer-events: none;
}
.inp-tools > * { pointer-events: auto; }
.inp-btn {
  width: 26px; height: 26px; padding: 0; display: inline-flex;
  align-items: center; justify-content: center;
  background: transparent; border: 0; border-radius: 50%;
  color: var(--ink-5); cursor: pointer; transition: background-color .12s, color .12s;
}
.inp-btn:hover { background: var(--paper-3); color: var(--ink-0); }
.inp-btn:focus-visible { outline: 2px solid var(--ink-0); outline-offset: 1px; }
.inp-btn[hidden] { display: none; }
.inp-btn.is-on { color: var(--ink-0); }
.inp-btn svg { display: block; }

/* 离开输入框时的即时校验提示（与后端 .err 分开：一个是「还没提交就告诉你」，
   一个是「提交后服务器说的」。两套同时出现也不会互相覆盖。） */
.field-msg { font-size: 12px; color: var(--ink-0); font-weight: 500; margin-top: 4px; }
.field-msg::before { content: "！"; margin-right: 2px; }
.field-row { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--s4); }
.field-row-3 { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: var(--s4); }

/* 形象照（会员档案 / 教练档案）：
   左边一块固定尺寸的方框放当前照片，右边是上传控件。
   照片框必须有底色与边框 —— 没照片时如果只是一片空白，
   人会以为页面没加载完，而不是「这里可以传照片」。
   尺寸取 96px：比头像（60px）大，能看清脸；又不会把基本信息挤下去。 */
.photo-field {
  display: flex; align-items: flex-start; gap: var(--s4);
  margin-bottom: var(--s5); padding-bottom: var(--s5);
  border-bottom: 1px solid var(--line-1);
}
.photo-thumb {
  flex: 0 0 auto; width: 96px; height: 96px;
  display: flex; align-items: center; justify-content: center;
  background: var(--paper-2); border: 1px solid var(--line-2); border-radius: var(--r2);
  overflow: hidden;
}
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-thumb.is-empty span { font-size: 11px; color: var(--ink-5); letter-spacing: .08em; }
.photo-field-side { flex: 1 1 auto; min-width: 0; }
.photo-field-side > label {
  display: block; font-size: 13px; color: var(--ink-3); margin-bottom: 6px;
}
@media (max-width: 560px) {
  .photo-field { flex-direction: column; gap: var(--s3); }
}
/* 收据凭据：一排小图 + 说明 + 删除。缩略图定高 96px、宽度按比例，
   这样一张竖拍手机截图和一张横拍回单混在一起也不会把网格撑变形。 */
.att-grid { display: flex; flex-wrap: wrap; gap: var(--s4); margin-bottom: var(--s5); }
.att-item { margin: 0; width: 168px; }
.att-thumb {
  display: block; height: 96px; border: 1px solid var(--line-2); border-radius: var(--r2);
  background: var(--paper-2); overflow: hidden;
}
.att-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.att-thumb:hover { border-color: var(--line-card); }
.att-meta { display: flex; flex-direction: column; gap: 3px; margin-top: 6px; }
.att-note { font-size: 13px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.att-sub { font-size: 11.5px; color: var(--ink-5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.att-del { margin: 4px 0 0; }
.att-del .btn-link { font-size: 11.5px; padding: 0; color: var(--ink-5); }
/* 货品图片：列表里 32px 小方块、详情页 64px，没图的占位留一块灰 ——
   不加占位的话有图和没图的两行会错开，一列名字忽高忽低。 */
.asset-name { display: inline-flex; align-items: center; gap: 8px; }
.asset-thumb {
  display: block; width: 32px; height: 32px; flex: none; object-fit: cover;
  border: 1px solid var(--line-2); border-radius: var(--r2); background: var(--paper-2);
}
.asset-thumb.is-empty { background: var(--paper-2); }
.asset-head { display: flex; align-items: center; gap: var(--s4); }
.asset-photo {
  display: block; width: 64px; height: 64px; flex: none; overflow: hidden;
  border: 1px solid var(--line-2); border-radius: var(--r2); background: var(--paper-2);
}
.asset-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.asset-photo:hover { border-color: var(--line-card); }
.att-form { padding-top: var(--s4); border-top: 1px solid var(--line-1); }
/* 上传行：文件框吃掉剩下的宽度，说明框定宽，按钮贴底。
   不用 .field-row-3 —— 三列等宽会让「选择文件 未选择任何文件」和下面那行提示
   各自换行、跟相邻两列对不齐（第一版就是那样，截图上很明显）。 */
.att-form-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s4) var(--s5); max-width: 760px; }
.att-file { flex: 1 1 260px; min-width: 0; }
.att-file .label, .att-note-field .label { display: block; font-size: 13px; color: var(--ink-3); margin-bottom: 6px; }
.att-file input[type="file"] { display: block; max-width: 100%; font-size: 13px; }
.att-file .hint { display: block; margin-top: 5px; }
.att-note-field { flex: 0 1 220px; }
.att-submit { flex: 0 0 auto; }
.att-flag { margin-left: 6px; height: 20px; padding: 0 7px; font-size: 11px; vertical-align: middle; }
@media (max-width: 560px) {
  .att-item { width: calc(50% - var(--s2)); }
  .att-form-row { flex-direction: column; align-items: stretch; }
  .att-note-field { flex: 0 0 auto; }
  .att-submit { width: 100%; justify-content: center; }
}
.check { display: flex; align-items: center; gap: 7px; font-size: 14px; color: var(--ink-2); cursor: pointer; }
.check input { accent-color: var(--ink-1); width: 15px; height: 15px; }
.check-group { display: flex; flex-wrap: wrap; gap: var(--s4); }

.form-actions { display: flex; gap: var(--s3); align-items: center; padding-top: var(--s3); }

/* ---------------------------------------------------------------- 多选会员（点选清单）
   原生 <select multiple> 必须按住 Ctrl/⌘ 才能多选，一松手就只剩一个，
   而且触摸屏上根本没法多选。这里改成「一行一个会员、点一下选中、再点一下取消」：
   状态仍由原生 checkbox 承载（提交格式不变、键盘可用），只是把它藏起来，
   用 :has(:checked) 把整行反色 —— 与后台筛选筹码共用同一套「选中 = 反色」语言。
*/
.pick-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); }
.pick-head .label { margin-bottom: 6px; }
.pick-count { font-size: 12.5px; color: var(--ink-3); font-family: var(--font-num); white-space: nowrap; }
.pick-panel { border: 1px solid var(--line-2); border-radius: var(--r2); background: var(--paper-0); overflow: hidden; }
.pick-tools { padding: 8px 10px; border-bottom: 1px solid var(--line-1); }
.pick-tools .input { padding: 7px 10px; font-size: 13px; }
.pick-list { max-height: 300px; overflow-y: auto; padding: 8px; display: grid; gap: 6px; }

.pick {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border: 1px solid var(--line-1); border-radius: var(--r1);
  background: var(--paper-0); color: var(--ink-1); cursor: pointer;
  transition: background-color .12s, border-color .12s, color .12s;
}
.pick:hover { border-color: var(--ink-4); background: var(--paper-2); }
.pick[hidden] { display: none; }
/* 藏起复选框但保留可聚焦与可点击 */
.pick input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.pick-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.pick-name { font-size: 14px; letter-spacing: .02em; }
.pick-meta { font-size: 12px; color: var(--ink-4); }
.pick-tick {
  flex: 0 0 18px; width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid var(--line-2); display: grid; place-items: center;
  color: transparent; transition: background-color .12s, border-color .12s, color .12s;
}
.pick-tick svg { width: 11px; height: 11px; }

.pick:has(input:checked) { background: var(--ink-0); border-color: var(--ink-0); color: var(--paper-0); }
.pick:has(input:checked):hover { background: var(--ink-1); border-color: var(--ink-1); }
.pick:has(input:checked) .pick-meta { color: rgba(255, 255, 255, .74); }
.pick:has(input:checked) .pick-tick { background: var(--paper-0); border-color: var(--paper-0); color: var(--ink-0); }
.pick:has(input:focus-visible) { outline: 2px solid var(--ink-0); outline-offset: 2px; }
.pick:has(input:checked):has(input:focus-visible) { outline-color: var(--ink-0); }
.pick-empty { margin: 4px; font-size: 13px; color: var(--ink-4); }

/* ---------------------------------------------------------------- 提示条
   同样不用色相：靠左侧竖线的「实线/虚线/粗细」+ 底色深浅区分。
*/
.alert {
  padding: 11px 16px; border-radius: var(--r2); font-size: 14px;
  border: 1px solid var(--line-2); border-left-width: 3px;
  background: var(--paper-2); color: var(--ink-2);
  margin-bottom: var(--s4);
}
.alert-ok { border-color: var(--line-2); border-left-color: var(--ink-6); background: var(--paper-1); color: var(--ink-2); }
.alert-err { border-color: var(--line-2); border-left-color: var(--ink-0); background: var(--paper-1); color: var(--ink-0); }
.alert-warn { border-color: var(--line-2); border-left-style: dashed; border-left-color: var(--ink-3); background: var(--paper-2); color: var(--ink-1); }

/* ---------------------------------------------------------------- 分页 */
.pager { display: flex; gap: var(--s1); align-items: center; margin-top: var(--s5); flex-wrap: wrap; }
.pager a, .pager span {
  min-width: 32px; text-align: center; padding: 5px 9px; font-size: 13px;
  border: 1px solid var(--line-1); border-radius: var(--r2); color: var(--ink-3); background: var(--paper-0);
}
.pager a:hover { border-color: var(--ink-4); color: var(--ink-0); }
.pager .is-on { background: var(--ink-1); border-color: var(--ink-1); color: var(--paper-0); }
.pager .info { border: 0; background: none; color: var(--ink-5); margin-right: auto; }
/* 2026-10-25：分页条补齐「每页条数」「跳页」。位置不动、只加两段控件 ——
   info 用 margin-right:auto 把它推到最左，其余一串靠右，和原来同一条基线。 */
.pager-size { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--ink-5); }
.pager-size select {
  padding: 4px 6px; font-size: 13px; color: var(--ink-3);
  border: 1px solid var(--line-1); border-radius: var(--r2); background: var(--paper-0);
}
.pager-size select:hover { border-color: var(--ink-4); }
.pager .pager-size-t, .pager .pager-jump-t {
  min-width: 0; padding: 0; border: 0; background: none; font-size: 12.5px; color: var(--ink-5);
}
/* 到头了的「上一页/下一页」留着但不响应 —— 藏掉会让整条左右跳位 */
.pager .is-off { color: var(--ink-7); background: var(--paper-1); cursor: default; }
.pager-jump { display: inline-flex; align-items: center; gap: 6px; margin-left: var(--s4); }
.pager-jump .input { width: 66px; padding: 4px 8px; font-size: 13px; text-align: center; }

/* ---------------------------------------------------------------- 空态 */
.empty { text-align: center; padding: var(--s8) var(--s5); color: var(--ink-5); }
.empty-title { font-family: var(--font-song); font-size: 17px; color: var(--ink-3); margin-bottom: var(--s2); }

/* ---------------------------------------------------------------- 页脚 */
.site-footer { border-top: 1px solid var(--line-1); background: var(--paper-0); padding: var(--s7) 0 var(--s5); margin-top: auto; }
.footer-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: var(--s6); }
.footer-grid h4 { font-size: 14px; font-family: var(--font-hei); font-weight: 500; color: var(--ink-2); margin-bottom: var(--s3); letter-spacing: .08em; }
.footer-grid p, .footer-grid li { font-size: 13.5px; color: var(--ink-4); margin: 0 0 5px; }
.footer-grid ul { list-style: none; padding: 0; margin: 0; }
.footer-bottom {
  margin-top: var(--s6); padding-top: var(--s4); border-top: 1px solid var(--line-1);
  display: flex; justify-content: space-between; gap: var(--s3); flex-wrap: wrap;
  font-size: 12.5px; color: var(--ink-5);
}

/* ---------------------------------------------------------------- 通用 */
.text-muted { color: var(--ink-4); }
.text-ink { color: var(--ink-1); }
.text-sm { font-size: 13px; }
.text-xs { font-size: 12px; }
.mono { font-family: var(--font-num); }
.mt0 { margin-top: 0; } .mt2 { margin-top: var(--s2); } .mt3 { margin-top: var(--s3); }
.mt4 { margin-top: var(--s4); } .mt5 { margin-top: var(--s5); } .mt6 { margin-top: var(--s6); }
.mb0 { margin-bottom: 0; } .mb2 { margin-bottom: var(--s2); } .mb3 { margin-bottom: var(--s3); }
.mb4 { margin-bottom: var(--s4); } .mb5 { margin-bottom: var(--s5); }
.flex { display: flex; } .flex-between { display: flex; justify-content: space-between; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.flex-center { display: flex; align-items: center; gap: var(--s2); }
.flex-wrap { flex-wrap: wrap; }
.gap2 { gap: var(--s2); } .gap3 { gap: var(--s3); } .gap4 { gap: var(--s4); }
.ml-auto { margin-left: auto; }
.right { text-align: right; } .center { text-align: center; }
.nowrap { white-space: nowrap; }
.hidden { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }

/* 定义列表：用于详情页 */
.dl { display: grid; grid-template-columns: max-content minmax(0,1fr); gap: 9px var(--s5); font-size: 14px; }
.dl dt { color: var(--ink-4); font-size: 13px; }
.dl dd { margin: 0; color: var(--ink-1); }

/* ==========================================================================
   后台
   ========================================================================== */
.admin-body { background: var(--paper-2); }

.admin-shell { display: flex; min-height: 100vh; }

.side {
  width: 216px; flex: 0 0 216px; background: var(--ink-0); color: var(--paper-2);
  display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.side-brand { padding: var(--s5) var(--s4); border-bottom: 1px solid rgba(255,255,255,.1); display: flex; align-items: center; gap: 10px; }
.side-brand .n { font-family: var(--font-song); font-size: 17px; color: #fff; letter-spacing: .14em; }
.side-brand .s { font-size: 11px; color: var(--on-dark-2); letter-spacing: .16em; margin-top: 3px; }
.side-nav { padding: var(--s3) 0; flex: 1; }
.side-group { padding: var(--s4) var(--s4) var(--s1); font-size: 11px; color: var(--on-dark-2); letter-spacing: .18em; }
.side-nav a {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding: 9px var(--s4); font-size: 14px; color: #C9C9C6;
  border-left: 2px solid transparent; transition: background .12s, color .12s;
}
.side-nav a:hover { background: rgba(255,255,255,.06); color: #fff; }
.side-nav a.is-on { background: rgba(255,255,255,.1); color: #fff; border-left-color: #fff; }
.side-nav .badge {
  font-family: var(--font-num); font-size: 11px; background: var(--paper-0); color: var(--ink-0);
  border-radius: 999px; padding: 0 6px; min-width: 18px; text-align: center; line-height: 17px;
}
.side-foot { padding: var(--s4); border-top: 1px solid rgba(255,255,255,.1); font-size: 12px; color: var(--on-dark-2); }

.admin-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.topbar {
  height: 56px; flex: 0 0 56px; background: var(--paper-0); border-bottom: 1px solid var(--line-1);
  display: flex; align-items: center; gap: var(--s4); padding: 0 var(--s5); position: sticky; top: 0; z-index: 30;
}
.crumb { font-size: 13px; color: var(--ink-4); }
.crumb b { color: var(--ink-1); font-weight: 500; }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: var(--s4); font-size: 13px; color: var(--ink-3); }

.admin-content { padding: var(--s5); flex: 1; }
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s4); margin-bottom: var(--s5); flex-wrap: wrap; }
.page-head h1 { font-size: 22px; letter-spacing: .06em; }
.page-head .sub { font-size: 13px; color: var(--ink-4); margin-top: 3px; }

/* 后台卡片：细线、无圆角、无阴影 */
.pg-card { background: var(--paper-0); border: 1px solid var(--line-1); border-radius: var(--pg-r); }
.pg-card + .pg-card { margin-top: var(--s5); }
.pg-card-head {
  padding: var(--s4) var(--s5); border-bottom: 1px solid var(--line-1);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3); flex-wrap: wrap;
}
.pg-card-head h2 { font-size: 15px; letter-spacing: .06em; }
.pg-card-body { padding: var(--s5); }
.pg-card-body.flush { padding: 0; }

/* 指标格 */
.pg-stats { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--s4); margin-bottom: var(--s5); }
/* 提醒中心：到期四档接在四个类型卡下面，一行四列（≤900px 会自动变两列）。
   2026-10-25 晚从五档改回四档：删掉「下月到期」之后，上下两排列数一致，
   竖线才会落在同一组 25% / 50% / 75% 上（用户原话「上下左右排版对齐」）。 */
.pg-stats.pg-stats-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
.pg-stat { background: var(--paper-0); border: 1px solid var(--line-1); padding: var(--s4) var(--s5); }
.pg-stat .k { font-size: 12.5px; color: var(--pg-muted); display: flex; align-items: center; gap: 5px; }
.pg-stat .v { font-family: var(--font-num); font-size: 26px; color: var(--ink-0); line-height: 1.3; margin-top: 2px; font-variant-numeric: tabular-nums; }
.pg-stat .v small { font-size: 13px; color: var(--pg-muted); }
.pg-stat .d { font-size: 12px; color: var(--pg-muted); margin-top: 2px; }
/* 指标数字的语义色：只染数字，卡片本身仍是白底黑框。
   空值（0）不染色 —— 一屏全是红数字会让人以为处处出事。 */
.pg-stat.is-alert .v,
.pg-stat.is-danger .v { color: var(--state-danger); }
.pg-stat.is-warn .v { color: var(--state-warn); }
.pg-stat.is-ok .v { color: var(--state-ok); }

/* 可点的指标卡：整张卡是 <a>，点一下就到「这个数字对应的那批记录」。
   来源是用户的一句话：「我框出来的地方，我点击要能够跳转到具体的提醒明细」——
   指标卡写着 39，下面列表就该是那 39 条，而不是让人自己去筛选条里凑条件。
   角上那个箭头只在 hover / 键盘聚焦时出现：平时不打扰，鼠标移上去才说「这里能点」。 */
a.pg-stat {
  position: relative; display: block; color: inherit; text-decoration: none;
  transition: border-color .16s ease, box-shadow .18s ease, background-color .16s ease;
}
a.pg-stat:hover { border-color: var(--line-card); background: var(--paper-1); box-shadow: var(--shadow-card); }
a.pg-stat:focus-visible { outline: 2px solid var(--ink-2); outline-offset: 2px; }
a.pg-stat::after {
  content: "→"; position: absolute; top: 12px; right: 14px;
  font-family: var(--font-num); font-size: 13px; color: var(--ink-5);
  opacity: 0; transform: translateX(-3px);
  transition: opacity .16s ease, transform .16s ease;
}
a.pg-stat:hover::after, a.pg-stat:focus-visible::after { opacity: 1; transform: translateX(0); }
/* 一张卡里有两个去处时（如「已处理 / 已忽略」）：铺一张透明的整卡链接，
   里面那个小数字自己抬到上层，点它就去另一个筛选。 */
.pg-stat.stat-split-host { position: relative; }
.pg-stat.stat-split-host .stat-cover { position: absolute; inset: 0; z-index: 0; }
.pg-stat.stat-split-host:hover { border-color: var(--line-card); background: var(--paper-1); box-shadow: var(--shadow-card); }
.pg-stat.stat-split-host::after {
  content: "→"; position: absolute; top: 12px; right: 14px;
  font-family: var(--font-num); font-size: 13px; color: var(--ink-5);
  opacity: 0; transform: translateX(-3px); transition: opacity .16s ease, transform .16s ease;
}
.pg-stat.stat-split-host:hover::after { opacity: 1; transform: translateX(0); }
.pg-stat .stat-split { position: relative; z-index: 1; color: inherit; text-decoration: none; border-bottom: 1px solid var(--line-2); }
.pg-stat .stat-split:hover { border-bottom-color: var(--ink-2); }
/* 整张卡就是一个链接时（2026-10-26：今日概览的「待续费」「今日到课」）——给和 stat-split-host 一样的
   悬停反馈与右上角箭头。否则它与旁边只读的卡长得一模一样，用户只会认为「点了没反应」。 */
.pg-stat[href] {
  position: relative; display: block; color: inherit; text-decoration: none; cursor: pointer;
  transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
}
.pg-stat[href]:hover { border-color: var(--line-card); background: var(--paper-1); box-shadow: var(--shadow-card); }
.pg-stat[href]::after {
  content: "→"; position: absolute; top: 12px; right: 14px;
  font-family: var(--font-num); font-size: 13px; color: var(--ink-5);
  opacity: 0; transform: translateX(-3px); transition: opacity .16s ease, transform .16s ease;
}
.pg-stat[href]:hover::after { opacity: 1; transform: translateX(0); }

@media (prefers-reduced-motion: reduce) {
  a.pg-stat, a.pg-stat::after, .pg-stat.stat-split-host::after { transition: none; }
}

/* 提醒概览：状态与到期合成一块面板（2026-10-25）
   用户说「两排卡看着乱」—— 乱在：两排各自是一排带边框的小卡、列数不同（4 / 5），
   第二排的说明句还会折成两行，于是两排的卡宽和卡高都对不齐。现在的处理是
   「八张卡 → 一块面板」：外面一层边框，格与格只剩一道竖细线，两排之间一道横细线；
   说明句挪进 title，每格只剩「档位 / 数字 / 覆盖范围」。
   同晚又删掉「下月到期」并统一成 4 列 —— 两个网格同构，竖线才贯得下来。 */
.pg-brief {
  background: var(--paper-0);
  border: 1px solid var(--line-1);
  margin-bottom: var(--s5);
}
.pg-brief .pg-stats {
  margin-bottom: 0;
  gap: 0;
  /* 两排都是 4 列（2026-10-25 晚删掉「下月到期」之后）。
     列宽一致，竖线就落在同一组 25% / 50% / 75% 上，从上到下贯成一条 ——
     这是「上下左右对齐」的全部秘密：不是调间距，而是让两个网格同构。 */
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
.pg-brief .pg-stat {
  border: 0;
  border-left: 1px solid var(--line-1);
  background: transparent;
  padding: var(--s4);
}
.pg-brief .pg-stats > :first-child { border-left: 0; }
.pg-brief .pg-stats-4 { border-top: 1px solid var(--line-1); }
/* 面板里不画那个「→」：格子挨着，箭头会挤在竖线上 */
.pg-brief a.pg-stat::after { display: none; }
.pg-brief a.pg-stat:hover { background: var(--paper-1); box-shadow: none; }
.pg-brief .pg-stat .d { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 1080px) {
  /* 窄屏两列：四格正好两行，不留跨行的收尾格 */
  .pg-brief .pg-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pg-brief .pg-stat { border-left: 0; border-top: 1px solid var(--line-1); }
  .pg-brief .pg-stats > :nth-child(-n+2) { border-top: 0; }
}

/* 筛选条 */
.pg-filter { display: flex; gap: var(--s3); align-items: flex-end; flex-wrap: wrap; }
.pg-filter .field { margin-bottom: 0; }
.pg-filter .input, .pg-filter .select { min-width: 140px; }
/* 窄屏「筛选条件」开关：桌面端不存在这回事，隐藏；手机上由下面 720 断点接管。
   按钮由 app.js 注入（>= 4 个条件才加），没有 JS 时条件全部展开。 */
.filter-toggle { display: none; }

/* 后台表格：更紧凑 */
.pg-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
/* 单元格左右内边距 12px（原 14px）：收费记录 11 列、退费管理 10 列，
   14px 时这两张表在 1440 宽下分别溢出 73px / 138px，最右侧的操作要横向滚动才看得见。
   收到 12px 省出 2px × 2 × 列数（约 40px），两张表都不用滚了，视觉上几乎无差别。 */
.pg-table th, .pg-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line-1); }
.pg-table th { background: var(--paper-2); font-weight: 500; font-size: 12px; color: var(--pg-muted); letter-spacing: .05em; white-space: nowrap; }
/* 表头里的小字补充说明。用在「操作」列：规则不写在表头上，用户只能靠点一次才知道。 */
.pg-table th .th-note {
  display: block;
  font-weight: 400;
  font-size: 11px;
  color: var(--ink-5);
  letter-spacing: 0;
  line-height: 1.5;
  white-space: nowrap;
}
/* 账号列表里「这个人留下过多少条记录」。删除账号会把这些记录的经办人置空，
   所以条数要摆在名字下面让人看见，而不是只藏在确认弹窗里。 */
.foot-note {
  margin-top: 3px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--ink-5);
  font-variant-numeric: tabular-nums;
  cursor: help;
}
.pg-table tbody tr:hover { background: var(--paper-1); }
.pg-table tbody tr:last-child td { border-bottom: 0; }
.pg-table .num { font-family: var(--font-num); text-align: right; white-space: nowrap; }
.pg-table td.acts { white-space: nowrap; text-align: right; }
/* 只给「文字型」操作上灰色：真正的 .btn 有自己的配色，不能被这条规则压掉。
   早先写成 `td.acts button { color: var(--ink-2) }`，特异性 (0,2,1) 高过 .btn 的
   (0,1,0)，结果表格里的实心按钮变成深灰底 + 深灰字（实测对比度 1.50:1），
   退费单的「批准」几乎看不见。改成 :not(.btn) 后实心按钮恢复白字。 */
.pg-table td.acts a,
.pg-table td.acts button:not(.btn) { font-size: 13px; color: var(--ink-2); }
.pg-table td.acts a:hover { color: var(--ink-0); text-decoration: underline; }
/* 行内按钮压到 34px 高：表格行本来就密，44px 会把整行撑到 150px+ */
.pg-table td.acts .btn { min-height: 34px; padding: 5px 12px; }
/* 文字按钮在表格里单独放宽横向内边距：
   .btn-link 原本只有 `padding: 6px 2px`，两个字的「删除」量出来 30×35，
   低于 40px 的横向触控下限（_verify_tables.js 会卡住这一点）。
   加到 8px 后约 42×35，视觉上还是「像链接的一行字」，点起来不再费劲。 */
.pg-table td.acts .btn-link { padding: 6px 8px; }
/* 需要「输入框 + 两个按钮」的操作列（退费审批）：
   横排会把这个单元格撑到 258px，整张表在 1440 宽下横向溢出 138px，
   而且「批准」被推到滚动条外面 —— 用户看到的正是「看不清审批两个字」。
   改成竖排两行：上面审批意见，下面批准/驳回等宽并排。 */
.pg-table td.acts .acts-form {
  display: flex; flex-direction: column; align-items: stretch; gap: 6px; min-width: 148px;
}
.pg-table td.acts .acts-row { display: flex; gap: 6px; }
.pg-table td.acts .acts-row .btn { flex: 1 1 0; }
/* 「删除前先把课转走」的行内表单：「转到 [场馆 ▾] 删除」排一行。
   select 限宽 104px —— 再不限，场馆名长一点就把「删除」顶到单元格外面去；
   窄屏允许换行，宁可两行也不要横向溢出。 */
.pg-table td.acts .acts-move {
  display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 4px;
  vertical-align: middle;
}
.pg-table td.acts .acts-move select { max-width: 104px; }
/* 表格里的迷你输入框（行内备注用） */
.input-xs { padding: 4px 8px; font-size: 12.5px; min-height: 30px; }
/* 长文本列：不给下限会被 nowrap 的列挤成 78px，中文竖着排、行高飙到 157px */
.pg-table .reason-cell { min-width: 130px; max-width: 240px; white-space: normal; }
/* 库存列里的「库存偏低 / 无库存」小标：跟在数字后面同一行，不另起一块。
   另起一块会让这一行比别的行高 15px（实测 56 → 71），整张表看着像断了一格。 */
.pg-table td .stock-flag { display: inline-block; margin-left: 6px; vertical-align: middle; }
/* 正在编辑的那一行：左侧压一条粗墨线，不用底色（全站不用色相区分状态） */
.pg-table tr.is-editing td { background: var(--paper-2); }
.pg-table tr.is-editing td:first-child { box-shadow: inset 3px 0 0 var(--ink-1); }

/* 窄屏（1366 / 1280 笔记本）：11 列的收费记录、10 列的退费管理放不下，
   横向滚动时最右边的「批准 / 驳回 / 删除」会跑到视口外 —— 那正是要点的按钮。
   收紧内边距与长文本列下限，1440 以下也能一屏看完。 */
@media (max-width: 1400px) {
  .pg-table th, .pg-table td { padding-left: 9px; padding-right: 9px; }
  .pg-table .reason-cell { min-width: 96px; max-width: 200px; }
}
/* 1280 及以下（13 寸笔记本的常见缩放）：再让一档，否则 11 列还是溢出 60px+ */
@media (max-width: 1320px) {
  .pg-table { font-size: 13px; }
  .pg-table th, .pg-table td { padding-left: 6px; padding-right: 6px; }
  .pg-table th { font-size: 11.5px; letter-spacing: .03em; }
}

/* 人名单元 */
.who { display: flex; align-items: center; gap: var(--s3); }
.who .av {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%;
  background: var(--paper-3); border: 1px solid var(--line-1);
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; color: var(--ink-2); font-family: var(--font-song);
}
.who .nm { font-size: 13.5px; color: var(--ink-1); }
.who .sub { font-size: 12px; color: var(--pg-muted); font-family: var(--font-num); }
/* 有照片的头像：圆里放图，图按 cover 裁 —— 人脸不在正中也不会被拉扁 */
.who .av.av-photo { padding: 0; overflow: hidden; }
.who .av.av-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 详情页头部用大一号的头像。注意：不能在标签上写 style="width:56px" ——
   .who .av 的 flex: 0 0 30px 会盖掉内联宽度（flex-basis 优先于 width），
   所以尺寸必须做成类。 */
.who .av-lg { width: 56px; height: 56px; flex: 0 0 56px; font-size: 22px; }

/* 点名面板 */
.pg-roster { display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: var(--s2); }
.pg-person {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 9px 12px; border: 1px solid var(--line-2); background: var(--paper-0);
  border-radius: var(--pg-r); cursor: pointer; font-family: var(--font-hei);
  transition: border-color .12s, background .12s;
}
.pg-person:hover { border-color: var(--ink-4); }
.pg-person .n { font-size: 14px; color: var(--ink-1); }
.pg-person .m { font-size: 12px; color: var(--pg-muted); font-family: var(--font-num); }
.pg-person.is-on { background: var(--ink-1); border-color: var(--ink-1); }
.pg-person.is-on .n, .pg-person.is-on .m { color: var(--paper-0); }
.pg-person.is-off { opacity: .55; }

/* 时间轴 / 流水 */
.tl { list-style: none; padding: 0; margin: 0; }
.tl li { display: grid; grid-template-columns: 128px minmax(0,1fr); gap: var(--s4); padding: 11px 0; border-bottom: 1px solid var(--line-1); font-size: 13.5px; }
.tl li:last-child { border-bottom: 0; }
.tl .t { color: var(--pg-muted); font-family: var(--font-num); font-size: 12.5px; }
.tl .b { color: var(--ink-1); }
.tl .d { font-size: 12.5px; color: var(--pg-muted); }

/* 简易柱状图（纯 CSS） */
.bars { display: flex; align-items: flex-end; gap: 3px; height: 132px; padding-top: var(--s3); }
.bars .bar { flex: 1; min-width: 3px; background: var(--ink-2); border-radius: 1px 1px 0 0; position: relative; transition: background .12s; }
.bars .bar:hover { background: var(--ink-0); }
.bars-x { display: flex; justify-content: space-between; font-size: 11px; color: var(--pg-muted); margin-top: 5px; }

/* 手机端 */
.mobile-bar { display: none; }

@media (max-width: 1080px) {
  .grid-4 { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .pg-stats { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .footer-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 900px) {
  /* 理念三条改单列：竖分隔线转成横分隔线，序号与标签并排一行，省高度 */
  .credos { grid-template-columns: 1fr; gap: 0; }
  .credo { padding: var(--s5) 0; }
  .credo + .credo { border-top: 1px solid var(--line-2); }
  .credo + .credo::before { display: none; }
  .credo-num { display: inline-block; }
  .credo-num::after { display: none; }
  .credo-label { display: inline-block; margin: 0 0 0 var(--s4); letter-spacing: .2em; }
  .credo-title { margin-top: var(--s3); }
}

/* ---------------- 角色权限对照（账号权限页） ----------------
   一张职责清单 + 一张 8 行 6 列的矩阵，回答「这个角色到底能干什么」。
   矩阵用 --line-1 做格线（最轻一档），因为它是密集表格不是卡片结构线；
   「没有权限」用 —— 而不是留空，留空会让人以为资料没加载出来。 */
.role-duties {
  list-style: none;
  margin: 0 0 var(--s4);
  padding: 1px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));
  gap: 1px;
  background: var(--line-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r2);
  overflow: hidden;
}
.role-duty {
  background: var(--paper-0);
  padding: 11px var(--s4) 12px;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 0 8px;
}
.role-duty-name {
  font-family: var(--font-song);
  font-size: 14.5px;
  color: var(--ink-1);
  letter-spacing: .1em;
}
.role-duty-code {
  font-size: 11px;
  color: var(--ink-5);
  letter-spacing: .04em;
  justify-self: start;
}
.role-duty-text {
  grid-column: 1 / -1;
  font-size: 12.5px;
  line-height: 1.75;
  color: var(--pg-muted);
  margin-top: 2px;
}
.role-duty.is-current {
  background: var(--paper-2);
  box-shadow: inset 3px 0 0 var(--ink-1);
}

.role-matrix { font-size: 13px; }
.role-matrix thead th { white-space: nowrap; }
.role-matrix thead th.is-current { background: var(--ink-0); color: var(--paper-0); }
.role-matrix tbody th[scope="row"] {
  background: transparent;
  font-family: var(--font-song);
  font-size: 13.5px;
  font-weight: 400;
  color: var(--ink-2);
  letter-spacing: .08em;
  white-space: nowrap;
}
.role-matrix tbody td {
  font-size: 12.5px;
  line-height: 1.65;
  color: var(--ink-2);
  white-space: normal;
  min-width: 96px;
}
.role-matrix tbody td.is-none { color: var(--ink-5); }
.role-notes { margin-top: var(--s3); line-height: 1.9; }

/* 窄屏：6 列硬挤在 375px 里，一屏只看得到「模块 + 会长 + 半个店长」，
   等于把最关键的信息推出视口。改成按模块堆叠 —— 每行变成一个模块，
   下面 6 行「角色名 + 能干什么」，不用横着滚。td 上的 data-role 就是左侧标签。 */
@media (max-width: 720px) {
  .role-matrix thead { display: none; }
  .role-matrix, .role-matrix tbody, .role-matrix tr { display: block; width: 100%; }
  .role-matrix tbody tr {
    display: block;
    padding: var(--s3) var(--s4);
    border-bottom: 1px solid var(--line-2);
  }
  .role-matrix tbody tr:last-child { border-bottom: 0; }
  .role-matrix tbody th[scope="row"] {
    display: block;
    padding: 0 0 6px;
    border: 0;
    font-size: 14px;
    color: var(--ink-1);
  }
  .role-matrix tbody td {
    display: grid;
    grid-template-columns: 52px 1fr;
    gap: 8px;
    padding: 2px 0;
    border: 0;
    min-width: 0;
    font-size: 12.5px;
  }
  .role-matrix tbody td::before {
    content: attr(data-role);
    color: var(--ink-5);
    font-size: 12px;
    letter-spacing: .06em;
  }
}

/* ---------------- 账号弹窗编辑（账号权限页） ----------------
   列表里的「编辑」原来是整页跳转：改一个手机号也要离开列表、看不到旁边的行，
   回来还得重新滚动。改成原生 <dialog>，零依赖，ESC / 点遮罩 / 点 × 都能关。
   表单仍是普通 POST（同一套后端校验），只是走 fetch 提交，422 时把逐字段
   错误写回弹窗里，不整页刷新。 */
.modal {
  width: min(640px, calc(100vw - 32px));
  max-height: calc(100vh - 48px);
  padding: 0;
  border: 1px solid var(--line-card);
  border-radius: var(--r2);
  background: var(--paper-0);
  color: var(--ink-1);
  box-shadow: 0 24px 64px rgba(14, 14, 14, .22);
}
.modal::backdrop { background: rgba(14, 14, 14, .42); }
.modal-form { display: flex; flex-direction: column; max-height: calc(100vh - 48px); }
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line-2);
}
.modal-head h2 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: .04em; }
.modal-x {
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center;
  border: 1px solid transparent; background: none;
  color: var(--ink-4); font-size: 20px; line-height: 1; cursor: pointer;
}
.modal-x:hover, .modal-x:focus-visible { color: var(--ink-0); border-color: var(--line-card); outline: none; }
.modal-body { padding: var(--s5); overflow-y: auto; }
.modal-body > .field:last-of-type { margin-bottom: 0; }
.modal-foot {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s4) var(--s5);
  border-top: 1px solid var(--line-2);
  background: var(--paper-2);
}
.modal-error {
  margin-bottom: var(--s4);
  padding: 10px 12px;
  border: 1px solid var(--state-danger-line);
  background: var(--state-danger-bg);
  color: var(--state-danger);
  font-size: 13px; line-height: 1.7;
}
.modal-error.is-ok { border-color: var(--state-ok-line); background: var(--state-ok-bg); color: var(--state-ok); }
.modal-note { margin: 0; font-size: 12px; color: var(--ink-4); line-height: 1.8; }
.modal-note a { color: var(--ink-2); text-decoration: underline; text-underline-offset: 3px; }
.modal-note a:hover { color: var(--ink-0); }

/* ---------------- 系统参数：地址「地图选点」 ----------------
   地址是列表里一格，右边挂一个「地图选点」小按钮；点了开弹窗。
   弹窗里上半是搜索（后台代理百度联想，只给地点名），下半是可拖动的高德底图，
   点图落点写回经纬度。为什么不做成两侧并排：搜索结果最多 8 条，
   纵向排完再让地图横着占满宽度，比左右各半更省高度，窄屏也不会挤成两列。 */
.addr-field { display: flex; align-items: center; gap: var(--s2); }
.addr-field .input { flex: 1 1 auto; min-width: 0; }
.addr-field .btn { flex: none; }

.addr-modal { width: min(760px, calc(100vw - 32px)); }

.addr-search { display: flex; gap: var(--s2); }
.addr-search .input { flex: 1 1 auto; min-width: 0; }
.addr-search .btn { flex: none; }

.addr-hint {
  margin: var(--s2) 0 0;
  min-height: 2.8em;          /* 文案长短不一，留两行高度，下面地图不跳 */
  font-size: 12px;
  line-height: 1.7;
  color: var(--ink-4);
}

.addr-list {
  margin: var(--s3) 0 0;
  padding: 0;
  list-style: none;
  max-height: 202px;              /* 5 行 × 40px + 上下各 1px 边框：正好切在条目之间，不露半行 */
  overflow-y: auto;
  scrollbar-gutter: stable;       /* 出滚动条时右侧不跳 */
  border: 1px solid var(--line-1);
  border-radius: var(--r1);
  background: var(--paper-0);
}
.addr-list:empty { display: none; }
.addr-list > li + li .addr-item { border-top: 1px solid var(--line-1); }

.addr-item {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  height: 40px;                   /* 固定行高：8 条结果滚动时节奏一致，列表高度也才算得准 */
  padding: 0 16px;
  border: 0;
  background: none;
  font: inherit;
  line-height: 1.5;
  color: var(--ink-1);
  text-align: left;
  cursor: pointer;
  transition: background-color .16s ease, box-shadow .16s ease;
}
.addr-item:hover { background: var(--paper-2); }
.addr-item:focus-visible { outline: 2px solid var(--ink-2); outline-offset: -2px; }
.addr-item.is-on {
  background: var(--paper-2);
  box-shadow: inset 2px 0 0 var(--ink-1);   /* 选中记号：左侧 2px 竖条，不靠颜色 */
}
.addr-item-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;        /* 长店名截断，保证每行都是 40px */
}
.addr-item-area { flex: none; font-size: 11px; color: var(--ink-5); white-space: nowrap; }

.addr-modal .addr-map {
  height: 320px;              /* 盖住 .map 的 380px 与两档媒体查询（选择器更具体） */
  margin-top: var(--s3);
  border: 1px solid var(--line-2);
}


.addr-note {
  margin: var(--s4) 0 0;
  font-size: 12px;
  line-height: 1.7;
  color: var(--ink-4);
}

.addr-coord { font-size: 12px; color: var(--ink-4); white-space: nowrap; }
.addr-foot-gap { flex: 1 1 auto; }

@media (max-width: 720px) {
  .addr-modal .addr-map { height: 260px; }
  .addr-list { max-height: 162px; }   /* 4 行 × 40px + 边框 */
  /* 系统参数是一张自动宽的表格，手机上「当前值」列只剩 82px，地址输入框缩得没法打字。
     给它一个下限，宁可让表格横滚 —— 与列表页同一个取舍。 */
  .addr-field { min-width: 260px; }
  .addr-field .input { min-width: 160px; }
}
@media (max-width: 560px) {
  .addr-field { flex-wrap: wrap; }
  .addr-field .btn { margin-left: auto; }
  .addr-modal .addr-map { height: 220px; }
  .addr-coord { display: none; }   /* 窄屏优先给按钮，坐标在「保存」前仍可在两栏里看 */
}

@media (max-width: 720px) {
  .modal, .modal-form { width: 100%; max-height: 92vh; }
  .modal-head { padding: var(--s3) var(--s4); }
  .modal-body { padding: var(--s4); }
  .modal-foot { padding: var(--s3) var(--s4); }
  .modal .field-row { grid-template-columns: 1fr; gap: 0; }
}

/* ============================================================
   到馆路线图（联系我们）
   ------------------------------------------------------------
   自己铺高德瓦片（assets/js/map.js），不引 SDK。底图用地图本身的颜色
   （蓝的水、绿的园、米黄的路）—— 底图是地图，不是排版；黑白只留给界面骨架。
   定位点与标签仍用纯黑，压在彩色底图上更醒目。
   ============================================================ */
.map-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}
.map-head h2 { margin: 4px 0 0; }
.map-head-note {
  margin: 0;
  font-size: 12px;
  line-height: 1.8;
  color: var(--ink-4);
  max-width: 46ch;
}
.map-card { padding: 0; overflow: hidden; }

/* ---------------------------------------------------------------- 联系我们：左「到馆 + 路线图」／右「留言」
   两列必须**等高**：顶部对齐、底部齐平，右边留言卡跟着长高，
   多出来的高度由留言框吸收（textarea 变高），而不是在按钮下面留一块空白。
   地图就放在「到馆」卡片下面那一格里，宽度跟着列走，比原来通栏 1130px 小一半。

   地图卡的高度是**定死**的（一张附件，不是一块画布）：头 + 一张 2.3:1 的图 + 脚。
   以前这里写 flex:1 让地图去吸收左栏余高，表单长一点地图就跟着长成一大片，
   看着像进了地图页。现在反过来——余高交给上面的「到馆」卡（提示条压到卡底），
   地图卡永远只占自己那一格，和右边留言卡的下边缘仍然齐平。 */
.contact-grid { align-items: stretch; }
.contact-col { display: flex; flex-direction: column; gap: var(--s5); min-width: 0; }
.contact-col > .card:first-child { flex: 1 1 auto; display: flex; flex-direction: column; }
/* 卡片被拉高时，多出来的留白落在文字与提示条之间，提示条贴着卡底 */
.contact-col > .card:first-child > .alert { margin-top: auto; margin-bottom: 0; }
.contact-col > .map-card { flex: 0 0 auto; display: flex; flex-direction: column; }
/* 图身高宽比 2.3 : 1（跟用户画的那块红框同一个比例），随列宽一起缩，另有上下限兜底 */
.contact-col > .map-card .map {
  flex: 0 0 auto;
  height: auto;
  aspect-ratio: 2.3 / 1;
  min-height: 200px;
  max-height: 260px;
}
.map-head-in {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--s4); flex-wrap: wrap;
  padding: 16px 24px 12px; border-bottom: 1px solid var(--line-1);
  background: var(--paper-0);
}
.map-head-title { margin: 4px 0 0; font-size: 17px; letter-spacing: .06em; }

.contact-form-card { display: flex; flex-direction: column; }
.contact-form-card form { flex: 1 1 auto; display: flex; flex-direction: column; }
.contact-form-card .field-grow { flex: 1 1 auto; display: flex; flex-direction: column; }
/* 留言框最多长到 260px：再多就不像「留言」，像一块空地了；
   剩下的高度停在提示与按钮之间，当作呼吸留白，按钮不跟着跑到卡片中间。 */
.contact-form-card .field-grow .textarea { flex: 1 1 auto; min-height: 112px; max-height: 260px; }

.map {
  position: relative;
  height: 380px;
  overflow: hidden;
  background:
    linear-gradient(var(--line-1) 1px, transparent 1px) 0 0 / 100% 38px,
    linear-gradient(90deg, var(--line-1) 1px, transparent 1px) 0 0 / 38px 100%,
    var(--paper-2);
  cursor: grab;
  touch-action: none;
  outline: none;
  user-select: none;
}
.map:focus-visible { box-shadow: inset 0 0 0 2px var(--ink-2); }
.map.is-drag { cursor: grabbing; }

/* 瓦片层本身不接事件，拖动由外层统一处理 */
.map-tiles {
  position: absolute;
  inset: 0;
  /* 底图保留地图本色（水蓝、绿地、道路米黄）；黑白只留给界面骨架。 */
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
}
.map.is-loaded .map-tiles { opacity: 1; }
.map-tile {
  position: absolute;
  width: 256px;
  height: 256px;
  display: block;
  pointer-events: none;
}
.map-tile.is-dead { visibility: hidden; }

/* 定位点：translate 由脚本按经纬度算，这里只补锚点偏移 */
.map-pin {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 2;
  pointer-events: none;
  will-change: transform;
}
.map-pin[hidden] { display: none; }
.map-pin-dot {
  position: absolute;
  left: -7px;
  top: -7px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--ink-0);
  box-shadow: 0 0 0 6px rgba(20, 20, 20, .12), 0 0 0 1px rgba(255, 255, 255, .9);
}
.map-pin-label {
  position: absolute;
  left: 0;
  top: 12px;
  transform: translateX(-50%);
  white-space: nowrap;
  /* 气泡文字现在读的是「地址」，可能是一长串省市区街道：
     限制宽度后用省略号收尾 —— 完整地址就印在地图正下方，不靠气泡兜底。
     这里必须用 vw 而不是 %：.map-pin 是个零尺寸的定位点，百分比会算成 0。 */
  max-width: min(72vw, 320px);
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 3px 10px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--paper-0);
  color: var(--ink-1);
  font-size: 12px;
  letter-spacing: .02em;
  box-shadow: 0 2px 10px rgba(20, 20, 20, .08);
}

/* 控件 */
.map-ctl {
  position: absolute;
  right: 12px;
  top: 12px;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.map-btn {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-2);
  border-radius: 0;
  background: var(--paper-0);
  color: var(--ink-1);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.map-btn:hover { background: var(--ink-0); color: var(--paper-0); }
.map-btn:focus-visible { outline: 2px solid var(--ink-2); outline-offset: 1px; }
.map-btn-reset { font-size: 13px; }

.map-tip {
  position: absolute;
  left: 12px;
  bottom: 34px;
  z-index: 3;
  padding: 4px 10px;
  border: 1px solid var(--line-1);
  background: var(--paper-0);
  color: var(--ink-4);
  font-size: 11px;
  letter-spacing: .02em;
  pointer-events: none;
}
.map-zoom {
  position: absolute;
  left: 12px;
  bottom: 10px;
  z-index: 3;
  padding: 2px 8px;
  border: 1px solid var(--line-1);
  background: var(--paper-0);
  color: var(--ink-5);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.map-attr {
  position: absolute;
  right: 8px;
  bottom: 8px;
  z-index: 3;
  color: var(--ink-5);
  font-size: 10px;
  letter-spacing: .02em;
  white-space: nowrap;
  pointer-events: none;
}

/* 提示文案分桌面版和手机版：手机上谈「滚轮」是废话 */
.map-tip-narrow { display: none; }

/* 一张瓦片都拉不下来时才出现（例如内网、断网）：不留一块空白，换成能照着走的文字 */
.map-fail {
  display: none;
  position: absolute;
  inset: 0;
  z-index: 4;
  align-items: center;
  justify-content: center;
  padding: var(--s5);
  background: var(--paper-2);
  text-align: center;
}
.map.is-offline .map-fail { display: flex; }
.map.is-offline .map-tip,
.map.is-offline .map-zoom,
.map.is-offline .map-attr { display: none; }
.map-fail-in { max-width: 560px; }
.map-fail-title {
  margin: 10px 0 0;
  font-family: var(--font-hei);
  font-size: 17px;
  letter-spacing: .06em;
  color: var(--ink-1);
}
.map-fail-text { margin: 8px 0 0; font-size: 13px; line-height: 1.8; color: var(--ink-4); }
.map-fail-addr {
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--line-1);
  font-size: 13px;
  line-height: 1.8;
  color: var(--ink-2);
}
.map-fail .btn { margin-top: 14px; }

.map-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
  /* 左右内边距跟卡片（24px）对齐，地址与上面「到馆」卡里的字左边缘能对成一条线 */
  padding: 10px 24px;
  border-top: 1px solid var(--line-1);
  background: var(--paper-2);
}
.map-foot-addr {
  font-size: 12px;
  line-height: 1.8;
  color: var(--ink-3);
}

@media (max-width: 860px) {
  .map { height: 300px; }
  /* 单列时地图卡横贯整页，按比例长会变成一块大色块，压低到 200 上下 */
  .contact-col > .map-card .map { aspect-ratio: auto; height: 220px; max-height: 220px; }
  .map-head { align-items: flex-start; }
  .map-head-note { max-width: none; }
}

@media (max-width: 560px) {
  .map { height: 240px; }
  .contact-col > .map-card .map { height: 200px; max-height: 200px; }
  .map-pin-label { max-width: 62vw; overflow: hidden; text-overflow: ellipsis; }
  .map-foot { padding: 10px 14px; }
  .map-tip-wide { display: none; }
  .map-tip-narrow { display: inline; }
}

@media (max-width: 860px) {
  /* ---------------------------------------------------------------- 后台窄屏
     2026-10-23 改：原来 .side 塌成一条 49px 高的横滚条，18 个菜单项连起来
     1596px 宽，手机上要横滑四次才够得到「数据维护」，而且横滑条和页面纵向
     滚动会互相抢手势。现在改成「56px 品牌条 + 抽屉菜单」：
       · 品牌条常驻在顶部，右侧一个「菜单」按钮（44×44 触控区）
       · 点开是整屏抽屉，按原来的 6 个分组竖排，一眼能看全
     抽屉关着时链接仍在 DOM 里，读屏和 Tab 都能到，只是移出视口。 */
  .admin-shell { flex-direction: column; }

  .side {
    width: 100%; flex: 0 0 56px; height: 56px; min-height: 56px;
    /* z-index 必须高过 .side-scrim(44)：.side 自己是一个层叠上下文，
       里面的 .side-nav(z-index 45) 再高也出不了这个上下文 ——
       给 .side 40 时，遮罩(44)会盖在整个侧栏之上，
       手指点在菜单项上，实际点到的是遮罩：抽屉一收、页面不跳。 */
    position: sticky; top: 0; z-index: 46;
    flex-direction: row; align-items: center; overflow: visible;
  }
  .side-brand { flex: 1 1 auto; min-width: 0; padding: 0 var(--s4); border-bottom: 0; gap: 9px; }
  .side-brand .logo-plate { width: 30px; height: 30px; flex: 0 0 30px; padding: 2px; }
  .side-brand .n { font-size: 15px; letter-spacing: .12em; }
  .side-brand .s { display: none; }          /* 副标题在 56px 的条里没有位置 */

  .side-toggle {
    flex: 0 0 auto; margin-right: var(--s4);
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    min-width: 72px; height: 40px; padding: 0 12px;
    background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.18);
    border-radius: var(--r2); color: #E8E8E5; font-size: 13px; letter-spacing: .1em;
    cursor: pointer; transition: background-color .15s ease, border-color .15s ease;
  }
  .side-toggle:hover { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.3); }
  .side-toggle:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

  /* 抽屉：从品牌条下面展开，铺满剩余屏幕 */
  .side-nav {
    position: fixed; left: 0; right: 0; top: 56px; bottom: 0;
    display: none; flex-direction: column; gap: 0;
    padding: 0 0 var(--s6); overflow-y: auto;
    background: var(--ink-0); border-top: 1px solid rgba(255,255,255,.1);
    z-index: 45; -webkit-overflow-scrolling: touch;
    /* 抽屉自己滚到底就停，别把滚动传给背后的页面 */
    overscroll-behavior: contain;
  }
  .side-nav.is-open { display: flex; }
  /* 手指点在菜单项上时不要在「等双击缩放」的 300ms 里把它当滚动手势 */
  .side-nav a { touch-action: manipulation; }
  /* 分组标题在抽屉里恢复显示 —— 18 项平铺没有层级，比横滚条还难找 */
  .side-group {
    display: block; padding: var(--s4) var(--s5) var(--s1);
    font-size: 11px; color: var(--on-dark-2); letter-spacing: .18em;
  }
  .side-nav a {
    border-left: 0; border-bottom: 1px solid rgba(255,255,255,.07);
    white-space: nowrap; padding: 13px var(--s5); font-size: 15px;
  }
  .side-nav a.is-on { border-left: 0; border-bottom-color: rgba(255,255,255,.07); background: rgba(255,255,255,.1); }
  /* 当前项左侧那条 3px 白标记。
     最先写成 .is-on::before 的 flex 子元素，结果它参与了 justify-content: space-between ——
     链接本来只有「文字 + 可选徽标」两段，多出一个 3px 的伪元素后文字被挤到正中，
     于是「提醒中心」居中、其余左对齐，看起来像错位。改成 inset 阴影，不占布局。 */
  .side-nav a.is-on { box-shadow: inset 3px 0 0 #fff; }
  .side-foot { display: none; }

  /* 抽屉打开时锁住页面纵向滚动，否则背后会跟着动 */
  html.side-locked, html.side-locked body { overflow: hidden; }

  /* 顶栏原为 sticky top:0，会盖住 56px 的品牌条 —— 挪到品牌条下面 */
  .topbar { top: 56px; height: 52px; flex: 0 0 52px; padding: 0 var(--s4); gap: var(--s3); }
  .admin-content { padding: var(--s4) var(--s3) var(--s5); }

  /* 抽屉铺满整屏（不透明的深色），下面没有任何「外面」可点，
     所以没有遮罩层 —— 收起靠「菜单」按钮、Esc、或点菜单项跳走。 */

  .grid-3 { grid-template-columns: 1fr; }
  .field-row, .field-row-3 { grid-template-columns: 1fr; }
  .role-duties { grid-template-columns: 1fr; }
}

/* 桌面端不显示抽屉的开关 */
@media (min-width: 861px) {
  .side-toggle { display: none; }
}

@media (max-width: 900px) {
  /* Banner 在 900 以下改单列：768 的平板若还分两列，每列只剩约 330px，
     标题和图片都憋屈。单列后图片整幅展开，反而更有气势。 */
  .banner-inner {
    grid-template-columns: 1fr;
    gap: var(--s6);
    padding-top: var(--s6);
    padding-bottom: var(--s6);
  }
  /* 单列：左列独占一行，不再需要 space-between 撑高度，改为自然流式排布 */
  .banner-copy { display: block; max-width: 560px; }
  /* 单列后标题若沿用桌面的 5.05vw，手机上只剩 30px：第一行 5 个字才占栏宽 45%，
     第二行又贴到右沿，中间空出近 200px，两行读起来是散的。
     这里改成按栏宽等比：两行 5 + 4 字、字距 6%，取 f ≈ 栏宽 / 9.55 时两行首尾相接，
     第二行自然落到第一行尾巴的右侧，与桌面 62px 那版的交错节奏一致。
     320 宽约 33px、390 宽约 41px；560 的限宽让 768 的平板也不会把字撑到 70px。 */
  .banner-title { font-size: min(10.4vw, 58px); }

  .banner-eyebrow { margin-bottom: var(--s5); }
  .banner-core { margin-bottom: var(--s5); }
  .banner-desc { max-width: none; }
}

@media (max-width: 720px) {
  body { font-size: 14.5px; }
  .wrap { padding: 0 var(--s4); }
  .section { padding: var(--s7) 0; }
  .site-nav {
    display: none; position: absolute; top: var(--header-h); left: 0; right: 0;
    background: var(--paper-0); border-bottom: 1px solid var(--line-1);
    flex-direction: column; gap: 0; padding: var(--s3) var(--s4); margin: 0;
  }
  .site-nav.is-open { display: flex; }
  .site-nav a { padding: 9px 0; border-bottom: 1px solid var(--line-1); }
  .site-nav a:last-child { border-bottom: 0; }
  /* 菜单按钮原为 48×31（文字 13px + 上下 6px 内边距），手指点偏一点就落空 */
  .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 64px; min-height: 40px; padding: 9px 12px; font-size: 13.5px;
  }
  .grid-2, .grid-4 { grid-template-columns: 1fr; }
  .dl { grid-template-columns: 1fr; gap: 2px var(--s3); }
  .dl dd { margin-bottom: var(--s3); }
  .tl li { grid-template-columns: 1fr; gap: 2px; }

  /* ---------------------------------------------------------------- 官网窄屏的触控
     官网 390 宽实测出的三处偏小：区块头「全部课程 →」92×28、页脚链接 27×18、
     联系页地图的三个圆钮 32×32。桌面上鼠标能点 1px，手指不能 —— 一律补到 40 上下。 */
  .link-more { min-height: 40px; }
  .link-more .go { width: 32px; height: 32px; }
  /* 区块头在窄屏放不下「标题块 + 更多」：右上的「更多」会折到下一行并靠左，
     看上去像正文的一条注脚。改成整行右对齐，仍是一眼能找到的出口。 */
  .section-head.flex-between { flex-direction: column; align-items: stretch; gap: var(--s3); }
  .section-head.flex-between .link-more { align-self: flex-end; }
  .footer-grid a { display: inline-block; padding: 8px 0; }
  .map-btn { width: 40px; height: 40px; }
  /* 轮播主图在单列下已占满整宽，标题/副题/按钮之间再收一档 */
  .banner-actions { display: flex; gap: var(--s3); }
  .banner-actions .btn { flex: 1 1 0; justify-content: center; text-align: center; }
  .admin-content { padding: var(--s4); }
  .pg-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .footer-grid { grid-template-columns: 1fr; }
  .stats { grid-template-columns: repeat(2, minmax(0,1fr)); }

  /* ---------------------------------------------------------------- 后台窄屏细节
     2026-10-23 补：抽屉菜单解决了「找菜单」，剩下三处仍是「桌面版缩到手机」：
       ① 筛选条：4 个 140px 的输入框挤成一列，两个按钮飘到最后一个框右边，
          视线要横向跳一下才找得到「筛选」；
       ② 卡片头：标题与说明横排，说明被压成 3 行（276×42），把卡片头撑到 107px；
       ③ 页头：标题与「新增/导出」按钮挤在同一行，长标题会把按钮顶到下一行。 */
  .pg-filter { flex-direction: column; align-items: stretch; gap: var(--s3); }
  .pg-filter .field { width: 100%; }
  .pg-filter .input, .pg-filter .select { min-width: 0; width: 100%; }
  /* 标签从「压在输入框上方」改成「左侧一列」：每个条件从 70px 高收到 44px，
     四条条件 + 按钮一共省下 120px —— 手机上首屏才看得见第一行数据。 */
  .pg-filter .field {
    display: grid; grid-template-columns: 68px minmax(0, 1fr);
    align-items: center; gap: var(--s3);
  }
  .pg-filter .field > label { margin-bottom: 0; white-space: nowrap; line-height: 1.2; }
  /* 两个按钮并排铺满，拇指够得着，也不再「飘」在某个输入框右边 */
  .pg-filter > .btn,
  .pg-filter > .btn-ghost,
  .pg-filter > .flex-center {
    width: 100%;
  }
  .pg-filter > .flex-center > .btn { flex: 1 1 0; }
  .pg-filter > .btn, .pg-filter > .btn-ghost { justify-content: center; }

  /* 折叠开关本身：一行、44px 高、左「筛选条件」右箭头，有生效条件时带数字 */
  .filter-toggle {
    display: flex; align-items: center; gap: 8px;
    width: 100%; min-height: 44px; padding: 0 var(--s4); margin-bottom: var(--s3);
    background: var(--paper-1); border: 1px solid var(--line-2); border-radius: var(--r2);
    color: var(--ink-2); font-family: inherit; font-size: 14px; letter-spacing: .04em;
    text-align: left; cursor: pointer;
  }
  .filter-toggle:focus-visible { outline: 2px solid var(--ink-2); outline-offset: 2px; }
  .filter-toggle-n {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
    background: var(--ink-1); color: var(--paper-0);
    font-family: var(--font-num); font-size: 11.5px; line-height: 1;
  }
  .filter-toggle-i { margin-left: auto; transition: transform .18s ease; }
  .pg-filter.is-filter-open > .filter-toggle .filter-toggle-i { transform: rotate(180deg); }
  /* 收起时把条件藏起来 —— 只有 JS 加过 has-filter-toggle 才生效。
     提交/重置按钮一起藏：条件都看不见了，留两个按钮只会让人以为能空提交。 */
  .pg-filter.has-filter-toggle:not(.is-filter-open) > .field,
  .pg-filter.has-filter-toggle:not(.is-filter-open) > .btn,
  .pg-filter.has-filter-toggle:not(.is-filter-open) > .btn-ghost,
  .pg-filter.has-filter-toggle:not(.is-filter-open) > .flex-center { display: none; }

  .pg-card-head { flex-direction: column; align-items: flex-start; gap: var(--s2); padding: var(--s4) var(--s4); }
  .pg-card-head .text-xs { line-height: 1.6; }

  .page-head { align-items: flex-start; gap: var(--s3); margin-bottom: var(--s4); }
  .page-head h1 { font-size: 20px; }
  .page-head > .flex-center { width: 100%; }
  .page-head > .flex-center > .btn { flex: 1 1 0; }

  .pg-stat { padding: var(--s3) var(--s4); }
  .pg-stat .v { font-size: 23px; }
}

/* 手机上的首屏标题（≤600 才动，平板单列仍用桌面的对角排法）。
   390 宽实测：30px 的两行，第一行 5 个字只占 162px，第二行又贴到 358px 的右沿，
   中间空出近 200px —— 一左一右被拉成一条横跨整栏的斜线，看着就是「散」。
   这里缩一档字号，并把第二行从「贴右」改成「缩进」：
   两拍错落的节奏还在，但整组字收成一个紧凑的阶梯，不再去够两边的边线。 */
/* 手机上表格：横向滚动是设计（列多、删不掉），但要让「这是哪一行」一直看得见。
   第一列吸附在左边，横滑到操作列时仍然知道自己在看谁。 */
@media (max-width: 720px) {
  .pg-table th:first-child,
  .pg-table td:first-child {
    position: sticky; left: 0; z-index: 2;
    background: var(--paper-0);
    box-shadow: 1px 0 0 var(--line-1);
  }
  .pg-table th:first-child { background: var(--paper-2); z-index: 3; }
  .pg-table tbody tr:hover td:first-child { background: var(--paper-1); }
  .pg-table th, .pg-table td { padding: 9px 10px; }
  .pg-table { font-size: 13px; }
  /* 表格按内容取宽度，不再被压成容器宽：原来 8 列硬塞进 324px，「会员」列只剩 69px，
     姓名被拆成一字一行（黄/丽/娟 竖排），表头「等级」也被截成「等」。
     改成 width:auto + min-width:100% 后列宽按内容算，放不下就横滚。
     但中文可以在任意两字之间断行，max-content 会把「黄丽娟」算成一个字宽 ——
     所以短字段一律 nowrap，长文本（备注、说明）用 .wrap 显式放开。 */
  .pg-table { width: auto; min-width: 100%; }
  .pg-table th, .pg-table td { white-space: nowrap; }
  .pg-table td.wrap { white-space: normal; min-width: 160px; }
  /* 表格容器加一条右侧渐隐，暗示「右边还有内容」 */
  .table-wrap {
    background-image: linear-gradient(to left, rgba(20,20,20,.05), rgba(20,20,20,0) 24px);
    background-position: right;
    background-repeat: no-repeat;
    background-size: 24px 100%;
    background-attachment: local;
  }
}

/* 手机上的操作列保持一行：
   2026-10-21 这里原本允许换行（「转到 [场馆 ▾] 删除」比单个按钮宽 59px，
   390 宽时把 5 列表格从 394 撑到 453）—— 当时的表格是 width:100%，宽度会顶到屏幕。
   2026-10-23 表格改成 width:auto + 横滚后，多 59px 只是多滚一点；
   而允许换行会让「详情 / 收据 / 删除」三个按钮竖着摞起来，把整行撑到 122px（资产页 180px），
   一屏只看得到两行。所以改回 nowrap：宁可横滚，也不要一屏两行。 */
@media (max-width: 560px) {
  .pg-table td.acts { white-space: nowrap; }
  .pg-table td.acts .acts-move,
  .pg-table td.acts .acts-row { flex-wrap: nowrap; justify-content: flex-end; }
}

@media print {
  .side, .topbar, .site-header, .site-footer, .no-print { display: none !important; }
  .admin-content { padding: 0; }
  .pg-card { border: 0; }
}




/* ---- 图片字段的「手动尺寸」（2026-10-25） ----
   两个小输入框 + 一句说明，压在图片上传框下面。不做成 field 是为了
   不把表单的行距撑开：它默认不用，只在想换比例时才填。 */
.img-size {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.img-size-label {
  font-size: 12px;
  letter-spacing: .08em;
  color: var(--ink-4);
  white-space: nowrap;
}
.img-size-input {
  width: 82px;
  padding: 6px 8px;
  font-size: 13px;
  text-align: center;
}
.img-size-x { font-size: 12px; color: var(--ink-5); }
.img-size-now {
  font-size: 12px;
  color: var(--ink-4);
  white-space: nowrap;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--paper-2, #f1efe9);
}
.img-size-now b { font-weight: 600; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.img-size-tip { margin: 0; flex: 1 1 240px; }
