/* ============================================================
 * 组件层 · 按钮
 * ============================================================
 *
 * 迁移前，按钮在 style.css 里有 27 处定义。`.btn-primary` 的最终外观是三处
 * 叠加的结果：内边距来自第 319 行、圆角与字重来自 3155 行、渐变与阴影来自
 * 3550 行。想把按钮改小一点，得先读完 6187 行才知道该改哪一处。
 *
 * 这里每个变体只定义一次。数值取自迁移前**真实生效**的声明，因此除下面
 * 「有意统一」列出的几项外，界面不变样。
 *
 * ## 有意统一的三处
 *
 * 1. `.btn-premium` 圆角 8px → `--radius-lg`（20px）。它与 `.btn-primary`
 *    并排出现在个人中心，一个方一个圆，正是诉求 #5 说的「没有统一」。
 * 2. `.btn-premium` 内边距 11px 20px → 与 `.btn` 一致。差 1px 的上下内边距
 *    会让并排的两个按钮高度差 2px，看着像没对齐。
 * 3. `.btn-secondary` 白字白框 → 令牌色。它原本只为深色 Hero 背景设计，
 *    放到浅色卡片上是白底白字，等于隐形。
 *
 * ## 删掉的死代码（诉求 #6）
 *
 * `.btn-large`、`.btn-submit` 在模板与 JS 中零引用，随本次迁移移除。
 * `.btn-action` 在 JS 中用了 15 次，但只有 admin/index.html 内联定义了它，
 * 全部调用点也都在 admin.js —— 属于后台页，不在本层范围。
 */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-6);

    /* 触摸目标下限。低于这个高度，手指点按的失败率明显上升，
       而失败的点按在移动端表现为「按钮没反应」。 */
    min-height: 44px;

    border: none;
    border-radius: var(--radius-md);
    font-family: inherit;
    font-size: var(--font-size-md);
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
    transition: transform var(--transition-fast),
                box-shadow var(--transition-fast),
                background-color var(--transition-fast),
                color var(--transition-fast);
}

.btn:disabled,
.btn[aria-disabled='true'] {
    opacity: 0.55;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

/* ── 主按钮 ──
 *
 * `.btn-send-chat` / `.btn-premium` 是同一外观的历史别名，分别散落在聊天页、
 * 个人中心和发布表单。合并到一处而不是改模板，是因为改模板需要同时改 JS 里
 * 的字符串拼接，风险大于收益；名字统一留给诉求 #6 的后续清理。
 *
 * `.btn-publish` 不在此列：它只出现在 admin/index.html，且那页自带内联样式，
 * 与前台四页无关。
 */
.btn-primary,
.btn-send-chat,
.btn-premium {
    background: var(--gradient-brand);
    color: var(--color-on-brand);
    border: none;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-brand);
}

.btn-primary:hover,
.btn-send-chat:hover,
.btn-premium:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-brand-strong);
}

.btn-primary:active,
.btn-send-chat:active,
.btn-premium:active {
    transform: translateY(0);
}

/* ── 次级按钮 ── */
.btn-secondary {
    background: var(--color-surface);
    color: var(--color-brand-strong);
    border: 1px solid var(--color-border);
}

.btn-secondary:hover {
    background: var(--color-surface-solid);
    border-color: var(--color-brand);
}

/* ── 描边按钮 ── */
.btn-outline {
    background: transparent;
    color: var(--color-brand-strong);
    border: 2px solid var(--color-brand);
}

.btn-outline:hover {
    background: var(--color-brand);
    color: var(--color-on-brand);
}

/* ── 尺寸修饰符 ──
 *
 * 桌面版个人中心把「关注 / 聊天 / 拉黑」收进了 240~300px 宽的左侧身份卡，
 * 默认尺寸的 .btn 在那里一行只放得下一颗。收窄内边距而不是收窄 min-height：
 * 44px 是触摸目标下限，这三颗按钮在手机上也是同一批。
 */
.btn-sm {
    padding: var(--space-2) var(--space-3);
    font-size: var(--font-size-sm);
}

/* ── 布局修饰符 ── */
.btn-full {
    width: 100%;
}

/* 聊天页的发送按钮要窄一档，否则和输入框挤在一行会换行。
   原 style.css 里写的是 `height: 42px`——低于 44px 触摸下限，改回
   min-height 由 .btn 统一控制，只收窄左右内边距。 */
.btn-send-chat {
    padding: var(--space-2) var(--space-4);
    font-size: var(--font-size-sm);
}

/* 上下文变体。迁移前分别写在 style.css 第 1583、3127 行，
   保留是因为这两处确实需要不同尺寸，不是重复定义。 */
.payment-actions .btn {
    flex: 1;
}

.comment-input-area button,
.comment-input-area .btn-primary {
    /* 评论框是胶囊形，按钮跟着圆到底才不显得突兀。选择器保留 `button`：
       评论区里有几个按钮没挂 .btn 类，只按类名迁移会让它们变回方角。 */
    border-radius: var(--radius-full);
    padding: var(--space-3) var(--space-5);
}

/* ============================================================
 * 组件层 · 表单控件
 * ============================================================
 *
 * 迁移前，表单控件在 style.css 与两个模板的内联块里共 12 处定义。同一个
 * `.form-group label`，三页三个样子：
 *
 *   个人中心   #7c2d12 / 600 / 0.90rem   ← style.css:3394 胜出
 *   发布弹窗   #7c2d12 / 700 / 0.88rem   ← style.css:4215+4303 特异性更高
 *   登录注册   #431407 / 600 / 0.85rem   ← index.html 内联块，不加载 style.css
 *
 * 字号差 0.02rem、字重差一档、颜色差一个色阶——每一处单看都说得通，
 * 并排看就是诉求 #5 说的「没有统一」。这里收敛成一份。
 *
 * 输入框数值取自迁移前**真实生效**的规则，即第 3030 行那个 `!important` 块
 * （1px 暖橙边框 + 12px 圆角 + 0.8 白底），不是第 1006 行的 2px 灰蓝版本
 * ——那条早被压死，照抄它反而会让界面变样。
 *
 * ## 有意统一的三处
 *
 * 1. label 统一为 `--color-text-muted` / 600 / `--font-size-sm`。
 *    取三页里占多数的那组值（两页已是 #7c2d12）。
 * 2. 聚焦环 `rgba(245,158,11,0.15)` → `--focus-ring`（0.22 浓度）。
 *    0.15 在暖底上几乎看不见，等于聚焦无反馈。
 * 3. 禁用态。迁移前 `.form-group input:disabled` 写在个人中心内联块里，
 *    却被 style.css:3030 的 `!important` 背景压死——邮箱、用户名、身份
 *    三个禁用字段和可编辑字段长得一模一样，用户点进去才发现打不了字。
 *    现在它们有可见的禁用底色。
 */
.form-group {
    /* 迁移前三处分别是 20px / 18px / 20px。取令牌里的 16px 而非 24px：
       登录页的 body 是 `overflow: hidden`，注册表单有五组字段，往上取一档
       会让提交按钮在矮屏手机上被推出视口且无法滚动。 */
    margin-bottom: var(--space-4);
}

.form-group label {
    display: block;
    margin-bottom: var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    font-weight: 600;
}

.form-group input,
.form-group textarea,
.form-group select {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    transition: border-color var(--transition-fast),
                box-shadow var(--transition-fast),
                background-color var(--transition-fast);
}

.form-group input::placeholder,
.form-group textarea::placeholder {
    color: var(--color-text-muted);
    opacity: 0.75;
}

.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
    outline: none;
    border-color: var(--color-brand);
    background: var(--color-surface-solid);
    box-shadow: var(--focus-ring);
}

.form-group input:disabled,
.form-group textarea:disabled,
.form-group select:disabled {
    background: var(--color-surface-disabled);
    color: var(--color-text-muted);
    cursor: not-allowed;
}

.form-row {
    display: flex;
    gap: var(--space-4);
}

.form-row .form-group {
    flex: 1;
    min-width: 0; /* 输入框的 width:100% 在 flex 行里不再撑破容器 */
}

/* 窄屏改为纵向。两栏在 576px 以下每栏不足 130px，中文标签会折行，
   标签比输入框还高。阈值对齐个人中心原有的 576px，不另起一档。 */
@media (max-width: 576px) {
    .form-row {
        flex-direction: column;
        gap: var(--space-5);
    }
}

/* ── 带图标的输入框 ──
 *
 * 登录注册页和广场搜索框都是「输入框内嵌一个图标」，迁移前各写一份：
 * 前者在 index.html 内联块里，后者靠 square.html 的 style 属性 +
 * style.css 第 2088 行的 `!important` 拼出来。两处图标的左边距差 3px、
 * 颜色一个灰一个橙。
 *
 * 图标必须写在 input **之后**（`input:focus + i` 靠相邻兄弟选择器变色），
 * 视觉上靠绝对定位挪回左边——两个模板的 DOM 顺序都已符合。
 */
.input-group {
    position: relative;
}

.input-group i {
    position: absolute;
    left: var(--space-3);
    top: 50%;
    transform: translateY(-50%);
    color: var(--color-text-muted);
    /* 点在图标上要能聚焦到输入框，否则用户以为点歪了。 */
    pointer-events: none;
    transition: color var(--transition-base);
}

/* 给图标让位：12px 左边距 + 约 16px 字形 + 12px 间隙 = 40px。 */
.input-group input {
    padding-left: calc(var(--space-4) + var(--space-5));
}

.input-group input:focus + i {
    color: var(--color-brand);
}

/* 身份字段的角色色条。个人中心把用户身份放在一个 disabled 输入框里显示，
   靠左边框的颜色区分——JS 在 js/profile.js:219 挂这几个类。
   放在组件层是因为迁移后 `.form-group input:disabled` 的底色统一了，
   这三条得跟着改，否则色条和底色对不上。 */
.form-group input#prof-identity {
    font-weight: 600;
    letter-spacing: 0.5px;
    border-left: 4px solid var(--color-border-strong);
    padding-left: var(--space-3);
}

.form-group input#prof-identity.photographer-badge {
    border-left-color: var(--color-brand);
    color: var(--color-brand-strong);
    background: var(--color-brand-tint);
}

.form-group input#prof-identity.client-badge {
    border-left-color: var(--color-success);
    color: var(--color-success);
    background: var(--color-success-tint);
}

.form-group input#prof-identity.admin-badge {
    border-left-color: var(--color-accent);
    color: var(--color-text-muted);
    background: rgba(245, 158, 11, 0.12);
}

/* ============================================================
 * 组件层 · 标签与角标
 * ============================================================
 *
 * 迁移前，帖子卡片上的四种标签在 style.css 里共 11 处定义，其中
 * `.post-tag` 被写了两遍，而**后写的那遍是绿色**：
 *
 *   第 3688 行   橙底橙字   rgba(251,146,60,0.12) / #ea580c
 *   第 5201 行   绿底绿字   rgba(16,185,129,0.10) / #059669   ← 实际生效
 *
 * 两条都带 `!important`，同特异性下后者胜出。于是同一张卡片里，题材标签
 * 是绿的、价格标签是橙的、地点图标又是橙的——三个并排的小色块三种色系，
 * 这正是诉求 #5 说的「不美观、没有统一」。
 *
 * 这里统一回品牌橙。取橙不取绿的理由：`.price-tag`、`.location-tag i`、
 * 导航高亮、按钮渐变全站都是橙，绿色只有 `.post-tag` 这一处（而且是被
 * 后加的一条规则意外改成的）——改一处比改全站合理。
 *
 * ## 删掉的死代码（诉求 #6）
 *
 * `.badge-photographer` / `.badge-client`（style.css:3291、3301）在前台四页
 * 零引用：后台 admin/index.html 用的是 `.badge.photographer` 两段式类名，
 * 且那页不加载 style.css。这两条从写下起就没匹配过任何元素。
 */

/* 题材标签。卡片正文下方那一排 #风光 #人像。 */
.post-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

/* 卡片里的标签行锁成一行高，无标签的帖子也照样占住这块位置。
   组件对每条帖子都渲染 `<div class="post-tags">`（可能是空的），不锁高度的话
   「有标签」和「没标签」两张卡片差一整行——诉求「所有帖子大小保持一致」。
   overflow:hidden 是给超长标签兜底：宁可裁掉第二行，也不让某张卡片变高。 */
.post-card .post-tags {
    height: calc(var(--line-height-base) * var(--font-size-xs) + 4px);
    overflow: hidden;
}

.post-tag {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--color-brand-wash);
    color: var(--color-brand-strong);
    font-size: var(--font-size-xs);
    font-weight: 600;
    transition: background-color var(--transition-fast);
}

.post-tag:hover {
    background: var(--color-brand-tint);
}

/* 价格。卡片底栏左侧，与地点左右分列。 */
.price-tag {
    display: inline-flex;
    /* 报价不能截也不能压：数字少一位就是错的信息。压缩额度全留给地名。 */
    flex: 0 0 auto;
    align-items: center;
    padding: 2px var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-brand-wash);
    color: var(--color-brand-strong);
    font-size: var(--font-size-sm);
    font-weight: 700;
    white-space: nowrap;
}

/* 需求帖的「预算」用青色，和作品帖的「报价」区分开——同一个位置上，
   一个是我出多少钱、一个是我收多少钱，颜色是唯一的区分信号。
   迁移前这也是 JS 模板串里的行内 style（渐变底 + var(--sea-teal-text)），
   见 .card-badge--request 处的说明。 */
.price-tag--budget {
    background: var(--color-success-tint);
    color: var(--color-success);
}

/* 地点。迁移前桌面端 #7c2d12、移动端被改成 #9ca3af（冷灰）——同一个
   信息在手机上突然褪色成"不重要"，而它和价格是并列关系。统一用 muted。 */
.location-tag {
    display: inline-flex;
    /* 底条里只有它能被压缩，所以 min-width: 0——flex 项默认最小尺寸是 auto，
       不清零的话它撑着最小内容宽度不让，底条整条溢出卡片。 */
    flex: 0 1 auto;
    align-items: center;
    gap: var(--space-1);
    min-width: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    font-weight: 600;
}

.location-tag i {
    flex: 0 0 auto;
    color: var(--color-brand);
}

/* 地名是变长的（「杭州·西湖景区」是「上海」的三倍），底条宽度却由卡片定死。
   省略号写在这层内嵌 span 上而不是 .location-tag 上：后者是 inline-flex，
   裸文字节点在 flex 容器里算匿名 flex 项，text-overflow 对匿名项无效，
   结果是硬裁在半个字上、连「…」都没有。 */
.location-tag-text {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* 封面右上角的类型角标（作品 / 需求）。压在图片上，所以要毛玻璃底。 */
.card-badge {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    /* 与 .post-card::before 那层白色渐变同为 z-index 1，靠 DOM 顺序决胜——
       角标在渐变之后出现，所以盖在上面。迁移前写的是 2（桌面）/ 5（移动），
       两个数字都只是「比 1 大就行」，没有第三方参与竞争。 */
    z-index: var(--z-base);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-full);
    background: rgba(255, 255, 255, 0.85);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    box-shadow: var(--shadow-sm);
    font-size: var(--font-size-xs);
    font-weight: 700;
}

/* 「约拍需求」用青色区分于「作品集」的橙。迁移前这条是写在 JS 模板串里的
   `style="color: var(--sea-teal-text)"` —— 行内样式特异性最高，组件层再怎么
   统一都盖不过它，等于给这一族开了个后门。改成修饰类后，四个渲染点
   （js/square.js:876、js/profile.js:562 及各自的作品集分支）共用本层定义。 */
.card-badge--request {
    color: var(--color-success);
}

/* 窄屏收一档。角标压在 165px 高的封面上，桌面尺寸会盖住三分之一画面。
   题材标签整排隐藏：移动端卡片把「节省高度」做到了极致，容器在 768px
   以下不占位（迁移前同一条规则写在 style.css 的 .post-card .post-desc
   边上，现在随本族归位）。 */
@media (max-width: 768px) {
    .card-badge {
        top: var(--space-2);
        right: var(--space-2);
        padding: 2px var(--space-2);
    }

    .post-tags {
        display: none;
    }
}

/* ── 身份角标 ──
 *
 * 聊天页联系人列表里的「摄影师 / 客户 / 管理员」。迁移前形状不一致，
 * 原因见 style.css 第 3291 行处的说明：两个变体带 `!important` 拿到胶囊形，
 * 第三个没有、只能吃基础样式的方角。这里三个变体只在颜色上分叉。
 *
 * 颜色改用主题令牌：旧值是蓝(#3b82f6)/紫(#a855f7)/琥珀，是站点换成暖橙
 * 主题之前留下的，紫色在整站已无第二处出现。
 */
.contact-badge {
    display: inline-flex;
    /* 身份徽标不参与收缩：联系人行里能让位的是昵称（它带省略号），
       徽标一被压就把「摄影师」挤成「摄影…」。 */
    flex: 0 0 auto;
    align-items: center;
    padding: 2px var(--space-2);
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    font-size: var(--font-size-xs);
    font-weight: 600;
    white-space: nowrap;
}

.contact-badge.badge-photographer {
    background: var(--color-brand-wash);
    color: var(--color-brand-strong);
    border-color: var(--color-border-strong);
}

.contact-badge.badge-client {
    background: var(--color-success-tint);
    color: var(--color-success);
    border-color: rgba(13, 148, 136, 0.3);
}

.contact-badge.badge-admin {
    background: rgba(245, 158, 11, 0.15);
    color: var(--color-text-muted);
    border-color: rgba(245, 158, 11, 0.35);
}

/* ============================================================
 * 组件层 · 吐司提示
 * ============================================================
 *
 * 迁移前站内有**三套**吐司，其中两套共用 `.toast-title` 这个类名：
 *
 *   .toast            style.css:1627   右下角深色块。前台四页零引用——
 *                                      唯一的使用点 admin/index.html:503
 *                                      不加载 style.css，自带内联定义。死代码。
 *   .notification-toast  style.css:2970  互动通知（诉求 #2 的产出物）。
 *                                      右下角，头像 + 标题 + 描述 + 关闭。
 *   .toast-item       style.css:3905    全局提示（window.showToast）。
 *                     index.html:255    右上角，图标 + 标题 + 正文。
 *
 * ## 同一个组件，两页两个样子
 *
 * `.toast-item` 在 style.css 和 index.html 的内联块里各写一份，四处不一致：
 *
 *                    style.css        index.html
 *   背景不透明度      0.75             0.9
 *   图标间距          12px             10px
 *   warning 左边框    #f59e0b 琥珀     #ea580c 橙
 *
 * 登录页弹一次「密码错误」，进站后再弹一次「保存成功」，两个吐司深浅不同、
 * 间距不同——这就是诉求 #5 说的「没有统一」，而原因只是同一份样式抄了两遍，
 * 抄的时候顺手改了几个数。
 *
 * ## 这次修掉的真实缺陷
 *
 * 1. **互动通知被底栏盖住。** `.notification-toast` 是 `bottom: 20px` +
 *    `z-index: 9999`，而 `.mobile-bottom-nav` 是 `bottom: 0` + 高 56px +
 *    `z-index: 10000`。手机上通知弹出来，下半截压在导航栏**底下**——诉求 #2
 *    要的「看到是谁点赞了」，在移动端有一半看不到。现在窄屏改为避让底栏。
 * 2. **冷灰配色残留。** 标题 #1e293b、描述 #64748b、关闭按钮 #94a3b8 是站点
 *    换成暖橙主题之前的板岩色，落在 #fffbeb 暖底上发青。改用文字令牌。
 * 3. **info 态是蓝色** (#3b82f6)。全站找不到第二处蓝，`--color-info` 是 #0284c7。
 * 4. **z-index 军备竞赛。** 9999 与 99999 两套吐司各占一档，都只是「比别人大」。
 *    统一到 `--z-toast`，相对次序不变（仍低于 lightbox）。
 *
 * ## 40 多个 `!important` 全部去掉
 *
 * `.notification-toast` 那一族每条声明都带 `!important`，但站内没有任何规则
 * 与它竞争——它压的是空气。搬进组件层后一个都不需要。
 */

/* ── 全局提示（window.showToast）── */
.toast-container {
    position: fixed;
    top: calc(var(--safe-top) + var(--space-5));
    right: calc(var(--safe-right) + var(--space-5));
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    /* 容器铺在页面右上角，不能挡住底下的按钮；单个吐司自己收回点击权限。 */
    pointer-events: none;
}

.toast-item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-width: 280px;
    max-width: 360px;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    border-left: 5px solid var(--color-brand);
    border-radius: var(--radius-md);
    background: rgba(255, 255, 255, 0.85);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    color: var(--color-text);
    box-shadow: var(--shadow-lg);
    pointer-events: auto;
    /* 两段动画：入场，然后停 3.2s 再淡出 0.3s。总计 3.5s，与 js/common.js
       里 setTimeout 摘除 DOM 的 3500ms 对齐——早于它元素会先消失再留白，
       晚于它会看到吐司「跳」一下才没。 */
    animation: toast-in var(--duration-base) var(--ease-out),
               toast-out var(--duration-base) forwards;
    animation-delay: 0s, 3.2s;
}

.toast-item.success {
    border-left-color: var(--color-success);
}

.toast-item.error {
    border-left-color: var(--color-danger);
}

.toast-item.warning {
    border-left-color: var(--color-warning);
}

.toast-item.info {
    border-left-color: var(--color-info);
}

.toast-icon {
    flex-shrink: 0;
    font-size: var(--font-size-lg);
}

.toast-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* `.toast-title` 被全局提示和互动通知两族共用。基础样式放这里，
   互动通知那族在下面按需覆盖——同文件内靠顺序决胜，不需要 `!important`。 */
.toast-title {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: 700;
}

.toast-item.success .toast-title {
    color: var(--color-success);
}

.toast-item.error .toast-title {
    color: var(--color-danger);
}

.toast-item.warning .toast-title {
    color: var(--color-brand);
}

.toast-item.info .toast-title {
    color: var(--color-info);
}

.toast-message {
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    line-height: var(--line-height-tight);
    font-weight: 500;
}

@keyframes toast-in {
    from { transform: translateX(120%) scale(0.9); opacity: 0; }
    to   { transform: translateX(0) scale(1); opacity: 1; }
}

@keyframes toast-out {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(-10px); }
}

/* ── 互动通知（点赞 / 评论 / 收藏，诉求 #2）──
 *
 * 与全局提示分列上下两角是有意的：一个是「你刚做的操作成功了」，一个是
 * 「别人对你做了什么」，同时出现时不该抢同一个位置。
 */
.notification-toast {
    position: fixed;
    bottom: calc(var(--safe-bottom) + var(--space-5));
    right: calc(var(--safe-right) + var(--space-5));
    z-index: var(--z-toast);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    max-width: 320px;
    padding: var(--space-4) var(--space-5);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    box-shadow: var(--shadow-lg);
    /* 整块可点，跳到 action_url。 */
    cursor: pointer;
    animation: notification-toast-in var(--duration-base) ease-out;
    transition: transform var(--transition-base),
                box-shadow var(--transition-base);
}

.notification-toast:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-brand);
}

.notification-toast .toast-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}

.notification-toast .toast-content {
    flex: 1;
    min-width: 0; /* 长昵称在 flex 子项里能省略，而不是把弹窗撑宽 */
}

.notification-toast .toast-title {
    margin-bottom: 2px;
    color: var(--color-text);
    font-weight: 600;
}

/* 描述文字复用 .toast-message 的排版，只是这一族的行高松一档——
   它是一句完整的话（「张三 赞了你的作品」），不是状态词。 */
.notification-toast .toast-message {
    font-size: var(--font-size-sm);
    line-height: var(--line-height-base);
}

.notification-toast .toast-close {
    flex-shrink: 0;
    align-self: flex-start;
    padding: 0 var(--space-1);
    /* 44px 触摸下限对这个 × 不适用：它和整块的「点击跳转」抢同一片区域，
       放大到 44px 会让想关闭的人误跳走。保持视觉尺寸，靠 padding 略微放宽。 */
    border: none;
    background: none;
    color: var(--color-text-muted);
    font-size: var(--font-size-lg);
    line-height: 1;
    cursor: pointer;
    opacity: 0.6;
    transition: opacity var(--transition-fast);
}

.notification-toast .toast-close:hover {
    opacity: 1;
}

@keyframes notification-toast-in {
    from { transform: translateX(100%); opacity: 0; }
    to   { transform: translateX(0); opacity: 1; }
}

/* 窄屏：两族都要让位。
 *
 * 全局提示改为左右撑满——360px 的手机上 `min-width: 280px` + 24px 右边距
 * 只剩 56px 余量，长一点的错误信息会挤成三行。
 *
 * 互动通知要抬到底栏之上。阈值用 768px 对齐 style.css:5729 显示底栏的那一档：
 * 底栏出现和吐司避让必须同时发生，两个数字不一致就会有一段宽度区间里
 * 通知又被盖住。这个耦合是 style.css 用裸数字写死阈值留下的，
 * Task 22 把底栏迁进布局层后会一起收敛到 html[data-layout] 上。 */
@media (max-width: 768px) {
    .toast-container {
        left: calc(var(--safe-left) + var(--space-3));
        right: calc(var(--safe-right) + var(--space-3));
        top: calc(var(--safe-top) + var(--space-3));
    }

    .toast-item {
        min-width: 0;
        max-width: none;
    }

    .notification-toast {
        left: calc(var(--safe-left) + var(--space-3));
        right: calc(var(--safe-right) + var(--space-3));
        max-width: none;
        bottom: calc(var(--bottom-nav-height) + var(--safe-bottom) + var(--space-3));
    }
}


/* ============================================================================
 * 帖子卡片 —— .post-card 及其内部元素
 *
 * 迁移前 .post-card 在 style.css 里有 42 处定义，散在 6 个簇（637 / 1819 /
 * 2789 / 3141 / 3348 / 5241）——同一张卡片被重写了 6 次。宽屏胜者是 3348，
 * 窄屏是 5241，其余全被压掉。每个值由 tools/resolve_cascade.py 解出。
 *
 * 顺带修掉：`.post-card *` 的冷灰蓝残留、`.post-header` 被误列进玻璃组、
 * 窄屏白底上多余的 backdrop-filter、图片 23px 圆角与卡角对不上、
 * 标题/昵称两个断点用不同色系、`.post-details span:first-child` 这类
 * 结构选择器后门（它压着 Task 20c 迁走的 .price-tag，青色预算标签从未显示）。
 *
 * 与迁移前不同的值：圆角 24→20、标题 1.12→1.15rem、描述与详情条 0.9/0.82→
 * 0.85rem、内边距 20→24px、底色 0.5→0.45、过渡 0.4→0.3s（归入令牌刻度）；
 * 身份行 700、窄屏描边 0.05 与投影 0.03、两处 inset 高光、图片缓动曲线
 * （照抄真实渲染值，令牌里没有对应档）。其余逐条一致。
 * ========================================================================== */

.post-card {
    /* python tools/resolve_cascade.py .post-card  → 除 color/cursor 外全部来自 3348 */
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur));
    -webkit-backdrop-filter: blur(var(--glass-blur));
    /* 字面值：第二层 inset 是玻璃上沿高光，令牌里没有对应档。 */
    box-shadow: 0 12px 40px -15px rgba(234, 88, 12, 0.08),
                inset 0 1px 1px rgba(255, 255, 255, 0.8);
    color: var(--color-text);
    cursor: pointer;
    transition: transform var(--transition-base), box-shadow var(--transition-base),
                border-color var(--transition-base);
}

/* resolve_cascade.py .post-card --states hover → 全部来自 3285 行 */
.post-card:hover {
    transform: translateY(-8px) scale(1.01);
    border-color: var(--color-border-strong);
    box-shadow: var(--shadow-xl), inset 0 1px 2px rgba(255, 255, 255, 0.9);
}

/* hover 位移 8px，滚一屏就是十几次——尊重系统的减少动效设置。 */
@media (prefers-reduced-motion: reduce) {
    .post-card,
    .post-card:hover,
    .post-card .post-image,
    .post-card:hover .post-image {
        transform: none;
        transition: none;
    }
}

/* 卡片可点击（JS 绑了 click），必须有键盘焦点态。原先整族只有 :hover。 */
.post-card:focus-visible {
    outline: none;
    border-color: var(--color-brand);
    box-shadow: var(--focus-ring), var(--shadow-xl);
}

/* 玻璃光泽，取自 3273 原样保留。pointer-events:none 让点击穿到卡片。 */
.post-card::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: var(--z-base);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%,
                                        rgba(255, 255, 255, 0) 100%);
    pointer-events: none;
}

/* ── 图片区 ──
   高度只写在容器上，图片 height:100% 撑满——换断点只改一处。 */
.post-card .card-img-container {
    position: relative;
    width: 100%;
    height: 220px;
    overflow: hidden;
    border-bottom: 1px solid var(--glass-border);
}

.post-card .post-image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* 字面曲线而非 --ease-out：原值 cubic-bezier(0.165,0.84,0.44,1) 是
       easeOutQuart，比令牌的易出快得多（150ms 内完成大半段位移）。
       这是缩放动画的观感本身，不属于「刻度」。 */
    transition: transform 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
}

.post-card:hover .post-image {
    transform: scale(1.03);
}

/* ── 内容区 ─────────────────────────────────────────────────────────────── */
.post-card .post-content {
    display: flex;
    flex: 1;
    flex-direction: column;
    justify-content: space-between;
    padding: var(--space-5);
    /* 由透明渐到半透白，文字压得住。取自 3519。 */
    background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%,
                                        rgba(255, 255, 255, 0.2) 100%);
}

/* 作者行。原先误列进玻璃组，自己也成了一块玻璃面板；
   这些值原本是 js 行内 style（4 处），特异性最高，是后门。 */
.post-card .post-header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-1);
}

.post-card .post-avatar {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border: 2px solid rgba(255, 255, 255, 0.8);
    border-radius: 50%;
    object-fit: cover;
}

.post-card .post-info {
    flex: 1;
    min-width: 0;
}

/* overflow + min-width:0 让长昵称省略，否则会把右侧时间挤出卡片。 */
.post-card .post-name {
    overflow: hidden;
    color: var(--color-text);
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-bold);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 原先宽屏被 `.post-card *` 刷成冷灰蓝、窄屏是品牌橙。统一用品牌橙，
   两种身份的区分交给文案，配色不必分叉。 */
.post-card .post-role {
    color: var(--color-brand);
    font-size: var(--font-size-xs);
    /* 700 照抄真实渲染值：它原是 js 行内 style，压过 style.css 里
       那些 0.62rem 的窄屏规则——那些从未生效。 */
    font-weight: var(--font-weight-bold);
}

.post-card .post-time {
    flex-shrink: 0;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}

/* 标题与描述都锁成整两行高（`height` 而非 `max-height`）。
 *
 * 诉求「所有发布的帖子，大小保持一致」：原来这两条只写 `max-height`，
 * 一行标题的卡片就比两行的矮 24px，一行描述的又矮 18px，配上没有标签的帖子，
 * 同一屏里能出现四五种卡片高度。栅格的 `align-items` 也曾是 `start`，
 * 同一行内互不拉平，参差直接暴露在眼前。
 *
 * 锁死高度的代价是短标题下方留白，换来的是任何内容组合下卡片尺寸都相同——
 * 图片 220、标题两行、描述两行、标签一行、底条一行，全是定值。 */
.post-card .post-title {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin: 0 0 var(--space-1);
    height: calc(var(--line-height-tight) * 2em);
    color: var(--color-text);
    font-size: var(--font-size-lg);
    font-weight: 800;
    line-height: var(--line-height-tight);
}

.post-card .post-desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin: 0 0 var(--space-1);
    height: calc(var(--line-height-tight) * 2em);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    line-height: var(--line-height-tight);
}

/* 无描述时压暗占位（段落仍渲染以撑住高度，否则同行卡片高度不齐）。
   原是 js 行内 style，两个渲染点各写一次。 */
.post-card .post-desc--empty {
    opacity: 0.5;
}

/* 底部信息条。margin-top:auto 顶到卡片底部，标题一行和两行的卡片底条仍对齐。 */
.post-card .post-details {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-top: auto;
    padding-top: var(--space-3);
    border-top: 1px dashed var(--color-border);
    font-size: var(--font-size-sm);
}

/* ── 小卡变体 .post-card--mini ──
 * 个人中心的发布 / 点赞 / 收藏三个列表用它，参考小红书的个人主页：一屏
 * 塞下更多条目，每条只保留「认出这是哪一条」所需的信息。
 *
 * 只覆盖尺寸，不覆盖结构与配色——DOM 和广场卡片一字不差（同一个
 * renderPostCard），差别全在这一块的数值里。所以广场卡片改了圆角、
 * 改了 hover、改了标签色，小卡自动跟着改，两边不会再漂移（诉求 #1）。
 *
 * 高度全用 em：标题/描述/标签在基础规则里写的是 `calc(... * 2em)`，
 * 这里把字号收一档，两行的高度自己就跟着收，不必逐条重算。 */
.post-card--mini .card-img-container {
    height: 150px;
}

.post-card--mini .post-content {
    gap: var(--space-1);
    padding: var(--space-3);
}

.post-card--mini .post-header {
    margin-bottom: 2px;
}

.post-card--mini .post-avatar {
    width: 24px;
    height: 24px;
    border-width: 1px;
}

.post-card--mini .post-name {
    font-size: var(--font-size-xs);
}

/* 小卡宽度不到广场卡的一半，「3 小时前」和昵称抢不到位置。作者仍在，
   点进他主页的入口（诉求 #2）不受影响。 */
.post-card--mini .post-time {
    display: none;
}

.post-card--mini .post-title {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-bold);
}

/* 描述整段隐藏：小卡认的是封面和标题，两行灰字在这个尺寸下只是噪点。
   段落仍然渲染（display:none 不占位），高度由其余定高元素决定。 */
.post-card--mini .post-desc,
.post-card--mini .post-tags {
    display: none;
}

.post-card--mini .post-details {
    padding-top: var(--space-2);
    font-size: var(--font-size-xs);
}

.post-card--mini .price-tag,
.post-card--mini .location-tag {
    font-size: var(--font-size-xs);
}

.post-card--mini .card-badge {
    top: var(--space-2);
    right: var(--space-2);
    padding: 2px var(--space-2);
}

/* ── 窄屏：双列紧凑卡片 ──
 * 阈值 768px 与 --bp-tablet 人工对齐（自定义属性不能用在 @media 条件里）。
 * 原条件里还有 `(pointer: coarse) and (max-width: 1024px)` 半条，属设备判断，
 * 归 Task 22 的布局层统一处理，不在组件层各自为政。
 *
 * 窄屏下小卡与普通卡片收敛到同一档：两列的宽度已经够窄，再缩一档标题会
 * 断成三行。所以这块不写 .post-card--mini 的分支，让它吃 .post-card 的值。 */
@media (max-width: 768px) {
    .post-card {
        border-radius: var(--radius-md);
        background: var(--color-surface-solid);
        /* 0.05 / 0.03 近乎看不见：窄屏双列白卡片贴着排，可见边框会把列表
           切成格子。令牌里最淡的 0.2 / 0.08 都会让边界跳出来。 */
        border-color: rgba(251, 146, 60, 0.05);
        box-shadow: 0 4px 10px rgba(124, 45, 18, 0.03);
        /* 底色不透明，模糊背景纯属浪费——每张卡片一次 GPU 合成。 */
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        transition: transform var(--transition-fast);
    }

    .post-card .card-img-container {
        height: 165px;
    }

    .post-card .post-content {
        gap: var(--space-1);
        padding: var(--space-2) var(--space-3);
    }

    .post-card .post-header {
        gap: var(--space-1);
        margin-bottom: 2px;
    }

    .post-card .post-avatar {
        width: 22px;
        height: 22px;
        border-width: 1px;
    }

    .post-card .post-name {
        font-size: var(--font-size-xs);
    }

    /* 窄屏宽度紧张，时间与描述让位给标题和价格。 */
    .post-card .post-time {
        display: none;
    }

    /* 高度不必重写：基础规则用的是 em，字号一收，两行的高度跟着收。 */
    .post-card .post-title {
        font-size: var(--font-size-sm);
        font-weight: var(--font-weight-bold);
    }

    .post-card .post-desc {
        display: none;
    }

    /* 虚线改实线：窄屏行距只有 4px，虚线的点在这个尺度上像噪点。 */
    .post-card .post-details {
        margin-top: 2px;
        padding-top: var(--space-1);
        border-top-style: solid;
        border-top-color: rgba(251, 146, 60, 0.05);
    }
}

/* ============================================================================
 * 作者区 / 骨架屏 / 空态 —— 由 js/components/post-card.js 渲染。
 * ========================================================================== */

/* 作者区改成 <a>：诉求 #2 要求「从通知里能看到是谁、并点进他的主页」，
   而卡片上的头像昵称此前只是死文本，唯一入口在详情弹窗里。
   包一层而不是给 .post-avatar / .post-name 各挂一个链接：两个相邻的小
   热区在手机上很难点中，包起来是一个至少 32px 高的整块。 */
.post-author {
    display: flex;
    flex: 1;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    color: inherit;
    text-decoration: none;
}

.post-author:focus-visible {
    outline: none;
    border-radius: var(--radius-sm);
    box-shadow: var(--focus-ring);
}

/* 标签本身就是一次搜索，指针要说明这件事。 */
.post-tag.hashtag-link {
    cursor: pointer;
}

/* ── 骨架屏 ──
 * 加载中先占住栅格。原先两页都塞一个居中的转圈图标，列表会从 0 高度
 * 直接弹到满屏——骨架让首屏高度从一开始就是对的。 */
.post-card--skeleton {
    cursor: default;
    pointer-events: none;
}

.post-card--skeleton:hover {
    transform: none;
    box-shadow: 0 12px 40px -15px rgba(234, 88, 12, 0.08),
                inset 0 1px 1px rgba(255, 255, 255, 0.8);
}

.skeleton {
    border-radius: var(--radius-sm);
    background: linear-gradient(90deg, var(--color-surface-disabled) 25%,
                                       rgba(255, 255, 255, 0.6) 50%,
                                       var(--color-surface-disabled) 75%);
    background-size: 200% 100%;
    animation: skeleton-sweep 1.4s ease-in-out infinite;
}

.skeleton--media {
    height: 220px;
    border-radius: 0;
}

.skeleton--line {
    height: 14px;
    margin-bottom: var(--space-2);
}

.skeleton--short {
    width: 60%;
}

@keyframes skeleton-sweep {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}

/* 扫光是纯装饰，关掉动效后留一块静止底色即可。 */
@media (prefers-reduced-motion: reduce) {
    .skeleton {
        animation: none;
    }
}

/* ── 空态 ──
 * 空列表、无搜索结果、加载失败共用。`grid-column: 1/-1` 让它横跨整行，
 * 否则它会被当成一张卡片挤在第一列。 */
.empty-state {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-7) var(--space-5);
    color: var(--color-text-muted);
    text-align: center;
}

.empty-state__icon {
    color: var(--color-brand);
    font-size: 2.5rem;
    opacity: 0.55;
}

.empty-state__title {
    margin: 0;
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-bold);
}

.empty-state__hint {
    margin: 0;
    font-size: var(--font-size-sm);
    opacity: 0.85;
}

.empty-state--error .empty-state__icon {
    color: var(--color-danger);
}

@media (max-width: 768px) {
    .skeleton--media {
        height: 165px;
    }

    .empty-state {
        padding: var(--space-6) var(--space-4);
    }

    .empty-state__icon {
        font-size: 2rem;
    }

    .empty-state__title {
        font-size: var(--font-size-md);
    }
}

/* ── Tab 选项卡（个人中心五个分类）──
 *
 * 三处旧定义合成这一处：profile.html 的内联 <style>（版式）、
 * 行内 style=""（display/border/gap）、style.css:2857 的
 * `color: #431407 !important`。那条 !important 把选中态压成和未选中同色，
 * 于是「我在哪个 Tab」只能靠下方 2px 横线判断——诉求 #5 的一例。
 *
 * 窄屏靠横向滚动容纳五个 Tab，不换行：换行会把内容区推下去半屏。 */
.profile-tabs {
    display: flex;
    gap: var(--space-5);
    margin-bottom: var(--space-5);
    border-bottom: 1px solid var(--color-border);
    overflow-x: auto;
    scrollbar-width: none;
}

.profile-tabs::-webkit-scrollbar {
    display: none;
}

.profile-tab-btn {
    position: relative;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 0 0 var(--space-3);
    border: none;
    background: none;
    color: var(--color-text-muted);
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-bold);
    white-space: nowrap;
    cursor: pointer;
    transition: color var(--transition-base);
}

.profile-tab-btn i {
    color: var(--color-brand);
}

.profile-tab-btn:hover {
    color: var(--color-text);
}

.profile-tab-btn.active {
    color: var(--color-brand);
}

/* 下划线用伪元素做宽度动画。若改成 border-bottom，切换 Tab 时按钮会跳 2px。 */
.profile-tab-btn::after {
    content: '';
    position: absolute;
    bottom: -1px;
    left: 0;
    width: 0;
    height: 2px;
    background: var(--color-brand);
    transition: width var(--transition-base);
}

.profile-tab-btn.active::after {
    width: 100%;
}

.profile-tab-btn:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
    border-radius: var(--radius-sm);
}

/* 计数徽标。内容由 js/profile.js 从后端 total 填；为空时整个徽标不占位，
   否则首屏会先显示一圈空底色。 */
.profile-tab-count {
    min-width: 1.25rem;
    padding: 0 var(--space-1);
    border-radius: var(--radius-full);
    background: var(--color-brand-tint);
    color: var(--color-brand);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-medium);
    line-height: 1.5;
    text-align: center;
}

.profile-tab-count:empty {
    display: none;
}

/* 「加载更多」按钮的居位。广场用的是行内 style，这里给它一个类名，
   两页最终都该指向这一处。 */
.posts-load-more {
    display: flex;
    justify-content: center;
    margin-top: var(--space-5);
}

@media (max-width: 768px) {
    .profile-tabs {
        gap: var(--space-4);
    }

    .profile-tab-btn {
        font-size: var(--font-size-sm);
        padding-bottom: var(--space-2);
    }
}

/* ============================================================================
 * 地区选择器 —— .region-picker，面板内容由 js/components/region-picker.js 生成
 *
 * 触发器故意**没有边框和底色**：它只出现在广场工具条那颗胶囊里，自带一圈边框
 * 会让胶囊看起来是三个控件拼的（这正是「过滤地区栏位不美观」的来源）。
 *
 * 面板向右对齐而不是向左：它比触发器宽得多，左对齐会伸出胶囊右缘、在 1400px
 * 以下的窗口里被视口裁掉。宽度再夹一道 `100vw - 32px`，窄屏上不会横向溢出。
 * ========================================================================== */
.region-picker {
    display: flex;
    position: relative;
    flex: 0 0 auto;
    font-size: var(--font-size-sm);
}

.region-trigger {
    display: flex;
    flex: 1;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-3);
    max-width: 200px;
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    font-size: inherit;
    font-weight: var(--font-weight-bold);
    white-space: nowrap;
    transition: color var(--transition-fast), background var(--transition-fast);
}

.region-trigger:hover,
.region-picker.open .region-trigger,
.region-picker.has-value .region-trigger {
    color: var(--color-brand);
}

.region-pin {
    color: var(--color-brand-soft);
}

/* 选中「内蒙古自治区」这类长名时截断而不是把胶囊顶宽。
   flex: 1 把插入符推到触发器右端，和风格触发器的 space-between 对齐——
   窄屏两个筛选器并排各占半行，一个插入符贴右、另一个跟在文字后面就很显眼。 */
.region-trigger-text {
    flex: 1;
    overflow: hidden;
    text-align: left;
    text-overflow: ellipsis;
}

.region-caret {
    font-size: var(--font-size-xs);
    transition: transform var(--transition-base);
}

.region-picker.open .region-caret {
    transform: rotate(180deg);
}

.region-panel {
    position: absolute;
    top: calc(100% + var(--space-3));
    right: 0;
    z-index: var(--z-dropdown);
    width: min(460px, calc(100vw - var(--space-6)));
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface-solid);
    box-shadow: var(--shadow-lg);
    text-align: left;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity var(--transition-base), transform var(--transition-base),
                visibility var(--transition-base);
}

.region-picker.open .region-panel {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* 面板内部靠 `hidden` 属性在「浏览」与「搜索结果」两种视图间切换。这一条是
   必需的：`.region-columns` 自己声明了 display，浏览器默认的
   `[hidden] { display: none }` 特异性更低，压不住它。 */
.region-panel [hidden] {
    display: none;
}

.region-search {
    position: relative;
}

.region-search i {
    position: absolute;
    top: 50%;
    left: var(--space-3);
    transform: translateY(-50%);
    color: var(--color-text-muted);
    pointer-events: none;
}

/* 不设 font-size：base.css 给输入框兜的 16px 下限是为了防 iOS 聚焦时自动放大
   整页，面板里这个框同样吃这条。 */
.region-search-input {
    padding: var(--space-2) var(--space-3);
    padding-left: calc(var(--space-4) + var(--space-5));
    width: 100%;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-bg);
    color: var(--color-text);
    outline: none;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.region-search-input:focus {
    border-color: var(--color-brand);
    background: var(--color-surface-solid);
    box-shadow: var(--focus-ring);
}

.region-hot {
    margin-top: var(--space-3);
}

.region-group-title {
    display: block;
    margin-bottom: var(--space-2);
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-bold);
}

.region-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
}

.region-chip {
    padding: var(--space-1) var(--space-3);
    border: 1px solid transparent;
    border-radius: var(--radius-full);
    background: var(--color-brand-tint);
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    transition: border-color var(--transition-fast), color var(--transition-fast);
}

.region-chip:hover {
    border-color: var(--color-brand);
    color: var(--color-brand);
}

.region-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* 两列各自滚动。34 个省级单位在 260px 高里放不下，整个面板跟着长会顶出视口。 */
.region-list {
    max-height: 260px;
    overflow-y: auto;
}

.region-results {
    margin-top: var(--space-3);
    max-height: 300px;
}

.region-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    width: 100%;
    border-radius: var(--radius-sm);
    color: var(--color-text);
    font-size: var(--font-size-sm);
    text-align: left;
    transition: background var(--transition-fast), color var(--transition-fast);
}

.region-option:hover {
    background: var(--color-brand-tint);
    color: var(--color-brand);
}

.region-option.active {
    background: var(--gradient-brand);
    color: var(--color-on-brand);
    font-weight: var(--font-weight-bold);
}

.region-option i {
    font-size: var(--font-size-xs);
    opacity: 0.55;
}

/* 同名地区不少（「吉林」既是省也是市，「朝阳」在北京和辽宁都有），
   搜索结果靠这行小字说明点的是哪一个。 */
.region-option-hint {
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
}

.region-option.active .region-option-hint {
    color: var(--color-on-brand);
    opacity: 0.85;
}

.region-hint {
    padding: var(--space-2) var(--space-3);
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    line-height: var(--line-height-base);
}

.region-panel-footer {
    display: flex;
    justify-content: flex-end;
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--color-border);
}

.region-reset {
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-full);
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    transition: background var(--transition-fast), color var(--transition-fast);
}

.region-reset:hover {
    background: var(--color-brand-tint);
    color: var(--color-brand);
}

/* ── 窄屏：面板改成底部弹层 ─────────────────────────────────────────────────
 * 桌面那种「挂在触发器下方、向右对齐」的下拉在手机上是坏的：面板宽 460px，
 * 而触发器只占半行，`right: 0` 于是让面板向左伸出屏幕——省份那一整列被裁在
 * 屏幕外，点不到；body 上还有 `overflow-x: hidden`，连滚过去看都不行。
 *
 * 改成从底部升起的弹层：宽度就是屏幕宽，配一层压暗背景。手机上的筛选面板本就
 * 该落在拇指够得着的地方，这也顺手解决了「面板比视口还高」。
 *
 * 断点条件与 css/layout.css 里把胶囊拆成独立字段的那一档逐字相同——两者必须
 * 同时发生，否则会出现「字段拆了、面板还是桌面下拉」的中间态，也就是这个 bug。
 */
@media (max-width: 768px), (pointer: coarse) and (max-width: 1024px) {
    .region-panel {
        position: fixed;
        top: auto;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: var(--z-sheet);
        padding: var(--space-3) var(--space-3)
                 calc(var(--space-3) + var(--safe-bottom));
        width: auto;
        max-height: 82vh;
        border-width: 1px 0 0;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        overflow-y: auto;
        transform: translateY(100%);
    }

    /* 顶上那道小横条是「这是可关掉的弹层」的通用信号，比再挤一个关闭按钮省地方。 */
    .region-panel::before {
        content: '';
        display: block;
        margin: 0 auto var(--space-2);
        width: 36px;
        height: 4px;
        border-radius: var(--radius-full);
        background: var(--color-border);
    }

    .region-picker.open .region-panel {
        transform: translateY(0);
    }

    /* 压暗层挂在 .region-picker 上而不是面板里：面板自己是层叠上下文，
       写在里面的伪元素只会盖住面板自己的内容。 */
    .region-picker.open::after {
        content: '';
        position: fixed;
        inset: 0;
        z-index: var(--z-sheet-backdrop);
        background: var(--color-scrim);
    }

    /* 两列在弹层里仍各自滚动。让整个弹层一起滚的话，滚到省份列底部会把城市列
       一起带走，选完省再找市得往回滚。 */
    .region-list {
        max-height: 44vh;
    }

    .region-results {
        max-height: 50vh;
    }

    /* 触摸档把选项行撑到 44px，并让插入符号贴右——和风格弹层的行高一致。 */
    .region-option {
        min-height: 44px;
    }

    .region-search-input {
        min-height: 44px;
    }

    .region-chip {
        padding: var(--space-2) var(--space-3);
    }

    /* ── 卡片底部信息条 ───────────────────────────────────────────────────
     * 窄屏是两列栅格，一张卡只有 183px 宽，底条要塞「¥599/小时」和
     * 「杭州·西湖景区」两块。sm 字号下这两块加起来超过可用宽度，于是各自
     * 折成两行——同一行里的两张卡因为地名长短不同还会折得不一样高，
     * 卡片底边就参差不齐。
     *
     * 收一档字号，让常见长度的地名整个放得下；放不下的由基础规则里的
     * .location-tag-text 省略号收尾，底条恒定一行。
     */
    .post-card .post-details {
        gap: var(--space-1);
        font-size: var(--font-size-xs);
    }

    .post-card .price-tag,
    .post-card .location-tag {
        font-size: var(--font-size-xs);
    }

    .post-card .price-tag {
        padding: 2px var(--space-2);
    }
}
