/* ==========================================================================
   screens.css — 工程师星球「村子」功能 6 屏版式与组件样式
   画布约定：.screen = 750 × 1623（设计稿 1:1，1rpx = 1px），外围 .phone 缩放 0.52
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. 手机外框 / 舞台 / 演示控制台
   -------------------------------------------------------------------------- */
.frame {
  position: relative;
  width: 410px;
  height: 864px;
}

/* P0-2：外框按「边框外扩」计算，保证内容区恰好等于缩放后的 390×844 画布 */
.phone {
  position: absolute;
  top: 0;
  left: 0;
  width: 410px;  /* 390 + 10 × 2 */
  height: 864px; /* 844 + 10 × 2 */
  border-radius: 44px;
  overflow: hidden;
  background: var(--ep-white);
  border: 10px solid #2B2B2F;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.18);
}

.screen {
  position: relative;
  width: 750px;
  height: 1623px;
  transform-origin: 0 0;
  transform: scale(0.52);
  overflow: hidden;
  background: var(--ep-white);
}

/* toast 的固定宿主（B1-1）--------------------------------------------------
   原来 toast 直接插进 `.screen`（= `#screen-{key}`），而 app.js 的 render() 会整体
   重写屏内 innerHTML —— 「先 toast 再 render」的提示（joinHamlet 就是）创建后立刻
   被销毁，状态生效但用户看不到反馈。
   宿主改为 `.screen` 的**兄弟**节点：不参与 render() 的 innerHTML 重写，提示不会被
   销毁。几何与 `.screen` 完全一致（750×1623 + scale 0.52），所以 toast 的 rpx 尺寸
   与定位口径一个字都不用改；`.phone` 在概览模式下的额外缩放它也会同步继承。 */
.toast-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 750px;
  height: 1623px;
  transform-origin: 0 0;
  transform: scale(0.52);
  overflow: hidden;
  z-index: var(--ep-z-toast);
  pointer-events: none;
}

/* 单屏模式：只显示当前屏 */
.stage {
  padding: 48px 0 64px;
}
.stage--single {
  display: flex;
  justify-content: center;
}
.stage--single .frame {
  display: none;
}
.stage--single .frame.is-active {
  display: block;
}

/* 一览模式：6 屏平铺 */
.stage--overview {
  display: grid;
  grid-template-columns: repeat(3, 234px);
  gap: 44px 32px;
  justify-content: center;
}
.stage--overview .frame {
  width: 234px;
  height: 493px; /* 234 / 410 × 864 */
}
/* P0-2 连带：origin 归零 + 比例重算，消除 +78px 偏移 */
.stage--overview .phone {
  transform-origin: 0 0;
  transform: scale(0.5707); /* 234 / 410 */
}
.ov-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -28px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: #5A5A63;
}
.ov-label span {
  display: block;
  font-size: 11px;
  font-weight: 400;
  color: #8A8A93;
}

/* 演示控制台 */
.console {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 16px 24px;
  background: #FFFFFF;
  border-bottom: 1px solid #E6E6EA;
}
.console__title {
  font-size: 15px;
  font-weight: 600;
  color: #1D1D20;
  margin-right: 8px;
}
.console__title small {
  display: block;
  font-size: 11px;
  font-weight: 400;
  color: #8A8A93;
}
.console__group {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  background: #F2F2F4;
  border-radius: 10px;
}
.console__btn {
  padding: 7px 14px;
  font-size: 13px;
  color: #4A4A52;
  border-radius: 8px;
  cursor: pointer;
  transition: background 160ms, color 160ms;
}
.console__btn:hover {
  background: #E4E4E9;
}
.console__btn.is-on {
  background: var(--ep-orange-500);
  color: #FFFFFF;
}
.console__sep {
  width: 1px;
  height: 24px;
  background: #E6E6EA;
  margin: 0 4px;
}
.console__hint {
  font-size: 12px;
  color: #8A8A93;
  margin-left: auto;
}

/* --------------------------------------------------------------------------
   1. 页面骨架
   -------------------------------------------------------------------------- */
.page {
  position: relative;
  width: 750px;
  height: 1623px;
  display: flex;
  flex-direction: column;
  background: var(--ep-white);
  overflow: hidden;
}

.page-body {
  flex: 1 1 auto;
  min-height: 0;
  background: var(--ep-fill-1);
  border-radius: var(--ep-radius-page) var(--ep-radius-page) 0 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 底部 tabbar 安全区 */
.gutter-tabbar {
  flex: none;
  height: 187px; /* 115 + 38 + 34 */
}

/* 状态栏 */
.statusbar {
  flex: none;
  height: 88px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  padding: 0 38px 8px;
  font-size: var(--ep-fs-body);
  font-weight: var(--ep-fw-medium);
  color: var(--ep-text-1);
}
.statusbar__icons {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* 导航栏 */
.navbar {
  flex: none;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 38px;
}
.navbar__title {
  font-size: var(--ep-fs-display);
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
  line-height: 1.2;
}
.navbar__side {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* 返回按钮（热区 140：纵向扩热）
   P2-6：带上级名称后字号降到 body-l，避免与封面/页标题（h1/h2）抢层级 */
.nav-back {
  position: relative;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
  cursor: pointer;
}
.nav-back::after {
  content: "";
  position: absolute;
  left: -20px;
  right: -40px;
  top: 50%;
  height: 140px;
  transform: translateY(-50%);
}

/* 通用热区扩展（≥140×140） */
.hit::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 140px;
  transform: translateY(-50%);
}
/* 只向上扩热区：避免盖住下方元素 */
.hit--up::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 140px;
}

/* --------------------------------------------------------------------------
   2. 胶囊 tab（复用现有）
   -------------------------------------------------------------------------- */
.capsule-tabs {
  flex: none;
  /* v2.2：客户要求「P1 村落顶部留白至少减去一半」——tab 下边距 24 → 12，
     配合 .note-bar 同步收，把省下的高度让给版图（多露一行）。 */
  height: 64px;
  margin: 0 var(--ep-space-page) var(--ep-space-3);
  display: flex;
  gap: var(--ep-space-4);
}
/* v4：3 个 tab → 4 个（全部 / 热门 / 最新 / 我的）。
   不再用固定 min-width —— 4 × 148 + 3 × 16 = 640 虽然还能塞进 674，
   但第 5 个 tab 或更长的标签一加就会溢出。改成等分：
   (674 - 3 × 16) / 4 = 156.5rpx / 个，「全部」等 2 字标签（25rpx × 2 = 50）余量充足。 */
.capsule-tab {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 64px;
  padding: 0 var(--ep-space-4);
  border-radius: var(--ep-radius-capsule);
  background: transparent;
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-medium);
  color: var(--ep-text-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background var(--ep-dur-fast) var(--ep-ease), color var(--ep-dur-fast) var(--ep-ease);
}
.capsule-tab.is-on {
  background: var(--ep-orange-100);
  color: var(--ep-text-1);
}

/* v4：「我的营地」横向航线（.camp-rail / .camp-card / .camp-link）已整块删除。
   客户看到顶部三张卡问「这是什么，热门还是最新」—— 没被认出来的模块等于设计失败，
   村子快捷入口收敛到「全部」版图与「我的」列表两条路径。
   连带删除：.flag--on-light（只有营地卡的浅橙底需要，版图上旗标一律白描边）。 */

/* --------------------------------------------------------------------------
   4. 版图容器
   -------------------------------------------------------------------------- */
.map {
  flex: none;
  position: relative;
  width: 750px;
  /* v2.4（审查 P1-2）：这里原有的 `height: 880px` 是**死值** —— 版图实际都用
     `.map--flex`（height: auto），被它覆盖，实测 getComputedStyle 为 1108 / 1036。
     版图高度一律由 app.js 的 MAP_H_LG / MAP_H_SM 经 inline style 下发，
     CSS 里不得再出现高度字面量（否则改了一处漏一处，本文件曾因此裂出 441rpx 空白）。 */
  border-radius: var(--ep-radius-page);
  overflow: hidden;
  background: var(--ep-white);
  z-index: var(--ep-z-map-bg);
}
.map--flex {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
}
.map__bg {
  position: absolute;
  inset: 0;
  z-index: var(--ep-z-map-bg);
  background: linear-gradient(var(--ep-map-bg-angle), var(--ep-map-bg-from), var(--ep-map-bg-to));
}
.map__scroll {
  position: absolute;
  inset: 0;
  z-index: var(--ep-z-map-texture);
  overflow-y: auto;
  overflow-x: hidden;
}
/* 版图高度 —— v2.4 根治（客户第五轮「高度问题必须根治」）
   ---------------------------------------------------------------------------
   旧问题：**同一个高度在两个地方各存一份** ——
     · app.js  `MAP_H_LG = 1020` / `MAP_H_SM`     （用来画地形、算地名空档）
     · 本文件   `.map__inner { min-height: 1476px }` / `.map__content { height: 1400px }`
   两份数不对，实测内容底 1035、容器 1476 → 滚到底有 **441rpx 空白**；
   `.map--dense` 又各存了 836 / 760 第二份村子版图的数。
   修法（结构性，不是改数值）：
     ① `.map__inner` 删掉写死的 min-height，改为 `min-height: 100%` —— 只保证
        「不小于可视区」，内容多高它自然多高（内容高度见下）；
     ② `.map__content` 删掉写死的 height，**改由 app.js 以 inline style 下发**
        （`style="height:<MAP_H>px"`），并回到文档流（margin:0 auto 居中），
        于是 `.map__inner` 的高度 = 上下页边距 + 内容高，自动等于地图 SVG 的高度；
     ③ `.map--dense` 的两条（`.map__inner` / `.map__content`）一并删除 ——
        村子版图的压缩已由 POS_SM + MAP_H_SM 表达，不需要第二份数。
   从此**版图高度只有一个数据源：app.js 的 MAP_H_LG / MAP_H_SM**。CSS 不得再写死。 */
.map__inner {
  position: relative;
  width: 750px;
  padding: var(--ep-space-page) 0;
  min-height: 100%;
}
/* 内容区 = 674 × H（容器 750 减去左右各 38 内边距）；
   height 由 JS 下发 inline style（唯一数据源 = app.js 的 MAP_H_LG / MAP_H_SM）。 */
.map__content {
  position: relative;
  width: 674px;
  margin: 0 auto;
}
.map__texture {
  position: absolute;
  inset: 0;
  z-index: var(--ep-z-map-texture);
  pointer-events: none;
}
/* v2.2：.map__route 随航线原子一并删除（客户要求去掉村与村之间的连线） */

/* v3：申请建村入口已按客户要求删除，.entry-card 样式一并移除（避免死代码） */
/* --------------------------------------------------------------------------
   5. 原子 1 — 聚落卡片（ep-settlement）
      = SVG 建筑本体（§2.1.2 / §2.2.2）+ HTML 白底名牌（§3.1.3），合并为一个热区。
      热区：村落 240 × 344 / 村子 160 × 256，均 ≥ 140 × 140（§6.6）。
   -------------------------------------------------------------------------- */
.surface {
  position: absolute;
  z-index: var(--ep-z-map-surface);
  cursor: pointer;
  /* rotate 由 JS 以 --ep-surface-rotate 变量下发，在这里与 scale 合成。
     不能写成 inline transform —— inline 优先级高于样式表，会把 :active /
     .is-zooming 的 scale 整个盖掉。 */
  transform: rotate(var(--ep-surface-rotate, 0deg));
  transition: transform var(--ep-dur-base) var(--ep-ease), opacity var(--ep-dur-base) var(--ep-ease);
}
.surface:active {
  transform: rotate(var(--ep-surface-rotate, 0deg)) scale(0.97);
}
/* v2.3：容器**只定宽、不定高** —— 高度由「帐篷 + 8rpx 间距 + 名牌」自然撑开。
   上一版把高度写死成 344 / 286（对应旧的 240 + 96），改了帐篷和名牌尺寸后没同步，
   名牌被 bottom:0 推到容器底部，中间留出 56rpx 死空白（客户一眼看出来）。
   改成自动高度后，以后再调帐篷或名牌尺寸都不会再出现空隙或重叠。
   v2.4：宽度 220 → **240**（令牌 §3.1.3 允许的「2 列余量充足时选回 240」）。
   原因：字号整档 +2~3rpx 后文字变宽 ——
     · 村子第 2 行「头像 28 + 间距 8 + 昵称 8 字 × 22 = 176」= **212rpx**，
       而 220 名牌的内容区只有 200rpx → 8 字昵称被 overflow 裁掉 12rpx（客户硬要求不省略）；
     · 村落第 2 行「26 个村子 · 1.9万 帖」= **199rpx** > 内容区 192rpx。
   240 名牌内容区 = 240 − 2×8 − 2×2 = 220rpx ≥ 212 ✅（村落行内容区 216 ≥ 199 ✅）。 */
.surface[data-size="lg"] { width: 240px; }
.surface[data-size="sm"] { width: 240px; }

/* 建筑本体占卡片顶部正方形区域；名牌挂在下方 8rpx 处 */
/* v2.3：回到文档流并由 margin 居中（原来 absolute + left:0 会靠左，
   而名牌是居中的，两者横向错位 20rpx）。 */
.surface-art {
  position: relative;
  display: block;
  margin: 0 auto;
  z-index: var(--ep-z-map-surface);
  transition: transform var(--ep-dur-base) var(--ep-ease);
  pointer-events: none;
}
/* v2.2：帐篷显示尺寸整体收一档（客户要求「帐篷占用高度缩小」），
   SVG 内部仍是 240 / 160 坐标系（width/height 100% 自适应），所以帐篷坐标不用重画。
   v2.3：容器 220 宽，帐篷由 .surface-art 的 margin:0 auto 居中（村落左右各 10、村子各 40）。 */
.surface[data-size="lg"] .surface-art { width: 200px; height: 200px; }
/* v2.4：村子帐篷 140 → **120**。字号整档上调后村子名牌 124 → 137，单元随之变高，
   行距（= 单元高，见下）被 ×3 撑开，第 4 行从「露出约 1/2」掉到 1/3。
   客户上一轮已经明确要求「村子的高度也稍稍压缩一下，尽量一页能露出 4 行」，
   所以这里按客户诉求把帐篷压一档，把第 4 行的露出量补回来（实测 34% → 62%）。
   帐篷在 220 容器内居中，故 x 偏移 40 → 50（mapGeom 的 artOff 同步）。 */
.surface[data-size="sm"] .surface-art { width: 120px; height: 120px; }
.settle { display: block; }

/* --- 状态只换 CSS 变量，SVG 不重画 ---
   v2.1：客户把「点亮」改成「点亮火把」，旧的 亮灯 / 橙瓦 双模式整块废弃。
   「已加入」= 四重编码（不靠颜色单独传达信息，WCAG 1.4.1）：
     ① 火把点燃（未加入是空火盆 —— 空盆 #6F6656 本身 4.59:1，不会出现「找不到火把」）
     ② 顶旗（形状）
     ③ 地垫 3rpx #A33F08 描边
     ④ 名牌「已加入」胶囊

   为什么不能整卡刷橙：橙色 #FA6A1C 在素背景上只有 2.11:1，而且会把篷布褶皱、
   骨架杆、门帘这些刚画出来的细节全盖掉 —— 与客户「画精细一点」的要求直接冲突。 */
.settle-glow { fill: transparent; }

/* 顶旗与火焰默认不渲染，data-led=1 才出现。
   data-led 的来源：村子看是否已加入；村落看「该村落里我已加入几个村子」
   （村落本身不可加入，§5.1 第 16 条）。 */
.tent-flag,
.torch-flame { display: none; }

[data-led="1"] .tent-flag,
[data-led="1"] .torch-flame { display: inline; }
[data-led="1"] .torch-bowl-in { display: none; }
[data-led="1"] .settle-mat {
  stroke: var(--ep-orange-800);
  stroke-width: 3;
}

/* 选中：帐篷 scale(1.04) + 地面投影加深 + 地面橙晕（椭圆 path，非 box-shadow）
   ⚠️ B2-6 备案：`state="selected"` 目前**在视觉上不可达** ——
   app.js 里 STATE.selectedVillage 只在点村落的那一瞬间被写入，而那一刻并没有调用
   render()（240ms 后直接 go('p2')，重渲染出来的是隐藏中的 P1）；等用户返回 P1 时
   selectedVillage 已被清零。也就是说这套样式不会被真正渲染出来。
   本次**保留 CSS 并注明**（不删）：它是「村落被选中」这一状态的唯一视觉定义，
   后续真机如果要加「长按预览」之类的交互会直接用到。 */
.surface[data-state="selected"] .surface-art { transform: scale(1.04); }
.surface[data-state="selected"] .settle { --ep-settle-ground-shadow: rgba(74, 58, 44, 0.22); }
.surface[data-state="selected"] .settle-glow { fill: rgba(250, 106, 28, 0.16); }

/* 锁定：整顶帐篷覆 #DCD8CF（纯色覆盖，不用 opacity —— 避免连带把文字对比度拉低到不可控），
   墨线降为三级，门洞中央加线性锁图标。 */
.surface[data-state="locked"] .settle {
  --ep-tent-body: var(--ep-settle-locked-veil);
  --ep-tent-body-shade: var(--ep-settle-locked-veil);
  --ep-tent-roof-l: var(--ep-settle-locked-veil);
  --ep-tent-roof-r: var(--ep-settle-locked-veil);
  --ep-tent-flap: var(--ep-settle-locked-veil);
  --ep-tent-door-dark: var(--ep-settle-locked-veil);
  --ep-tent-mat: var(--ep-settle-locked-veil);
  --ep-tent-mat-edge: var(--ep-settle-locked-veil);
  --ep-tent-tie: var(--ep-settle-locked-veil);
  --ep-tent-pole: var(--ep-settle-locked-veil);
  --ep-tent-cord: var(--ep-settle-locked-veil);
  --ep-ink-1: var(--ep-ink-3);
  --ep-ink-2: var(--ep-ink-3);
}
.surface[data-state="locked"] .torch-flame,
.surface[data-state="locked"] .tent-flag { display: none; }
.surface-lock {
  position: absolute;
  z-index: 6;
  color: var(--ep-text-4);
}
/* 锁图标挂在门洞中央。帐篷 A/B 共用，所以不再按主题分叉（v4 按城邦 / 民居分过两套坐标）。 */
/* v2.3：锁图标挂在门洞中央，坐标要跟着 .surface-art 的居中偏移走（村 +10 / 村子 +40） */
.surface[data-size="lg"] .surface-lock { left: 130px; top: 142px; transform: translate(-50%, -50%); }
/* v2.4：容器 220 → 240、帐篷 140 → 120（居中偏移 60），门洞中心 = 60 + 60 = 120 */
.surface[data-size="sm"] .surface-lock { left: 120px; top: 96px; transform: translate(-50%, -50%); }

/* P1-1：P1 → P2 的 zoom-in 过渡（≤400ms 红线） */
.surface.is-zooming {
  transform: rotate(var(--ep-surface-rotate, 0deg)) scale(1.18);
  transition: transform var(--ep-dur-base) var(--ep-ease);
}
.surface.is-zooming ~ .surface {
  opacity: 0.35;
  transition: opacity var(--ep-dur-base) var(--ep-ease);
}

/* 村落「已加入 N 个村子」数字牌 56 × 40（N = 0 时 JS 不渲染 DOM） */
.surface-badge {
  position: absolute;
  z-index: var(--ep-z-map-flag);
  width: 56px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ep-white);
  border: 2px solid var(--ep-orange-500);
  border-radius: var(--ep-radius-chip);
  color: var(--ep-orange-700);
  font-size: var(--ep-fs-caption);
  font-weight: var(--ep-fw-semibold);
  box-shadow: var(--ep-shadow-surface);
  pointer-events: none;
}
/* v2.1：数字牌原挂在建筑画布顶部（top 2~6），渲染出来浮在帐篷尖顶上像「飘着的标签」。
   改挂到帐篷左侧地面（left 4 / top 152），与大帐篷左下角轻微相切、避开左后小帐篷。
   帐篷 A/B 共用，所以这里不再按主题分叉。 */
/* v2.3：数字牌同样跟着 .surface-art 的居中偏移走（v2.4：偏移 10 → 20，故 14 → 24） */
.surface[data-size="lg"] .surface-badge { left: 24px; top: 152px; }

/* --------------------------------------------------------------------------
   5.1.1 态胶囊（.surface-cap）—— v2.5 从名牌移到「帐篷右上角外侧」
   --------------------------------------------------------------------------
   客户第六轮：「态胶囊从名牌里拿出来，放到帐篷右上角」。于是它不再是名牌的第 3 行
   成员，而改为相对 `.surface` 绝对定位（`.surface` 已是 position:absolute，本身即
   包含块）。已加入 / 申请加入 仍是**同一位置、同一尺寸**，只有填充 / 描边之差。

   位置取值（实测核对，见 _build/capfit.mjs）：
     · 横向 `right: 0` —— 贴 .surface 右边缘，任何状态下胶囊都不超出 240 宽。
     · 纵向 `top: 0` —— 帐篷主轮廓（三角形）顶点在 .surface 局部坐标 (120, 34.5)，
       顶点之上没有任何墨线；`top:0` 时胶囊占 y[0,34]，与顶点还差 0.5rpx。
       直觉上的 `top:16` 会让胶囊下缘（y 16~50）切进右坡墨线（右坡过 (129.3, 50)），
       故否决。
     · 右对齐后两种宽度都落在顶点右侧：「申请加入」120rpx 起点 x=120（= 顶点 x，
       且该态不显示顶旗）；「已加入」98rpx 起点 x=142 > 顶旗右缘 138 —— 都不压旗。
       （本轮新增的「管理」76rpx 起点 x=164，比「已加入」还靠右，同样不压旗；
        宽度与「已加入」同源同值，故上表的 98 / 120 两个极值仍是本位置的包络。）

   尺寸与列表行 `.cap` **完全统一**（高 34 / 圆角 17 / 左右内边距 14 / 字号 22rpx）。
   已加入 = 实心 #FFECD5 + #FA6A1C；管理 / 申请加入 = 描边 2rpx #FA6A1C + #FA6A1C 字。 */
.surface-cap {
  position: absolute;
  right: 0;
  top: 0;
  z-index: 6; /* 盖在帐篷（.surface-art z-index 3）与名牌（.plate z-index 5）之上 */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 34px;
  padding: 0 14px;
  border: 2px solid transparent; /* 实心态 / 描边态外框一致，两者等高同宽 */
  border-radius: 17px;
  font-size: var(--ep-fs-mini); /* 22rpx */
  font-weight: var(--ep-fw-medium);
  line-height: 1;
  white-space: nowrap;
}
.surface-cap--joined {
  background: var(--ep-orange-100);
  color: var(--ep-orange-500);
}
.surface-cap--apply {
  background: transparent;
  border-color: var(--ep-orange-500);
  color: var(--ep-orange-500);
  cursor: pointer;
  pointer-events: auto;
}
/* 本轮（顺手项 2）：「管理」是**可点入口**，此前复用 `--joined`，与不可点的「已加入」
   计算样式全等（同 bg #FFECD5 / 同 color #FA6A1C / 同 34rpx×17rpx），用户判断不出哪个能点。
   改为**描边** —— 与「申请加入」共用「描边 = 可点」这套视觉语言（文案仍不同，不靠颜色单独传达，
   WCAG 1.4.1）。零几何变化：`.surface-cap` 早已预留 `border: 2px solid transparent` 占位，
   改的只是 border-color，宽高与「已加入」完全一致（实测 P2「已加入」98rpx / 「管理」76rpx 不变）。
   `pointer-events: auto` 必须有：P2 整层 `.map__content` 是 `pointer-events: none`
   （可点元素各自放行），少了它这个「可点入口」在 P2 上其实点不动 —— 与 `--apply` 一致。 */
.surface-cap--manage {
  background: transparent;
  border-color: var(--ep-orange-500);
  color: var(--ep-orange-500);
  cursor: pointer;
  pointer-events: auto;
}
/* v3：灰态（申请中 / 冷却期 / 满名额）—— 与列表行 .cap--muted 完全同一套色与高度，
   保证 P1 列表胶囊与 P2 名牌胶囊「同一状态长得一模一样」（F1 第 11 条）。
   仍然可点（热区保留给 toast 解释）。 */
.surface-cap--muted {
  background: var(--ep-fill-2);
  border-color: transparent;
  color: var(--ep-text-3);
  cursor: pointer;
  pointer-events: auto;
}
/* 纵向热区 34 → 88rpx（= 44pt，iOS HIG 最小触摸目标）。
   次要动作按 44pt 执行，**不套用** §5.1 第 8 条的 140rpx 主热区标准（会把行高撑开）。
   视觉仍是 34rpx 胶囊。v3：灰态胶囊同样需要这份热区（点击给 toast 解释）。 */
.surface-cap--apply::after,
.surface-cap--muted::after {
  content: "";
  position: absolute;
  left: -8px;
  right: -8px;
  top: 50%;
  height: 88px;
  transform: translateY(-50%);
}

/* --------------------------------------------------------------------------
   5.2 聚落名牌（§3.1.3）—— 所有文字一律收进白底名牌，禁止直接压在地形上
   -------------------------------------------------------------------------- */
.plate {
  /* v2.3：回到文档流，「与帐篷的间距」用 margin-top 表达（= --ep-settle-plate-gap 8rpx），
     高度由内容决定。这样容器高度永远等于 帐篷 + 间距 + 名牌，不会再出现死空白。 */
  position: relative;
  z-index: var(--ep-z-map-plate);
  margin: var(--ep-settle-plate-gap) auto 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  /* 8px 10px：12/16 会把「1280 帖」挤出 152 宽（§3.1.3 原值放不下，实测收 4rpx） */
  padding: 8px 10px;
  background: var(--ep-settle-plate-bg);
  border: 2px solid rgba(0, 0, 0, 0.06);
  border-radius: var(--ep-settle-plate-radius);
  box-shadow: var(--ep-shadow-surface);
  pointer-events: none;
}
.surface[data-size="lg"] .plate {
  /* v2.2：96 → 88（三行文字的最小高度：名称 38 + meta 28 + gap 4 + padding 16 = 86）
     v2.4：宽度 220 → 240（见 .surface 的说明：字号上调后 199rpx 的文字行放不进 192 内容区） */
  width: 240px;
  min-height: 88px;
  padding: 8px 12px;
}
.surface[data-size="sm"] .plate {
  /* v2.1：客户「村长位置太少，村长头像要有，用户名预留 8 个字符」。
     v2.4：字号 +2~3rpx 后该行需求 = 28 + 8 + 8×22(176) = **212rpx**，
     故宽度用 240（内容区 220），8 字昵称完整不省略（实测由 _build/platefit.mjs 复核）。

     v4 的三列已验证无解：212 宽旋转后外接矩形 216.1，三列中心距上限只有 231，
     净间距仅 14.9rpx（Playwright 实测 204 宽时真实重叠 2.2rpx）。
     令牌 §3.1.4 据此把**村子版图定为 2 列**——这是几何硬约束，不是审美选择，
     POS_SM 的坐标与它联动，不能单独调。三行结构：村子名 / 村长 / N 帖
     （v2.5：态胶囊已移出名牌，见 §5.1.1 `.surface-cap`，故第 3 行只剩 N 帖）。 */
  /* v2.2：118 → 112（名称 33 + 村长 28 + 帖数 28 + gap 4×2 + padding 16 = 113 取下限） */
  width: 240px;
  min-height: 112px;
  padding: 8px;
  gap: 4px;
}
/* 村长行：头像固定，昵称独占剩余宽度，两端不挤压（间距 8 = --ep-space-2） */
.plate-row--chief { gap: 8px; }
.plate-chief__name {
  /* v2.1：客户要求「用户名预留 8 个字符的位置」。
     原用 min-width: 8em —— 短名只占内容宽（名牌左右不齐），长名会一路撑开。
     改成固定 width: 8em：短名也占满 8 字（各行对齐），超过 8 字截断，
     这才是「预留 8 个字符位置」的正确语义。 */
  flex: none;
  width: 8em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.plate-row { display: flex; align-items: center; gap: 8px; min-width: 0; }

/* v2.5（客户第六轮第 3 条）：态胶囊**已从名牌里拿出来**，移到「帐篷右上角外侧」，
   改名为 `.surface-cap`（定义见 §5.1 `.surface-badge` 之后）。
   所以第 3 行现在只剩「N 帖」，名牌保持 3 行（村子名 / 村长 / N 帖）——
   单元几何不变（胶囊移出前后，第 3 行行高都由 22rpx×1.47≈32.3 的 `.plate-meta` 决定）。 */
.plate-row--meta {
  justify-content: space-between;
  gap: var(--ep-space-2);
}
.plate-row--meta .plate-meta { flex: 0 1 auto; }
.plate-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ep-text-1);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.31;
}
.surface[data-size="lg"] .plate-name { font-size: var(--ep-fs-h3); } /* 29rpx，最多 6 字 */
/* v4：对齐 v2.1 §3.1.3 —— 村子名牌第 1 行名称用 --ep-fs-body-l（25rpx） */
.surface[data-size="sm"] .plate-name { font-size: var(--ep-fs-body-l); } /* 25rpx，最多 5 字 */
/* v2.4（主理人验收）：`.plate-joined` 整块删除 ——
   那个「浮在名牌右上角」的绝对定位（top:-14 / right:-6 / z-index:1）会被村名与帐篷压住，
   同一语义不该一个浮角、一个在行内。
   v2.5（客户第六轮）：态胶囊（已加入 / 申请加入）再从名牌整体移出，改由 `.surface-cap`
   承担，绝对定位到**帐篷右上角外侧**（见 §5.1.1）。此处不留死代码。 */
.plate-meta {
  flex: none; /* 帖子数不许被压缩 —— 宁可省略村长昵称，也不能把数字截断 */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ep-fs-mini);
  line-height: 1.47;
  color: var(--ep-text-2);
}
.plate-chief {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: var(--ep-fs-mini);
  line-height: 1.47;
  color: var(--ep-text-2);
  overflow: hidden;
}
/* v4：昵称单独成 span，min-width: 8em 预留 8 个汉字（.plate-chief__name） */
.plate-chief > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 村长头像占位：首字母色块，不引外链（令牌 §3.1.3：28rpx，无边框）
   v4 第 5 条「村长头像要有」：原值 rgba(0,0,0,0.04) 底 + text-2 字，在白底名牌上
   只有 1.09:1，**几乎看不见**（截图实测）。三处改动：
   ① 底改 --ep-orange-100（#FFECD5）—— 暖色符合「人」的语义，白名牌上比原值清晰；
   ② 首字**不能用橙** —— 实测 #C24E0C on #FFECD5 = **4.15:1**，而 21rpx 在 750 基准
      下 = 10.5 CSS px 属小字号，AA 要 4.5:1 → 不达标。改回 --ep-text-2 = **6.59:1** ✅；
   ③ 补 2rpx --ep-line-2 描边 —— 28rpx 的小圆，任何浅填充都只有 1.1~1.3:1，
      **真正让它"看得见"的是边界**。
   语义：橙色在本项目已承担「已加入（火把）/ 航线 / CTA」三重含义，所以头像只留
   橙底，**不加橙字、不加橙环**，避免稀释"已加入"这套状态编码。 */
.ava {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--ep-orange-100);
  border: 2px solid var(--ep-line-2);
  color: var(--ep-text-2);
  font-weight: var(--ep-fw-medium);
  line-height: 1;
  overflow: hidden;
}

/* P3 村子头部：村长（头像 + 昵称）
   v4：昵称单独成 span，预留 8 个汉字（--ep-fs-caption 21rpx × 8 = 168rpx），
   与村子名牌上的村长行同一套宽度口径；「村长」二字拆成独立标签，
   避免昵称变长时把「村长」挤成省略号。 */
.chief {
  display: inline-flex;
  align-items: center;
  gap: var(--ep-space-2);
  font-size: var(--ep-fs-caption);
  color: var(--ep-text-2);
  white-space: nowrap;
  min-width: 0;
}
.chief__label { flex: none; }
.chief__name {
  flex: none;
  min-width: 8em;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ep-text-1);
  font-weight: var(--ep-fw-medium);
}

/* P5 标题必填标记 */
.req {
  margin-left: var(--ep-space-2);
  padding: 0 var(--ep-space-1);
  border-radius: var(--ep-radius-chip);
  background: var(--ep-orange-100);
  color: var(--ep-text-1);
  font-size: var(--ep-fs-mini);
  font-weight: var(--ep-fw-medium);
}

/* P4：帖子详情页的发帖悬浮按钮，避让底部 96rpx 输入条（P3 版避让的是 tabbar） */
.fab--over-bar {
  bottom: 120px; /* 96（bottom-bar）+ 24 间隙 */
}

/* --------------------------------------------------------------------------
   6. 原子 2 — 旗标（v2.1：载体从 DOM 换成 SVG）
   --------------------------------------------------------------------------
   原 .flag / .flag-pole / .flag-cloth 是一套独立 DOM 旗标，按主题分别定位到
   「屋顶右侧」「中央旗杆」等处。改用帐篷后这套定位全部失效（没有屋顶和旗杆了），
   而帐篷 SVG 里已经画了顶旗（.tent-flag，由 data-led 点亮）。
   同一个「已加入」状态不该同时出现两面旗，所以 DOM 旗标整个删除，
   旗标的语义改由 SVG 顶旗承担 —— 仍是形状编码，不靠颜色单独传达。 */

/* --------------------------------------------------------------------------
   7. 原子 3 — 航线（v2.2 整块删除）
   --------------------------------------------------------------------------
   客户明确要求「已加入的村子和村子之间连线没必要，去掉」。
   原来这条橙色虚线用来指认「哪几个是我加入的村子」，现在改由
   **已加入优先排序**承担（见 app.js screenP2 的 ordered）——我的村子永远排在
   版图最靠前的位置，不需要一条穿过整张图的线去指。
   `.route-path` / `.route-path--stroke` / `.route-dot` / `.map__route` 一并删除，
   `--ep-route-*` 变量保留在 tokens 里以防真机代码引用断链，无引用方。 */

/* --------------------------------------------------------------------------
   8. 底部 tabbar（复用现有悬浮胶囊）
   -------------------------------------------------------------------------- */
.tabbar {
  position: absolute;
  left: var(--ep-space-page);
  right: var(--ep-space-page);
  bottom: 72px; /* 38 + 34 安全区 */
  height: 115px;
  border-radius: var(--ep-radius-capsule);
  background: var(--ep-white);
  box-shadow: var(--ep-shadow-base);
  z-index: var(--ep-z-tabbar);
  display: flex;
  align-items: center;
}
.tabbar__item {
  position: relative;
  flex: 1;
  height: 115px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-size: var(--ep-fs-micro);
  color: var(--ep-text-3);
  cursor: pointer;
}
.tabbar__icon {
  width: 46px;
  height: 46px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}
.tabbar__item.is-on {
  color: var(--ep-orange-500);
  font-weight: var(--ep-fw-medium);
}
.tabbar__item.is-on .tabbar__icon {
  background-image: var(--icon-fill);
}
/* v3（规格 11）：tabbar「消息」的未读红点。
   挂在图标右上角（不是标签下方 —— 那会把 tabbar 的高度撑开）。
   红点用 --ep-danger（#C0392B），在白底 tabbar 上 5.97:1；
   未读**不靠红点单独传达**：item 的 aria-label 里另外报了「N 条未读」，
   P6 的每张卡片也有独立的未读点与「N 条未读」的读屏文案。 */
.tabbar__dot {
  position: absolute;
  top: 12px;
  left: 50%;
  margin-left: 10px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--ep-danger);
}
.tabbar__plus {
  position: relative;
  flex: none;
  width: 70px;
  height: 70px;
  margin: 0 20px;
  border-radius: 50%;
  background: var(--ep-orange-500);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.tabbar__plus img {
  width: 34px;
  height: 34px;
}
.tabbar__plus::after {
  /* 热区扩到 140×140 */
  content: "";
  position: absolute;
  left: -35px;
  right: -35px;
  top: -35px;
  bottom: -35px;
}

/* --------------------------------------------------------------------------
   9. 通用组件：按钮 / chip / 卡片 / FAB
   -------------------------------------------------------------------------- */
.btn {
  /* v2.4：对齐真机项目 —— 胶囊按钮高 84rpx、圆角 42rpx、字号 30rpx。
     （字号走组件级令牌 --ep-fs-btn，见 tokens.css §1.6，不在本文件散落字面值。） */
  height: 84px;
  padding: 0 var(--ep-space-10);
  border-radius: 42px;
  font-size: var(--ep-fs-btn);
  font-weight: var(--ep-fw-medium);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform var(--ep-dur-fast) var(--ep-ease);
}
.btn:active {
  transform: scale(0.97);
}
.btn--primary {
  background: var(--ep-orange-500);
  /* ⚠️ 对比度备案（v2.4）：客户原话「照着项目来」，项目里橙底按钮一律白字（12+ 处）。
     实测 #FFFFFF on #FA6A1C = **2.94 : 1**，AA 正文(4.5)与大字(3.0) **都不达标**
     —— 这是项目自身的历史缺陷，按客户要求原样保留，已在 04-revision-log 第十四节备案。
     若要合规，把背景换成 --ep-orange-700 **#C24E0C**（白字 **4.78:1** ✅，令牌里已有）。
     ⚠️ 不要用 #E05A0F —— 白字仅 **3.72:1**，仍不达 AA，且容易被误当成「接近达标」而放行。
     此处**不擅自改**。 */
  color: #FFFFFF;
}
.btn--secondary {
  /* v2.4：次按钮 = 透明底 + 2rpx 描边（项目规格），与主按钮同高同圆角 */
  background: transparent;
  border: 2px solid rgba(0, 0, 0, 0.04);
  color: var(--ep-text-1);
}
.btn--joined {
  background: var(--ep-orange-100);
  color: var(--ep-text-1); /* 84rpx 大字，但深色字 15:1，安全性高于 #FA6A1C(2.55:1) */
}
/* 本轮（顺手项 2）：P3 大按钮的「管理」也从 `--joined` 拆出来。
   原先「村长看自己的村子」= 管理 与「已加入」都是 `.btn--joined`，同 bg #FFECD5 / 同文字色，
   计算样式全等，用户判断不出哪个能点。现在**填充与描边两处都不同**：
   已加入 = 实心 #FFECD5 无描边；管理 = 透明底 + 2rpx #FA6A1C 描边 + `margin-left: auto` 的主位。
   为什么这里**只改描边、不改文字色**（与 `.cap--manage` 的橙字做法不同）：沿用 `.btn--joined`
   的既有判断 —— 84rpx 大字用深色字是 15:1，而 #FA6A1C on #FFFFFF 只有 **2.94:1**
   （≈ `.btn--primary` 白字那对，AA 大字 3.0 仍差一点点）。大按钮没必要为了「描边」把
   对比度从 15:1 换到 2.94:1，可点性由描边承担就够了。
   几何：`.btn` 基类没有描边，全局 `box-sizing: border-box`，所以 `.village-head__title-row .btn`
   的 height: 56px 不变（描边吃内容框不吃外框），宽度 +4rpx；该行标题是 `flex: 1 1 auto`，
   多出的宽度由标题吸收 —— 行高与外部版图几何都不受影响（实拍 A/B 复核见报）。
   颜色只取既有令牌（--ep-orange-500），不新增颜色。 */
.btn--manage {
  background: transparent;
  border: 2px solid var(--ep-orange-500);
  color: var(--ep-text-1);
}
/* v3（规格 16 / C4）：灰态按钮 —— 「申请中」/ 冷却期 / 名额已满三种不可提交的状态。
   ⚠️ 灰态**不加 disabled 属性**：热区必须保留，点击由 joinCta 给出对应解释
   （「申请已提交」「N 月 N 日后可再次申请」「最多加入 5 个村子」），
   否则用户点了没反应，反而更像坏了。所以这里只是一层视觉降级。
   灰底用 --ep-fill-2、文字用 --ep-text-3（硬约束：灰态不许自造色值）。 */
.btn--disabled {
  background: var(--ep-fill-2);
  color: var(--ep-text-3);
}
/* 小号按钮（P8 村民列表的「移除」）—— 行高 160rpx，84rpx 的基类按钮会把行撑开。
   视觉仍是胶囊，只是收到 56rpx，与 P3 标题行的按钮同一档。 */
.btn--sm {
  height: 56px;
  padding: 0 var(--ep-space-5);
  font-size: var(--ep-fs-cap);
}
/* 危险动作用语（拒绝申请 / 移除村民 / 退出村子）——
   底色走 --ep-danger（#C0392B），白字 **5.97:1** ✅ 达 AA，
   比主按钮的白字 on #FA6A1C（2.94:1，已备案）更安全，不需要再备案。 */
.btn--danger {
  background: var(--ep-danger);
  color: var(--ep-white);
}
.btn--block {
  width: 100%;
}

/* --------------------------------------------------------------------------
   9.1 状态 / 动作胶囊（v2.4 新增；v2.5 客户第六轮第 2 / 3 条统一尺寸）
   --------------------------------------------------------------------------
   胶囊出现在两处，共用同一套配色与**同一套尺寸**：
     · 已加入 = **实心** #FFECD5 底 + #FA6A1C 字            （不可点）
     · 管理   = **描边** 透明底 + 2rpx #FA6A1C 描边 + #FA6A1C 字（可点 → P8；本轮顺手项 2 新增）
     · 申请加入 = **描边** 透明底 + 2rpx #FA6A1C 描边 + #FA6A1C 字
   即：**实心 = 无动作，描边 = 可点**。三者尺寸完全一致，只有文案与填充 / 描边之差。
   两个载体（v2.5 起尺寸也统一）：
     · 列表行 `.cap`           —— 高 34rpx / 圆角 17rpx / 左右内边距 14rpx / 字号 22rpx
     · 村子名牌 `.surface-cap` —— 同一套值，位置在「帐篷右上角外侧」（见 §5.1.1）
   为什么把 `.cap` 从 61 缩到 34：客户第六轮「那么大按钮把下面文案挤错行了」——
   34rpx ≤ 首行文字块高（村名 28rpx × 1.3 ≈ 36.4rpx），胶囊不再把首行撑开。

   ⚠️ 对比度备案：#FA6A1C on #FFECD5 = **2.55 : 1**，34rpx（22 字号）属小字号，**不过 AA**。
   这对色是客户明确给的（与真机项目一致），已在 04-revision-log 第十四节备案，
   未擅自改成 #C24E0C —— 与主按钮同一处理原则。
   状态不靠颜色单独传达：已加入 / 管理 / 申请加入 的**文案各不相同**（WCAG 1.4.1）。 */
.cap {
  position: relative; /* ::after 热区以它为锚 */
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* v2.5（客户第六轮）：「那么大按钮把下面文案挤错行了」——
     原值 height 61 / padding 0 22 / font 26 时，胶囊高过首行文字块，把行撑开、
     与下方简介的间距被顶乱。整组缩到「不撑行」的尺寸： */
  height: 34px;
  padding: 0 14px;
  border-radius: 17px;
  font-size: var(--ep-fs-mini); /* 22rpx，辅助档，与列表 meta 同级 */
  font-weight: var(--ep-fw-medium);
  line-height: 1;
  white-space: nowrap;
  border: 2px solid transparent; /* 描边态与实心态占位一致，两者等高 */
}
.cap--joined {
  background: var(--ep-orange-100);
  color: var(--ep-orange-500);
}
.cap--apply {
  background: transparent;
  border-color: var(--ep-orange-500);
  color: var(--ep-orange-500);
  cursor: pointer;
}
/* 本轮（顺手项 2）：列表行的「管理」同样从 `--joined` 拆出来改描边 —— 理由与
   `.surface-cap--manage` 一样（原先与不可点的「已加入」计算样式全等）。
   `.cap` 也已预留 2px 透明描边占位，故宽高零变化。
   注：刻意**不**把 `--manage` 加进下面的 88rpx 热区规则 —— 管理胶囊的点击热区
   保持现状（视觉 34rpx 本身即热区），避免顺手改动点按面积。 */
.cap--manage {
  background: transparent;
  border-color: var(--ep-orange-500);
  color: var(--ep-orange-500);
  cursor: pointer;
}
/* v3（规格 16 / C4）：灰态胶囊 —— 「申请中」/ 冷却期 / 满名额。
   与 .surface-cap--muted 同一套色（灰底 --ep-fill-2 + 灰字 --ep-text-3）。
   仍然是**可点的**（热区保留给 toast 解释），所以有 cursor:pointer。 */
.cap--muted {
  background: var(--ep-fill-2);
  border-color: transparent;
  color: var(--ep-text-3);
  cursor: pointer;
}
/* v2.4：列表行的「申请加入」按 **44pt（88rpx）** 给纵向热区（视觉仍是 34rpx 胶囊）。
   安全性实测：行 min-height 160rpx、行间距 16rpx，88rpx 热区以胶囊中心对齐后
   落在本行内，不会跨到相邻行误触（相邻行同样可点）。
   v3：灰态胶囊（申请中 / 冷却期 / 满名额）同样要吃这份热区 —— 它也要能收到点击、
   给出 toast 解释，否则用户点了没反应。 */
.cap--apply::after,
.cap--muted::after {
  content: "";
  position: absolute;
  left: -8px;
  right: -8px;
  top: 50%;
  height: 88px;
  transform: translateY(-50%);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--ep-space-1);
  padding: var(--ep-space-1) var(--ep-space-3);
  border-radius: var(--ep-radius-chip);
  background: var(--ep-fill-2);
  font-size: var(--ep-fs-mini);
  font-weight: var(--ep-fw-medium);
  color: var(--ep-text-2);
}
.chip--on {
  background: var(--ep-orange-100);
  color: var(--ep-text-1);
}

/* v2.4：`.divider` 删除 —— 唯一使用方（P4 帖子详情里「正文 ↔ 全部评论」之间那条线）
   已按客户「评论区不加分隔线」的要求去掉，类不再有引用方。 */

.fab {
  position: absolute;
  right: var(--ep-space-page);
  bottom: 211px; /* 原 153px 会与 tabbar（top=72+115=187）叠压 34px，提到 211 留出 24rpx 间隙 */
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--ep-orange-500);
  box-shadow: var(--ep-shadow-float);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ep-white);
  cursor: pointer;
  z-index: var(--ep-z-fab);
  transition: transform var(--ep-dur-fast) var(--ep-ease);
}
.fab::after {
  /* 热区扩到 140×140 */
  content: "";
  position: absolute;
  left: -22px;
  right: -22px;
  top: -22px;
  bottom: -22px;
}
.fab:active {
  transform: scale(0.97);
}

/* --------------------------------------------------------------------------
   10. P1 计数行（tab 正下方）
   v4：从「底部正向引导条」改成 tab 下方的纯计数行 —— 只报「已加入 N 个村子」，
   删掉「再加入 1 个即可连成航线」这类引导（客户没有要求，且航线引导指向的
   「我的营地」模块本轮已删除）。上移后边距由「上 24 / 下 0」改为「上 0 / 下 24」。
   -------------------------------------------------------------------------- */
.note-bar {
  flex: none;
  /* v2.2：高度 68 → 56、下边距 24 → 12，与 tab 一起给版图让出高度 */
  height: 56px;
  margin: 0 var(--ep-space-page) var(--ep-space-3);
  padding: 0 var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-white);
  display: flex;
  align-items: center;
  gap: var(--ep-space-3);
  font-size: var(--ep-fs-mini);
  color: var(--ep-text-2);
}
.note-bar b {
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
}

/* --------------------------------------------------------------------------
   10.2 村落列表（热门 / 最新 / 我的）
   「全部」走版图，其余三个 tab 走列表。行高 ≥140rpx（令牌 §6.6 热区下限），
   实测内容 150rpx，min-height 取 160 留余量。
   -------------------------------------------------------------------------- */
.vlist {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 var(--ep-space-page) var(--ep-space-6);
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-4);
}
.vrow {
  flex: none;
  min-height: 160px;
  padding: var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-white);
  box-shadow: var(--ep-shadow-base);
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
  cursor: pointer;
  transition: transform var(--ep-dur-fast) var(--ep-ease);
}
.vrow:active { transform: scale(0.985); }
.vrow__main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* v2.4（客户第五轮第 3 条）：首行重排为 [村子名] [N 条新帖] …… [已加入 | 申请加入]。
   胶囊靠右（margin-left:auto），所以「新帖」标签紧跟在名字后面、
   状态胶囊永远贴右端，中间留白把两者分开。 */
.vrow__head {
  display: flex;
  align-items: center;
  gap: var(--ep-space-2);
  min-width: 0;
}
.vrow__head .cap { margin-left: auto; }
.vrow__fresh {
  flex: none;
  font-size: var(--ep-fs-mini);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.4;
  color: var(--ep-orange-700); /* 白底 4.78:1 ✅ */
}
.vrow__name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.3;
  color: var(--ep-text-1);
}
.vrow__desc {
  font-size: var(--ep-fs-caption);
  line-height: 1.5;
  color: var(--ep-text-2);
  /* 简介只留 1 行：列表里 4 行以上的简介会把整屏挤成 2 条 */
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.vrow__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--ep-space-2) var(--ep-space-3);
  font-size: var(--ep-fs-mini);
  line-height: 1.4;
  color: var(--ep-text-3);
}
.vrow__stat { flex: none; }
.vrow__time { flex: none; }
/* v2.2：村子列表行的村长（头像 + 昵称）—— 与名牌上的村长行同一套口径 */
.vrow__chief {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
/* v3（规格 4）：冷却期明细「M-D 后可再申请」—— 只在被拒且冷却未过的那一行出现。
   用 --ep-warning（#9E5E00，白底 4.83:1 ✅）表达"受限但可恢复"，
   与橙色系（已加入 / CTA）区分开，不与状态编码抢色。
   v3 同时删除 .vrow__joined —— 它唯一的引用方 villageRowHtml 已按 B2-6 作为死代码删除。 */
.vrow__cool {
  flex: none;
  color: var(--ep-warning);
}
.vrow__arrow {
  flex: none;
  color: var(--ep-text-4);
}
.list-empty {
  flex: none;
  margin-top: var(--ep-space-12);
  padding: var(--ep-space-12) var(--ep-space-6);
  border: 2px dashed var(--ep-line-2);
  border-radius: var(--ep-radius-card);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ep-space-3);
  text-align: center;
}
.list-empty__title {
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
}
.list-empty__desc {
  font-size: var(--ep-fs-caption);
  line-height: 1.5;
  color: var(--ep-text-2);
}

/* --------------------------------------------------------------------------
   11. P2 村落封面条
   -------------------------------------------------------------------------- */
.cover {
  flex: none;
  /* v2.2：客户指出「底部留这么大空白干嘛」——348 → 216（标题 42 + 简介 38 + chips 48
     + 两处 16 间距 + 底部 32 内边距 = 192，取 216 留一点呼吸）。
     省下的 132rpx 全部让给下面的村子版图。 */
  height: 216px;
  padding: 0 var(--ep-space-page);
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-4);
}
.cover__title {
  font-size: var(--ep-fs-h1);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.24;
  color: var(--ep-text-1);
}
.cover__desc {
  font-size: var(--ep-fs-body);
  line-height: 1.58;
  color: var(--ep-text-2);
}
.cover__stats {
  display: flex;
  gap: var(--ep-space-4);
  flex-wrap: wrap;
}
.cover__crumb {
  display: flex;
  align-items: center;
  gap: var(--ep-space-1);
  font-size: var(--ep-fs-caption);
  color: var(--ep-text-2);
}

/* --------------------------------------------------------------------------
   12. P3 村子主页
   -------------------------------------------------------------------------- */
.village-head {
  flex: none;
  padding: 0 var(--ep-space-page) var(--ep-space-8);
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-4);
}
.village-head__title {
  font-size: var(--ep-fs-h1);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.24;
  color: var(--ep-text-1);
}
.village-head__desc {
  font-size: var(--ep-fs-body);
  line-height: 1.58;
  color: var(--ep-text-2);
}
.village-head__row {
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
}
/* v3（规格 7 / 8）：P3 的「退出村子」入口 —— 只对**非村长的已加入村民**渲染。
   退出是独立动作，不能和「已加入」挤成同一个按钮（那个按钮是灰态、不可点的），
   所以做成行内次要动作靠右放。村长不渲染它（规格 8：村长不能主动退出）。
   文字用 --ep-text-2（白底 7.0:1 ✅），不用红 —— 退出不需要"危险色"来吓人，
   真正的危险提示放在二次确认弹层里。 */
.head-quit {
  position: relative;
  flex: none;
  margin-left: auto;
  font-size: var(--ep-fs-caption);
  color: var(--ep-text-2);
  cursor: pointer;
}
.head-quit::after {
  /* 热区扩到 140×140（§6.6 主热区下限） */
  content: "";
  position: absolute;
  left: -20px;
  right: -20px;
  top: 50%;
  height: 140px;
  transform: translateY(-50%);
}
/* v2.5（客户第六轮）：「之前是让你跟村子名称放在一行里！现在文案都给我撑乱了」——
   v2.4 的写法是「左列 = 标题 + 简介，右列 = 按钮」，简介被按钮挤窄、观感凌乱。
   改为三段式：
     ① .village-head__title-row —— 标题 + 按钮严格同行，按钮右对齐
     ② .village-head__desc     —— 简介独占整行（674rpx），不再受按钮挤压
     ③ .village-head__row      —— 帖数 + 村长（维持原样） */
.village-head__title-row {
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
}
.village-head__title-row .village-head__title {
  flex: 1 1 auto;
  min-width: 0;
}
.village-head__title-row .btn {
  flex: none;
  margin-left: auto;
  /* v2.5：原来沿用 .btn 基类的 84rpx 高，在标题行里过大、压过标题 —— 收到 56rpx */
  height: 56px;
  padding: 0 var(--ep-space-6);
  font-size: var(--ep-fs-cap);
}

.post-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--ep-space-6) var(--ep-space-page) 200px;
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-4);
}
.post-card {
  background: var(--ep-white);
  border-radius: var(--ep-radius-card);
  padding: var(--ep-space-6);
  cursor: pointer;
  transition: transform var(--ep-dur-fast) var(--ep-ease);
}
.post-card:active {
  transform: scale(0.99);
}
.post-card__top {
  display: flex;
  align-items: center;
  gap: var(--ep-space-3);
  margin-bottom: var(--ep-space-3);
}
.avatar {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: var(--ep-fill-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ep-text-3);
  flex: none;
}
.post-card__author {
  font-size: var(--ep-fs-caption);
  color: var(--ep-text-1);
  font-weight: var(--ep-fw-medium);
}
.post-card__time {
  font-size: var(--ep-fs-mini);
  color: var(--ep-text-3);
}
.post-card__title {
  font-size: var(--ep-fs-h3);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.31;
  color: var(--ep-text-1);
  margin-bottom: var(--ep-space-2);
}
.post-card__body {
  font-size: var(--ep-fs-body);
  line-height: 1.58;
  color: var(--ep-text-2);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.post-card__tags {
  display: flex;
  gap: var(--ep-space-2);
  margin-top: var(--ep-space-3);
  flex-wrap: wrap;
}
.post-card__foot {
  display: flex;
  align-items: center;
  gap: var(--ep-space-6);
  margin-top: var(--ep-space-4);
  padding-top: var(--ep-space-3);
  border-top: 1px solid var(--ep-line-1);
  font-size: var(--ep-fs-mini);
  color: var(--ep-text-3);
}
.post-card__foot span {
  display: flex;
  align-items: center;
  gap: var(--ep-space-1);
}

.notice-card {
  background: var(--ep-orange-100);
  border-radius: var(--ep-radius-card);
  padding: var(--ep-space-6);
  display: flex;
  gap: var(--ep-space-3);
}
.notice-card__body {
  font-size: var(--ep-fs-body);
  line-height: 1.58;
  color: var(--ep-text-1);
}
.notice-card__title {
  font-size: var(--ep-fs-caption);
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
  margin-bottom: var(--ep-space-1);
}

/* --------------------------------------------------------------------------
   13. P4 帖子详情
   -------------------------------------------------------------------------- */
.article {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 var(--ep-space-page) var(--ep-space-6);
}
.article__title {
  font-size: var(--ep-fs-h1);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.24;
  color: var(--ep-text-1);
  margin-bottom: var(--ep-space-4);
}
.article__author {
  display: flex;
  align-items: center;
  gap: var(--ep-space-3);
  margin-bottom: var(--ep-space-6);
}
.article__body {
  font-size: var(--ep-fs-body-l);
  line-height: 1.52;
  color: var(--ep-text-1);
}
.article__body p {
  margin-bottom: var(--ep-space-4);
}
.article__imgs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--ep-space-2);
  margin: var(--ep-space-4) 0 var(--ep-space-6);
}
.article__img {
  aspect-ratio: 1 / 1;
  border-radius: var(--ep-radius-chip);
  background: var(--ep-fill-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ep-text-4);
}
.section-title {
  font-size: var(--ep-fs-h2);
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
  padding: var(--ep-space-6) 0 var(--ep-space-4);
}
/* --------------------------------------------------------------------------
   13.4 评论区（v2.4：照搬真机「人生轨迹详情页」的两级结构）
   --------------------------------------------------------------------------
   来源：`engineer-planet-miniprogram/pages/trajectory/trajectory.vue`
        模板 :143–179、样式 :1037–1119（SCSS 嵌套原样翻译成扁平的类选择器，
        优先级靠选择器层数还原：`.im_list_wapper .child_msg .content`(3类)
        > `.im_list_wapper .content`(2类)，所以子评论的 62rpx 缩进能盖住 76rpx）。

   **本页的核心痛点：正文绝不能再和用户名挤在同一行。**
   结构 = 两级、每级两个**兄弟块**：
     .title   （header 行）头像 + 姓名 +（可选）作者标 + .recall（时间/回复，margin-left:auto）
     .content （正文块）与 .title 平级，padding-left:76rpx 对齐到姓名起始位
   子评论 `.msg.child_msg` 整体内缩（padding-left:74rpx，不是 margin），
   头像缩到 46rpx，正文缩进 62rpx；`.replay_wapper` 只在有 replyTo 时内联渲染「回复 张三：」。

   按客户要求**不做**的：无灰底、无边框、无分隔线；无单条评论点赞；无「查看全部 N 条回复」折叠。 */
.im_list_wapper .msg {
  font-size: var(--ep-fs-body);
  color: rgba(0, 0, 0, 0.88);
}
.im_list_wapper .msg .title {
  display: flex;
  align-items: center;
}
/* 主评论头像 60rpx（子评论覆盖成 46rpx） */
.im_list_wapper .msg .title > .ava {
  width: 60px;
  height: 60px;
  margin-right: 15px;
  position: relative;
  top: -4px;
}
.im_list_wapper .msg .title .name {
  margin-left: 4px;
  color: var(--ep-black); /* 原页面是 rgba(0,0,0)：纯黑 */
  font-weight: var(--ep-fw-semibold); /* 原页面 bold(700)；令牌 §1.7 禁 700，取 600 */
}
.im_list_wapper .msg .title .tag {
  margin-left: 7px;
  padding: 4px 11px;
  border-radius: 8px;
  background: var(--ep-orange-100); /* 原页面 #FFF7ED，令牌里无此值，取同色相浅橙 */
  font-size: var(--ep-fs-caption);
  color: var(--ep-orange-500);
}
.im_list_wapper .msg .title .recall {
  margin-left: auto;
  display: flex;
  align-items: center;
  color: var(--ep-text-2);
  font-size: var(--ep-fs-caption);
}
.im_list_wapper .msg .title .recall > span:first-child {
  margin-right: 30px;
  font-size: var(--ep-fs-body);
}
.im_list_wapper .msg > .content {
  padding-left: 76px;
  margin-bottom: 8px;
}
/* 子评论：整体内缩靠 padding（不是 margin），再往下留 22rpx 与父评论分开 */
.im_list_wapper .child_msg {
  padding-left: 74px;
  margin-top: 22px;
}
.im_list_wapper .child_msg > .title > .ava { width: 46px; height: 46px; }
.im_list_wapper .child_msg > .content { padding-left: 62px; }
.im_list_wapper .replay_wapper {
  display: inline;
  color: rgba(0, 0, 0, 0.88);
}

.bottom-bar {
  flex: none;
  height: 96px;
  padding: 0 var(--ep-space-page);
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
  border-top: 1px solid var(--ep-line-1);
  background: var(--ep-white);
}
.bottom-bar__input {
  flex: 1;
  height: 72px;
  padding: 0 var(--ep-space-5);
  border-radius: var(--ep-radius-button);
  background: var(--ep-fill-2);
  font-size: var(--ep-fs-body);
  color: var(--ep-text-3);
  display: flex;
  align-items: center;
}
.bar-action {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: var(--ep-fs-mini);
  color: var(--ep-text-2);
  cursor: pointer;
  min-width: 96px;
}
.bar-action::after {
  /* 热区扩到 140×140 */
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 140px;
  transform: translateY(-50%);
}
.bar-action.is-on {
  color: var(--ep-orange-700);
}

/* --------------------------------------------------------------------------
   14. P5 发帖页表单
   -------------------------------------------------------------------------- */
.form {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--ep-space-6) var(--ep-space-page) var(--ep-space-10);
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-6);
}
.field__label {
  font-size: var(--ep-fs-caption);
  font-weight: var(--ep-fw-medium);
  color: var(--ep-text-2);
  margin-bottom: var(--ep-space-3);
}
.field__input {
  width: 100%;
  min-height: 88px;
  padding: var(--ep-space-5) var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-fill-2);
  font-size: var(--ep-fs-body-l);
  line-height: 1.52;
  color: var(--ep-text-1);
}
.field__input::placeholder {
  color: var(--ep-text-3);
}
textarea.field__input {
  min-height: 320px;
  resize: none;
}
.field__count {
  margin-top: var(--ep-space-2);
  text-align: right;
  font-size: var(--ep-fs-mini);
  color: var(--ep-text-3);
}
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ep-space-2);
}
.chip--selectable {
  position: relative;
  padding: var(--ep-space-2) var(--ep-space-4);
  cursor: pointer;
}
.uploader {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--ep-space-2);
}
.uploader__cell {
  aspect-ratio: 1 / 1;
  border-radius: var(--ep-radius-card);
  background: var(--ep-fill-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ep-text-3);
}
.uploader__cell--add {
  border: 2px dashed var(--ep-line-2);
  background: var(--ep-fill-1);
}
.form-tip {
  padding: var(--ep-space-5) var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-orange-100);
  font-size: var(--ep-fs-caption);
  line-height: 1.52;
  color: var(--ep-text-1);
}
.form-foot {
  flex: none;
  padding: var(--ep-space-4) var(--ep-space-page) 68px;
  border-top: 1px solid var(--ep-line-1);
  background: var(--ep-white);
  display: flex;
  gap: var(--ep-space-4);
}
.form-foot .btn {
  flex: 1;
}

/* --------------------------------------------------------------------------
   15. 弹层 / Toast
   -------------------------------------------------------------------------- */
.mask {
  position: absolute;
  inset: 0;
  z-index: var(--ep-z-modal);
  background: var(--ep-mask);
  display: flex;
  align-items: flex-end;
}
.sheet {
  width: 750px;
  background: var(--ep-white);
  border-radius: var(--ep-radius-page) var(--ep-radius-page) 0 0;
  padding: var(--ep-space-8) var(--ep-space-page) var(--ep-space-10);
}
.sheet__title {
  font-size: var(--ep-fs-h2);
  font-weight: var(--ep-fw-semibold);
  color: var(--ep-text-1);
  text-align: center;
  margin-bottom: var(--ep-space-4);
}
.sheet__desc {
  font-size: var(--ep-fs-body);
  line-height: 1.58;
  color: var(--ep-text-2);
  text-align: center;
  margin-bottom: var(--ep-space-8);
}
/* v3：弹层里的理由输入区（申请加入 / 拒绝申请 / 移除村民三类都要填理由。
   规格 15：拒绝申请也必填理由；规格 14：移除村民必填理由）。
   复用 P5 的 .field__input 外观，只补「标签 + 输入」之间的间距。 */
.sheet__field {
  margin-bottom: var(--ep-space-8);
}
.sheet__label {
  font-size: var(--ep-fs-caption);
  font-weight: var(--ep-fw-medium);
  color: var(--ep-text-2);
  margin-bottom: var(--ep-space-3);
}
.sheet__input {
  display: block;
}
/* 弹层里的理由框比 P5 的正文框矮一档：弹层只有 3 行内容，
   320rpx 的正文框（§14 的 `textarea.field__input`）会把弹层顶到屏幕外。
   选择器带上 textarea 才压得住上一条（同为 (0,1,1)，靠"后来居上"取胜）。 */
textarea.sheet__input {
  min-height: 176px;
}
.sheet__input::placeholder {
  color: var(--ep-text-3);
}
.sheet__actions {
  display: flex;
  gap: var(--ep-space-4);
}
.sheet__actions .btn {
  flex: 1;
}

.toast {
  position: absolute;
  left: 50%;
  bottom: 320px;
  transform: translateX(-50%);
  z-index: var(--ep-z-toast);
  max-width: 560px;
  padding: var(--ep-space-5) var(--ep-space-8);
  border-radius: var(--ep-radius-card);
  background: rgba(0, 0, 0, 0.88);
  color: #FFFFFF;
  font-size: var(--ep-fs-body-l);
  line-height: 1.52;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--ep-dur-base) var(--ep-ease);
}
.toast.is-on {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   16. 空态 / 骨架
   -------------------------------------------------------------------------- */
.empty {
  padding: var(--ep-space-24) 0;
  text-align: center;
  color: var(--ep-text-3);
  font-size: var(--ep-fs-caption);
}

/* ==========================================================================
   17. 消息中心（v3 新增）
   --------------------------------------------------------------------------
   P6 列表 = `.msg-list` > `.msg-card`；P7 详情 = `.msg-body`。
   列表行的结构照搬 `.vrow`（卡片 + 图标 + 主列 + chevron），
   但图标按 `data-type` 三态着色：
     apply  申请       → --ep-orange-700 on --ep-orange-100（"有人在敲门"）
     ok     已通过 / … → --ep-success
     no     已拒绝 / 被移出 → --ep-danger
   颜色只是辅助：标题文案本身已把结果说全（WCAG 1.4.1 不靠颜色单独传达信息）。
   ========================================================================== */
.msg-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 var(--ep-space-page) var(--ep-space-6);
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-4);
}
.msg-card {
  flex: none;
  padding: var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-white);
  box-shadow: var(--ep-shadow-base);
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
  cursor: pointer;
  transition: transform var(--ep-dur-fast) var(--ep-ease);
}
.msg-card:active { transform: scale(0.985); }
.msg-card__ico {
  flex: none;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ep-fill-2);
  color: var(--ep-text-3);
}
.msg-card[data-type="apply"] .msg-card__ico {
  background: var(--ep-orange-100);
  color: var(--ep-orange-700); /* on #FFECD5 = 4.6:1 ✅ */
}
.msg-card[data-type="ok"] .msg-card__ico { color: var(--ep-success); }
.msg-card[data-type="no"] .msg-card__ico { color: var(--ep-danger); }
.msg-card__main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.msg-card__top {
  display: flex;
  align-items: center;
  gap: var(--ep-space-2);
  min-width: 0;
}
.msg-card__title {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.3;
  color: var(--ep-text-1);
}
/* 未读点：红点与 aria-label 双通道（不靠颜色单独传达） */
.msg-card__dot {
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--ep-danger);
}
.msg-card__desc {
  font-size: var(--ep-fs-caption);
  line-height: 1.5;
  color: var(--ep-text-2);
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.msg-card__meta {
  display: flex;
  align-items: center;
  gap: var(--ep-space-3);
  font-size: var(--ep-fs-mini);
  line-height: 1.4;
  color: var(--ep-text-3);
}
.msg-card__state { color: var(--ep-text-2); }
.msg-card__arrow {
  flex: none;
  color: var(--ep-text-4);
}

/* --- P7 详情 / 审批 -------------------------------------------------------
   沿用 P4 评论区的「头部行 / 正文块」分离原则：
   `.msg-hero` 只有头像 + 昵称 + 时间（头部行），理由进 `.msg-block`（正文块），
   两者是**兄弟块**，正文绝不与昵称挤在同一行。 */
.msg-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--ep-space-6) var(--ep-space-page) var(--ep-space-10);
}
.msg-hero {
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
}
.msg-hero__ava { flex: none; cursor: pointer; }
.msg-hero__main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.msg-hero__name {
  font-size: var(--ep-fs-h2);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.24;
  color: var(--ep-text-1);
  cursor: pointer;
}
.msg-hero__sub {
  font-size: var(--ep-fs-caption);
  line-height: 1.45;
  color: var(--ep-text-3);
}
.msg-title {
  margin-top: var(--ep-space-6);
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.45;
  color: var(--ep-text-1);
}
/* 理由独立成块（规格 10：审批页信息 = 头像 + 昵称 + 理由 + 申请时间） */
.msg-block {
  margin-top: var(--ep-space-6);
  padding: var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-white);
}
.msg-block__label {
  font-size: var(--ep-fs-caption);
  color: var(--ep-text-3);
  margin-bottom: var(--ep-space-3);
}
.msg-block__text {
  font-size: var(--ep-fs-body-l);
  line-height: 1.55;
  color: var(--ep-text-1);
}
.msg-actions {
  margin-top: var(--ep-space-8);
  display: flex;
  gap: var(--ep-space-4);
}
.msg-actions .btn { flex: 1; }
/* 「拒绝」用的 .btn--secondary 默认描边是 rgba(0,0,0,.04)（项目规格），
   在审批页这种"二选一"的场景里几乎看不见，作为并列的次要动作太弱 —— 改用 --ep-line-2。
   与 P8 村民列表「移除」同一处理原则。 */
.msg-actions .btn--secondary {
  border-color: var(--ep-line-2);
}
.msg-note {
  margin-top: var(--ep-space-8);
  font-size: var(--ep-fs-caption);
  line-height: 1.5;
  color: var(--ep-text-3);
  text-align: center;
}

/* ==========================================================================
   18. 村民列表（v3 新增 · 规格 13：P3「管理」= 村民列表）
   -------------------------------------------------------------------------- */
.member-head {
  flex: none;
  padding: 0 var(--ep-space-page) var(--ep-space-6);
}
.member-head__name {
  font-size: var(--ep-fs-h1);
  font-weight: var(--ep-fw-semibold);
  line-height: 1.24;
  color: var(--ep-text-1);
}
.member-head__meta {
  margin-top: var(--ep-space-2);
  font-size: var(--ep-fs-caption);
  color: var(--ep-text-2);
}
.member-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 var(--ep-space-page) var(--ep-space-6);
  display: flex;
  flex-direction: column;
  gap: var(--ep-space-4);
}
.member-row {
  flex: none;
  min-height: 140px;
  padding: var(--ep-space-5) var(--ep-space-6);
  border-radius: var(--ep-radius-card);
  background: var(--ep-white);
  box-shadow: var(--ep-shadow-base);
  display: flex;
  align-items: center;
  gap: var(--ep-space-4);
}
.member-row__main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.member-row__name {
  display: flex;
  align-items: center;
  gap: var(--ep-space-2);
  font-size: var(--ep-fs-body-l);
  font-weight: var(--ep-fw-medium);
  line-height: 1.3;
  color: var(--ep-text-1);
}
/* 村长标 —— 与名牌上的「已加入」胶囊同一套浅橙底，但语义不同（身份 ≠ 加入状态），
   所以文案与位置都区分开，不共用 .cap（避免把两种编码混成一种）。 */
.member-row__tag {
  flex: none;
  padding: 0 var(--ep-space-2);
  border-radius: var(--ep-radius-chip);
  background: var(--ep-orange-100);
  color: var(--ep-orange-700);
  font-size: var(--ep-fs-mini);
  font-weight: var(--ep-fw-medium);
  line-height: 1.6;
}
.member-row__time {
  font-size: var(--ep-fs-mini);
  color: var(--ep-text-3);
}
/* 行内的「移除」：.btn--secondary 的默认描边是 rgba(0,0,0,.04)（项目规格），
   在白卡片上几乎看不见，作为唯一的破坏性动作太弱 —— 这里改用 --ep-line-2。 */
.member-row .btn--secondary {
  flex: none;
  border-color: var(--ep-line-2);
}
