/* ==========================================================================
   布鲁网 · 样式表
   浅色为主，跟随系统的深色偏好；全部尺寸基于 CSS 变量，便于统一调整。
   ========================================================================== */

:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f0f2f5;
  --border: #e3e6ea;
  --text: #1b1f24;
  --text-2: #5b636d;
  --text-3: #8a929c;
  --brand: #2f6fed;
  --brand-weak: #eaf1fe;
  --danger: #d93b3b;
  --ok: #1f9d55;
  --warn: #b7791f;
  --radius: 12px;
  --radius-sm: 8px;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px rgba(16, 24, 40, 0.05);
  --gap: 16px;
  --maxw: 1080px;

  /* 正文排版色板（2026-09-26）：富文本编辑器只给这 8 个颜色，不接受取色器。
     用变量而不是写死十六进制，是为了让同一条内容在深色模式下也读得清 ——
     否则作者选的深色字在深色背景上会整段消失。 */
  --rt-ink: #1b1f24;
  --rt-gray: #5b636d;
  --rt-red: #c0392b;
  --rt-orange: #a8650d;
  --rt-green: #1d7a45;
  --rt-blue: #2f6fed;
  --rt-purple: #6a4ec9;
  --rt-pink: #c2185b;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171b;
    --surface: #1c2026;
    --surface-2: #232830;
    --border: #2e343d;
    --text: #e8ebef;
    --text-2: #a8b1bc;
    --text-3: #7c848f;
    --brand: #6a9bff;
    --brand-weak: #1e2a44;
    --danger: #ef6b6b;
    --ok: #4ec27f;
    --warn: #e0b357;
    /* 同一套色板的深色取值：都挑了在 #1c2026 上对比度够的版本 */
    --rt-ink: #e8ebef;
    --rt-gray: #a8b1bc;
    --rt-red: #ff8a80;
    --rt-orange: #e8b464;
    --rt-green: #6bd39a;
    --rt-blue: #6a9bff;
    --rt-purple: #a99bff;
    --rt-pink: #ff8ab0;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 4px 16px rgba(0, 0, 0, 0.3);
  }
}

* {
  box-sizing: border-box;
}

/*
 * hidden 属性必须显式兜底。
 * 浏览器默认样式里 `[hidden] { display: none }` 属于 UA 层，**作者样式里的任何
 * display 都会盖掉它** —— 例如 `.btn { display: inline-flex }`，于是 `btn.hidden = true`
 * 只改了属性、按钮照样显示（首页/个人中心的「加载更多」一直不消失就是这个原因）。
 * 这里用 !important 统一压住，凡是 hidden 就该真的看不见。
 */
[hidden] {
  display: none !important;
}

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

body {
  background: var(--bg);
  color: var(--text);
  font: 15px/1.6 -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
    'Hiragino Sans GB', 'Microsoft YaHei', Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

a {
  color: inherit;
  text-decoration: none;
}

img {
  max-width: 100%;
  display: block;
}

/* ------------------------------------------------------------------ 布局 */

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.boot {
  margin: auto;
  text-align: center;
  color: var(--text-3);
}

.view {
  flex: 1;
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 20px 16px 96px;
}

/*
 * 顶栏（2026-09-24 改）：**内容限宽居中，背景照旧通栏**。
 *
 * 原来这里是 padding: 10px 16px，顶栏内容被拉到整屏两侧 —— 而正文是
 * --maxw: 1080px 居中的，屏幕越宽，顶栏两端离正文越远，
 * 表现为「导航在最左、功能在最右，鼠标要横跨整屏」。
 *
 * 用 padding 而不是给 .topbar 加 max-width + margin auto：
 *   后者会把**背景和底边框也一起收窄**，顶栏就变成一条浮在内容里的卡片，
 *   毛玻璃也不再铺满整条。padding 只收内容，背景仍是通栏的。
 *
 * ⚠️ 用 100% 而不是 100vw：100vw 含滚动条宽度，页面有滚动条时顶栏会整体
 *    左偏几个像素，而正文不会 —— 两边对不齐比不居中更难看。
 * ⚠️ max(16px, …) 保证窄屏（结果 ≤ 0 时）退回原来的 16px 边距，
 *    700px 断点里的 padding 简写会整条覆盖它，两条叠加才覆盖得住。
 */
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px max(16px, calc((100% - var(--maxw)) / 2));
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
}

.topbar__brand {
  display: flex;
  align-items: center;
  text-decoration: none;
  /* 原来的文字样式保留，作为 img 加载失败 / 无 CSS 时的兜底 */
  font-weight: 700;
  font-size: 17px;
  letter-spacing: 0.2px;
  white-space: nowrap;
}

.topbar__logo {
  height: 28px;
  width: auto;
  display: block;
}

.topbar__brand:hover .topbar__logo {
  opacity: 0.92;
}

.topbar__nav {
  display: flex;
  gap: 4px;
  margin-left: 8px;
  flex: 1;
}

.navlink {
  /* 未读红点是 absolute 叠在右上角的（见 .navlink .badge[data-notify-badge]），
     导航项自己得先成为定位参照 —— 否则红点会飞到顶栏外面去 */
  position: relative;
  padding: 7px 12px;
  border-radius: 999px;
  color: var(--text-2);
  font-size: 14px;
  transition: background 0.15s, color 0.15s;
}

.navlink:hover {
  background: var(--surface-2);
  color: var(--text);
}

.navlink.is-active {
  background: var(--brand-weak);
  color: var(--brand);
  font-weight: 600;
}

/*
 * 中频工具区（2026-09-24）：排行榜入口 + 语言切换。
 * 从 .topbar__right 挪出来单独成组，排在导航与搜索框之间 ——
 * 详见 app.js 里的注释（最右端现在只剩用户区）。
 */
.topbar__tools {
  display: flex;
  align-items: center;
  gap: 10px;
}

.topbar__right {
  display: flex;
  align-items: center;
  gap: 10px;
}

.langbtn {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-2);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 13px;
  cursor: pointer;
}

.langbtn:hover {
  color: var(--text);
  border-color: var(--text-3);
}

/* 移动端底部 tab */
.tabbar {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom);
}

.tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 0 6px;
  color: var(--text-3);
  font-size: 11px;
}

.tab.is-active {
  color: var(--brand);
}

.tab__icon {
  font-size: 18px;
  line-height: 1;
}

/* ------------------------------------------------------------------ 通用块 */

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
}

/*
 * 页头右侧的操作区（2026-09-22）。
 *
 * .page-head 是 space-between：直接往里放两个按钮，它们会被"均分"成
 * 一个居中、一个靠右，看着像两个互不相干的按钮。
 * 包一层才能作为一组整体贴右边 —— 内容管理页的「返回个人主页 + 发布」就是这样。
 */
.page-head__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  flex: 0 0 auto;
  /* 窄屏时两个按钮会挤不下，允许换行而不是把标题压变形 */
  flex-wrap: wrap;
}

/*
 * 页头左侧的「标题格」。
 *
 * ⚠️ 必须撑满（flex:1）：.page-head 是 flex，这一格默认收缩到**内容宽度**，
 * 于是里面的「返回所有帖子」用 margin-left:auto 推右时**没有剩余空间可推**，
 * 按钮会老老实实贴在话题后面（2026-09-24 实测：线上文件已更新但视觉没变，
 * 就是这个原因 —— 不是没部署，是没宽度）。
 * min-width:0 是配套的：不写的话长话题会把这一格顶宽、把右侧操作区挤出去。
 */
.page-head__main {
  flex: 1 1 auto;
  min-width: 0;
}

.page-title {
  font-size: 22px;
  margin: 0 0 4px;
  letter-spacing: 0.2px;
}

.page-sub {
  margin: 0;
  color: var(--text-2);
  font-size: 14px;
}

/*
 * 页头「标题行 + 右侧按钮」：标题靠左，按钮被推到**最右**。
 *
 * 两处共用（2026-09-30 起）：
 *   · 按话题浏览 #/search?tag=xxx —— 标题是「#话题」，按钮是「返回所有帖子」
 *   · 关键词搜索 #/search?q=xxx   —— 标题是「搜索」，按钮同样是「返回所有帖子」
 * 两者是同一件事：进到一个**被收窄**的结果集里，都需要一个回到全部帖子的出口。
 *
 * 位置：与标题**同一行**最右。不放进右上角的 .page-head__actions ——
 * 那是整页级操作区（返回个人主页 / 发布），而这个按钮属于当前这次筛选
 * （回到「没筛」的状态），放在标题这一行才说得通。
 *
 * ⚠️ 用 margin-left:auto 推右，不用 .headrow{justify-content:space-between}：
 *    窄屏换行后 space-between 会把孤零零的按钮甩到第二行最右、离标题更远，
 *    auto 同理 —— 但这里的换行只在话题接近 10 个汉字 + 极窄屏时才发生，可接受。
 * ⚠️ flex-wrap 不能删：话题最长 10 个汉字，挤不下时换行比把标题压变形好。
 * ⚠️ 间距由 .headrow 的 gap 给，别再给按钮加 margin-top。
 */
.headrow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* 行间距小一点（换行时才用得上），列间距是"话题与按钮之间的最小距离" */
  gap: 6px 12px;
  /* 标题原本的 4px 下边距（留给副标题）移到这一行上：
     留在 h1 上会让 align-items:center 多算 4px，按钮看着偏高 */
  margin-bottom: 4px;
}

/* 标题原本带 4px 下边距（留给副标题），放进 .headrow 后由 .headrow 统一管 */
.headrow .page-title {
  margin-bottom: 0;
}

.page-head__cta {
  margin: 0;
}

/* 推到这一行的最右端（用户要求：同行最右边，不是紧贴标题） */
.headrow .page-head__cta {
  margin-left: auto;
}

.section-title {
  font-size: 16px;
  margin: 26px 0 12px;
}

.muted {
  color: var(--text-2);
}

.small {
  font-size: 13px;
}

.dot {
  color: var(--text-3);
}

.link {
  color: var(--brand);
}

.linkbtn {
  border: 0;
  background: none;
  color: var(--brand);
  font-size: 13px;
  padding: 0;
  cursor: pointer;
}

.stat {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.icon {
  width: 14px;
  height: 14px;
}

/* ------------------------------------------------------------------ 按钮 */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 9px 16px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: filter 0.15s, background 0.15s, border-color 0.15s;
  white-space: nowrap;
}

.btn:disabled,
.btn.is-busy {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn--primary {
  background: var(--brand);
  color: #fff;
}

.btn--primary:hover:not(:disabled) {
  filter: brightness(1.06);
}

.btn--ghost {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

.btn--ghost:hover:not(:disabled) {
  border-color: var(--text-3);
}

.btn--danger {
  background: transparent;
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  color: var(--danger);
}

.btn--danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}

.btn--sm {
  padding: 6px 12px;
  font-size: 13px;
}

.btn--block {
  width: 100%;
}

/* ------------------------------------------------------------------ 表单 */

.form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 560px;
}

.form--inline {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
  max-width: none;
  margin-bottom: 20px;
}

.form__actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.grid-inline {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr auto;
  gap: 12px;
  align-items: end;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 0;
  padding: 0;
  margin: 0;
}

.field__label {
  font-size: 13px;
  color: var(--text-2);
  font-weight: 500;
}

.field__hint {
  font-size: 12px;
  color: var(--text-3);
}

/* 字数超限：签名实时计数超上限时标红，让用户当场看到而不是等保存失败 */
.field__hint.is-over {
  color: var(--danger);
}

.field__hint-inline {
  font-weight: 400;
  color: var(--text-3);
  margin-left: 4px;
}

/* 字数计数器超限态（发帖标题 30 字上限）。
   用等宽数字，否则输入时数字跳动会带着标签一起抖 */
.field__hint-inline.is-over {
  color: var(--danger);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* 邮箱已验证标记：红字，紧跟邮箱值之后。
   用 flex: 0 0 auto 保证在 input-row 里不被压缩换行 */
.tag-verified {
  flex: 0 0 auto;
  color: var(--danger);
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  align-self: center;
}

.req {
  color: var(--danger);
}

.input,
.select {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 9px 12px;
  font-size: 14px;
  color: var(--text);
  font-family: inherit;
}

.input:focus,
.select:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-weak);
}

.input--area {
  resize: vertical;
  min-height: 76px;
  line-height: 1.6;
}

.select-row,
.input-row {
  display: flex;
  gap: 10px;
}

.input-row .btn {
  flex-shrink: 0;
}

/* 手机号固定的 +86 区号前缀（只读，用户只填 11 位） */
.tel-prefix {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--muted);
  font-size: 14px;
  font-family: inherit;
  line-height: 1;
}

/* ------------------------------------------------------------------ 筛选 */

.filters {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 18px;
}

.filters .select {
  width: auto;
  min-width: 148px;
}

.seg {
  display: inline-flex;
  background: var(--surface-2);
  border-radius: 999px;
  padding: 3px;
  gap: 2px;
  /* 2026-09-19：排序从 2 个增加到 4 个（最新/最多浏览/最多点赞/最多收藏），
     窄屏下一个圆角条放不下，允许换行而不是溢出 */
  flex-wrap: wrap;
}

.seg--full {
  width: 100%;
}

.seg__btn {
  border: 0;
  background: transparent;
  color: var(--text-2);
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 13px;
  cursor: pointer;
  flex: 1;
  white-space: nowrap;
}

.seg__btn.is-active {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: var(--shadow);
}

/*
 * ⚠️ 这一套 .seg 是**排序（最新/最多浏览/最多点赞/最多收藏）和时间窗
 * （不限时间/近 7 天）共用**的（2026-09-24 起）。
 * 时间窗以前是 .pillbar + .pill--sm 的描边胶囊，选中态是实心品牌蓝，
 * 和排序摆在一起像两种控件，所以整组统一成了 .seg。
 * 改 .seg / .seg__btn 的样式 = 同时改这两组，别只顾一组。
 */

/* Tab 上的条数徽标（2026-09-20 新增）。
   服务端没返回计数时由 profile.js 保持 hidden —— 不显示好过显示一个不准的数。
   底色用半透明而不是 --surface-2：.seg 自己的背景就是 surface-2，
   同色会完全看不见；选中态走品牌色，和按钮的激活语义一致。 */
.seg__count {
  display: inline-block;
  margin-left: 6px;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  /* 兜底色：color-mix 在 iOS Safari 16.4 以下不被支持，整条声明会被丢掉，
     徽章就变成透明底。先给一个跟随主题的实色，支持 color-mix 的浏览器再覆盖。 */
  background: var(--surface-2);
  background: color-mix(in srgb, var(--text-3) 18%, transparent);
  color: var(--text-2);
  font-size: 11px;
  line-height: 17px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/*
 * ⚠️ 这条不能省：HTML 的 hidden 属性本身靠 UA 样式表的 `[hidden]{display:none}` 生效，
 * 而上面那条作者样式 `display: inline-block` **优先级高于 UA 样式**，会把 hidden 直接盖掉。
 * 结果是"没数出来"时露出一块 min-width:18px + 左右各 5px 的**空灰胶囊** ——
 * 一排四个就多出 110px 以上，正是窄屏上这一行被撑到换行的元凶；
 * 在 iOS 上则表现为"只有个小圆块、看不到数字"。
 */
.seg__count[hidden] {
  display: none;
}

.seg__btn.is-active .seg__count {
  background: var(--brand-weak);
  color: var(--brand);
}

/* 大类导航（首页探索 Tab）里的帖子数徽章（2026-09-30 新增）。
   与上面共用同一套 .seg__count，只在 .pill 上补两点：
   1) 激活态：.pill.is-active 是实色品牌底 + 白字，徽章得换成半透明白，
      沿用品牌色的半透明底在实色底上会糊成一片；
   2) 位数预留：帖数会长到 5 位（全站 / 电影），徽章是 inline-block，
      宽度随位数自动撑开，配合 tabular-nums（等宽数字）变化时按字符步进、不抖动。
      ⚠️ 这里**不给死宽度**：写死 5 位宽，2 位数时两边空一大截，
      且中英字号不同、预留值根本对不准。 */
.pill .seg__count {
  margin-left: 6px;
  text-align: center;
  vertical-align: 1px;
  line-height: 16px;
}

.pill.is-active .seg__count {
  background: rgba(255, 255, 255, 0.24);
  color: #fff;
}

/* ------------------------------------------------------- 按钮式筛选（类别 / 属性） */

.pillbar {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

.pill {
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-2);
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 13px;
  line-height: 1.4;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}

.pill:hover {
  border-color: var(--brand);
  color: var(--text);
}

.pill.is-active {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
  font-weight: 600;
}

.pill--sm {
  padding: 4px 10px;
  font-size: 12px;
}

/* 小胶囊（小类）里的计数徽章跟着小一号：11px 的数字摆在 12px 的文字旁边偏重，
   且小类一行可能有十几个，紧凑一点才不挤。 */
.pill--sm .seg__count {
  margin-left: 5px;
  min-width: 16px;
  padding: 0 4px;
  font-size: 10px;
  line-height: 15px;
}

.pill__caret {
  font-size: 10px;
  margin-left: 3px;
  opacity: 0.75;
}

.catnav {
  position: relative; /* 建一层栈上下文，保证浮层压在下方卡片之上 */
  z-index: 5;
  margin-bottom: 18px;
}

.catnav__row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

.catnav__item {
  position: relative;
  display: inline-flex;
}

/* 鼠标移上去浮出子类别（快捷直达） */
.catnav__drop {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  margin-top: 6px;
  padding: 8px;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 150px;
  max-width: 280px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
}

/*
 * 桥接区：补上按钮与浮层之间那 6px 的 margin-top 空隙。
 * 没有它，鼠标从按钮移向浮层时会先经过「谁都不属于」的死区，
 * :hover 立刻失效 → 浮层消失 → 子类别点不到。
 */
.catnav__drop::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -7px;
  height: 7px;
}

/* JS 维持的 hover 态：鼠标离开后延迟收起，抖动一下也不会消失 */
.catnav__item.is-hover .catnav__drop {
  display: flex;
}

.catnav__item:hover .catnav__drop,
.catnav__item:focus-within .catnav__drop {
  display: flex;
}

/* 已经展开子类别行时不再浮出，避免两处重复 */
.catnav__item.is-open .catnav__drop {
  display: none;
}

/* 触屏没有真正的 hover：浮层一律关闭，子类别走点击展开的那一行 */
@media (hover: none) {
  .catnav__item:hover .catnav__drop,
  .catnav__item:focus-within .catnav__drop,
  .catnav__item.is-hover .catnav__drop {
    display: none;
  }
}

/* 选中某个一级类别后展开的子类别行 */
.catnav__sub {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 10px;
  padding: 8px 0 8px 12px;
  border-left: 2px solid var(--brand-weak);
}

/* ----------------------------------------------------------------- 筛选区两行布局
   2026-09-22：首页筛选区改为两行 ——
   第一行：左侧「大类（含小类 hover 菜单）」，右侧「属性（帖子类型）」；
   第二行：排序（最新发布 / 最多浏览 / 最多点赞 / 最多收藏）+ 时间窗 + 话题。
   用 flex + space-between 让属性贴右，移动端自动换行。 */
.filterbar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  align-items: flex-start;
  justify-content: flex-start;
  margin-bottom: 14px;
}

.filterbar .catnav {
  flex: 0 1 auto;
  min-width: 0;
  margin-bottom: 0; /* 覆盖 .catnav 默认的 18px，避免与右侧属性错位 */
}

.filterbar .typefilter {
  flex: 0 0 auto;
  align-self: flex-start;
  justify-content: flex-start;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  align-items: center;
  margin-bottom: 18px;
}

/* ------------------------------------------------------------------ 卡片网格 */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: var(--gap);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: box-shadow 0.18s, transform 0.18s;
}

.card:hover {
  box-shadow: var(--shadow);
  transform: translateY(-2px);
}

.card__media {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  background: var(--surface-2);
  overflow: hidden;
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.card__count {
  position: absolute;
  right: 8px;
  bottom: 8px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 999px;
}

.card__body {
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  flex: 1;
}

.card__title {
  font-weight: 600;
  font-size: 15px;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__tags {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.card__meta,
.card__stats {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--text-3);
}

.card__author {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-2);
  min-width: 0;
}

.card__author span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card__stats {
  margin-top: auto;
  gap: 14px;
  /* 现在是四项（浏览/评论/点赞/收藏）。卡片在窄屏网格里可能只有 150px 宽，
     不换行会把最后一项挤出去。 */
  flex-wrap: wrap;
  row-gap: 6px;
}

/* 骨架屏 */
.card--skeleton {
  padding: 12px;
  gap: 10px;
}

.sk {
  background: var(--surface-2);
  border-radius: 6px;
  animation: pulse 1.4s ease-in-out infinite;
}

.sk--img {
  aspect-ratio: 4 / 3;
}

.sk--line {
  height: 12px;
}

.sk--short {
  width: 55%;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* ------------------------------------------------------------------ 徽章 */

.badge {
  display: inline-block;
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 999px;
  font-weight: 500;
  white-space: nowrap;
}

.badge--type {
  background: var(--brand-weak);
  color: var(--brand);
}

.badge--status.badge--pending {
  background: color-mix(in srgb, var(--warn) 18%, transparent);
  color: var(--warn);
}

.badge--status.badge--rejected {
  background: color-mix(in srgb, var(--danger) 15%, transparent);
  color: var(--danger);
}

.badge--muted {
  background: var(--surface-2);
  color: var(--text-3);
}

.chip {
  display: inline-block;
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
}

/*
 * 卡片上的类别链接（2026-09-22）：大类 → 小类，两级都可点。
 * hover 从灰底转成品牌色 —— 与可点的话题 chip 用同一套反馈语言，
 * 用户不用重新分辨「哪个能点」。
 */
a.chip--cat {
  text-decoration: none;
  transition: background 0.15s, color 0.15s;
}

a.chip--cat:hover {
  background: var(--brand-weak);
  color: var(--brand);
}

/*
 * 大类：内描边 + 稍重，把两级在视觉上分开。
 * 用 inset box-shadow 而不是 border —— border 会多出 1px 让大类比小类高一截，
 * 同一行里两个 chip 不等高很显眼。
 */
.chip--cat-root {
  box-shadow: inset 0 0 0 1px var(--border);
  font-weight: 600;
}

/*
 * 管理后台帖子行的「大类 › 小类 › 属性」路径（2026-09-22 新增）。
 *
 * 与卡片上的类别 chip 刻意不同：这里是**纯文本 + 装饰箭头**，不可点
 * （后台要点的是右侧的审核按钮，类别做成链接只会误触跳出筛选上下文）。
 * 所以没有 hover、没有指针光标 —— 让它一眼看上去就不像个按钮。
 */
.catpath {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  font-size: 11px;
  color: var(--text-2);
}

.catpath__seg {
  white-space: nowrap;
}

.catpath__sep {
  color: var(--text-3);
}

/* 可点的内容属性（跳 #/?type=xxx） */
a.badge--type {
  text-decoration: none;
  transition: filter 0.15s;
}

a.badge--type:hover {
  filter: brightness(0.94);
}

/* ------------------------------------------------------------------ 头像 */

.avatar {
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--surface-2);
}

.avatar--fallback {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-weak);
  color: var(--brand);
  font-weight: 600;
  font-size: 0.85em;
  border-radius: 50%;
  flex-shrink: 0;
}

/*
 * 可点击头像（2026-09-24，见 components.js 的 avatarLinkHtml）——点了进 TA 的主页。
 *
 * 头像本身很小，不给反馈用户不知道它能点；这里用**一圈描边**而不是改透明度：
 * 透明度在深浅两套主题下的观感不一致，描边则两套都一样明显。
 * border-radius: 50% 让描边贴着圆头像，而不是框出一个方块。
 */
.avatar-link {
  display: inline-flex;
  line-height: 0;
  border-radius: 50%;
  text-decoration: none;
  transition: box-shadow 0.15s;
}

.avatar-link:hover {
  box-shadow: 0 0 0 2px var(--brand);
}

.avatar-link:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ 状态块 */

.state {
  text-align: center;
  padding: 48px 16px;
  color: var(--text-2);
}

/**
 * 空态被塞进网格容器（帖子 `.grid`、头像墙 `.people-wall`）时，它只是**一个**网格项，
 * 会被切进窄列里 —— 于是「还没有关注任何人……」这句话被挤成 5 行，和别的区块长得不一样。
 * 空态不是卡片，让它横跨整行即可，三个区块的空态自然就长得一模一样。
 */
.grid > .state,
.people-wall > .state {
  grid-column: 1 / -1;
}

.state__title {
  font-size: 15px;
  margin: 0 0 6px;
  color: var(--text);
}

.state__hint {
  margin: 0 0 14px;
  font-size: 13px;
  color: var(--text-3);
}

.state__cta {
  margin-top: 12px;
}

.spinner {
  width: 22px;
  height: 22px;
  margin: 0 auto 10px;
  border: 2px solid var(--border);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.notice {
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  margin: 0 0 14px;
}

.notice--info {
  background: var(--brand-weak);
  color: var(--brand);
}

.notice--warn {
  background: color-mix(in srgb, var(--warn) 15%, transparent);
  color: var(--warn);
}

.list-foot {
  text-align: center;
  padding: 22px 0;
}

.list-end {
  color: var(--text-3);
  font-size: 13px;
  margin: 0;
}

/* 个人主页「未关注只看到 N 条」的提示 —— 不是错误，用中性色而不是告警色，
   免得看起来像"加载失败" */
.list-foot__note {
  color: var(--text-2);
  font-size: 13px;
  margin: 0;
  background: var(--surface-2);
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 10px 14px;
  display: inline-block;
}

/*
 * 无限滚动哨兵（2026-10-02）。
 * 放在 .list-foot 里、网格**外**：网格会被增量 append 改写，但不会整块替换，
 * 哨兵待在网格外面才不会被 observer 断掉。
 * 高度 1px：不占版面，又要能被 IntersectionObserver 命中（0 高度的元素不产生交叉）。
 */
.scroll-sentinel {
  display: block;
  width: 100%;
  height: 1px;
}

/*
 * ⚠️ 光写 display:block 会**盖掉** [hidden] 的 display:none（作者样式优先于 UA 样式），
 *    于是 `sentinel.hidden = true`（到底了要收哨兵）根本不生效 —— 哨兵一直挂在视口里。
 *    虽然回调里还有 done 守卫兜着、不会真去请求，但这条必须补，别靠兜底。
 */
.scroll-sentinel[hidden] {
  display: none;
}

/*
 * 回到顶部浮钮（2026-10-02）。
 * 无限滚动之后列表长度无上限，而列表页的排序/筛选栏**不是吸顶**的 ——
 * 滚深了就改不了筛选也回不了头，所以这个钮是配套，不是装饰。
 *
 * z-index 60：在手机底栏 tabbar(40) 之上，又在 toast(100)/modal(110)/
 * lightbox(200) 之下 —— 弹窗打开时 backdrop 自己会盖住它，不用额外处理。
 */
.back-to-top {
  position: fixed;
  right: 16px;
  bottom: 24px;
  z-index: 60;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  color: var(--text-2);
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
  /* 默认收起：不显示也不吃点击（pointer-events 挡住，避免透明时误触） */
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  pointer-events: auto;
}

.back-to-top:hover,
.back-to-top:focus-visible {
  color: var(--text);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
}

.back-to-top__arrow {
  font-size: 18px;
  line-height: 1;
}

@media (prefers-reduced-motion: reduce) {
  .back-to-top {
    transition: none;
  }
}

/* 手机端：底部有固定 tabbar（高约 56px，z-index 40），浮钮要**浮在它上方**，
   不能压住最右那个「＋发帖」；同时给 iPhone 底部安全区留位置 */
@media (max-width: 700px) {
  .back-to-top {
    bottom: calc(env(safe-area-inset-bottom, 0px) + 64px);
  }
}

/* ------------------------------------------------------------------ 提示条 */

.toast-host {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  pointer-events: none;
  width: min(92vw, 460px);
}

.toast {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--text-3);
  box-shadow: var(--shadow);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 13.5px;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.2s, transform 0.2s;
  max-width: 100%;
}

.toast.is-in {
  opacity: 1;
  transform: translateY(0);
}

.toast--ok { border-left-color: var(--ok); }
.toast--err { border-left-color: var(--danger); }
.toast--info { border-left-color: var(--brand); }

/* ------------------------------------------------------------------ 弹窗 */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(12, 16, 22, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 110;
  padding: 20px;
}

.modal {
  background: var(--surface);
  border-radius: var(--radius);
  padding: 20px;
  width: min(92vw, 380px);
  box-shadow: var(--shadow);
}

/*
 * 编辑帖子那类**带表单**的弹窗（2026-09-20 新增）。
 * 默认 380px 塞不下标题+正文+两个下拉，所以放宽到 520px，
 * 并给自身滚动：手机上键盘弹起时高度被压缩，内容必须能滚而不是被裁掉。
 */
.modal--wide {
  width: min(94vw, 520px);
  max-height: 88vh;
  overflow-y: auto;
}

.modal--wide .field {
  margin-bottom: 12px;
}

.modal__text {
  margin: 0 0 18px;
  font-size: 14.5px;
}

/* 弹窗里的说明文字要紧贴标题，别留 18px 大空隙 */
.modal--wide .modal__text {
  margin-bottom: 6px;
}

.modal__actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

/* ------------------------------------------------------------------ 登录页 */

.auth-wrap {
  display: flex;
  justify-content: center;
  padding-top: 3vh;
}

.auth-card {
  width: min(100%, 420px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px 22px;
  box-shadow: var(--shadow);
}

.auth-card .page-title {
  text-align: center;
}

.auth-card .page-sub {
  text-align: center;
  margin-bottom: 20px;
}

.auth-card .form {
  margin-top: 18px;
  max-width: none;
}

/* ------------------------------------------------------------------ 发布页 */

.opt {
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.opt.is-active {
  border-color: var(--brand);
  background: var(--brand-weak);
}

.opt__title {
  font-size: 14px;
  font-weight: 600;
}

.opt__desc {
  font-size: 12px;
  color: var(--text-3);
}

.opts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
}

.thumbs {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: 10px;
}

.thumb {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /*
    安卓长按 <img> 会弹「下载图片 / 搜索图片」菜单并派发 pointercancel，
    iOS 则弹「存储图像」—— 两者都会把拖拽打断。
    pointer-events: none 让长按**命中不到 img**，图片菜单就没有触发目标了
    （拖拽是绑在 li 上的委托，不依赖 img 接收事件，所以不受影响）。
  */
  pointer-events: none;
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}

/* 图片拖拽排序（2026-09-27）。
   排序期间只改 transform、不动 DOM —— 所以这里只需要两件事：
   让位元素有补间，被拖的那张没有（否则跟手会拖泥带水）。 */
/* 长按不该选中文字 / 弹「选择文本」菜单（否则拖之前先弹一层）。
   注意这里挂在 .thumbs 而不是 .thumbs--sortable：只有一张图时也会长按。 */
.thumbs {
  -webkit-user-select: none;
  user-select: none;
}

.thumbs--sortable .thumb {
  cursor: grab;
  /* 提前提升为合成层：移动端每帧改 transform 时少一次重绘 */
  will-change: transform;
}

.thumbs.is-sorting .thumb {
  /* 0.14s：再长就跟不上手指，再短则看不出"让位" */
  transition: transform 0.14s ease;
  /*
    拖拽期间把 touch-action 关掉：告诉浏览器「这个手势我自己处理」，
    别再按自己的规则去判定长按/滚动/缩放 —— 安卓上这能减少
    pointercancel（也就是拖到一半被系统接管的那种断手感）。
    只在 .is-sorting 期间生效：平时手指在图片区滑动仍要能滚页面。
  */
  touch-action: none;
}

.thumb.is-dragging {
  z-index: 2; /* 浮在其它格子上方（.thumbs 是 grid，父级没有层叠上下文干扰） */
  cursor: grabbing;
  transition: none; /* 被拖的那张要跟手，不能有补间 */
  opacity: 0.92;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
}

@media (prefers-reduced-motion: reduce) {
  .thumbs.is-sorting .thumb {
    transition: none;
  }
}

/* 还在上传的那张（2026-09-27）：本地预览 + 转圈。
   它不能拖（没有真实 key），但**其它已传好的图照样能拖** —— 所以要让用户
   一眼分清「哪张还在传」，否则拖不动会以为功能坏了。 */
.thumb.is-pending {
  cursor: default;
}

.thumb.is-pending img {
  opacity: 0.55;
}

.thumb__spin {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.85);
  border-top-color: transparent;
  animation: thumb-spin 0.8s linear infinite;
}

@keyframes thumb-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .thumb__spin {
    animation-duration: 2s;
  }
}

.thumb__x {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 0;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}

.picker {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 18px;
  border: 1.5px dashed var(--border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  font-size: 14px;
}

.picker:hover {
  border-color: var(--brand);
  color: var(--brand);
}

/*
 * 按下反馈（2026-09-28）。
 *
 * 安卓上从点击 input 到系统相册弹出有 0.5~3s 的间隔（要起 Intent、扫媒体库），
 * 期间页面毫无变化，用户会以为「没点到」而反复猛戳。给个按下态，至少让手指
 * 立刻看到「收到了」。
 */
.picker:active {
  border-color: var(--brand);
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 8%, transparent);
}

/*
 * ⚠️ [hidden] 必须显式写：.picker 自带 display:flex，会**盖掉** hidden 属性
 * 默认的 display:none，结果就是「加了 hidden 却还看得见」。
 * 和 .share__panel[hidden] 是同一个坑。
 */
.picker[hidden] {
  display: none;
}

/* 「+ 添加图片」现在是 div.picker，file input 透明铺满它（见 compose.js 的注释）：
   手指点下去命中的是 input 本身，而不是「按钮 → 脚本 click()」。
   脚本触发的 click 在部分国产浏览器上会丢掉 multiple（vivo 自带浏览器实测），
   真实点击是最接近原生的一条路径。
   ⚠️ 必须 position: relative，否则 input 的 absolute 会相对更外层定位。 */
.picker {
  position: relative;
  overflow: hidden; /* input 探出的部分裁掉，避免撑出滚动条 */
}

.picker__file {
  position: absolute;
  inset: 0; /* 铺满整个按钮，不留空隙 */
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  /* 不能 display:none / visibility:hidden：部分安卓内核对未渲染的 file input
     响应极慢（表现为「点了没反应」）。透明但仍参与渲染。 */
  opacity: 0;
  cursor: pointer;
  /* 键盘可达：Tab 能聚焦到它（视觉上是那个按钮），Enter 即打开选择器 */
  font-size: 0;
}

/* 键盘用户聚焦时把焦点画在按钮上（input 本身是透明的，看不见） */
.picker:focus-within {
  border-color: var(--brand);
  color: var(--brand);
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.picker__plus {
  font-size: 20px;
  line-height: 1;
}

/* ------------------------------------------------------------------ 详情页 */

.post {
  max-width: 760px;
  margin: 0 auto;
}

.post__tags {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.post__title {
  font-size: 25px;
  line-height: 1.35;
  margin: 0 0 12px;
}

.post__meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-3);
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.post__author {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  font-weight: 500;
}

/*
 * 详情页图片墙。
 *
 * 默认三列，只把 1 张（整幅）和 2/4 张（两列更好看）单独挑出来覆盖 ——
 * 2026-09-23 之前是"3/5/6/7/8/9 各写一条三列"，超过 9 张就没有规则可命中，
 * 会退回单列把 18 张图竖着铺满整页（特殊等级会员现在可以放 18 张）。
 * 改成"默认三列 + 特例覆盖"后，以后再放宽上限也不用动这里。
 */
.post__gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-bottom: 20px;
}

.post__gallery[data-count='1'] {
  grid-template-columns: 1fr;
}

.post__gallery[data-count='2'],
.post__gallery[data-count='4'] {
  grid-template-columns: 1fr 1fr;
}

.shot {
  display: block;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  aspect-ratio: 1;
}

.shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.post__gallery[data-count='1'] .shot {
  aspect-ratio: 16 / 10;
}

/* ──────────────────────────────────────────────────────────────────────
   页内大图查看（2026-09-23，配合 src/lightbox.js）

   背景刻意用**固定深色**、不跟主题的 --surface 走：看图是"看内容"的场景，
   深色把注意力压在图上，也避免浅色主题下四周一片白、看不出图片边界。

   适配屏宽的关键在 .lb__stage：它是 grid 里那一行 1fr，靠 min-height: 0
   才能真正被压缩 —— 缺了这句，1fr 行会被图片撑破，整层跟着溢出屏幕。
   ────────────────────────────────────────────────────────────────────── */

/*
 * 打开期间锁住页面滚动：否则滚轮被背后的页面吃掉，看着像"点了没反应"。
 *
 * 背景色一起换成灯箱的深色：手机横屏时浏览器会在安全区（刘海 / 圆角那两条）
 * 留出边距，那块露的是**页面背景**而不是灯箱 —— 不换就是左右两条白边，
 * 看着像"图没撑满"。换成同色之后，黑幕一直铺到屏幕边缘。
 */
.lb-open {
  overflow: hidden;
  background: #0c0e11;
}

.lb {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 200;
  display: grid;
  /*
   * ⚠️ 两处都不能写成 auto / 1fr 的裸写法：
   *   minmax(0, 1fr) 而不是 1fr —— 行的最小尺寸默认是 auto，图片原始高度
   *     （2736px 这种）会把这一行顶高，整层溢出视口，下面看不到且滚不动；
   *   columns 必须显式钉成 minmax(0, 1fr) —— 隐式列是 auto，列宽会去问
   *     图片的 max-content，而图片的 max-width:100% 又要问列多宽，又是一个
   *     互为前提，结果是按原图宽度（3648px）撑开，右边被推到屏幕外。
   * 钉死之后，图片区就等于"视口减去顶栏"，横屏竖屏同一套算法。
   */
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  /*
   * index.html 是 viewport-fit=cover（内容延伸到屏幕边缘），所以左右要避开安全区：
   * 手机横过来时刘海那一侧的图会被压住 —— 黑幕照旧铺满（背景在 padding 之外），
   * 只有内容往里让。上下不动：那两条是浏览器自己的地址栏，本来就轮不到网页。
   */
  padding-right: env(safe-area-inset-right, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  background: rgba(12, 14, 17, 0.94);
  color: #fff;
  /* 整层淡入：黑幕渐显比"啪"地盖一层黑柔和（.is-open 由 JS 在下一帧加上） */
  opacity: 0;
  transition: opacity 0.18s ease;
}

.lb.is-open {
  opacity: 1;
}

/*
 * 真·全屏（Fullscreen API）时背后已经没有页面了，
 * 上面 0.94 的半透明会透出底下的白 —— 补成不透明。
 */
.lb:fullscreen,
.lb:-webkit-full-screen {
  background: #0c0e11;
}

.lb__bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px 12px;
}

.lb__count {
  margin-right: auto;
  font-size: 13px;
  /* 等宽数字：翻页时 "9 / 18" → "10 / 18" 不会左右抖 */
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.75);
}

.lb__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  cursor: pointer;
  text-decoration: none;
  /* 与 .lb__nav 同理：移动端点按钮的系统高亮块在深色底上特别扎眼 */
  -webkit-tap-highlight-color: transparent;
}

.lb__btn:hover {
  background: rgba(255, 255, 255, 0.22);
}

.lb__btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.lb__btn .icon {
  width: 18px;
  height: 18px;
}

.lb__stage {
  position: relative;
  /*
   * 两张图叠在**同一个格子**里，而不是用 absolute 覆盖：
   * 覆盖层要 inset:0 就得先知道外框尺寸，而外框尺寸恰恰由图片自己撑出来 ——
   * 互为前提。grid 同格 + place-items:center 让两层各自算出尺寸又共享居中区域，
   * 同比例的两张图因此天然重合，淡入时不会有位移。
   */
  display: grid;
  grid-template-areas: 'pic';
  /*
   * ⚠️ 这两行不能省 —— 省了就退化成隐式行列 auto，格子尺寸由图片内容决定，
   * 而图片的 max-height:100% 又要反过来问格子多高，互为前提 ⇒ 百分比失效，
   * 图片按原始像素（3648×2736 这种）铺开，右边和下面被 overflow 裁掉且滚不动。
   * minmax(0, 1fr) 把格子钉死成 stage 的内容区，`contain` 才有依据 ——
   * 窗口横的竖的都按同一套算，所以不必为竖向比例另写规则。
   */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  /* fit 模式下整张图进屏，不滚动 */
  overflow: hidden;
  min-height: 0;
  padding: 0 12px 12px;
}

/*
 * 1:1（原始尺寸）：图比屏幕大，看全靠**滚动条**。
 *   ⚠️ 这里刻意不做"按住拖动"：拖动会和"点空白关闭""移动端滑动翻页"抢手势，
 *   而滚动条是浏览器原生行为，触控板 / 触屏 / 鼠标滚轮都一样能用。
 *   所以光标也别用 grab —— 那会暗示用户"可以拖"，拖不动就是骗人。
 */
.lb--actual .lb__stage {
  overflow: auto;
  /* 格子交回内容决定：原图多大，格子就多大，再由容器滚动 —— 反之上面的 contain 会把它压回屏幕 */
  grid-template-rows: auto;
  grid-template-columns: auto;
  /*
   * 对齐默认左上：**居中的内容一旦溢出，左/上那半截会落在滚动区之外，永远滚不到**
   * （flex 与 grid 通病）。所以"居中"由 JS 分轴处理（centerScroll）：
   * 放得下的轴切成 center，放不下的轴保持 start 并把滚动位置推到中心。
   */
  place-items: start;
}

/*
 * 深色底上的滚动条既要看得见、也要抓得住：
 *   width 14px 是能舒服按住的宽度；thumb 的透明边框只留 2px ——
 *   之前留 3px（12-3×2=6px 可见）看着就是"细细一条"，用户以为没滚动条。
 */
.lb--actual .lb__stage::-webkit-scrollbar {
  width: 14px;
  height: 14px;
}

.lb--actual .lb__stage::-webkit-scrollbar-track {
  background: rgba(255, 255, 255, 0.1);
}

.lb--actual .lb__stage::-webkit-scrollbar-thumb {
  /* 透明边框 + background-clip：滑块比轨道窄一点，不至于整条压在图上 */
  border: 2px solid transparent;
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.55);
  background-clip: content-box;
}

.lb--actual .lb__stage::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.8);
  background-clip: content-box;
}

.lb--actual .lb__stage::-webkit-scrollbar-corner {
  background: transparent;
}

.lb__img {
  grid-area: pic;
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /*
   * 默认透明：图片从 0 撑到实际尺寸的那一帧发生在这里看不见的时候。
   * 加载完成（缩略图 / 原图各自的 onload）才点亮，观感上是"浮现"不是"跳出来"。
   */
  opacity: 0;
  transition: opacity 0.18s ease;
}

.lb__img.is-ready {
  opacity: 1;
}

/* 原图层：压在缩略图之上，尺寸一致、像素更清晰 —— 加载完淡入，看着是"变清晰" */
.lb__img--full.is-on {
  opacity: 1;
}

.lb--actual .lb__img {
  max-width: none;
  max-height: none;
  width: auto;
  height: auto;
}

/*
 * ⚠️ 这里**刻意不给灯箱里的图做自动旋转**（2026-09-23 定）。
 *
 * 曾经做过"窄屏 + 竖屏时把横图转 90°，用满屏幕高度"，用户否掉了：
 * 打开时必须保持照片的**原始朝向**；手机转成横屏时按新方向重新撑满，
 * 转回竖屏又恢复原样 —— 转 90° 会让"我拍的明明是横的"这件事变得不确定。
 *
 * 所以 fit 模式的适配完全交给下面 .lb__img 的 max-width/max-height + object-fit：
 * 都是百分比，屏幕横竖怎么变、图片区多大多高，浏览器自己重算，
 * 横屏时横图自然撑满，竖屏时回到上下留白的原始形态 —— 不需要任何 JS 参与。
 * （1:1 模式的居中是 JS 算的，转屏后由 lightbox.js 的 orientationchange 重摆。）
 */

/* 用户在系统里要求减少动效时，直接给结果，不做淡入 */
@media (prefers-reduced-motion: reduce) {
  .lb,
  .lb__img {
    transition: none;
  }
}

.lb__loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.7);
}

.lb__loading[hidden] {
  display: none;
}

/* 上下贴着图片区（跳过顶栏），垂直居中于图 */
.lb__nav {
  position: absolute;
  top: 56px;
  bottom: 12px;
  /*
   * ⚠️ z-index 不能省：DOM 里「上一张」排在图片区**之前**，同为定位元素时
   * 后面的盖在上面 —— 不加这个，左箭头会被图片区整块压住，表现是：
   * 悬停没有手型光标（事件落在图片区上），点一下命中"点空白关闭"直接退出灯箱。
   * 「下一张」因为在图片区之后，看起来正常 —— 所以这个 bug 只在一侧出现。
   */
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: rgba(255, 255, 255, 0.85);
  cursor: pointer;
  /*
   * ⚠️ 整条按钮**不接收鼠标事件**，只有里面的图标收（见 .lb__nav .icon）。
   *
   * 这条按钮从顶栏一直竖到底（top:56 bottom:12），正好压在图片区的右边缘，
   * 而 1:1 的垂直滚动条就画在那条边上 —— 全区域可点时，鼠标落在箭头上
   * （手型光标），滚动条看得见却**根本拖不动**；水平滚动条的右下角同理。
   * 把命中区收成图标本身（垂直居中、横向距边 8+(44-24)/2 = 18px 起），
   * 滚动条那条（14px）就整个空出来了 —— 箭头照旧贴边，不必往里挪。
   *
   * 事件委托不受影响：点在 svg 上照样冒泡到 #lb-next（closest 仍能找到按钮）。
   */
  pointer-events: none;
  /*
   * 移动端点按钮会出现系统自带的**高亮块**：这条按钮是竖着的，
   * 高亮出来就是一条竖向蓝条 —— 用透明把它去掉（键盘可达性由下面的 focus-visible 保证）。
   */
  -webkit-tap-highlight-color: transparent;
}

.lb__nav:hover {
  color: #fff;
}

/* 键盘 Tab 到按钮时给可见焦点 —— 上面关掉了指针事件，焦点样式更要留着 */
.lb__nav:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -4px;
  border-radius: 8px;
}

.lb__nav--prev {
  left: 8px;
}

.lb__nav--next {
  right: 8px;
}

/* 真正能点的就是这块图标：贴边留出的那 14px 留给滚动条 */
.lb__nav .icon {
  width: 24px;
  height: 24px;
  pointer-events: auto;
}

/* 手机上没有 hover，按钮再大一点更好按；顶栏也收紧些给图让位 */
@media (max-width: 640px) {
  .lb__bar {
    gap: 6px;
    padding: 8px;
  }

  .lb__btn {
    width: 32px;
    height: 32px;
  }

  /*
   * 手机上图片区不留白：屏上就这一张图，边距只会让它显得比实际小
   * （用户要的是"转过来就撑满屏"）。留白交给上面的顶栏。
   */
  .lb__stage {
    padding: 0;
  }

  /* 窄屏箭头收到 36px（滚动条避让靠 pointer-events，不靠留白 —— 见 .lb__nav） */
  .lb__nav {
    top: 48px;
    width: 36px;
    /* 手机上滚动条是浮层（不占位），看图靠滑动，命中区可以放回整条，更好按 */
    pointer-events: auto;
  }
}

/*
 * 手机横屏（矮屏）：顶栏改成**浮在图上**，不再占掉一整行高度。
 *
 * 横屏可视高度本来就小（手机横过来约 340px），再被浏览器地址栏、顶栏各吃掉一条，
 * 图片只剩中间一小块 —— 用户反馈"转横屏图反而变小了，不像撑满"。
 * 所以这里把顶栏那一行压成 0（grid-template-rows 的第一轨），顶栏改 absolute 浮在
 * 图片上方、带渐变底色压住画面；图片区因此**用满整个高度**。
 * 按钮同时收小，别在矮屏上喧宾夺主。
 */
@media (orientation: landscape) and (max-height: 520px) {
  /*
   * ⚠️ 必须改成**单轨**，不能写成 `0 minmax(0, 1fr)`。
   * 顶栏一旦 absolute 就脱离了 grid 的自动放置，图片区会变成第一个（也是唯一一个）
   * 在流内的 item —— 两轨写法会把它放进**第 1 轨**，而第 1 轨是给顶栏留的 0 高，
   * 于是图片区整块 0 高，图直接消失（2026-09-23 线上就是这个现象）。
   * 单轨之后它顺理成章落到唯一那一行，用满整个高度。
   */
  .lb {
    grid-template-rows: minmax(0, 1fr);
  }

  /* 矮屏上每一像素都金贵，图片区不留白 */
  .lb__stage {
    padding: 0;
  }

  .lb__bar {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    /*
     * ⚠️ z-index 不能省：图片区在 DOM 里排在顶栏**之后**，同为定位元素时后者盖住前者
     * （和 .lb__nav 吃过的一样的亏）—— 不加就是按钮看得见却点不到。
     */
    z-index: 3;
    padding: 6px 8px;
    /* 半透明渐变：按钮压在图上时依旧看得清，又不至于糊掉画面 */
    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  }

  .lb__btn {
    width: 30px;
    height: 30px;
  }

  .lb__btn .icon {
    width: 16px;
    height: 16px;
  }

  /* 顶栏浮起来之后，翻页箭头跟着从最顶上开始，别再给顶栏留那 48/56px */
  .lb__nav {
    top: 8px;
  }
}

.post__content {
  font-size: 16px;
  line-height: 1.8;
  /*
   * 富文本化之后换行由 <br> / <p> 负责，原始换行符不再参与排版（2026-09-26）。
   * 之前是 pre-wrap + 渲染时把 \n 转成 <br> 的"双保险"，富文本下两套换行会叠成空行，
   * 所以这里回到 normal，只留渲染层那一种。
   */
  white-space: normal;
  word-break: break-word;
  margin-bottom: 26px;
}

.post__content p {
  margin: 0 0 10px;
}

.post__content p:last-child {
  margin-bottom: 0;
}

/*
 * 正文下方的话题行（2026-09-20：话题从标题上方的徽章排挪到了正文下面）。
 * 只有 chip，没有「话题：」label（2026-09-20 晚按用户要求去掉）。
 */
.post__topics {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  /* -14px 抵掉 .post__content 的 26px 下边距，让话题贴着正文又不黏住 */
  margin: -14px 0 20px;
}

/* 只有 chip，没有 label —— chip 自带 # 前缀已经说明身份（见 post.js 的注释） */

/* 正文下方那行「最新编辑时间」 */
.post__edited {
  margin: 0 0 22px;
  color: var(--text-3);
  font-size: 13px;
}

/* 没有话题行时（老帖一个话题都没有），编辑时间照旧紧贴正文下方 */
.post__content + .post__edited {
  margin-top: -14px;
}

.post__actions {
  display: flex;
  gap: 10px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 22px;
}

/* 点赞 / 收藏操作条（2026-09-19 新增） */
.reaction-bar {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 22px;
}

/*
 * 选中态用品牌色而非红色：这是「已操作」的语义，不是警告。
 * 红色在本项目里已被 --danger 占用（删除类操作），混用会让用户误判。
 */
.reaction.is-active {
  border-color: color-mix(in srgb, var(--brand) 55%, transparent);
  color: var(--brand);
  background: color-mix(in srgb, var(--brand) 10%, transparent);
}

/* 分享按钮与面板（2026-09-21 新增）。按钮本身复用 .reaction，与点赞收藏同款。 */
.share {
  position: relative;
}

.share__panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  left: 0;
  min-width: 168px;
  max-width: calc(100vw - 32px);
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
}

/*
 * 这条不能省：上面给了 display:flex，而 flex 会盖掉 hidden 属性自带的 display:none，
 * 结果就是面板永远收不起来。凡是用 hidden 控制显隐又自带 display 的，都要补一刀。
 */
.share__panel[hidden] {
  display: none;
}

/* 桥接触发器与面板之间的 6px 空隙，与 .usermenu__list 同一套做法 */
.share__panel::before {
  content: '';
  position: absolute;
  top: -6px;
  left: 0;
  right: 0;
  height: 6px;
}

.share__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.share__item:hover {
  background: var(--surface-2);
}

.share__item .icon {
  flex: none;
  color: var(--text-2);
}

/*
 * 窄屏改成向左展开：分享按钮排在点赞、收藏之后，靠左定位会让面板顶出右边界，
 * 进而把整页撑出横向滚动条。
 */
@media (max-width: 700px) {
  .share__panel {
    left: auto;
    right: 0;
  }
}

/*
 * 评论点赞：未点赞走中性色，点赞过（is-active）才上品牌色。
 * 之前直接继承 .linkbtn 的 --brand，于是「没点过的心形也是蓝的」，
 * 跟真正的已点赞态完全分不出来 —— 颜色必须是「是否已操作」的唯一信号。
 */
.cmt-like {
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  transition: color 0.15s ease;
}

.cmt-like:hover {
  color: var(--text-1);
}

.cmt-like.is-active {
  color: var(--brand);
  font-weight: 600;
  opacity: 1;
}

/* 已点赞时 hover 不要退回灰色，否则鼠标一放上去就像取消了 */
.cmt-like.is-active:hover {
  color: var(--brand);
}

/* 评论区 */

.cmt-section {
  margin-top: 8px;
}

.cmt-title {
  font-size: 16px;
  margin: 0 0 14px;
}

.cmt-count {
  font-size: 13px;
  color: var(--text-3);
  font-weight: 400;
  margin-left: 8px;
}

.cmt-list {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.cmt {
  display: flex;
  gap: 10px;
  align-items: flex-start;
}

.cmt--reply {
  margin-left: 40px;
  padding-left: 12px;
  border-left: 2px solid var(--border);
}

/*
 * 评论头像（2026-09-25 起可点，跳 TA 主页）。
 *
 * ⚠️ .cmt 是 flex 容器，头像是**固定宽度**的子项 —— 不锁 flex 的话，
 *    长评论/长用户名会把头像压扁成椭圆。链接版（.avatar-link）和
 *    无 id 时的降级版（裸 .avatar）都得锁，两种情况都会出现。
 */
.cmt .avatar-link,
.cmt > .avatar {
  flex: 0 0 auto;
}

.cmt__body {
  min-width: 0;
  flex: 1;
}

.cmt__head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 2px;
}

.cmt__name {
  font-weight: 600;
  font-size: 13.5px;
}

.cmt__time {
  font-size: 12px;
  color: var(--text-3);
}

.cmt__text {
  margin: 0 0 4px;
  font-size: 14.5px;
  line-height: 1.65;
  word-break: break-word;
  white-space: pre-wrap;
}

/* 评论下方的操作行：回复（所有人）/ 删除（仅管理员） */
.cmt__tools {
  display: flex;
  align-items: center;
  gap: 14px;
}

/*
 * 操作行里的「回复」也不要默认品牌色（2026-09-20）。
 * 同排的点赞已经按「未点过=灰、点过=蓝」走，回复若还是蓝色，
 * 视觉上会跟「已点赞」抢注意力，整排看起来都像被点过。
 * 排除 .cmt-like（它有自己的中性/品牌两套色）与 .linkbtn--danger（删除，hover 有专用红色）。
 */
.cmt__tools .linkbtn:not(.cmt-like):not(.linkbtn--danger) {
  color: var(--text-3);
  transition: color 0.15s ease;
}

.cmt__tools .linkbtn:not(.cmt-like):not(.linkbtn--danger):hover {
  color: var(--text-1);
}

.linkbtn--danger {
  color: var(--text-3);
}

.linkbtn--danger:hover {
  color: #e5484d;
}

.cmt-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-start;
}

.reply-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--text-2);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 6px 12px;
  width: 100%;
}

/* ------------------------------------------------------------------ 个人主页 */

.profile-head {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  margin-bottom: 8px;
}

.profile-head__info {
  flex: 1;
  min-width: 0;
}

/* 用户名 + 签名同一行：签名紧跟在用户名右边，窄屏时自动换行 */
.profile-head__nameline {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}

.profile-head__name {
  margin: 0;
  font-size: 20px;
}

.profile-head__sig {
  min-width: 0;
  color: var(--text-3);
  font-size: 13px;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.profile-head__actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

/* 个人主页的三类内容切换条（2026-09-20 新增）
   三个等宽按钮在小屏下由 .seg 的 flex-wrap 兜底换行，不会溢出。 */
.profile-tabs {
  margin: 16px 0 4px;
}

.profile-tabs .seg {
  width: 100%;
  max-width: 420px;
}

.profile-tabs__hint {
  margin: 8px 0 0;
}

/* 关注按钮（个人主页 / 帖子详情页共用，2026-09-20 新增）
   文案在三种之间切换：关注 / 已关注 / 互相关注。
   给一个最小宽度是为了让按钮在这三种之间切换时**不左右跳动** ——
   宽度跟着文案变，鼠标停在按钮上点一下，按钮会缩一截，很容易误点旁边的元素。
   底色由 JS 在 btn--primary（未关注，引导操作）与 btn--ghost（已关注，弱化）之间切换。 */
.btn--follow {
  white-space: nowrap;
  min-width: 84px;
}

.btn--follow.is-active {
  font-weight: 600;
}

/* 「TA 关注了你」：与关注按钮配套的说明，不需要额外强调 */
.profile-head__info .muted + .muted {
  margin-top: 2px;
}

/*
 * 资料区「加入时间 + 两种积分」一行（2026-09-21 新增）。
 *
 * 积分跟在加入时间后面而不是单开一行：资料区每一行都是稀缺的垂直空间，
 * 而积分与加入时间同属"账号信息"，放一起语义也顺。
 * 窄屏会自动折行（flex-wrap），不需要额外断点。
 */
.profile-head__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
}

.profile-head__points {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* 积分 chip：图标 + 数字。布鲁币用品牌色描边区分（特殊积分要一眼看出不同） */
.points-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  line-height: 1.6;
}

.points-chip .icon {
  width: 13px;
  height: 13px;
}

.points-chip--credit {
  border-color: color-mix(in srgb, var(--brand) 45%, transparent);
  color: var(--brand);
}

/* a 标签形态的 chip（本人可点进「我的积分」）：去掉链接默认下划线 */
a.points-chip {
  text-decoration: none;
}

a.points-chip:hover {
  border-color: color-mix(in srgb, var(--brand) 45%, transparent);
  color: var(--brand);
}

/*
 * 等级徽章 chip（2026-09-21）。
 *
 * 普通等级与特殊等级用两种色调区分：前者中性（它是"进度"，人人都有），
 * 后者金色（它是"荣誉"，只有被授予的人才有）。
 * 徽章图片可留空 —— 那时 chip 只显示等级名，不会有空图占位。
 */
.level-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px 1px 6px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-2);
  font-size: 12px;
  line-height: 1.7;
  white-space: nowrap;
}

.level-chip__badge {
  width: 14px;
  height: 14px;
  object-fit: contain;
  border-radius: 3px;
  flex: 0 0 auto;
}

/* 特殊等级：金色，与普通等级一眼分开 */
.level-chip--special {
  border-color: color-mix(in srgb, var(--warn) 50%, transparent);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  color: var(--warn);
}

/*
 * 管理员（2026-10-02）：与「特殊等级」同为**授予型**身份，用品牌色而非金色区分 ——
 * 金色留给特殊等级（荣誉），管理员是"职务"，认品牌色更贴合站内语义。
 */
.level-chip--admin {
  border-color: color-mix(in srgb, var(--brand) 55%, transparent);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  color: var(--brand);
}

/* ------------------------------------------------------------------ 我的积分页 */

.points-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-bottom: var(--gap);
}

/* 窄屏两栏会挤到看不清数字，直接堆叠 */
@media (max-width: 560px) {
  .points-cards {
    grid-template-columns: 1fr;
  }
}

.points-card {
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.points-card__head {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text-2);
  font-size: 13px;
}

.points-card__value {
  margin: 6px 0 4px;
  font-size: 28px;
  font-weight: 500;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}

/* 布鲁币用品牌色：两种积分在视觉上必须一眼分得开 */
.points-card--credit .points-card__value {
  color: var(--brand);
}

.points-exchange {
  display: flex;
  gap: 10px;
  align-items: flex-end;
  margin-top: 10px;
}

.points-exchange .field {
  flex: 0 1 180px;
}

/* 兑换预估行：超出余额时变红，用户按错数量级能立刻看出来 */
#ex-hint.is-error {
  color: var(--danger);
}

.ledger {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ledger__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.ledger__row:last-child {
  border-bottom: none;
}

.ledger__main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.ledger__reason {
  font-size: 14px;
}

.ledger__amount {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ledger__amount.is-plus {
  color: var(--ok);
}

.ledger__amount.is-minus {
  color: var(--text-2);
}

/*
 * 布鲁币的增删统一用品牌蓝（2026-09-21 按需求）。
 * 与会员积分刻意不同色：两种币在同一列表里混排，颜色先用来回答
 * "这是哪一种"，方向由正负号表达就够了。
 */
.ledger__amount--credit {
  color: var(--brand);
}

.ledger__coin {
  font-size: 11px;
  color: var(--text-3);
}

/* ------------------------------------------------------------------ 内容管理 */

/*
 * 列表页脚：总数 + 「加载更多」（2026-09-21 新增）。
 * 放在 .rows 外面，不吃 .rows 的 gap，看起来才像页脚而不是列表项。
 */
.rows__more {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 12px;
  flex-wrap: wrap;
}

.rows {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.row {
  display: flex;
  gap: 14px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
}

.row__thumb {
  /* 2026-09-21：多图时在右下角叠 `1/N` 角标（.card__count 是 absolute，需要定位父级） */
  position: relative;
  width: 78px;
  height: 78px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
  flex-shrink: 0;
}

.row__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.row__main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.row__title {
  font-weight: 600;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row__tags {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.row__stats {
  display: flex;
  gap: 14px;
  font-size: 12px;
  color: var(--text-3);
}

.row__actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* ------------------------------------------------------------------ 类别管理 */

.cat-tree {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.cat-group {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}

.cat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
}

.cat-row--root .cat-row__name {
  font-weight: 600;
  font-size: 15px;
}

.cat-row--child {
  padding-left: 16px;
  border-top: 1px solid var(--border);
}

/*
 * 类别行右侧操作区：↑ ↓ + 删除。
 * 包一层是为了保住 .cat-row 的 space-between 仍是「名称 | 统计 | 操作」三段 ——
 * 直接把两个排序按钮也塞成顶层子元素，间距会被重新均分，整列对齐全乱。
 */
/*
 * 举报管理（2026-09-25 新增）
 *
 * 一行 = 一条举报。信息分三层，从上往下读：
 *   ① 类型 + 状态 + 处理时间（扫描用）
 *   ② 被举报内容 / 举报人（点进去看）
 *   ③ 举报理由 + 处理说明（判断依据）
 * 然后才是操作按钮。
 *
 * ⚠️ 用**块级行**而不是 .cat-row 那样的横向单行：一条举报有六七个字段，
 *    塞进一行在手机上必然换行成一片，谁也看不出哪段属于哪条。
 */
.report-row {
  padding: 14px 0;
  border-top: 1px solid var(--border);
}

.report-row__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}

.report-row__line {
  margin: 0 0 4px;
  font-size: 13.5px;
}

/* 举报理由：判断依据，要和其它字段明显分开 */
.report-row__reason {
  margin: 8px 0 0;
  padding: 8px 10px;
  background: var(--bg);
  border-radius: 8px;
  font-size: 14px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}

.report-row__note {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--text-2);
  word-break: break-word;
}

.report-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 10px;
}

/* 后台分页：三个元素横排，窄屏时自然换行而不是挤出面板 */
.report-pager {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.cat-row__ops {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

.cat-move {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-2);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.cat-move:hover:not(:disabled) {
  border-color: color-mix(in srgb, var(--brand) 55%, transparent);
  color: var(--brand);
}

/* 两端置灰而不是隐藏按钮：位置保持固定，视线不用重新找它 */
.cat-move:disabled {
  opacity: 0.35;
  cursor: default;
}

.cat-row__name {
  font-size: 14px;
}

/* ------------------------------------------------------------------ 设置页 */

.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 16px;
}

.panel__title {
  font-size: 15px;
  margin: 0 0 14px;
}

.kv {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
  font-size: 14px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 10px;
}

.kv__k {
  color: var(--text-2);
}

.avatar-row {
  display: flex;
  gap: 16px;
  align-items: center;
  margin-bottom: 18px;
}

/*
 * 文件选择框：**视觉隐藏，但仍参与渲染**（2026-09-27）。
 *
 * 不能写成 `hidden` / `display:none` —— 部分安卓内核（含若干 WebView）对
 * 未渲染的 file input 响应极慢甚至不弹系统选择器，用户看到的就是
 * 「点了更换头像没反应」。这里改成 1px + 透明 + 裁剪，肉眼不可见，
 * 但元素确实在渲染树里，点得动。
 *
 * `pointer-events: none` 是配套的：它由旁边的按钮用 JS 触发，
 * 自己不该被真实指针点到（点到一个看不见的框只会让人困惑）。
 */
.file-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  opacity: 0;
  pointer-events: none;
}

.bullets {
  margin: 0;
  padding-left: 20px;
  color: var(--text-2);
  font-size: 14px;
  line-height: 1.9;
}

/* ------------------------------------------------------------------ 用户菜单 */

.usermenu {
  position: relative;
}

/* 触发器本身是链接：点击直接进个人中心，悬停才展开菜单 */
.usermenu__trigger {
  display: flex;
  align-items: center;
  gap: 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px 3px 3px;
  color: var(--text);
  font-size: 13px;
  transition: border-color 0.15s, background 0.15s;
}

.usermenu__trigger:hover {
  border-color: var(--text-3);
  background: var(--surface-2);
}

.usermenu__name {
  max-width: 92px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.usermenu__list {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  min-width: 156px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  display: none;
  flex-direction: column;
  z-index: 50;
}

/*
 * 桥接触发器与菜单之间的 6px 空隙。
 * 没有它的话，鼠标从头像移向菜单的途中会离开 .usermenu 的悬停范围，
 * 菜单在到达之前就收起来了。
 */
.usermenu__list::before {
  content: '';
  position: absolute;
  top: -6px;
  left: 0;
  right: 0;
  height: 6px;
}

/**
 * 桌面端：鼠标悬停展开（CSS `:hover`），或键盘聚焦时展开（JS 加 `.is-open`）。
 * 移动端不展开，见下方媒体查询。
 *
 * ⚠️ 这里**不能**用 `:focus-within`：点过菜单项后焦点会留在那个 `<a>` / `<button>` 上，
 *    `:focus-within` 一直成立 —— 鼠标移走了菜单还钉在屏幕上，必须在菜单外再点一下才消失。
 *    键盘态改由 JS 管理（见 app.js 的 bindUserMenu），鼠标移出时把焦点一并收回。
 */
@media (min-width: 701px) {
  .usermenu:hover .usermenu__list,
  .usermenu.is-open .usermenu__list {
    display: flex;
  }
}

.usermenu__list a,
.usermenu__list button {
  text-align: left;
  padding: 8px 10px;
  border: 0;
  background: none;
  border-radius: 6px;
  font-size: 13.5px;
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
}

.usermenu__list a:hover,
.usermenu__list button:hover {
  background: var(--surface-2);
}

/* ------------------------------------------------------------------ 响应式 */

@media (max-width: 860px) {
  .grid-inline {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 700px) {
  .topbar__nav {
    display: none;
  }

  .tabbar {
    display: flex;
  }

  /*
   * 底部 TAB 里的「发布」加一枚浅色底框（2026-09-24，实验性）。
   *
   * 为什么只改移动端：PC 上「发布」是顶栏导航里的一项，宽屏的导航项是
   * 长条形，套个底框很怪；而移动端底部是 5 个等宽小格，中间那格（发布
   * 正好排在第 3 格）套个框才成立 —— 也顺便给整排 TAB 一个视觉落点。
   *
   * ⚠️ 用属性选择器而不是给 HTML 加类：底部 TAB 的每一项都在 app.js 里
   *    由同一个数组渲染，加类要动 JS；`[href="#/compose"]` 一行 CSS 就够，
   *    而且不依赖渲染顺序（发布换位置也不会失效）。
   *
   * ⚠️ margin + padding 合起来要**等于**原来 .tab 的 padding（8px / 6px）：
   *    margin 让底框从整格里缩进来（看着像一枚胶囊，而不是把整格染色），
   *    padding 相应减掉，否则底框会变高、把整条 TAB 撑高。
   *
   *    横向 6px：胶囊 60px、占格子 83%。
   *    2026-09-24 试过收到 8px（胶囊 56px / 78%），用户看完说"不如原来的"，
   *    已改回 6px —— 再收会显得憋，6px 是这枚胶囊最稳的比例。
   *    别再往里收了；想更有存在感就改颜色（--brand-weak → 实心 --brand），
   *    改颜色一眼能看出来，改这几 px 看不出来。
   *    胶囊宽度 = 单格宽 - 2×6px（360px 屏上单格 72px → 胶囊 60px，约 83%）。
   *
   * 效果不好就整段删掉 —— 全部规则都在这个媒体查询里，删了不影响别处。
   */
  .tabbar a.tab[href="#/compose"] {
    margin: 5px 6px;
    padding: 3px 0;
    border-radius: 12px;
    background: var(--brand-weak);
    color: var(--brand);
  }

  /*
   * 选中态：文字本来就是品牌色，再换色看不出变化 —— 改成"底框加深一档 + 加粗"。
   * 用 color-mix 而不是写死色值：深色主题下 --brand-weak 是深蓝，
   * 写死浅蓝会在深色主题下变成一块突兀的亮斑。
   */
  .tabbar a.tab[href="#/compose"].is-active {
    background: color-mix(in srgb, var(--brand) 18%, transparent);
    font-weight: 600;
  }

  .view {
    padding: 16px 12px 92px;
  }

  .grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 12px;
  }

  .card__body {
    padding: 10px;
  }

  .page-title {
    font-size: 19px;
  }

  .post__title {
    font-size: 21px;
  }

  .profile-head {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .profile-head__actions {
    justify-content: center;
  }

  .row {
    flex-wrap: wrap;
  }

  .row__actions {
    width: 100%;
    justify-content: flex-end;
  }

  .grid-inline {
    grid-template-columns: 1fr;
  }

  .filters .select {
    flex: 1;
    min-width: 0;
  }

  .cmt--reply {
    margin-left: 18px;
  }

  .toast-host {
    bottom: 76px;
  }
}

/* ------------------------------------------------------------------ 管理后台 */

.admin-tabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 10px;
}

.admin-tab {
  padding: 7px 14px;
  border-radius: 999px;
  color: var(--text-2);
  font-size: 14px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.admin-tab:hover {
  background: var(--surface-2);
  color: var(--text);
}

.admin-tab.is-active {
  background: var(--brand-weak);
  color: var(--brand);
  font-weight: 600;
}

/* 状态徽章（会员 / 帖子通用）：绿/黄/红/灰 */
.badge--ok {
  background: color-mix(in srgb, var(--ok) 16%, transparent);
  color: var(--ok);
}

.badge--warn {
  background: color-mix(in srgb, var(--warn) 18%, transparent);
  color: var(--warn);
}

.badge--danger {
  background: color-mix(in srgb, var(--danger) 15%, transparent);
  color: var(--danger);
}

/* ------------------------------------------------------------------ 顶部奖杯入口（2026-09-22） */

/*
 * 圆形描边 + 表面底色 + emoji 图标：与底部导航同一套视觉语言。
 * 之前用线稿 SVG，和实心 emoji 放一起风格不统一，小屏上尤其明显。
 */
.trophy {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: 16px;
  text-decoration: none;
  transition: border-color 0.15s;
}

.trophy:hover { border-color: var(--brand); }

.trophy__icon {
  line-height: 1;
}

/* ------------------------------------------------------------------ 探索页（2026-09-22） */

.hometabs {
  display: flex;
  gap: 6px;
  background: var(--surface-2);
  border-radius: 999px;
  padding: 4px;
  margin-bottom: 16px;
}

.hometab {
  flex: 1;
  border: 0;
  background: transparent;
  color: var(--text-2);
  padding: 8px 10px;
  border-radius: 999px;
  font-size: 14px;
  cursor: pointer;
  white-space: nowrap;
}

.hometab.is-active {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: var(--shadow);
}

/*
 * 大类文字导航（首页探索 Tab 与推荐页共用）。
 *
 * 2026-09-30 改：**永不换行**，一行放不下就在这行内部横滑。
 * 原来是 flex-wrap: wrap —— 加上帖子数徽章后每个按钮宽了约 28px，
 * 安卓窄屏上一排放不下就折成两行，把下面的区块整体往下顶，
 * 看起来像"页面自己跳了一下"。横滑与 .pillbar / .catnav__row 一致，
 * 是这类胶囊行的通用解法。
 *
 * 滚动条一律藏掉：移动端既难看又占位，可滑这件事由右缘渐隐提示
 * （is-scrollable 由页面渲染后实测打上）。
 */
.catnav-text {
  display: flex;
  gap: 8px;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  margin-bottom: 18px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
}

.catnav-text::-webkit-scrollbar {
  display: none;
}

.exblock {
  margin-bottom: 26px;
}

.exblock__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}

.exblock__title {
  font-size: 17px;
  margin: 0;
  letter-spacing: 0.2px;
}

.exblock__sub {
  margin: -4px 0 10px;
  color: var(--text-3);
  font-size: 13px;
}

/*
 * 「关注的人」/「随缘漂流」头像墙（2026-09-22）。
 * 与 .collector-wall 结构一致，唯一区别是**不带排名角标** ——
 * 这两屏的排序没有名次含义（一个按关注时间，一个是随机），
 * 打上 1/2/3 会误导用户以为有排名。
 */
.people-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 14px;
}

.person {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: var(--text);
}

.person__name {
  font-size: 12px;
  /* 与列宽同宽：名字再长也只能占自己这一格，否则会顶宽整个 grid */
  max-width: 84px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}

/* 收藏达人头像墙 */
.collector-wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 14px;
}

.collector {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: var(--text);
}

.collector__rank {
  position: absolute;
  top: -2px;
  left: 50%;
  transform: translateX(-34px);
  width: 20px;
  height: 20px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--surface);
}

.collector__rank--1 { background: #f6c343; color: #5a3d00; }
.collector__rank--2 { background: #cfd6dd; color: #3a4250; }
.collector__rank--3 { background: #e0a86b; color: #4a2e10; }

.collector__name {
  font-size: 12px;
  max-width: 84px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: center;
}

/*
 * 头像墙里的特殊等级徽章（2026-09-22 推荐页「头号玩家」）。
 *
 * 复用 .level-chip 的形状与配色，只在**尺寸**上收一档：
 * 这一列最窄时只有 ~60px（移动端 5 列），12px 的字号加 padding 会把格子顶宽，
 * 进而把整页撑出横向滚动条 —— 与头像收到 44px 是同一个原因。
 * 超过一行的徽章靠 flex-wrap 换行，不用 nowrap，免得横向溢出。
 */
.collector__lv {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 3px;
  max-width: 100%;
}

.collector__lv .level-chip {
  font-size: 10px;
  padding: 0 6px;
  line-height: 1.6;
  max-width: 100%;
  overflow: hidden;
}

.collector__lv .level-chip__badge {
  width: 11px;
  height: 11px;
}

/* ------------------------------------------------------------------ 会员排行页（2026-09-22） */

/*
 * 会员列表页筛选行与下面名单之间的间距。
 *
 * 18px 不是随手取的 —— 与帖子列表 `.toolbar`（排序 + 时间窗 + 话题那一行）
 * 到帖子网格的距离**同一个值**：两页的「筛选 → 内容」节奏必须一致，
 * 否则用户在这两页之间来回切会觉得一页松一页紧。
 * .pillbar 本身没有下边距（它是通用组件，间距交给使用方定），
 * 所以这一条必须由 .membfilter 补上，否则筛选行会紧贴第一名会员。
 */
.membfilter {
  margin-bottom: 18px;
}

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

.rankrow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  transition: border-color 0.15s, box-shadow 0.15s;
}

.rankrow:hover {
  border-color: var(--brand);
  box-shadow: var(--shadow);
}

.rankrow__no {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-weight: 700;
  font-size: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.rankrow__no--1 { background: #f6c343; color: #5a3d00; }
.rankrow__no--2 { background: #cfd6dd; color: #3a4250; }
.rankrow__no--3 { background: #e0a86b; color: #4a2e10; }

.rankrow__main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.rankrow__name {
  flex: 0 1 auto;
  min-width: 0; /* 允许被压窄出省略号，好把位置让给后面的等级/粉丝数 */
  font-weight: 600;
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rankrow__bio {
  font-size: 12px;
  color: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * 会员等级 + 粉丝/关注数要**跟在用户名后面同一行**，不能另起一行 ——
 * 榜单是扫着看的，身份信息被挤到第二行会让人以为是下一条的内容。
 *
 * 所以把「名字 + 徽章」单独包一层横向容器：名字可压窄出省略号，
 * 徽章整块不收缩；真放不下时裁的是名字，不是把徽章挤下去。
 */
.rankrow__head {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}

/*
 * 用户名后面那一段：会员等级（含特殊等级）+ 粉丝数 / 关注数。
 * 复用个人主页的 .level-chip —— 同一个人在两个地方看到的是同一套身份信息，
 * 写成两套样式迟早会长歪。
 */
.rankrow__badges {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  flex-wrap: nowrap;
  gap: 5px;
  font-size: 12px;
  color: var(--text-3);
}

.rankrow__fc {
  white-space: nowrap;
}

.rankrow__metric {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 14px;
  color: var(--text);
}

.rankrow__metric .icon {
  width: 15px;
  height: 15px;
  color: var(--brand);
}

.rankrow__metric b {
  font-variant-numeric: tabular-nums;
}

.rankrow__mlabel {
  font-size: 12px;
  color: var(--text-3);
  margin-left: 2px;
}

/* ------------------------------------------------------------------ 骨架占位 */

.sk--avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
}

.skeleton-avatar-row {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 14px;
}

.skeleton-rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.skeleton-rows .sk--line {
  height: 56px;
  border-radius: var(--radius-sm);
}

/* 移动端微调 */
@media (max-width: 700px) {
  /*
   * 话题榜在窄屏**不再单独降一档字号**：
   * 以前单独给 15px / 13px 是为了补偿旧的 11px，现在三档是 16 / 15 / 14px，
   * 都落在正常正文区间，手机上再降反而退回"看不清"；
   * 圆点间距写的是 em，本来就会跟着各自那一档的字号缩放，也不用在这里重设。
   *
   * ⚠️ 骨架条必须写成 .topicline .sk--topic（多一层类）：这条规则在文件里
   *    排在后面的 .sk--topic 之前，同特异性时"后来者胜"，窄屏这条会失效 ——
   *    加一层 .topicline 提高特异性，小屏才真的收窄。
   */
  .topicline .sk--topic {
    width: 64px;
    height: 17px;
    margin-right: 10px;
  }

  /* 关注的人 / 随缘漂流同样 2 行 × 5 个，与收藏达人保持一致的密度 */
  .people-wall {
    grid-template-columns: repeat(5, 1fr);
    gap: 10px 6px;
  }
  .person .avatar {
    width: 44px !important;
    height: 44px !important;
  }
  .person__name {
    max-width: 100%;
    font-size: 11px;
  }

  .collector-wall {
    grid-template-columns: repeat(5, 1fr);
    gap: 10px 6px;
  }
  /*
   * 5 列时每列只剩 ~60px：头像 52px 再叠上左上角排名角标，合计宽度会顶出容器
   * （表现为整块把页面撑宽、横向出屏）。收到 44px 才稳稳落在列内。
   */
  .collector .avatar {
    width: 44px !important;
    height: 44px !important;
  }
  /* 角标跟着头像一起收，并按新的头像半径重新定位到左上角 */
  .collector__rank {
    width: 18px;
    height: 18px;
    font-size: 10px;
    transform: translateX(-26px);
  }
  .collector__name {
    max-width: 100%;
    font-size: 11px;
  }
  .skeleton-avatar-row {
    grid-template-columns: repeat(5, 1fr);
  }
  /*
   * 窄屏一行挤不下「名字 + 等级 + 粉丝/关注」，身份信息回到用户名**下一行**。
   * PC 上是横着扫榜单（同行更好读），手机上是竖着扫（挤在一行反而糊成一团），
   * 两种排布各有各的读法，所以这里按断点分开，不做统一。
   */
  .rankrow__head {
    flex-wrap: wrap;
  }

  /*
   * 徽章独占一行后宽度够用，chip 多时才换行（回到改动前的表现）。
   * max-width: 100% 不能省 —— badges 是 flex:0 0 auto，
   * 默认按 max-content 取宽，等级一多就会整块撑出容器被 .rankrow__head 的
   * overflow:hidden 裁掉，排在后面的粉丝/关注数就成了「看不见」。
   */
  .rankrow__badges {
    flex-wrap: wrap;
    max-width: 100%;
  }

  /*
   * 粉丝数 / 关注数单独占一行。
   * 它排在等级徽章后面，两者挤在同一行时，等级多几个就把数字顶出可视区 ——
   * 数字是榜单里真正要比的东西，宁可多占一行也不能被裁。
   */
  .rankrow__fc {
    flex: 0 0 100%;
    margin-top: 2px;
  }

  .rankrow__bio {
    display: none;
  }
}

/*
 * 推荐位徽章（2026-09-20 新增）。
 * 用品牌色而不是绿色：绿色在本项目里是「已通过 / 正常」的状态语义，
 * 推荐是**运营标记**，两者混用会让管理员以为"推荐"是一种审核状态。
 */
.badge--rec {
  background: var(--brand-weak);
  color: var(--brand);
  font-weight: 600;
}

/* 控制台统计卡 */
.stat-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--gap);
  margin-bottom: 18px;
}

.stat-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  transition: box-shadow 0.18s, transform 0.18s;
}

.stat-card:hover {
  box-shadow: var(--shadow);
  transform: translateY(-2px);
}

.stat-card__value {
  font-size: 26px;
  font-weight: 700;
  color: var(--brand);
}

.stat-card__title {
  font-size: 13px;
  color: var(--text-2);
}

/* 软删除的类别行置灰 */
.cat-group.is-deleted {
  opacity: 0.55;
}

/* 开关行 */
.switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 14px;
  flex-wrap: wrap;
}

.switch-row__title {
  font-weight: 600;
  font-size: 15px;
}

/* iOS 风格开关 */
.switch {
  position: relative;
  display: inline-block;
  width: 46px;
  height: 26px;
  flex-shrink: 0;
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.switch__slider {
  position: absolute;
  inset: 0;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background 0.18s, border-color 0.18s;
}

.switch__slider::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 50%;
  width: 18px;
  height: 18px;
  background: #fff;
  border-radius: 50%;
  transform: translateY(-50%);
  transition: transform 0.18s;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}

.switch input:checked + .switch__slider {
  background: var(--brand);
  border-color: var(--brand);
}

.switch input:checked + .switch__slider::before {
  transform: translate(20px, -50%);
}

.switch input:disabled {
  cursor: not-allowed;
}

@media (max-width: 700px) {
  .stat-cards {
    grid-template-columns: repeat(2, 1fr);
  }

  /*
   * 类别那一支的**外壳**也要把宽度钉在容器里（2026-10-02 修）。
   *
   * 症状：移动端类别行放不下时，拖它拖不动这一行，整屏跟着动 ——
   * 因为 .catnav 比 .filterbar 还宽（实测 435px vs 366px），
   * 里面的 .catnav__row 于是**没有可滑的区间**（clientWidth == scrollWidth），
   * 手指这一下自然落到页面上。属性行（.pillbar）没这个毛病，
   * 正是因为它自己带 max-width: 100%。
   *
   * ⚠️ 别指望 flex-shrink 替你缩：.catnav__row 既是横滑容器又写了
   *    max-width: 100%（百分比依赖父级宽度），两者构成循环依赖，
   *    Chrome 实测会保持内容宽，即使 .catnav 是 flex: 0 1 auto + min-width: 0。
   *    所以这里显式写 max-width，和 .pillbar 用同一套兜底办法。
   */
  .filterbar .catnav {
    min-width: 0;
    max-width: 100%;
  }

  /*
   * 移动端没有 hover：类别 / 子类别 / 属性都改成横滑行。
   *
   * min-width: 0 是这里的关键 —— flex 子项默认 min-width: auto，
   * 内容一宽就会把**整个页面**撑出横向滚动条（看起来就是「出屏很多」），
   * 而不是只在这一行内部滑。配合 max-width: 100% 把宽度钉死在容器里，
   * 溢出部分才真正交给 overflow-x 处理。
   */
  .catnav-text,
  .catnav__row,
  .catnav__sub,
  .pillbar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    min-width: 0;
    max-width: 100%;
    gap: 6px;
    padding-bottom: 4px;
    -webkit-overflow-scrolling: touch;
    /* 滚动条在移动端既难看又占位，直接藏掉，靠下面的渐隐提示可滑 */
    scrollbar-width: none;
    -ms-overflow-style: none;
  }

  /*
   * 横滑行自带 padding-bottom: 4px（给滚动留白），这里把下边距补回 14px，
   * 加起来仍是 18px —— 与 PC 端、与下面区块的间距一致。
   */
  .catnav-text {
    margin-bottom: 14px;
  }

  .catnav-text::-webkit-scrollbar,
  .catnav__row::-webkit-scrollbar,
  .catnav__sub::-webkit-scrollbar,
  .pillbar::-webkit-scrollbar {
    display: none;
  }

  /*
   * 右缘渐隐：提示「后面还有，可以往左滑」，而不是让人以为内容被切掉了。
   * 只在**真的滑得动**时才加 —— 放得下的行若也渐隐，
   * 最后一个按钮会平白被弄淡。is-scrollable 由各页渲染后实测打上
   * （home.js 的 markScrollableRows / explore.js、recommend.js 的 markCatNavScrollable）。
   */
  .catnav-text.is-scrollable,
  .catnav__row.is-scrollable,
  .catnav__sub.is-scrollable,
  .pillbar.is-scrollable {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
  }

  /* 胶囊收一档：属性行就 5 个（全部/资讯/收藏/二手/交友），
     紧凑后窄屏基本能一行放下，不用滑 */
  .pillbar .pill {
    padding: 5px 11px;
    font-size: 12px;
  }

  /*
   * 移动端横滑行自带 padding-bottom: 4px（给滚动留白），
   * 这里补成 14px，加上它就是 18px —— 与 PC 端、与帖子列表完全一致。
   */
  .membfilter {
    margin-bottom: 14px;
  }

  .filterbar,
  .toolbar {
    min-width: 0;
    max-width: 100%;
  }

  .catnav__drop {
    display: none !important;
  }

  /* 移动端两行布局：大类占满一行（横滑），属性换到下一行左对齐 */
  .filterbar {
    flex-direction: column;
    align-items: stretch;
  }

  .filterbar .typefilter {
    align-self: stretch;
    justify-content: flex-start;
  }
}

/* ==========================================================================
   站内消息通知（2026-09-20 新增）
   ========================================================================== */

/* ── 未读小红点 ──────────────────────────────────────────────────
   挂在顶栏导航项与底部 Tab 上，数字由 notifications.js 统一写入。
   不写死 id：外壳会被重建（切换语言、登录登出），用属性选择器才跟得上。 */
.badge[data-notify-badge] {
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  margin-left: 5px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 10px;
  line-height: 16px;
  font-weight: 600;
  text-align: center;
  vertical-align: middle;
}

/* 底部 Tab 的红点叠在图标右上角 —— 图标自己得先成为定位参照 */
.tab__icon {
  position: relative;
}

.tab__icon .badge[data-notify-badge] {
  position: absolute;
  top: -4px;
  left: 13px;
  margin-left: 0;
  /* 描边用底色，红点压在图标上时不会和图标糊在一起 */
  box-shadow: 0 0 0 2px var(--surface);
}

/* ── 顶栏导航「消息」上的红点（2026-09-24）────────────────────────
   之前红点挂在顶栏右侧的**铃铛**上（导航项里也有一份，但跟在文字后面不起眼）。
   铃铛下线后红点回到导航项，并改成和铃铛一样的**右上角叠放** ——
   行内跟在「消息」后面的小数字太容易被当成文字的一部分而忽略。

   ⚠️ 用 absolute 而不是行内：行内红点会让导航项在有/无未读时宽度变化，
      整排导航跟着左右抖一下；叠放不占布局，也不挤到右边的「关于」。
      偏移量刻意压到 -3/-2px：红点只吃掉 4px 的 gap 和「关于」的 12px 左内边距，
      不会盖住下一个导航项的文字。 */
.navlink .badge[data-notify-badge] {
  position: absolute;
  top: -3px;
  right: -2px;
  margin-left: 0;
  /* 描边用页面底色：红点压在导航项边框上时才有浮起来的分界 */
  box-shadow: 0 0 0 2px var(--bg);
}

/* ── 类型筛选（可点的 chip） ─────────────────────────────────────── */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}

.chips .chip {
  border: 1px solid transparent;
  cursor: pointer;
  font-size: 12px;
  padding: 4px 12px;
}

.chips .chip.is-active {
  background: var(--brand-weak);
  color: var(--brand);
  border-color: var(--brand);
}

/* ── 搜索行 ────────────────────────────────────────────────────── */
.searchbar {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

.searchbar .input {
  flex: 1;
  min-width: 0;
}

/*
 * 搜索页控件区与卡片网格的间距（2026-09-27 反馈：按话题浏览时紧贴着）。
 *
 * 18px 不是随手取的 —— 与帖子列表页 .toolbar、.page-head、.filters 的
 * margin-bottom 一致，三个页面「控件条 → 列表」的呼吸感才统一。
 *
 * 为什么必须挂在 .search-page 上：.searchbar 是搜索页与消息页共用的，
 * 消息页那一条下面紧跟的是批量操作条（.msgbar），间距语义是另一回事，
 * 动它会连带把消息页撑开。
 *
 * ⚠️ 只写在 search-page 作用域里，别改成全局 .searchbar。
 */
.search-page .searchbar {
  margin-bottom: 18px;
}

/*
 * 关键词搜索时排序切换（相关度/时间）才渲染，它才是网格前**最后一个**控件；
 * 按话题浏览不渲染它（同话题内容排相关度没意义），所以上面那条必须独立存在，
 * 不能指望这一条兜底。
 */
.search-page .seg--inline {
  margin: 12px 0 18px;
}

/* ── 消息列表 ──────────────────────────────────────────────────── */
.msgs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.msg {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}

.msg.is-unread {
  border-color: color-mix(in srgb, var(--brand) 35%, var(--border));
  background: color-mix(in srgb, var(--brand-weak) 45%, var(--surface));
}

.msg__link {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex: 1;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

.msg__avatar {
  flex-shrink: 0;
}

.msg__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--surface-2);
  font-size: 18px;
}

.msg__main {
  min-width: 0;
  flex: 1;
}

.msg__title {
  margin: 0;
  font-size: 14px;
  color: var(--text);
  word-break: break-word;
}

/* 摘要最多两行：消息中心要的是"扫一眼"，撑开三行会把列表拉得很长 */
.msg__body {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--text-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.msg__meta {
  margin: 6px 0 0;
  display: flex;
  gap: 8px;
  align-items: center;
  font-size: 12px;
  color: var(--text-3);
}

.msg__type {
  background: var(--surface-2);
  border-radius: 999px;
  padding: 1px 8px;
}

.msg__side {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.msg__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--danger);
  display: inline-block;
}

/* ── 账号设置里的通知开关（一行一个，说明文字在标题下面）───────────── */
.switch-list {
  display: flex;
  flex-direction: column;
  margin-top: 14px;
}

.switch-row--stack {
  border-top: 1px solid var(--border);
  padding-top: 12px;
  margin-top: 12px;
}

.switch-row--stack:first-child {
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}

.switch-row__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.switch-row__hint {
  font-size: 12px;
  color: var(--text-3);
  font-weight: 400;
}

@media (max-width: 700px) {
  /* 窄屏放不下「文字 + 按钮」两列：按钮折到下一行并右对齐 */
  .msg {
    padding: 10px 12px;
    flex-wrap: wrap;
  }

  .msg__side {
    width: 100%;
    justify-content: flex-end;
  }
}

/* ==========================================================================
   私信（2026-09-20 新增）
   ========================================================================== */

/*
 * 顶栏**没有铃铛入口**了（2026-09-24 下线）：未读红点统一挂在导航项「消息」上，
 * 红点样式见上文 .navlink .badge[data-notify-badge]。
 *
 * 为什么下线：铃铛和导航项「消息」各带一个红点，同一个数字出现在两个地方，
 * 用户不知道该看哪个；而且它们指向同一个页面，铃铛只是多一次点击的捷径。
 * ⚠️ 别再把它加回来 —— 顶栏右侧（奖杯 / 语言 / 头像+用户名）小屏已经很挤了。
 */

/* ── 会话列表 ──────────────────────────────────────────────────── */
.dmconv-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.dmconv {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
}

.dmconv:hover { border-color: var(--brand); }

/* 有未读的会话整条染色：红点只在会话页里清，列表上要一眼看得出 */
.dmconv.is-unread { background: var(--brand-weak); }

/* 头像同样可点（跳主页），但整行已经是 <a>，这里只能用 span + 事件委托 ——
   见 dm.js convHtml 的注释。hover 反馈与 .avatar-link 保持一致。 */
.dmconv__avatar {
  flex: 0 0 auto;
  line-height: 0;
}

.dmconv__avatar:hover .avatar,
.dmconv__avatar:hover .avatar--fallback {
  box-shadow: 0 0 0 2px var(--brand);
}

.dmconv__main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  /* min-width:0 是让 text-overflow 生效的前提，flex 子项默认不肯收缩 */
  min-width: 0;
}

.dmconv__name { font-weight: 600; }

.dmconv__snippet {
  overflow: hidden;
  color: var(--muted, #6b7280);
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dmconv__side {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  font-size: 12px;
  color: var(--muted, #6b7280);
}

.dmconv__badge {
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 11px;
  line-height: 18px;
  text-align: center;
}

/* ── 会话页 ────────────────────────────────────────────────────── */
.dm-head {
  align-items: center;
  gap: 12px;
}

.dm-head__peer {
  display: flex;
  align-items: center;
  gap: 8px;
}

.dm-head__name {
  color: inherit;
  font-weight: 600;
  text-decoration: none;
}

/* 消息区自己滚动，而不是整页滚 —— 输入框要一直待在视口里 */
.dmthread {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 60vh;
  overflow-y: auto;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.dmmsg-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
 * ⚠️ 这里**故意不写** .dmmsg 的对齐：气泡的左右分栏在后面「气泡：谁发的要一眼看出来」
 * 那一节统一定义（.dmmsg / .dmmsg.is-mine / .dmmsg__col）。
 *
 * 曾经这里有一句 `.dmmsg.is-mine { justify-content: flex-end; }`，
 * 和后面的 `flex-direction: row-reverse` **叠在一起正好把我的消息推到左边**：
 * row-reverse 之后主轴的 start 在右、end 在左，flex-end = 左端。
 * 结果就是「我发的消息也靠左，但气泡里的字又右对齐」——看着很乱。
 * 分栏只在一个地方定义，别再往这里加对齐声明。
 */
.dmmsg__bubble {
  max-width: 74%;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg);
}

.dmmsg.is-mine .dmmsg__bubble {
  border-color: transparent;
  background: var(--brand-weak);
}

.dmmsg__body {
  margin: 0;
  /* pre-wrap：用户敲的换行要保留，但长串也要能折行 */
  white-space: pre-wrap;
  word-break: break-word;
}

.dmmsg__time {
  margin: 2px 0 0;
  font-size: 11px;
  opacity: 0.6;
}

.dmform {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin-top: 12px;
}

.dmform .input { flex: 1 1 auto; }

.panel__actions {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

@media (max-width: 640px) {
  .dmthread { max-height: 55vh; }
  .dmmsg__bubble { max-width: 86%; }
}

/* ==========================================================================
   消息中心合并私信 / 会话页可读性（2026-09-20）
   ========================================================================== */

/* ── 消息中心里私信行的未读数 ────────────────────────────────────
   私信一行代表一个对话，未读可能是好几条，红点表达不了数量，
   所以给一个数字徽标，和「未读小圆点」并列但不重复表达同一件事。 */
.msg__count {
  min-width: 18px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 11px;
  line-height: 18px;
  text-align: center;
}

/* ── 勾选删除（2026-09-23）────────────────────────────────────────
   消息会越攒越多，只靠「全部已读」压不住量，所以每行前面给了勾选框，
   上面一条批量操作条（全选 / 已选计数 / 删除选中 / 清空全部）。

   ⚠️ 勾选框在 .msg__link 外面：链接包住整行内容，放里面的话
      点一下复选框就顺带跳走了。 */
.msg__check {
  display: flex;
  align-items: center;
  /* 与右侧「标为已读」按钮同高，视觉上不至于一上一下 */
  padding-top: 10px;
  flex-shrink: 0;
}

.msg__check-input {
  width: 16px;
  height: 16px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--brand);
}

.msgbar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  padding: 8px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.msgbar__all {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  cursor: pointer;
  user-select: none;
}

.msgbar__all input {
  width: 16px;
  height: 16px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--brand);
}

.msgbar__count {
  font-size: 12px;
  color: var(--text-3);
}

/* 按钮组贴右边：中间那块 flex:1 是"撑开"用的，不是内容 */
.msgbar__actions {
  margin-left: auto;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.msg.is-selected {
  border-color: color-mix(in srgb, var(--brand) 50%, var(--border));
}

/* 窄屏：操作条两行排（全选一行、按钮一行），别把按钮挤成两半 */
@media (max-width: 480px) {
  .msgbar {
    gap: 8px;
  }

  .msgbar__actions {
    margin-left: 0;
    width: 100%;
  }

  .msgbar__actions .btn {
    flex: 1;
  }
}

/* ── 会话页顶部的一句话说明 ────────────────────────────────────── */
.dm-hint {
  margin: 0 0 10px;
  color: var(--muted, #6b7280);
  font-size: 12px;
}

/* ── 气泡：谁发的要一眼看出来 ────────────────────────────────────
   对齐（左=对方 / 右=我）+ 头像 + 名字，三条线索冗余，
   因为只靠对齐在长对话里是认不出来的。 */
.dmmsg {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

/*
 * 我发的：整行镜像，头像位空着，视觉上右边是「我这一侧」。
 *
 * ⚠️ justify-content 必须**显式写 flex-start**，不能依赖默认值：
 * row-reverse 之后主轴 start 在**右**、end 在**左** ——
 * 一旦别处（或后人）补一句 justify-content: flex-end，我的消息就会被推到左边，
 * 而气泡里的字仍是右对齐的（.dmmsg__col 有 align-items: flex-end），
 * 现象是"我发的消息靠左但字靠右，整列看起来很乱"。这条已经真实发生过一次。
 */
.dmmsg.is-mine {
  flex-direction: row-reverse;
  justify-content: flex-start;
}

.dmmsg__avatar {
  flex: 0 0 auto;
  line-height: 0;
}

.dmmsg__col {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-width: 74%;
  /* flex 子项默认不肯收缩，不写这行长单词会把布局撑破 */
  min-width: 0;
}

.dmmsg.is-mine .dmmsg__col { align-items: flex-end; }

.dmmsg__name {
  margin: 0;
  color: var(--muted, #6b7280);
  font-size: 11px;
}

/* 宽度改由 .dmmsg__col 管，气泡自己撑满 */
.dmmsg__bubble { max-width: 100%; }

/* ── 日期分隔 ──────────────────────────────────────────────────── */
.dmmsg__day {
  display: flex;
  justify-content: center;
  margin: 8px 0 2px;
}

.dmmsg__day span {
  padding: 2px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted, #6b7280);
  font-size: 11px;
}

.dmmsg__empty {
  padding: 18px 0;
  color: var(--muted, #6b7280);
  text-align: center;
}

.dmmsg__empty span {
  display: block;
  margin-top: 4px;
  font-size: 12px;
}

@media (max-width: 640px) {
  .dmmsg__col { max-width: 82%; }
}

/* ══════════════════════════════════════════════════════════════════════
   话题（Tag）与全站搜索 —— 2026-09-20 新增
   ══════════════════════════════════════════════════════════════════════
   ⚠️ 工程口径叫 tag，界面文案一律是「话题」。本段类名因此混用：
      .tagfield / .tagchip 是输入控件（工程概念），
      .chip--topic / .tagfilter 是展示用的话题 chip（产品概念）。
*/

/* ── 顶栏搜索框 ────────────────────────────────────────────────── */
.searchbox {
  display: flex;
  align-items: center;
  gap: 6px;
  /* 桌面上跟着导航排，最多占 260px；移动端由下面的断点放开 */
  flex: 0 1 260px;
  min-width: 120px;
}

.searchbox__input {
  width: 100%;
  min-width: 0;
  background: var(--surface-2);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 7px 14px;
  font-size: 13px;
  color: var(--text);
  font-family: inherit;
}

.searchbox__input::placeholder {
  color: var(--text-3);
}

.searchbox__input:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-weak);
}

/* 搜索按钮做成圆角小图标，不抢主导航的视觉重量 */
.searchbox__btn {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}

.searchbox__btn:hover {
  background: var(--brand-weak);
  color: var(--brand);
}

/* ── 话题 chip（展示用，可点） ─────────────────────────────────── */
.chip--topic {
  background: var(--brand-weak);
  color: var(--brand);
  text-decoration: none;
  /* 与 .chip 的 11px 对齐，加粗一点让 # 前缀更清楚 */
  font-weight: 500;
  transition: filter 0.15s;
  /*
   * 2026-09-24：话题最长 32 个字（后端 TAG_NAME_MAX）。32 个汉字在 14px 下
   * 约 470px 宽，而手机整屏才 360px —— 一个话题就能把版面顶出横向滚动条。
   *
   * 后端那条限制管的是"能不能存进库"，这里管的是"存得下的长话题会不会撑破排版"：
   * 恶意用户根本不需要突破任何限制，老老实实写满 32 个字就够了。
   * 三行缺一不可：max-width 定上限，overflow/nowrap 让它不换行也不溢出，
   * text-overflow 给省略号 —— 少了省略号就是硬生生切掉半截字。
   */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /*
   * ⚠️ 配套：overflow 不是 visible 之后，inline-block 的基线会从"文字基线"
   * 变成"盒子下边缘"，话题会整体上跳、把后面的圆点甩在下面。
   * 圆点也一起改成 middle，两者按中线对齐，视觉上才还是齐的。
   */
  vertical-align: middle;
}

a.chip--topic:hover {
  filter: brightness(0.94);
}

/* 首页筛选区里的"当前话题"：描边加粗，与「可点的其它话题」区分开 */
.chip--topic.is-active {
  border: 1px solid var(--brand);
  font-weight: 600;
}

/* ── 首页的话题筛选行 ──────────────────────────────────────────── */
.tagfilter {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.tagfilter__label {
  font-size: 12px;
  color: var(--text-3);
}

/* ── TOP100 话题榜（2026-09-22）─────────────────────────────── */
/*
 * 话题之间是「连续的一行文字」，不是一排格子：
 * 需求要求换行点随屏幕宽度自然变化，所以这里用**普通行内流**，
 * 一个话题塞不下才折到下一行；grid / flex 都得先定死列数或列宽，做不到这点。
 *
 * 圆点写在 .ti 内部的 .ti__dot 上，且 .ti 是 nowrap ——
 * 这样圆点永远跟着它前面的话题走，不会掉到下一行变成孤零零一个点。
 */
/*
 * 2026-09-24 放大 + 分档：原来整行 14px，而话题 chip 自己又套了 .chip 的 11px
 * —— 榜上真正显示出来的是 **11px**，一屏一百个话题密密麻麻，看久了眼睛累。
 *
 * 现在按名次分三档：TOP10 16px / TOP30 15px / TOP100 14px。
 * 越靠后越小：大多数人真正要找的是前面那几个，全一样大就没有主次；
 * 但最小的一档也是 14px（正常正文大小），不会再退回到"看不清"那一档。
 *
 * ⚠️ 字号必须**成对出现**：.topicline 那一档 + .topicline .chip--topic 那一档。
 *    .chip 自己是 11px，只改 .topicline 完全看不出变化（已经发生过一次）。
 *
 * line-height 2.0：16 × 2 = 32px。字号变大后倍数不降，行距会松到看不出
 * 是一整段连续的文字流；反过来降过头（<1.6）三档挤在一起更难读。
 */
.topicline {
  /* 兜底档 = TOP100 那一档（万一某处渲染成没有 data-topicline 的行） */
  font-size: 14px;
  line-height: 2;
}

.topicline[data-topicline='TOP10'] {
  font-size: 16px;
}

.topicline[data-topicline='TOP30'] {
  font-size: 15px;
}

/* ⚠️ 这条和上面是**一对**：.chip 自己是 11px，不覆盖的话整行字号对它无效 */
.topicline .chip--topic {
  font-size: 14px;
  /*
   * 内边距用 em 而不是 px：三档字号各配一条 padding 迟早写漏一条，
   * em 自动跟着各自那一档的字号走（16px→4px 12px，14px→3.5px 10.5px）。
   */
  padding: 0.25em 0.75em;
}

/*
 * ⚠️ 下面三档的「字号 + 配色」写在**同一条**规则里，别拆成两条：
 *    同名选择器出现两次时，后一条里漏写的属性不会被前一条补上，
 *    拆开维护迟早出现"改了颜色没改字号"这种半生效的改动。
 *
 * 配色分档（2026-09-24）：TOP10 品牌蓝 / TOP30 深灰 / TOP100 更浅一档的灰。
 * 字号已经把三档拉开一层，颜色再拉一层 —— 榜单要的是扫一眼就知道哪些最热，
 * 一百个话题全是同一个蓝的话，眼睛没有落脚点。
 *
 * TOP10 虽与 .chip--topic 的默认值相同，仍然**显式写出来**：
 * 哪天有人改那个默认值，三档会一起变，分档就白做了（同上面字号那次的教训）。
 *
 * ⚠️ 灰色用**主题变量**而不是写死的灰：深色主题下"深灰 / 浅灰"是**反过来**的
 *    （--text-2 在浅色主题是深灰，在深色主题是浅灰）。写死颜色会让深色主题
 *    下的 TOP100 直接糊掉。两套变量保证的是同一件事：
 *    TOP30 比 TOP100 更"实"，TOP100 更"退"。
 */
.topicline[data-topicline='TOP10'] .chip--topic {
  font-size: 16px;
  background: var(--brand-weak);
  color: var(--brand);
}

.topicline[data-topicline='TOP30'] .chip--topic {
  font-size: 15px;
  /*
   * 介于「黑 --text」和「深灰 --text-2」正中间的灰（各取一半）。
   *
   * 为什么不直接写个色值：深色主题下灰阶是**反过来**的（--text 在深色主题
   * 是近白），写死 #3b4148 在深色主题下会变成一块"比底色还深"的死灰。
   * color-mix 在两个主题里**各自**取两端的中点，深浅关系永远成立：
   *   TOP30 比 TOP100(--text-2) 更深一点，又比黑(--text) 浅一些。
   *
   * 50% 是刻意的：偏任一端都会和相邻那一档混在一起（这次要的就是
   * "介于两者之间"，不是"更深"或"更浅"）。color-mix 全站已在用
   * （.badge--status 等），不用额外加前缀。
   */
  background: var(--surface-2);
  color: color-mix(in srgb, var(--text) 50%, var(--text-2));
}

/*
 * TOP100 这一档和兜底值一样是 14px，但仍然**显式写一条**：
 * 哪天有人把 .topicline 的兜底值调大（比如整体提到 16px），
 * 有这条在，TOP100 依然停在 14px，不会静默跟着变成最大的一档。
 */
.topicline[data-topicline='TOP100'] {
  font-size: 14px;
}

.topicline[data-topicline='TOP100'] .chip--topic {
  font-size: 14px;
  /* 深灰（--text-2）：比上面那档的黑退一档，但仍清楚可读 */
  background: var(--surface-2);
  color: var(--text-2);
}

.ti {
  display: inline-block;
  white-space: nowrap;
}

.ti__dot {
  display: inline-block;
  /* em：圆点间距跟着所在那一档的字号缩放，三档不用各写一条 */
  margin: 0 0.55em;
  color: var(--text-3);
  font-style: normal;
  /* 配套 .chip--topic 的 vertical-align: middle（见那里的说明），别单独删 */
  vertical-align: middle;
}

/* 最后一个话题后面不跟圆点 */
.ti:last-child .ti__dot {
  display: none;
}

/* 话题榜骨架：短横条，宽度不一才像一行文字（尺寸按最小那一档字号给，够用即可） */
.sk--topic {
  display: inline-block;
  width: 72px;
  height: 18px;
  margin-right: 12px;
  vertical-align: middle;
}

/* ── 发帖页的话题输入 ──────────────────────────────────────────── */
/*
 * 一行可增长的"输入区"：已选话题是实心 chip，后面跟着一个裸输入框，
 * 空状态时用灰色占位提示（#至少两个话题）。
 *
 * 为什么不用 <input> 的 placeholder：占位提示要能被"点上去就消失"，
 * 且要在有 chip 时让位；原生 placeholder 做不到在有值时还显示提示。
 */
.tagfield {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  cursor: text;
  min-height: 42px;
}

.tagfield:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-weak);
}

.tagfield__chips {
  display: contents;
  list-style: none;
  margin: 0;
  padding: 0;
}

.tagchip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--brand-weak);
  color: var(--brand);
  border-radius: 999px;
  padding: 4px 6px 4px 10px;
  font-size: 13px;
}

.tagchip__x {
  border: 0;
  background: none;
  color: inherit;
  opacity: 0.65;
  font-size: 15px;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
}

.tagchip__x:hover {
  opacity: 1;
}

/*
 * 灰色占位提示。**刻意做得不像 chip**：
 * 无背景、无边框、无 × 按钮、颜色更淡 —— 用户一眼能看出"这不是一个已添加的话题"。
 * 若长得和 .tagchip 一样，会被误认为已经加了一个话题（而它其实是提示文字）。
 */
.tagfield__ghost {
  color: var(--text-3);
  font-size: 13px;
  user-select: none;
  pointer-events: none;
}

.tagfield__input {
  flex: 1 1 90px;
  min-width: 90px;
  border: 0;
  background: none;
  outline: none;
  font-size: 14px;
  color: var(--text);
  font-family: inherit;
  padding: 4px 2px;
}

/* ── 搜索页 ────────────────────────────────────────────────────── */
.seg--inline {
  margin: 12px 0 0;
  align-self: flex-start;
}

.input--sm {
  padding: 5px 10px;
  font-size: 13px;
  width: auto;
  flex: 1 1 auto;
  min-width: 120px;
}

/* ── 响应式 ────────────────────────────────────────────────────── */

/*
 * 移动端：顶栏导航被隐藏（见上面 700px 断点），搜索框要放开占满剩余宽度 ——
 * 否则小屏用户根本没有搜索入口。
 *
 * 2026-09-22 加强：顶栏一行要塞「品牌 + 搜索框 + 铃铛 + 语言 + 头像+用户名」，
 * 登录后右侧的头像和用户名会被挤出屏外（看着像和下面的内容不对齐）。
 * 所以小屏**直接隐藏品牌名** —— 原来只是把字号降到 15px，省不够。
 * 品牌识别由页面内容承担，顶栏的有限宽度优先给搜索和用户区。
 */
@media (max-width: 700px) {
  .topbar {
    gap: 8px;
    padding: 8px 10px;
  }

  /*
   * 品牌位在手机上也显示（2026-09-24 最终决定）。
   * 中间反复过一次：先隐藏 → 换 logo 时短暂放开 → 又隐藏 → 最终用户要求显示。
   * 结论是 28px 的 logo 比原来的文字品牌「布鲁网」三个字（约 56px）还窄，代价可接受；
   * 而移动端宽度靠**隐藏用户名**（见下 .usermenu__name）补给搜索框，
   * 不是靠砍品牌位 —— 品牌位是站点的锚点，砍掉等于小屏没有回家的入口。
   */
  .topbar__brand {
    flex-shrink: 0;
  }

  .searchbox {
    flex: 1 1 auto;
    /*
     * 登录后右侧多出「头像 + 用户名」，搜索框会被压到只剩一两个字宽。
     * 对策是先把它右边那排挤紧（见下），再给搜索框抬一个下限 ——
     * 下限太低时输入框窄到点不中，比挤一挤头像更难受。
     */
    min-width: 96px;
  }

  /* 用户区（头像 + 用户名）不参与压缩，保证始终显示在屏内 */
  .topbar__right {
    flex-shrink: 0;
    gap: 5px;
  }

  /*
   * 中频工具区同样不参与压缩：小屏顶栏是「搜索 + 榜 + EN + 头像」，
   * 让它们跟着搜索框一起被压扁，图标和语言按钮会先变成不可点的小点。
   */
  .topbar__tools {
    flex-shrink: 0;
    gap: 5px;
  }

  /* 圆形入口 34→30，语言按钮内边距与字号下调 —— 省下的宽度都给搜索框 */
  .trophy {
    width: 30px;
    height: 30px;
    font-size: 15px;
  }

  .langbtn {
    padding: 3px 8px;
    font-size: 12px;
  }

  /*
   * 用户名在小屏整块隐藏，只留圆形头像（2026-09-24）。
   * 原先是 max-width: 48px 截断 —— 但截断后的两三个字既认不出是谁，
   * 又白白占掉 50px 宽度，而这些宽度本可以给搜索框。
   * 名称信息没有丢：点进去是个人中心，头像旁边 title 也有完整名字。
   *
   * ⚠️ 名字一藏，触发器 padding 的右侧 10px 就空了，胶囊会变成左窄右宽的
   *    怪形状 —— 所以 padding 和 gap 要一起归零，让它收成一枚正圆头像。
   */
  .usermenu__trigger {
    padding: 3px;
    gap: 0;
  }

  .usermenu__name {
    display: none;
  }

  .tagfield__input {
    flex: 1 1 100%;
    min-width: 100%;
  }
}

/* ------------------------------------------------------------------ 后台积分管理（2026-09-21） */

/*
 * 规则表在窄屏上会被压成一列一个字符，所以允许横向滚动而不是硬折行 ——
 * 「积分值 / 每日上限」是有对应关系的列，折行后行与行的对应就断了。
 */
.table-wrap {
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

.table th,
.table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  text-align: left;
  white-space: nowrap;
}

.table th {
  color: var(--text-2);
  font-weight: 500;
  font-size: 13px;
}

.table .cell-center {
  text-align: center;
}

.table .cell-reason {
  white-space: normal;
  min-width: 140px;
}

/* 数字输入窄一点：积分值最多 5 位，给一整行宽度反而难对齐 */
.input--num {
  width: 90px;
}

.filters--wrap {
  flex-wrap: wrap;
}

.points-settings {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}

.points-setting .field {
  margin-bottom: 4px;
}

.ledger-wrap {
  margin-top: 12px;
}

/* 等级徽章单元格（2026-09-21）：预览 + 上传 + 清空挤在一格，窄屏允许换行 */
.cell-badge {
  min-width: 230px;
}

.badge-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* 用虚线框表示"这是占位"：未设置徽章时不能只是一片空白，
   否则管理员看不出那里可以放图 */
.badge-cell__preview {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.badge-cell__preview img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.cell-actions {
  white-space: nowrap;
}

.cell-actions .btn + .btn {
  margin-left: 4px;
}

/* ------------------------------------------------------------------ 特殊等级：会员名单（行内展开） */

/* 展开行用淡底色与上面的等级行区分开 —— 否则它会看起来像"另一个等级"。
   浅到不用边框也能看出层级，但足够让人知道这几行是附属信息。 */
.special-members-row > td {
  background: color-mix(in srgb, var(--brand) 5%, transparent);
}

/* 「设置」按钮的展开态：同一个按钮兼作开关，得让人看出当前是开着的 */
[data-special-members].is-active {
  border-color: color-mix(in srgb, var(--brand) 55%, transparent);
  color: var(--brand);
}

.special-members {
  padding: 4px 2px;
}

.special-members__head {
  margin: 0 0 8px;
  font-size: 13px;
  color: var(--text-2);
}

.special-members__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

.special-members__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  font-size: 14px;
}

.special-members__name {
  font-weight: 600;
}

/* 名单里出现已禁用 / 已注销的会员时给一个显眼标记：
   管理员看名单的目的常常是"要不要收回"，账号已经废了却没提示等于白列一遍 */
.special-members__bad {
  font-size: 12px;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--danger) 45%, transparent);
  color: var(--danger);
}

/* ==========================================================================
   移动端：帖子卡片统计行单行显示（2026-09-22）
   ========================================================================== */

/*
 * 浏览 / 评论 / 点赞 / 收藏 四项在窄屏会折成两行，卡片被撑高、列表高低错落。
 * 这四项都是「图标 + 数字」的短单元，挤在一行放得下 ——
 * 所以强制不换行，并把间距与字号各收一档来腾空间。
 * 数字本身不许被压断，给 .stat 一个不收缩。
 */
@media (max-width: 700px) {
  .card__stats {
    flex-wrap: nowrap;
    gap: 10px;
    font-size: 11px;
  }

  .card__stats .stat {
    flex: 0 0 auto;
    gap: 3px;
  }

  .card__stats .icon {
    width: 13px;
    height: 13px;
  }
}

/* ==========================================================================
   iOS Safari 输入框聚焦自动放大（2026-09-24 修）
   --------------------------------------------------------------------------
   现象：iPhone 上点用户名/密码框，整个页面被放大并撑出屏幕；登录成功跳转后
   **仍停在这个放大比例**，要用户手动双指缩小才恢复。安卓 Chrome 没有这个行为
   （同一份代码在安卓上完全不复现），所以它是 iOS 独有的。

   成因：Safari 有一条"可读性缩放"规则 —— **聚焦元素的 font-size < 16px 时，
   它认为文字太小，自动把 viewport 缩放上去**。本项目 .input 是 14px、
   .searchbox__input 是 13px、.input--sm 是 13px，全部踩线。

   为什么缩放不复位：本站是哈希路由的 SPA，登录成功只是换 hash，**不发生页面
   加载** —— 而缩放复位只发生在页面加载时。所以放大状态会一直带到下一个页面。

   对策：在触摸设备上把所有可输入控件的字号提到 16px（Safari 的临界值就是 16，
   16 本身不会触发）。

   ⚠️ 用 `pointer: coarse`（触摸设备）而不是 `max-width` 宽度断点：
      iPad 的视口是 768 / 1024，宽度断点兜不住，但它同样会触发这个缩放。

   ⚠️ 不要用 `user-scalable=no` / `maximum-scale=1` 去禁缩放：
      iOS 10 起 Safari 直接忽略这两项（治不了），而且禁掉双指缩放是明确的
      可访问性倒退 —— 视障用户靠它读内容。

   ⚠️ 这条必须放在文件末尾：.input / .input--sm / .searchbox__input 各自都有
      font-size 声明，特异性与这里相同，靠"后来者胜"才覆盖得掉。
      移到前面去就失效了（不会报错，只是静默无效）。
   ========================================================================== */
@media (pointer: coarse) {
  .input,
  .input--area,
  .input--sm,
  .input--num,
  .select,
  .searchbox__input,
  .searchbar__input,
  .tagfield__input,
  input,
  select,
  textarea {
    font-size: 16px;
  }

  /*
   * 顶栏搜索框例外：13px → 16px 会让它长高三四像素，把整条 TAB 顶高。
   * 上下 padding 各减 2px 抵掉，盒子高度基本不变（这也是它和别的输入框的差别 ——
   * 别处高一点无所谓，顶栏高度一动整页都要跟着动）。
   */
  .searchbox__input {
    padding: 5px 14px;
  }
}

/* ==========================================================================
   富文本编辑器（2026-09-26 新增）
   只做文字排版：加粗 / 颜色 / 字号 / 段落 / 换行。
   工具栏在小屏上自动换行，按钮统一 32px 高，够手指点。
   ========================================================================== */

.rte {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  /*
   * 这里**不能**用 overflow: hidden：输入区右下角的拖拽把手（resize: vertical）
   * 会被外框的圆角裁掉一角，看着像"拉不动"。
   * 圆角改由内部两块自己负责（工具栏上圆角、输入区是透明背景无需圆角）。
   */
}

.rte__bar {
  display: flex;
  /* 工具栏强制单行：放不下时横向滚动，而不是换行挤到第二行 */
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  padding: 8px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: calc(var(--radius-sm) - 1px) calc(var(--radius-sm) - 1px) 0 0;
  overflow-x: auto;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}

/* 移动端（≤700px）：工具栏恢复换行，不再横向滚动 */
@media (max-width: 700px) {
  .rte__bar {
    flex-wrap: wrap;
    overflow-x: visible;
  }
}

.rte__sep {
  width: 1px;
  height: 20px;
  background: var(--border);
  margin: 0 2px;
}

.rte__btn {
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
  cursor: pointer;
}

.rte__btn:hover {
  border-color: var(--brand);
  color: var(--brand);
}

.rte__btn--bold {
  font-weight: 700;
}

.rte__swatches {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}

.rte__swatch {
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  cursor: pointer;
}

.rte__swatch:hover {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.rte__swatch--reset {
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}

.rte__sizes {
  display: inline-flex;
  gap: 4px;
}

.rte__input {
  /*
   * resize: vertical —— 右下角可拖拽拉高，和原来 textarea 的手感一致。
   * ⚠️ resize 只作用于 overflow 不是 visible 的元素，下面的 overflow-y
   *    不能改成 visible，否则把手直接消失（不报错，静默失效）。
   * max-height 只当护栏（防拖到几千像素），上限给得宽，日常拖不到。
   */
  resize: vertical;
  min-height: 170px;
  max-height: 900px;
  overflow-y: auto;
  padding: 12px 14px;
  /* iOS 在字号小于 16px 的输入框聚焦时会把整页放大，这里不给它机会 */
  font-size: 16px;
  line-height: 1.8;
  color: var(--text);
  outline: none;
  word-break: break-word;
}

.rte__input:focus {
  box-shadow: inset 0 0 0 2px var(--brand-weak);
}

.rte__input p {
  margin: 0 0 8px;
}

.rte__input p:last-child {
  margin-bottom: 0;
}

.rte__input:empty::before {
  content: attr(data-placeholder);
  color: var(--text-3);
}

/* ── 后台积分规则：该行有未保存改动（2026-09-30） ──
   规则表是逐行保存的，改了不点保存不会进库。给改动行加底色 + 左侧色条，
   配合顶部的「N 处改动未保存」，让"改了但没存"一眼可见。 */
.table tbody tr.is-dirty {
  background: rgba(240, 160, 32, 0.10);
}

.table tbody tr.is-dirty td:first-child {
  box-shadow: inset 3px 0 0 rgba(240, 160, 32, 0.85);
}

/* ── 后台帖子管理：勾选 + 批量操作条（2026-09-30 新增） ──
   批量条的定位是「常驻但不可用」：藏起来等于没有这个功能，
   置灰则是在告诉管理员这里能做什么。 */
.bulkbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 12px;
  margin-bottom: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.bulkbar__all {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  cursor: pointer;
  user-select: none;
}

.bulkbar__count {
  font-size: 14px;
  font-weight: 600;
  min-width: 72px;
}

.bulkbar .select {
  width: auto;
  min-width: 180px;
}

/* 勾选框单独一列，不参与 row__main 的挤压 */
.row__pick {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  cursor: pointer;
}

.row__pick input {
  width: 16px;
  height: 16px;
  cursor: pointer;
}

/* 已勾选的行给个底色：批量删之前一眼看清自己勾了哪几篇 */
.row--picked {
  background: rgba(240, 160, 32, 0.08);
  border-color: rgba(240, 160, 32, 0.55);
}

/* 用户名搜索命中的账号 chips */
.authorchips {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin: -6px 0 12px;
}

/* ============================================================================
 * 表情系统（emoji-picker / emoji-render）
 * 约束：面板只展示目录内表情；不引入第三方库，不扫描原生 unicode。
 * 浮层挂在 body 上，沿用全局 --surface/--border/--text 等变量，自动适配深色主题。
 * ========================================================================== */

/* 渲染出的表情图片：与文字基线居中对齐，不撑大行高 */
img.emoji {
  display: inline-block;
  vertical-align: -0.2em;
  width: 20px;
  height: 20px;
  margin: 0 1px;
  border: 0;
  object-fit: contain;
}

/* 正文工具栏里的表情按钮：复用 .rte__btn 基础风格 */
.rte__emoji-btn {
  font-size: 16px;
  line-height: 1;
  padding: 0 8px;
}

/* 评论框里的表情按钮 */
.cmt-emoji {
  min-width: 34px;
  height: 34px;
  padding: 0 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  flex: 0 0 auto;
}
.cmt-emoji:hover {
  border-color: var(--brand);
  color: var(--brand);
}

/* 评论表单：表情按钮与提交按钮并排 */
.cmt-form__row {
  display: flex;
  align-items: center;
  gap: 8px;
}
.cmt-emoji-host {
  display: inline-flex;
  align-items: center;
}
.cmt-form__row .btn--sm {
  margin-left: auto; /* 提交按钮靠右 */
}

/* 表情选择浮层 */
.emoji-popover {
  position: fixed;
  z-index: 1000;
  width: 268px;
  max-height: 300px;
  display: flex;
  flex-direction: column;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
  /* 防桌面端文本被选中时浮层闪动 */
  user-select: none;
}

.emoji-popover__search {
  width: 100%;
  height: 32px;
  padding: 0 10px;
  margin-bottom: 8px;
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
  outline: none;
}
.emoji-popover__search:focus {
  border-color: var(--brand);
}

.emoji-popover__grid {
  flex: 1 1 auto;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 4px;
  padding-right: 2px;
}

.emoji-grid__item {
  height: 36px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.emoji-grid__item:hover,
.emoji-grid__item:focus-visible {
  background: var(--surface-2);
  border-color: var(--border);
  outline: none;
}

.emoji-popover__empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: 16px 0;
  font-size: 13px;
}

/* ------------------------------------------------------------------ 首页轮动公告栏（2026-10-07 新增） */

/* 类别导航行：左为类别胶囊（可横滑），右为公告栏（仅 PC 端显示）。
   JS 端已用 matchMedia 在移动端不渲染公告栏，这里再叠一层 CSS 隐藏做双重保险。 */
.catnav-row {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 18px;
}

/* 类别胶囊在 flex 行里要能收缩，把右侧空间让给公告栏 */
.catnav-row .catnav-text {
  flex: 1 1 auto;
  min-width: 0;
  margin-bottom: 0; /* 独立时有 18px 下边距，放进行里由 .catnav-row 统一控制 */
}

.announce-bar {
  flex: 0 0 auto;
  max-width: 42%;
  text-align: right;
  opacity: 0;
  transition: opacity 0.22s ease;
  font-size: 13px;
  color: var(--text-2);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.announce-bar.is-visible {
  opacity: 1;
}

.announce-item {
  color: var(--text-2);
  text-decoration: none;
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: middle;
}

.announce-item:hover {
  color: var(--accent, #2b6cff);
}

.announce-date {
  color: var(--text-3, #9aa0aa);
  font-variant-numeric: tabular-nums;
}

/* 移动端：公告栏完全不显示（与 JS 端 matchMedia 判定双重保险） */
@media (max-width: 640px) {
  .announce-bar {
    display: none !important;
  }
  .catnav-row {
    display: block;
  }
  .catnav-row .catnav-text {
    margin-bottom: 18px;
  }
}

/* ------------------------------------------------------------------ 后台公告管理（2026-10-07 新增） */

.ann-check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: end;
  padding-bottom: 9px;
  font-size: 13px;
  color: var(--text-2);
  white-space: nowrap;
  cursor: pointer;
}

.ann-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  margin-bottom: 8px;
}

.ann-row__title {
  font-weight: 600;
  color: var(--text);
  flex: 0 0 auto;
  max-width: 240px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.ann-row__link {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-2);
  text-decoration: none;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 13px;
}

.ann-row__link:hover {
  color: var(--accent, #2b6cff);
  text-decoration: underline;
}

.ann-row__ops {
  flex: 0 0 auto;
  display: inline-flex;
  gap: 10px;
}

.linkbtn--danger {
  color: #d23b3b;
}

.badge--ok {
  background: rgba(40, 160, 90, 0.14);
  color: #2a9d5a;
}

.badge--muted {
  background: var(--surface-2, #eceff3);
  color: var(--text-2);
}
