/* ==================================================================
   交大学桥 · 设计系统
   ================================================================== */

:root {
  /* ★★ 2026-11-02：用户说「把颜色改成以前那个红色吧」——回到 2026-10-21～10-31
     在线上跑了十天的那套 **D 套「绛红 · 藏青 · 鎏金」**，10-31 的 F/G 两套都撤了。
     当前：主色绛红 #7A1B2E（师资广场）、强调色藏青 #1E3A5F（社会需求广场）、
           第三色鎏金 #8A6A2F（大学生需求广场）。
     ★ 为什么是这三个色相：绛红暖、藏青冷，色温上就分家，鎏金再把第三档
       从冷暖两端里摘出来——三个色相两两都拉得开，白字对比度也全部过 AA（见下方实测）。
     ★ 回滚是按**角色**换值（--brand / --accent / --rgb / 半透明的 literal），
       不是照着截图近似取色；--teal 这组 D 套本来就是鎏金，一行没动。
     ⚠️ 下面若干历史注释里的深紫/玫红/苍绿/赤陶/孔雀青等旧色值，只是当年测量过程的记录；
       当前色值以本块的变量和 render-check 在真像素上的实测为准。 */
  /* ★★ 历史：2026-10-06 第二轮整套换色（深紫 #4C1D95 + 玫红 #E11D48），
     2026-10-21 起由上面的 D 套取代。下面保留的是当年参数化改造的记录。
     ⚠️ 那次**色相真的动了**，所以下面那句老注释里的对应关系要重读：
       紫=师资广场 / 玫红=社会需求广场（家长）/ 青绿=大学生需求广场。
       三个色相依然两两拉得开，卡片和广场仍然一一对应，只是换了颜色。

     ★★★ 这次真正的工程改动不是颜色本身，是**把配色参数化了**：
       原来 style.css 里散着 38 处 rgba(36, 84, 230, .28) 这样的**字面量**
       （首屏光斑、卡片悬停光晕、输入框焦点圈、::selection…），它们不跟着 --brand 走。
       只改 --brand 会留下一堆蓝色孤儿——我换配色时第一版预览就是这么露馅的：
       红色方案里首屏还是蓝的。
       现在全部改成 rgb(var(--brand-rgb) / .28) 这种写法，色值只此一处。
       **以后换配色只动下面这两组：-rgb 三元组 + 十六进制值。** */
  --brand:        #7A1B2E;
  --brand-dark:   #57101F;
  --brand-light:  #9C3045;
  --brand-soft:   #F3E2E6;
  --accent:       #1E3A5F;
  /* ★★ 强调色有两个身份，**必须拆成两个变量**：一个是「浅藏青底上的藏青字」，
     要够深才读得清；另一个是「藏青按钮悬停时的底色」，要中等明度才能压住上面那行白字。
     合成一个数的话，字够深了按钮悬停就变成深底深字。 */
  --accent-dark:  #142944;   /* 悬停底色（白字 14.68:1） */
  --accent-ink:   #1B3354;   /* 浅藏青底上的藏青字（实测 10.8:1） */
  --accent-light: #3E6190;   /* ★ 白字 6.33:1——不能再浅，见 .avatar 那段注释 */
  --accent-soft:  #E6EDF5;
  /* 第三种色「鎏金」。
     ★ 必须新开一个色相，不能复用上面两个：首页那三张入口卡和三个广场是一一对应的，
       主色=师资广场（绛红）、强调色=社会需求广场（藏青）、第三色=大学生需求广场（鎏金）。
     ★ 10-31 的 G 套曾把这一档换成藏青，因为当时主色是赤陶、强调色是孔雀青，藏青和
       孔雀青明度太近；现在主色回绛红、强调色回藏青，鎏金与藏青 ΔE76≈66，是三档里
       最分得开的一组。
       变量名仍叫 --teal（历史名，值早就不青了），改色只看值别看名。 */
  --teal:         #8A6A2F;
  --teal-dark:    #6A501F;
  --teal-light:   #B49153;
  --teal-soft:    #F5EFE3;

  /* 第四种色「墨绿」（2026-11-02 加兼职/实习广场）。
     ★ 和前三个色相都拉得开：绛红暖、藏青冷、鎏金偏黄，墨绿是四档里唯一的绿。
       首页四张入口卡、兼职/实习广场、岗位标签共用这一套。
     ★ 白字对比度已量：--jade #2F5D50 上白字 7.49:1，--jade-light #4B7A6B 上 4.89:1，
       都过 AA；小字用 --jade-dark 压在 --jade-soft 上是 9.66:1。
     ⚠️ 换值必须重算这四个数，别只看「颜色像不像」。 */
  --jade:         #2F5D50;
  --jade-dark:    #1F4038;
  --jade-light:   #4B7A6B;
  --jade-soft:    #E5EFEA;

  --bg:           #F7F2EC;
  --surface:      #FFFFFF;
  --surface-alt:  #FBF7F1;

  --text:         #231A1B;
  --text-soft:    #4B3F41;
  /* ★★ 2026-10-07 再深一档：#66748C → #57647A。
     ★ 这是**第二次**调它了，两次都栽在同一件事上：这个色只在**纯白底**上量过。
       第一版 #7A8699 在白底上 3.6:1，改到 #66748C 以为过关了；
       可它实际落在首屏那片浅紫底（`--bg` #F6F4FB + 淡淡的品牌色雾）上，
       只有 4.37:1 —— **仍然不到 AA 的 4.5**，而它全是 13px 上下的小字
       （标签、说明、页脚、统计标签），是这个站里最该读得清的一档。
       2026-10-07 加校门背景时逐带实测才暴露出来（背景又把它压到 3.74:1）。
     ★ 教训：**浅色文字要在它真正落地的那个底色上量，不能在 #FFF 上量**。
       站里没有一个页面是纯白底，所以「白底过关」从来都不算过关。
     ★ 旧值 #57647A 压着旧首屏最暗的背景（#E8E2F2）实测 4.73:1，过 AA。
     ★ 2026-10-21 换 D 套后是 #55484A：在 --bg #F7F2EC 上 7.82:1，render-check 逐页复量。 */
  --muted:        #55484A;
  --border:       #E8DCD3;
  --border-soft:  #F2EAE3;
  /* ★★ 2026-10-09：加一档**深边框**，只给广场页那个独立检索框用。
     ★ 起因是用户反馈「三个广场的搜索框太不显眼，大家都看不到」——这是真的，
       而且能算出数来：原来用 --border #E5DFF2 描 1px，它压在广场页那块浅紫底
       （#F0ECF9 上下）上只有 **1.11:1**，等于没画。
     ★ 为什么不是「把 --border 整体调深」：那一档还被 15 处用到（卡片、表格、
       筛选 chip、虚线框…），那些地方本来就该是**淡的分隔线**，调深会把整站
       画成一堆框。检索框是**要人去找、去点**的控件，不是分隔线，两者不该同级。
     ★ 2026-10-21 换 D 套后取值 #8F7C6E：白底 3.98:1、页面底 #F7F2EC 3.58:1、
       --surface-alt #FBF7F1 上 3.73:1，**三处都过 WCAG 1.4.11 的 3:1 控件边界线**
       （这一条只管 UI 控件的边界，和文字那 4.5:1 不是一回事）。
       色相跟着 --border 走（同一族暖灰），不是另起一个色。
     ⚠️ 改这个值要重算上面两个对比度——它存在的全部理由就是「看得见」。 */
  --border-strong: #8F7C6E;

  /* ★ 2026-10-10 加深：原来是 #0E8F5D，压在 --success-soft 底上只有 **3.69:1**，
     掉出 AA（要 4.5）。它掉得不显眼——`.pill-ok` 是 12px、`.msg-kind` 是 11.5px，
     本来就是最难看清的那一档，而绿底绿字「看着挺清楚」。
     ★ 这两个色只在**纯白**上量过一次就定了（老毛病，和 --muted 栽的是同一件事）。
     现在 #0A7A4E：浅绿底 4.83:1，纯白底 5.6:1；`--success` 还被 .toast.is-success
     当成**底色**用（白字压在上面），加深只会让它更清楚，不会变差。 */
  --success:      #0A7A4E;
  --success-soft: #E4F7EF;
  --danger:       #C62828;
  --danger-soft:  #FDECEA;
  /* ★ 2026-10-10 加深：原来是 #A96F1C，注释里写着「白底 4.2:1，满足 AA」——
     **4.2 < 4.5，从来就没满足过**。而它真正的用武之地是 `.form-tip` 的暖底
     #FDF4E5，实测只有 **3.87:1**。
     2026-10-21 换 D 套后是 #7A5710：暖底 6.02:1、纯白底 6.57:1，
     卡片白底上的 `.stat-num.is-warn` 一并达标。 */
  --warn-ink:     #7A5710;

  /* ★★ RGB 三元组：上面每个颜色都在这儿有个「分身」。
     起因是全站有 38 处 rgba(36,84,230,.28) 这种字面量——它们不跟着 --brand 走，
     所以换配色时按钮变了、阴影和聚焦圈还是蓝的（改第一版配色时就踩了这个坑）。
     现在 CSS 里一律写 rgb(var(--brand-rgb) / .28)。
     ★ 下面是空格分隔的写法（CSS Color 4），不是逗号——逗号写法在 rgb() 里不合法。
     ★ 改主色时这里**必须一起改**，漏一个那个元素就变成透明（不是报错，是静默失效）。 */
  --brand-rgb:        122 27 46;
  --brand-dark-rgb:   87 16 31;
  --brand-light-rgb:  156 48 69;
  --accent-rgb:       30 58 95;
  --accent-dark-rgb:  20 41 68;
  --accent-light-rgb: 62 97 144;
  --teal-rgb:         138 106 47;
  --teal-light-rgb:   180 145 83;
  --jade-rgb:         47 93 80;
  --jade-light-rgb:   75 122 107;
  --shadow-rgb:       45 25 30;

  --radius:       14px;
  --radius-sm:    9px;
  --radius-lg:    22px;

  /* 阴影掺的是主色（--shadow-rgb）而不是纯黑：纯黑阴影落在带色的浅背景上
     会发灰、显脏，掺了主色之后影子像是「光被挡住了」而不是「糊了一层灰」。
     ★ 换配色时 --shadow-rgb 要跟着主色走，否则紫底子上会出现一层灰蓝的影子。 */
  --shadow-sm:    0 1px 2px rgb(var(--shadow-rgb) / .06), 0 1px 1px rgb(var(--shadow-rgb) / .04);
  --shadow:       0 6px 22px rgb(var(--shadow-rgb) / .09);
  --shadow-lg:    0 18px 48px rgb(var(--shadow-rgb) / .16);

  /* 有颜色的「光晕」，只给最该被看见的元素用（两张登记大卡、主按钮）。
     全站到处用就等于没有——它靠的是和周围的对比。 */
  --glow-brand:   0 10px 30px rgb(var(--brand-rgb) / .28);
  --glow-accent:  0 10px 30px rgb(var(--accent-rgb) / .32);
  --glow-teal:    0 10px 30px rgb(var(--teal-rgb) / .26);
  --glow-jade:    0 10px 30px rgb(var(--jade-rgb) / .26);

  /* ★ 三条渐变一律由上面那几个色**推导**出来，不再写死十六进制——
     写死的话换配色时按钮渐变会和主色对不上（这正是上一版的老毛病）。 */
  --grad-brand:   linear-gradient(135deg, var(--brand) 0%, var(--brand-light) 100%);
  --grad-accent:  linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
  --grad-teal:    linear-gradient(135deg, var(--teal) 0%, var(--teal-light) 100%);
  --grad-jade:    linear-gradient(135deg, var(--jade) 0%, var(--jade-light) 100%);

  /* ★★ 2026-10-06 第三轮：「实心品牌色渐变」全站只留给**能点的东西**（两个登记大按钮、
     主按钮）。装饰性的图标块改成浅色底 + 品牌色字——不然喊的人和干活的人穿一样的衣服，
     按钮就不突出了。这条规则换配色时要一起想。
     ---- 下面四个是「对接流程」1→4 的进度渐变 ----
     色相从 --brand 一路走到 --accent，让颜色自己说明「这是有先后的四步」，
     而不是四个并列的装饰。中间两档按 D 套的绛红→藏青在 sRGB 里插值
     （深的一档和浅的一档各插各的 1/3、2/3），四格连起来是一条直线。
     ★ 白字对比度实测（深端/浅端），2026-11-02 回到 D 套重算：
       1 10.38/7.19  2 11.80/7.59  3 12.11/7.22  4 14.68/11.50
       四格全部 ≥4.5:1，普通字号也过 AA——这四个数改色时得重算。 */
  --grad-accent-deep: linear-gradient(135deg, var(--accent-dark) 0%, var(--accent) 100%);
  --step-1: linear-gradient(135deg, var(--brand) 0%, var(--brand-light) 100%);
  --step-2: linear-gradient(135deg, #5B253E 0%, #7D405E 100%);
  --step-3: linear-gradient(135deg, #3D304F 0%, #5D5177 100%);
  --step-4: linear-gradient(135deg, var(--accent-dark) 0%, var(--accent) 100%);

  --container:    1140px;
  /* ★ 2026-10-06 字体升级：**拉丁字母和数字走最前面的那一档**，
     中文落回 PingFang / 雅黑。原来把中文字体写在最前面，西文和数字
     也跟着用雅黑的西文字形——那个字形又宽又旧，是整站最显廉价的一处。
     Segoe UI Variable 是 Win11 自带的，和雅黑的中文混排是标准搭配。 */
  --font: "Inter", "Segoe UI Variable Text", "Segoe UI", -apple-system,
          BlinkMacSystemFont, "PingFang SC", "HarmonyOS Sans SC", "MiSans",
          "Microsoft YaHei UI", "Microsoft YaHei", "Hiragino Sans GB",
          "Noto Sans SC", sans-serif;
  /* 标题专用：和正文只差在**先取哪一档西文**，中文那截完全一样，
     所以中英混排的标题不会在中间换一次字形。 */
  --font-display: "Inter", "Segoe UI Variable Display", "Segoe UI", -apple-system,
          BlinkMacSystemFont, "PingFang SC", "HarmonyOS Sans SC", "MiSans",
          "Microsoft YaHei UI", "Microsoft YaHei", "Hiragino Sans GB",
          "Noto Sans SC", sans-serif;
  /* 数字/编号：等宽数字（tabular-nums）让并排的几个数上下对齐 */
  --font-num: "Inter", "Segoe UI Variable Text", "Segoe UI", ui-monospace,
          "Cascadia Mono", Consolas, "PingFang SC", "Microsoft YaHei", monospace;
  /* ★★ 2026-10-09：楷体，**只给「说明性小字」用**，不给标题、不给正文。
     ★ 来历：2026-10-08 首屏导语的第一句先用了楷体（用户要「艺术一点的字体」），
       用户认可；这次要求「挑一部分合适的小字也换成同一个楷体」。
       所以**全站只有这一处定义**，.hero-lead-art 也改用它——「那个楷体」
       必须只有一份，改一次全站跟着走。
     ★★ 写法上最容易踩的一脚：**西文那几档必须留在最前面**。
       CSS 的字体回退是**逐字符**走的——「交」在 Inter 里没有，往后找到楷体；
       「A」在 Inter（或 Segoe UI）里有，就地停下。所以拉丁字母和数字仍然
       走原来的现代西文，只有汉字落进楷体。
     ★ 中文那档**故意不写 PingFang / 雅黑**：写了它们就轮不到楷体了。
     ★ 三家的族名各不相同，一个都不能少：
       Windows = KaiTi / 楷体（还有 华文楷体 这一档，Office 带）、
       Mac/iOS = Kaiti SC / STKaiti（**没有 KaiTi 这个族名**）、
       Android 多半没有前两个，落到泛型 serif。
       ⚠️ 末档必须是泛型 serif：楷体落空时掉进一个衬线体，比掉回黑体好——
         掉回黑体等于这个改动在某些设备上无声消失。
     ⚠️ 楷体的**字面比雅黑小一圈**，同样 px 看着更小。所以下面凡是用它的地方
       都要**把字号补大一档**（×1.06，见各处的注释），不补就成了「字缩水了」。 */
  --font-kai: "Inter", "Segoe UI Variable Display", "Segoe UI", -apple-system,
          BlinkMacSystemFont, "KaiTi", "STKaiti", "Kaiti SC", "楷体",
          "华文楷体", serif;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.7;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* 中文正文的标点挤压：让「，。」这些全角标点两侧的空白自动收一点，
     一段话读下来的节奏比默认紧实，是长文页面最容易被忽略的质感来源。
     不支持的浏览器直接忽略这两行，没有副作用。 */
  text-spacing-trim: trim-start;
  font-kerning: normal;
}

/* 标题统一走 --font-display（西文换一档更现代的），并把字距收紧。
   ★ text-wrap: balance 让标题自己挑断行位置——「我是大学生，我要提供教学」
     这种长标题原来会在最后两个字上落单，balance 之后两行长度接近。
     只对标题用，正文用 pretty 会让段落变短、留白变多。 */
h1, h2, h3, h4 {
  margin: 0; font-family: var(--font-display);
  font-weight: 700; line-height: 1.35; letter-spacing: -.015em;
  text-wrap: balance;
}
p  { margin: 0; }
a  { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
img, svg { max-width: 100%; }

.container { width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 20px; }
.container-narrow { max-width: 780px; }

/* ---------------------------------------------------------------- 页头 */

.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(255, 255, 255, .78);
  backdrop-filter: saturate(200%) blur(16px);
  border-bottom: 1px solid transparent;
  /* 页头自带一条**极淡的品牌色**下沿（而不是灰线）：滚动时它就是
     「内容从下面穿过去」的那条分界。用渐变是因为两头淡、中间实，
     比一条通体等深的直线轻，不会在第一屏上方压出一道横杠。 */
  background-image: linear-gradient(90deg,
    transparent, rgb(var(--brand-rgb) / .28) 22%, rgb(var(--brand-rgb) / .28) 78%, transparent);
  background-repeat: no-repeat;
  background-position: bottom;
  background-size: 100% 1px;
  transition: box-shadow .25s, background-color .25s;
}
/* 滚动之后才给投影（由 common.js 加 .is-scrolled）。
   停在页面顶上时页头是「平」的，一滚就浮起来——这一下让整站显得有层次。 */
.site-header.is-scrolled {
  background-color: rgba(255, 255, 255, .92);
  box-shadow: 0 6px 24px rgb(var(--shadow-rgb) / .07);
}

.header-inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; height: 64px;
}

.logo { display: flex; align-items: center; gap: 10px; color: var(--text); text-decoration: none; }
.logo:hover { text-decoration: none; }
/* 2026-10-23：新校门线稿四周自带留白，44px 时的视觉体量与原来的 34px 方块接近。 */
.logo-mark { width: 44px; height: 44px; flex: none; }
.logo-text { display: flex; flex-direction: column; line-height: 1.15; }
.logo-name { font-size: 17px; font-weight: 800; letter-spacing: .06em; }
.logo-sub  { font-size: 10px; color: var(--muted); letter-spacing: .04em; }

.site-nav { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }

.site-nav a {
  position: relative;
  padding: 7px 12px; border-radius: var(--radius-sm);
  font-size: 14px; color: var(--text-soft); text-decoration: none;
  transition: background .15s, color .15s;
  white-space: nowrap;
}
.site-nav a:hover { background: var(--brand-soft); color: var(--brand); text-decoration: none; }
.site-nav a.active { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
/* 当前页那条小下划线（渐变），比整块底色更「像官网」。
   只有 active 有，hover 时从中间展开——0 宽度到满宽，比淡入更有指向性。 */
.site-nav a.active::after {
  content: ""; position: absolute; left: 12px; right: 12px; bottom: 2px; height: 2px;
  border-radius: 2px; background: var(--grad-brand);
}

/* ---------------------------------------------------------------- 按钮 */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 11px 22px; border: 1px solid transparent; border-radius: var(--radius-sm);
  font-family: inherit; font-size: 15px; font-weight: 600; line-height: 1.4;
  cursor: pointer; text-decoration: none;
  transition: background .15s, border-color .15s, transform .1s, box-shadow .15s;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .55; cursor: not-allowed; transform: none; }

/* 主按钮走渐变 + 同色光晕。
   ★ 悬停时**不改 background**，而是盖一层半透明的深色：
     渐变是 background-image，直接换 background 会在「渐变↔纯色」之间跳一下；
     用 ::before 叠一层就永远是「同一个渐变，暗了一档」。
   ★ 深色只用 rgba(0,0,0,.14) 的黑而不是换一个色值，是为了让三个色相的按钮
     悬停观感一致——换色值的话橙色压下去会偏红、蓝色压下去会发紫。 */
.btn-primary, .btn-accent { position: relative; overflow: hidden; }
.btn-primary::before, .btn-accent::before {
  content: ""; position: absolute; inset: 0; opacity: 0;
  background: rgba(0, 0, 0, .14); transition: opacity .16s;
}
.btn-primary:hover:not(:disabled)::before,
.btn-accent:hover:not(:disabled)::before { opacity: 1; }

.btn-primary {
  background-image: var(--grad-brand); color: #fff;
  box-shadow: var(--glow-brand);
}
/* ★ 2026-10-06：底色从 --grad-accent 换成 --grad-accent-deep，字从 #4A2E00
   （深棕）换成白。原来那套是给**琥珀黄**配的——深棕字压在黄底上 6.04:1，很好；
   换成玫红之后同一组颜色只剩 3.58:1，直接掉出 AA。深棕是旧配色留下的化石。
   现在深端 6.30:1、浅端 5.94:1，两端都过。 */
.btn-accent {
  background-image: var(--grad-accent-deep); color: #fff;
  box-shadow: var(--glow-accent);
}
/* 按住时把光晕收掉一点，配合 .btn:active 的下沉，手感更"实" */
.btn-primary:active:not(:disabled), .btn-accent:active:not(:disabled) {
  box-shadow: var(--shadow-sm);
}

.btn-ghost { background: var(--surface); color: var(--text-soft); border-color: var(--border); }
.btn-ghost:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }

/* 删除这类不可逆的操作：平时低调成灰色，鼠标悬停才变红。
   后台表格里按钮很密，一列红按钮太扎眼，也容易点错。 */
.btn-danger { background: var(--surface); color: var(--text-soft); border-color: var(--border); }
.btn-danger:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }

.btn-sm { padding: 7px 14px; font-size: 13.5px; }
/* 手机紧凑行里那个「申请对接」（2026-10-24）。它和人数、姓名挤在同一行，
   再小也得保住 12px 的字和约 25px 的点击高度；光晕收掉，否则一排小按钮
   底下各拖一团藏青雾，比按钮本身还显眼。 */
.btn-xs { padding: 3px 9px; font-size: 12px; line-height: 1.5; border-radius: 7px; }
.btn-xs.btn-accent { box-shadow: none; }
.btn-block { width: 100%; }

/* ---------------------------------------------------------------- 卡片 */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.card-pad { padding: 24px; }

.section { padding: 56px 0; }
.section-sm { padding: 36px 0; }

.section-head { margin-bottom: 26px; }
.section-head.center { text-align: center; }
.section-head h2 { font-size: 25px; }
/* ★ 2026-10-09：章节副标题换楷体（用户要「挑一部分小字换成首屏导语那个楷体」）。
   ★ 为什么挑它：它是全站统一的「说明性小字」层——每个 h2 下面那句短话
     （「为您提供家教需求和老师列表」「从登记到开课，四步完成。」）。
     短、成对、紧挨着标题，是这个改动最自然的位置。
   ★ 14.5 → 15.4px：楷体字面小一圈，不补这一档看着像字缩水了（×1.06，
     和 .hero-lead-art 同一个系数）。⚠️ 改这个字号时别忘了这层是补过的。 */
.section-head p {
  margin-top: 7px; color: var(--muted);
  font-family: var(--font-kai); font-size: 15.4px;
}

.page-head { padding: 40px 0 26px; }
.page-head h1 { font-size: 29px; }
.page-head p { margin-top: 9px; color: var(--text-soft); max-width: 660px; }

.breadcrumb { margin-bottom: 14px; font-size: 13.5px; color: var(--muted); }
.breadcrumb a { color: var(--muted); }
.breadcrumb a:hover { color: var(--brand); }

/* ================================================================
   ★ 2026-10-08：**三个广场各配一所上海高校的校门**（社会需求 / 大学生需求 /
     师资，一人一张）。

   用户第一版原话：「社会需求广场，大学生需求广场，师资广场这三个各加一张
   不同的上海高校校门图片当背景，跟交大徐汇校门一样」。

   ★★ 2026-10-08 第二版——**用户看过第一版后打了回票**。原话：
     「三个广场的图片应该和首页交大那个背景一样大啊，作为内透，
       你为什么只放在右上角，应该作为整个页面的打底」。
     第一版做的是**页头右上角一个 430px 的小图**，当时的理由是
     「页头右边正好空着 440px，放那儿一个字都不压」。
     ⚠️⚠️ **这个理由本身没错，但把要求换掉了**：用户要的不是
        「在你找得到的空白处摆个装饰」，是「**像首屏那座一样铺满整幅**」。
        我为了对比度安全把图缩进角落，等于拿"不压字"顶替了"像首屏"——
        指标选错了，越优化越偏。教训记在这儿，别再犯。
     第二版就照 .hero::after 那套抄：**宽 1180、居中、锚在 <main> 顶上**。

   ★ 「跟交大徐汇校门一样」指的是**做法**一样，不是长一样：
     PNG 的 RGB 全黑、画全在 alpha 通道，颜色由这里的渐变给，
     和首屏那座、以及首页「三个广场」那节后面那座**共用同一条流水线**
     （%TEMP%\gate\mkg.py）和**同一段渐变**（102deg，紫 → 玫红）。
     ⚠️ 三张图必须**看起来像一个系列**——渐变、aspect-ratio 都要一致，
        只有 PNG 不同。**别给某一张单独换色。**
   ================================================================ */

/* ★ 类挂在 <main> 上，**不是 .page-head**：它现在铺的是整页。
   三个页面各自配哪一所，见 demands.html / college.html / teachers.html。 */
main.has-gate { position: relative; }

main.has-gate::before {
  content: ""; position: absolute; left: 50%; top: 0; z-index: 0;
  /* ⚠️ translateX(-50%) 是**居中的那一半**，跟 left:50% 是一对，少一个图就
     从中间往右铺、右边被切掉（第一版就漏了这个，页面上看得出来）。
     首屏那座是 left:50% + translateX(-50%) translateY(24.75%)，同一个道理。
     ★ 2026-10-08 加了 --gate-lift：三张图共用**同一个基类**，但上财/复旦要整体
        上提、华师大不用，所以把「提多少」做成变量，默认 0 = 原地不动。
        ⚠️ 别把这两个函数拆成两条 transform 声明——后一条会把前一条整个盖掉，
           图就又跑偏了。 */
  transform: translateX(-50%) translateY(var(--gate-lift, 0));
  /* 跟首屏那座同宽（.hero::after 也是 min(1180px, 100%)）。
     ⚠️ 容器内宽只有 1100，所以 1180 会往左右各探出 40px——**这是故意的**，
        和首屏一样在页边露一点，看着像「从内容底下铺开」，
        而不是「内容上面贴了一张图」。
     ⚠️ 别再改回 calc(100% - 700px) 那套「只放右边」的写法，
        用户已经明确否过一次了。 */
  width: min(1180px, 100%);
  aspect-ratio: 1200 / 658;
  pointer-events: none;
  background-image: linear-gradient(102deg,
    var(--brand-light) 0%, var(--brand) 46%, var(--accent) 100%);
  -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;    mask-size: 100% 100%;
  /* ★ 20%，比首屏那座（.23）还淡一档。
     为什么比首屏**更**淡：首屏那座是「屋顶淡、台基实」，绝大部分面积是浅的；
     这三张是**贴着门裁的**（上下都按门本体收过），所以同一浓度下，
     落在文字后面的"实的部分"比首屏多得多。同浓度会明显比首屏抢。

     ★★ 实测（2026-10-08，.20 这一档，无头 Edge 1400px，真渲染像素）：
        把内容 visibility:hidden 后截**真背景**，取每条文字带里**最暗**的那个像素
        （背景越暗、深色字越吃亏），再配该行的前景色算比值。
        **校门上提 30% 之后**的值（上财/复旦的牌匾现在正好压在页头文字后面，
        所以比上提前低——低是应该的，只要还过线就行）：
                        面包屑 --muted    h1 --text    说明段 --text-soft
          社会需求(上财)      5.04            10.45         ★ 4.96
          大学生需求(复旦)    5.28            10.67           5.24
          师资(华师大)        5.39            10.40         ★ 4.86
        全部过 AA（4.5）。**最紧的是说明段那两档 ≈4.9:1，余量不到 10%。**
        （对照，上提**之前**同一套量法：7.33/15.04/7.10、5.76/11.50/5.19、
          5.12/10.50/4.96 —— 上财那页掉得最多，因为它原来牌匾那一带**根本没墨**，
          只有纯底色，现在有墨了。华师大没动，所以基本不变。）
     ⚠️⚠️ 所以这个 .20 **基本就是天花板了**——想再加浓，先把 --muted/#475163
        或 --text-soft/#46536B 这两个色加深，**别直接改这个数**。
     ⚠️⚠️ 算比值**必须做 sRGB 线性化**：
        通道 c<=.03928 ? c/12.92 : ((c+.055)/1.055)^2.4，再加权 .2126/.7152/.0722。
        直接拿 0-255 去做加权平均会**全线算低**：同一组 #475163 / #F6F4FB
        会得出 2.77 而不是正确的 7.33，然后你就会去"修"一个根本不存在的
        对比度问题。（2026-10-08 这一版就踩了，白跑一轮。）
        （同一个坑 2026-10-07 在首屏踩过一次：--muted 在浅紫底上从来没过 AA。）
     ⚠️ 量的时候**必须按文字带逐条量**：拿全页最暗像素去配远处的文字，
        会造出一个根本不存在的低比值（也是踩过的坑）。
     ⚠️ 还要注意：**页面底色本身是 #F6F4FB（偏紫）**，
        拿"白底"或"纯中性灰"当参照去筛染色像素会全部误判。 */
  opacity: .20;
}

/* 内容重新回到背景**前面**。
   ::before 是定位元素（z-index:0），而普通流内容画在 stacking level 0 **之下**，
   不给 z-index 的话标题、筛选条、卡片会糊在门上面
   （首屏那次、以及首页「三个广场」那节，踩的是同一个坑）。 */
main.has-gate > .container { position: relative; z-index: 1; }

/* 三张各自的图。**只换图，别的都不动**——几何、渐变、浓度全在基类里。
   （唯一的例外是下面那个 --gate-lift，理由写在它自己那段注释里。） */
.gate-demands::before {
  -webkit-mask-image: url("/img/gate-demands.png");
          mask-image: url("/img/gate-demands.png");
}

/* ★★ 2026-10-08：上财、复旦这两张要**整体上提 30%**，华师大那张一点不动。
   （用户原话：「这几个除了华师大剩下的校名那个牌子都没露出来，往上移动一下」，
     以及「交大不用」。）

   为什么非提不可：三张图都是**按门本体贴着裁**的，但「校名牌」在门上的**高低
   位置天生不同**，裁完自然落在不同的行上——
       华师大  牌匾在源图 y≈  0– 95   ← 这一张就是「露出来了」的那张
       复旦    牌匾在源图 y≈210–282
       上财    牌匾在源图 y≈228–262
   而页面从 y≈260 往下被一条**不透明**的 `.form-note`（--brand-soft 实底色）
   压住，所以「牌匾露得出来」的窗口只有 y≈70–259 这一段。上财/复旦的牌匾原本
   落在 y≈295–330，整个躲在提示框后面 —— 这就是用户说的「没露出来」。
   把这两张的牌匾对到华师大那张**同一个页面高度**（约 y=117），两张都要上提
   ≈194 页面像素；元素自身高 646.9px（= 1180 × 658/1200），194/646.9 ≈ **30%**。
   两张算下来只差 1 个像素，所以**一个值就够，不必分页各写一个**。

   ⚠️ 必须用 %，不能用 px：translateY 的百分比是**相对元素自身高度**的，会跟着
      图一起缩，窄屏上比例不变。写成固定 px，窄屏的构图就整个坏掉。
   ⚠️ 上提 30% = 图的上沿跑到 `main` 盒子外面去了（屋顶那截伸到页头底下）。
      这是**安全的**，别去"修"：页头是 position:sticky + z-index:50 +
      rgba(255,255,255,.78) + backdrop-filter，会把它盖住。为了"不溢出"而给
      main 加 overflow:hidden 反而会切出一条硬边。
   ⚠️ 这 % 是**跟着图的内容算出来的**，不是随便挑的圆整数。哪张图换了、
      或者裁剪窗口改了，这个数就得重新量。 */
.gate-demands::before,
.gate-college::before { --gate-lift: -30%; }
/* 大学生需求广场 = **复旦大学**。
   （原配的是同济那张，用户看过之后说「同济大学那个不要了」。
     同济那张的取线本身也确实是三张里最弱的一张：门又宽又扁，
     且是阴天灰水泥，线短而碎——虽然已经用 auto 拉伸补救过，仍是最弱的一环。）
   ⚠️ 首页那份复旦背景**已经撤了**（用户后来说「首页那个复旦不要了」），
      所以 fudan-gate.png 现在**全站只有这一处在用**。
   ⚠️ 这张的牌匾原本也被提示框压住，靠上面的 --gate-lift 才露出来。 */
.gate-college::before {
  -webkit-mask-image: url("/img/fudan-gate.png");
          mask-image: url("/img/fudan-gate.png");
}
.gate-teachers::before {
  -webkit-mask-image: url("/img/gate-teachers.png");
          mask-image: url("/img/gate-teachers.png");
}

/* ---------------------------------------------------------------- 首页 */

.hero {
  position: relative;
  /* ★ 2026-10-08：`overflow: hidden` → `visible`，**为了让校门能往下淌**。
     原来那道 hidden 是拿来裁 ::after 的（它比 .hero 宽 6%），
     但顺手也把校门在首屏底边齐刷刷切断了——用户看到的就是「校门下半截没了」。
     现在横向上不靠裁了：::after 的宽度收到 100%（见下），
     mask 自带的左右淡出本来就在收边，所以不会溢出、也不会出横向滚动条。
     ⚠️ 别想着用 `html { overflow-x: hidden }` 兜底——页头是 position: sticky，
        祖先一旦 overflow 不为 visible，sticky 就会在祖先里滚，是另一个坑。 */
  overflow: visible;
  /* 底部从 62 加到 122：给下面那道「校门线稿」留出落脚的地方。
     ⚠️ 这个数**不能再往上加了**——首屏一高，「从哪里开始？」那两张登记大卡
        就掉出第一屏，而「让家长一眼找到在哪登记」是 2026-09-29 改版的全部动机。
     ★ 2026-10-08 补：校门往下淌的那 170px **不计在这里**——它是 ::after 自己
       溢出去的，不占布局、不推高首屏，所以「不能再往上加」这条依然成立。 */
  padding: 74px 0 122px;
  /* 2026-10-06 第三轮：「丙 · 一条渐变细线」。
     ★ 上一版是五层半透明渐变（这里两层 + ::before 三层）+ blur(6px) + 26s 漂移，
       用户的原话是「模模糊糊的，跟两坨大粪一样」。病根三条，记下来别再犯：
       ① 五层颜色各不相同，叠在一起必然互相污染，中间那片就成了灰的；
       ② blur(6px) 加在本来就柔和的径向渐变上 = 又糊一次；
       ③ **深色在低透明度下会发灰、不发紫**（明度上去、彩度掉下来），
          所以雾必须用 --brand-light 这一档，绝不能用 --brand 去兑水。
     ★ 现在的做法：首屏几乎全白，颜色全押在顶上那条 3px 渐变线上。
       高饱和但面积极小 —— 这是「鲜艳但不聒噪」的极端解法。
     ★ 细线用 z-index:2 压在内容之上；颜色用不透明的 --brand/--accent，
       所以它不受上面那条「深色兑水会发灰」的规律影响。 */
  background:
    linear-gradient(180deg, rgb(var(--brand-light-rgb) / .10) 0%, rgb(var(--brand-light-rgb) / .025) 38%, transparent 66%),
    linear-gradient(180deg, #FFFFFF 0%, var(--bg) 100%);
  /* ★ 2026-10-08：首屏原来在底边有一条 1px 的 --border 细线，作为
     「首屏到此为止」的分界。校门往下淌之后它就成了**横穿校门的一道横线**，
     正好切在门洞和石狮身上——「把校门做全」和「在门上划一刀」是矛盾的。
     ★ 去掉它不会让首屏失去边界：首屏的渐变本来就收在 var(--bg)，
     而 body 也是 var(--bg)，所以交界处**本来就没有色差**，
     那条线是唯一的分界物；现在分界改由校门自己承担。
     ⚠️ .hero 只在首页用，这条改动不外溢。 */
}

/* 顶上那条 3px 的紫→玫红细线。首屏唯一的「大声」元素。 */
.hero::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; z-index: 2;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 0%, var(--brand) 18%, var(--accent) 82%, transparent 100%);
}

/* ★★ 2026-10-08：**把校门做全，让它往下透过「从哪里开始？」**。
   用户原话：「校门的下半部分被『从哪里开始』以及下面遮住了，
   希望能把校门背景做全，往下补全作为内透」。

   病灶在**裁切**：源照片的 CROP 底边是 y=1280，正好切在额枋下沿——
   画里只有屋顶、额枋、灯笼、匾额上半、狮子头；
   红砖墙、三个门洞（连同**门洞里透出来的校园**）、石狮全身、台基，全在画外。
   到页面上就是「校门在首屏底边齐刷刷断掉」。这次只做三件事：

     ① CROP 底边 1280 → 1680（把台基和一小条地面也收进来当「脚」）；
        图从 1200x520 变成 1200x691，生成脚本 %TEMP%\gate\mk15.py（L 系列）。
     ② 竖淡出 vmap 从「按高度归一化」改成「**按绝对像素锚定**」——
        归一化的话图一长高，整条曲线就被摊薄，屋顶会变实、台基会变虚，
        首屏那套调好的浓淡立刻失效。锚定之后，png_y ≤ 520 那一段
        **和 2026-10-07 定稿逐点相同**，只在下面接一条新尾巴往下化开。
     ③ 连接生长的种子从「H 的 60%」改成**锚在源照片 y=792**（同 ①，
        比例写法会让种子随图高往下掉，实测首屏丢了 3106 像素的线，
        匾额上「交」字和灯笼弧就在里面）。

   ★ 往下淌是**溢出**，不是布局：.hero 不再裁（overflow: visible），
     ::after 底边仍钉在首屏底边，再用 translateY(24.75%) 把整幅下推。
     百分比相对**自身高度**，所以窄屏上校门缩小、下推量跟着缩，
     顶边始终停在原位——首屏的版式和对比度一点没动。
   ★ 首屏底边那条 1px --border 细线**取消了**（见 .hero 里的说明）：
     它原本和校门的底边重合，现在会变成横穿门洞和石狮的一道横线。
   ⚠️ 「从哪里开始？」那一节的内容要显式抬到 z-index:1，见下面的规则。

   ★ 复测（无头 Edge，量**真渲染的像素**，文字先 visibility:hidden）：
     统计标签 4.94 / 5.02 / 5.65，新压在门上的 h2 10.66，信任条 7.33 —— 全过 AA。

   ------------------------------------------------------------------
   ★★ 2026-10-07：首屏背景的**交大老校门**（华山路庙门），见 public/img/sjtu-gate.png。
   为什么是「门」：首屏下面紧接着那一节就叫「从哪里开始？」、内容是**两个登记入口**，
   背景立一扇门，是「入口」这件事的视觉回声——而且三开间横向展开，
   天生长成一条地平线，压在首屏底部正好。

   ★★ 这一版是**真实照片**做的，不是画的、也不是测绘图上色：
     2026-10-07 手画的那版被用户否掉（原话「这真是做了一坨屎…根本不是美观」）。
     病根是**手搓图元**——瓦垄、砖缝、门钉、彩画这些让画面「像真的」的东西，
     靠手写 path 根本画不出来，画出来就是剪贴画。
     随后拿测绘立面图上色（scipy.ndimage.label 切块逐像素上色）好了很多，
     但用户接着要**侧拍**：正视立面是工程图，**侧拍才有进深和阴影**。
     最终用的就是侧拍照片 —— 屋顶的起翘、彩画的团花、灯笼的圆、门洞的进深、
     砖墙的明暗，全都是照片自带的，不是画出来的。

   🚨 **授权**：素材 File:SJTU Xuhui Campus Gate.JPG
     （Wikimedia Commons，作者 **Jucember**，**CC BY-SA 3.0**）。
     这是**署名 + 相同方式共享**：上线必须在站上挂 credit（页脚一行即可），
     且本图作为演绎作品仍按 CC BY-SA 分发。改图不是问题，**不署名才是**。
     若想省掉 share-alike，Commons 上另有一张正视的
     Shanghai Jiao Tong University 1.jpg 是 **CC BY 3.0**（只要署名），
     代价是回到正视——用户明确要侧拍，所以没选它。

   ★★ **照片遮罩必须压曲线**（这次新踩的坑）：
     线稿遮罩的 alpha 接近二值（线＝255、纸＝0），所以 8.5% 下清清楚楚；
     照片遮罩的 alpha 是**一大片中间调**（实测均值 0.20），同一个 8.5% 乘下去
     只剩 1.7%，等于没有——**看上去跟没加载一样**。
     解法是先把 alpha 按 **^2.2** 压一遍：暗部留下、中间调掉光，
     重新变成「线稿」那种稀疏感，8.5% 才重新成立。
     ⚠️ 顺带否掉的路子：把照片降饱和 + 压低 alpha 当**淡彩照片**贴上去——
     在 34% 透明度下红墙绿瓦全部糊成一片灰，像块脏印子。低透明度**杀颜色**，
     彩照要么全强度上、要么就别上。

   ★ 做法是 CSS mask，不是 background-image：
     PNG 里 RGB 全是黑、只有 **alpha 通道**带画（哪里有墨哪里透光），
     颜色完全由下面这条渐变给。所以换配色只动 CSS，图一个字不用改——
     这正是 2026-10-06 那轮把 38 处色值参数化定下的规矩。
     ⚠️ 旧的 public/img/sjtu-gate.svg（手画线稿）已经没人引用了，留着只为对照。
   ★ 浓淡（opacity）压到两成出头（现值 23%）：它是**背景**，不是插画。
     首屏的「大声」只留给顶上那条 3px 细线和两张登记大卡，
     这里一抢戏整屏就散了。
   ★ 渐变的走向（紫→玫红、左到右）和顶上那条细线是**同一套**，
     一上一下把首屏框住，读者的眼睛不会觉得上下是两个世界。
   ★ 淡入淡出**烘焙在 PNG 的 alpha 里**（上淡下实 + 左右收边），不是靠 CSS opacity：
     后者只会把整幅一起调浅，那是「图变淡了」，不是「雾」。
   ★ z-index:0，内容在 .hero > .container 的 z-index:1 之上。
     ★★ 对比度必须**在真截的背景上量**。以前那版探针是「按 CSS 公式合成一个底」
     再量的，系统性地乐观了 ~0.5：它报统计标签 4.92 过 AA，而在真页面上
     （遮罩 opacity 置 0、正文整块 visibility:hidden 之后截一张纯背景）
     实测只有 **4.38**，从来就没过。教训和 10-06 那轮一样 ——
     **合出来的底 != 真的底**，量对比度只能用渲染出来的像素。
     现测（op .23、--muted #475163）：h1 14.43、lead 5.31、note 4.93、
     统计数字 7.92、**统计标签 4.56:1**（最紧的一档，过 AA）。
     ⚠️ **23% 是这套配色下的天花板**：.24 掉到 4.48、.26 掉到 4.33。
     再想加浓就只能继续压 --muted —— 但压到 #3F4858 这一档，
     「次要文字色」本身就不成立了，层级会塌。要更浓得改背景配色，不是改这个数。 */
.hero::after {
  content: ""; position: absolute; left: 50%; bottom: -2px; z-index: 0;
  width: min(1180px, 100%); aspect-ratio: 1200 / 691;
  /* ★ 往下淌的那一段，**必须按自身高度算，不能写死像素**。
     校门底边要落到首屏底边下面 (691-520)/691 = 24.75% 个自身高度处：
       · 底边仍钉在首屏底边（bottom:-2px），
       · 再用 translateY 把整幅往下推 24.75% 个自身高度。
     这样**顶边恰好停在原来的位置**——屋顶压在哪几行字后面、统计数字那一档多浓，
     全都跟 2026-10-07 定稿时量过的一致，一点没动。
     ⚠️ 别改成 `bottom: -170px`：170 是在 1180px 宽下算出来的，
        窄屏上校门只有 400px 宽、整幅才 230px 高，写死 170px 会把它整个推到下一节去。
        translateY 的百分比是**相对自身高度**的，天生跟着缩放走。 */
  transform: translateX(-50%) translateY(24.75%);
  pointer-events: none;
  background-image: linear-gradient(102deg,
    var(--brand-light) 0%, var(--brand) 46%, var(--accent) 100%);
  -webkit-mask-image: url("/img/sjtu-gate.png");
          mask-image: url("/img/sjtu-gate.png");
  -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;    mask-size: 100% 100%;
  /* 23%。★ 这个数配着 PNG 的 alpha 里那条**上淡下实**的竖渐变一起调才有意义：
     单看浓度会走两个极端——太抢（屋顶压在正文上像贴了张画），
     太虚（门整个化没了）。真正的解法是把屋顶压淡、把台基留实。
     这两个数要一起改。

     ★★ 2026-10-07 定稿 **23%**，配 --muted **#475163**。两条教训都压在这个数上：

     ① 「太浅」的第一反应是把 opacity 一路推高 —— 错。稀素描再怎么加透明度
        也只是一片更深的雾，线条本身不会变清楚，还白白把整片背景压暗、
        让文字对比度付代价。**要动的是线，不是这个数。**

     ② 但「把线加粗」踩了更大的坑（用户原话「模模糊糊的，甚至看不出来这是交大门」）。
        当时的组合是 σ 2.2 + 圆盘核膨胀 r=2 + `despeckle(min_area=12)`。
        病根在第三项：**细线本来就只有几个像素宽，连通块面积很小，
        被当成麻点整段删掉** —— 连续的线断成虚线，膨胀再把残线撑成墨疙瘩，
        合起来就是一坨认不出来的东西。
        正解是**在取线之前就把噪声平滑掉**（高斯 presmooth σ=1.8 先糊掉
        砖缝瓦垄的碎纹理），而不是等取完线再删小连通块——
        后者删掉的是**结构**，前者糊掉的是**噪声**。
        然后 XDoG 提高 p（.992）、把 eps 抬到 0 让线连起来、φ 提到 40 让线实而不发灰，
        **不删小连通块（只删 1–2 像素的孤立点）、不做膨胀**。
        生成脚本 %TEMP%\gate\mk12.py（J 系列），覆盖率 12.0%。
     ⚠️ 「先删后撑」和「先糊再取」看着都是去噪，结果差一个量级：
        前者把线拆碎，后者把线留住。**加粗永远不如一开始就取到连续的线。**

     ③ 线对了之后还剩最后一个毛病：**画面外的杂物**。
        照片右上是一棵枯树（细枝在白天空里就是一片麻点），
        左上角还露着一栋现代玻璃楼。在 1200px 宽、23% 透明度下
        它们读不出「树」也读不出「楼」，只读得出**脏**。
        赶走它们的办法**不是再删小连通块**（那正是第 ② 条翻车的路），
        而是**问连通性**：拿下半幅（斗拱/匾额/灯笼/门洞/砖墙/石狮——闭着眼
        撒都错不了的「门」的区域）当种子，在墨里向上连通生长，
        长得到的是门、长不到的是天。鸱吻探出屋脊也一样活得下来，
        因为它跟屋面连着。**「连通性」判归属，「面积」判大小，前者才对。**
        ⚠️ 生长前墨要先膨胀 1–2 像素架桥（素描里到处是一像素的断口，
        不架桥整个屋面会因为一个断点脱落），长完再跟原墨求交，线宽不变。
        脚本 %TEMP%\gate\mk13.py / mk14.py（K 系列）。

     ★ 顶端 vmap 抬到 .70：当初压 .30 是怕屋顶跟 h1 抢，但叠上低透明度
     之后屋顶基本就没了 —— 而屋顶是整个校门最认得出来的部分。
     **opacity 和 --muted 是一对，动一个要动另一个。** */
  opacity: .23;
}

/* 首屏内容压在细线下面（细线只有 3px，但 z-index 还是要给，否则被 ::before 盖住） */
.hero > .container { position: relative; z-index: 1; }

/* ★ 2026-10-08：让**下一节的内容也压在校门之上**。
   .hero::after 是定位元素（z-index:0），
   而「从哪里开始？」那一节的内容是没定位的普通流——
   普通流画在 stacking level 0 **之下**，所以校门一溢下来就会糊在 h2 上。
   给它的 .container 一个定位和 z-index，标题就重新回到背景前面。
   ⚠️ 只加在 `.hero + .section-sm`（紧挨着首屏的那一节）上，
      不要动 .section-sm 基类——全站还有「对接流程」等好几节在用。
   ⚠️ 那两张 .pick-card 本来就是 position: relative，它们自己就压得住，
      这里管的是标题和信任条。 */
.hero + .section-sm > .container { position: relative; z-index: 1; }

/* ★ 2026-10-08：这里原来有一整段 `.squares-section` —— 给首页「三个广场」
   那一节挂的**复旦校门背景**（绝对定位的 ::before，不占布局）。
   **当天就撤掉了**，用户原话：
     「首页那个复旦不要了，还是跟改之前一样吧，我跟你说换到大学生广场了哈」
   —— 那张复旦是**搬**走的，不是复制：现在它是 demands/college/teachers
   这三页里 **college.html（大学生需求广场）那一页的整页打底**，
   见上面 `main.has-gate`。首页这边恢复成没有背景的普通一节。
   ⚠️ 别再把这段抄回来。首页要加校门的话，先问清楚是"这一节"还是"这一页"——
      这次来回改的根因就是我把「换到某页」读成了「两边都留一份」。
   ⚠️ fudan-gate.png 这个文件**还在用**（college.html），别顺手删。 */

.hero-grid { display: grid; grid-template-columns: 1.06fr .94fr; gap: 52px; align-items: center; }

/* 2026-09-29 首页改版：首屏改单列居中。
   原来的两栏是「左边介绍 / 右边三张信任卡」，那三张卡收成 .trust-strip 之后
   右栏就空了，所以首屏只剩一栏内容，居中才不显得偏。 */
.hero-inner { max-width: 720px; margin: 0 auto; text-align: center; }
.hero-inner .hero-lead { margin-left: auto; margin-right: auto; }
.hero-inner .hero-note { margin-left: auto; margin-right: auto; text-align: left; }
.hero-inner .hero-stats { justify-content: center; text-align: center; }

/* ★ 2026-10-22：首屏那条徽标（星 + 团队说明）已按用户要求整体删除，
   它的两条规则一并删掉，别留一段没人用的样式当考古现场。 */

/* ★ 2026-10-19：H1 从两行标语改成「交大学桥」四个字，字号跟着放大一档——
   四个字用原来的 42px 会缩成小小一块，撑不起首屏。 */
.hero h1 { font-size: 60px; line-height: 1.15; letter-spacing: -.01em; }
/* 标题后半句走渐变字。★ 三行缺一不可：background-clip:text 把背景裁成字形，
   color:transparent 让字本身透明（不然会把渐变盖住），再加一条 -webkit- 前缀
   （Edge/Chrome 认的是带前缀的那个）。★ 必须配一条纯色兜底写在前面——
   万一哪家浏览器不认 clip，纯品牌色至少是好看的，而不是消失。 */
.hero h1 em {
  font-style: normal;
  color: var(--brand);
  background-image: var(--grad-brand);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
/* 原来那句标语降为标题下的小字（2026-10-19），走正文那档颜色，保证 AA。 */
.hero-tagline {
  margin-top: 14px;
  font-size: 15px;
  color: var(--text-soft);
  letter-spacing: .04em;
}
/* ★ 2026-10-22：新标语 24 个字，390px 上会自动折成两行，第二行只剩「平台」——
   一个两字孤儿。这里把后半句包成一块，窄屏上在「）」后整块换行，
   上一行「上海市高校大学生（及线上老师）」、下一行「与家长家教对接平台」。
   ⚠️ 不要靠缩字号去挤成一行：13.5px 在 360px 上只余不到 1px，字体会更小且随时会崩。 */
.hero-tagline-break { display: inline; }
@media (max-width: 480px) { .hero-tagline-break { display: block; } }

.hero-lead { margin-top: 20px; font-size: 16.5px; color: var(--text-soft); max-width: 704px; }
.hero-lead strong { color: var(--text); }

/* ★ 2026-10-08：首屏导语的**第一句**换楷体（用户要求「艺术一点的字体」）。
   只包那一句，下面那段仍是雅黑——一楷一黑的反差正是想要的效果。
   ★ 字号必须**补大一档**：楷体的字面比雅黑小一圈，同样 16.5px 看着更小，
     不补这一档，换了字体反而像是「字缩水了」。
   ★ 但只能补到 **1.06em，不能再大**——这句话必须**占满一行**，
     掉一个字到第二行就成了孤儿（实测 1.16em 时末尾「台。」掉下去）。
     这个宽度是**可以精确算**的：中文是全角，每个字 1em，
     这句现在 **35 个字**（2026-10-19 加了「免费公益性质」6 个字），
     其中那 6 个字 2026-10-22 起放大到 1.6em（见 .hero-lead-em）：
     29 × 17.49 + 6 × 27.98 ≈ 675px，所以 `.hero-lead` 的 max-width
     从 640px 放到 **704px**（仍留 29px 余量，不贴着边）。
     改 max-width、改字数或改强调字号都要回来重算这一档。
   ⚠️ Mac 上没有 `KaiTi` 这个族名，回退链里必须有 `Kaiti SC` / `STKaiti`，
     否则 Mac 用户看到的还是雅黑，等于没换。
     （字号上限不受换字体影响——中文全角，换哪个中文字体都是一字 1em。） */
.hero-lead-art {
  font-family: var(--font-kai);
  font-size: 1.06em;
}

/* ★ 2026-10-22：用户要求把「免费公益性质」六个字放大加粗，突出平台的公益属性。
   ★ 首版 1.3em 用户反馈「手机上没显示变大」——手机端正文只有 15.9px，
     差距不够一眼看出来，而且手机系统普遍没有楷体、会回退到衬线体，
     字形本身就比桌面小一圈。现在放到 **1.6em**（手机 ≈25.4px、桌面 ≈28px），
     加粗 700，无论在哪种字体回退下都能明显看出强调。
   line-height 压到 1.15，避免这个放大的行内盒把整行行高抬高、把下面的内容往下推。
   white-space: nowrap 保证这六个字不会被折行拆成两截。 */
.hero-lead-em {
  font-size: 1.6em;
  font-weight: 700;
  line-height: 1.15;
  white-space: nowrap;
}

/* ★ 2026-10-27：首屏导语下面那四个词——公平 / 公益 / 健康 / 免费
   （用户要求「加几个大一点的词语」，位置就在那句「……免费公益性质学习需求
   沟通平台」的下面）。
   ★ 字号刻意比正文大一档（桌面 25px、手机 21px），而且要**明显**大一档：
     和 .hero-lead-em 那次踩的坑一样，差 3px 手机上根本看不出来。
   ★ 和导语第一句同一套楷体（--font-kai），视觉上归成一组；颜色走品牌色，
     因为这四个词是平台的定位，不是补充说明。
   ★ flex + wrap：390px 窄屏上四个词加三个圆点约 315px，一行放得下；
     真挤到放不下会自动折行，绝不会横向溢出（render-check 会查溢出）。 */
.hero-keys {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 0 16px; margin-top: 18px;
  font-family: var(--font-kai);
  font-size: 25px; font-weight: 700; letter-spacing: 2px;
  color: var(--brand);
}
.hero-keys-word { white-space: nowrap; }
/* 圆点用方块画，别写「·」字符：那个字符在不同字体（尤其楷体回退）里
   位置和粗细都不一样，四个词之间会出现三种大小的点。 */
.hero-keys-dot {
  flex: none; width: 5px; height: 5px;
  border-radius: 50%; background: currentColor; opacity: .3;
}

/* 公益声明条（清单第 8 条）：比正文轻，但要比正文显眼——「不收费」和「欢迎推广」
   都是要给人看的，埋进段落里就白写了 */
.hero-note {
  display: flex; align-items: flex-start; gap: 8px;
  margin-top: 18px; max-width: 560px; padding: 11px 15px;
  background: var(--brand-soft); color: var(--brand-dark);
  border-radius: var(--radius-sm); font-size: 14px; line-height: 1.65;
}
.hero-note svg { flex: none; margin-top: 3px; color: var(--brand); }
.hero-note strong { font-weight: 700; color: var(--brand); }

.hero-stats {
  display: flex; gap: 34px; margin-top: 40px;
  padding-top: 26px;
  /* 原来是一条通体等深的灰线，改成两头淡的渐变——
     和页头下沿同一套做法，首屏这两条横线才不会显得像"表格边框"。 */
  border-top: 0;
  position: relative;
  justify-content: center;
}
/* ★ 两个伪元素都必须是 absolute，否则 flex 会把它们当成第 4、5 个格子，
   三个数字就被挤成 5 等分了。 */
.hero-stats::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--border) 18%, var(--border) 82%, transparent);
}
/* 四格统计（2026-11-02 由三格加到四格）。桌面等分一行；
   ≤720px 改成 2×2，四句 7～8 字的中文标签并排会挤到溢出。 */
.hero-stats > div { flex: 1 1 0; min-width: 0; }
.hero-stat-num {
  font-family: var(--font-display);
  font-size: 30px; font-weight: 800; color: var(--brand); line-height: 1.15;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
  background-image: var(--grad-brand);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.hero-stat-label { font-size: 13px; color: var(--muted); }

/* ---------------------------------------------- 两个登记入口（2026-09-29 改版）
   整页最显眼的元素。改版动机是用户原话：「让给孩子找家教的家长一眼就能找到
   在哪登记，大学生一眼就知道在哪登记」——所以它紧贴在首屏下面，
   中间不许再夹说明性长文。
   ★ 两张卡各一个色：橙=需求端（家长/学习者），蓝=供给端（大学生）。
     和三个广场的配色是一套语言，别在这里另起色相。 */
/* ★ 2026-10-21：首页原来那行「从哪里开始？」删了，但用户明确要求
   「不要把下面的东西往上放，那块空白好了」——所以留一个**占位空块**，
   高度 = 原 h2 的 33.75px（25px × 1.35 行高）＋ .section-head 的 26px 下边距。
   两张登记卡的位置和删标题之前完全一致。别顺手把这 60px 收掉。 */
.entry-gap { height: 33.75px; margin-bottom: 26px; }

.pick-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }

/* ★ 2026-10-23：登记卡换成桌面 card.zip 的「方案 D」。
   与旧版相比：不再用顶部 6px 色条和整卡色雾，改成**顶部封面色块**
   + 一条从左向右渐隐的通栏分隔线；身份标签去掉底块、只留文字；
   主按钮从方角改成胶囊。悬停时仍按各自色相描边、发光。
   ★ 两张卡只有 --pv-* 三个变量不同（需求=藏青、师资=绛红），
     所以下面所有规则都只写一遍。 */
.pick-card {
  --pv-rgb: var(--accent-rgb);
  --pv-grad: var(--grad-accent);
  --pv-ink: var(--accent-ink);
  position: relative; display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: var(--text); text-decoration: none;
  transition: transform .22s cubic-bezier(.2, .8, .3, 1), box-shadow .22s, border-color .22s;
}
.pick-card.is-teach {
  --pv-rgb: var(--brand-rgb);
  --pv-grad: var(--grad-brand);
  --pv-ink: var(--brand);
}
.pick-card:hover {
  text-decoration: none; transform: translateY(-4px);
  box-shadow: 0 20px 46px rgb(var(--shadow-rgb) / .15);
  border-color: rgb(var(--pv-rgb) / .4);
}
/* 顶部封面色块：左侧略深，向右柔和渐淡 */
.pick-cover {
  position: relative; display: flex; align-items: center; gap: 16px;
  padding: 22px 28px 23px;
  background: linear-gradient(110deg,
    rgb(var(--pv-rgb) / .18) 0%,
    rgb(var(--pv-rgb) / .08) 48%,
    rgb(var(--pv-rgb) / 0) 100%);
}
/* 通栏分隔线：左端实色，向右渐隐（方案 D 的「通栏渐隐分隔线」） */
.pick-cover::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: linear-gradient(to right,
    rgb(var(--pv-rgb) / 1) 0%,
    rgb(var(--pv-rgb) / 0) 100%);
}
/* 身份标签只留文字（方案 D 的调整：底块、图标都去掉） */
.pick-who {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0; border-radius: 0; background: transparent;
  font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
  color: var(--pv-ink);
}
.pick-body { display: flex; flex-direction: column; padding: 24px 28px 27px; }
.pick-card h3 { font-size: 26px; letter-spacing: -.02em; }
.pick-card p { margin-top: 10px; font-size: 14.5px; color: var(--text-soft); }

/* 卡内那颗「按钮」。整张卡已经是 <a> 了，所以这里只能是 span——
   嵌套 <a> 是非法 HTML，浏览器会把它拆开、点击区就碎了。
   ★ 悬停时只让箭头往右挪 3px，是「点进去」最省笔墨的表达。 */
.pick-btn {
  align-self: flex-start; display: inline-flex; align-items: center; gap: 8px;
  margin-top: 21px; padding: 12px 21px; border-radius: 999px;
  background: var(--pv-grad); color: #fff;
  font-size: 15px; font-weight: 700;
  box-shadow: 0 7px 18px rgb(var(--pv-rgb) / .24);
}
.pick-btn svg { transition: transform .22s cubic-bezier(.2, .8, .3, 1); }
.pick-card:hover .pick-btn svg { transform: translateX(3px); }

/* 信任条：身份核验 / 联系方式不公开 / 人工牵线。
   这三件原来是首屏右半边三张大卡（用户说「这三个变小」），内容一件没少，
   只是从「卖点」降级成登记入口下面的「注脚」——先看见去哪儿登记，
   再看凭什么信这个平台。 */
.trust-strip {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 10px 32px;
  margin-top: 26px;
  /* ★ 2026-10-09：这三条信任说明也换楷体（「身份核验，不碰运气」…）。
     和 .section-head p 同一层——短句、并列、是「注解」不是「控件」，
     楷体在这里读起来像一句题注，而不是像按钮。字号同样 ×1.06 补一档。 */
  font-family: var(--font-kai);
  font-size: 14.3px; color: var(--muted);
}
.trust-strip span { display: inline-flex; align-items: center; gap: 6px; }
.trust-strip svg { flex: none; color: var(--brand); opacity: .85; }

/* 双入口大卡 */
/* 三张入口卡正对三个广场（清单第 17 条把两张变成了三张）。
   ★ 2026-10-24：用户从本地预览里选定「广场卡 · 甲」——左侧 4px 色轨 +
     底部「进入广场」文字入口；原来那条 5px 顶部色条撤掉。
     颜色对应关系不变：绛红=社会需求、鎏金=大学生需求、藏青=师资。
   ★ 用 repeat(3, 1fr) 而不是 auto-fit：auto-fit 会在窄一点的屏上变成「两列 + 一个孤儿」，
     三张卡本来是并列关系，缺一个角看着像少了一张。中间档由 980px 那条断点接管。 */
/* 四张入口卡（2026-11-02）。桌面一行四张；≤980px 2×2；≤720px 单列。 */
.entry-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; }

.entry-card {
  --entry-rgb: var(--brand-rgb);
  --entry-grad: var(--grad-brand);
  --entry-ink: var(--brand);
  position: relative; display: flex; flex-direction: column; overflow: hidden;
  padding: 26px 24px 22px 27px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
  color: var(--text); text-decoration: none;
  transition: transform .22s cubic-bezier(.2, .8, .3, 1), box-shadow .22s, border-color .22s;
}
.entry-card.is-college {
  --entry-rgb: var(--teal-rgb);
  --entry-grad: var(--grad-teal);
  --entry-ink: var(--teal-dark);
}
.entry-card.is-demand {
  --entry-rgb: var(--accent-rgb);
  --entry-grad: var(--grad-accent);
  --entry-ink: var(--accent-ink);
}
.entry-card.is-job {
  --entry-rgb: var(--jade-rgb);
  --entry-grad: var(--grad-jade);
  --entry-ink: var(--jade-dark);
}
.entry-card:hover {
  text-decoration: none; transform: translateY(-4px);
  box-shadow: 0 20px 46px rgb(var(--shadow-rgb) / .15);
  border-color: rgb(var(--entry-rgb) / .45);
}
/* 广场卡甲的标志：左侧通高色轨，不再用顶部色条。 */
.entry-card::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--entry-grad);
}
.entry-card > * { position: relative; z-index: 1; }

.entry-tag {
  align-self: flex-start; display: inline-block; padding: 4px 11px;
  border-radius: 999px; font-size: 12.5px; font-weight: 600;
  background: var(--brand-soft); color: var(--brand);
}
.entry-card.is-demand .entry-tag { background: var(--accent-soft); color: var(--accent-ink); }
.entry-card.is-college .entry-tag { background: var(--teal-soft); color: var(--teal-dark); }
.entry-card.is-job .entry-tag { background: var(--jade-soft); color: var(--jade-dark); }

.entry-card h3 { margin-top: 13px; font-size: 19.5px; letter-spacing: -.015em; }
.entry-card p { margin-top: 10px; color: var(--text-soft); font-size: 13.8px; line-height: 1.68; }
/* 底部入口：margin-top:auto 把三张卡的入口压到同一条基线，
   描述长短不同时也不会一张高一张低。 */
.entry-go {
  display: inline-flex; align-items: center; gap: 6px; align-self: flex-start;
  margin-top: auto; padding-top: 18px;
  font-size: 13.5px; font-weight: 700; color: var(--entry-ink);
}
.entry-go svg { transition: transform .22s cubic-bezier(.2, .8, .3, 1); }
.entry-card:hover .entry-go svg { transform: translateX(3px); }

/* 特色说明 */
.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }

.feature {
  position: relative; overflow: hidden;
  padding: 24px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: transform .22s cubic-bezier(.2, .8, .3, 1), box-shadow .22s, border-color .22s;
}
.feature:hover {
  transform: translateY(-3px); box-shadow: var(--shadow);
  border-color: rgb(var(--brand-rgb) / .28);
}
.feature-icon {
  width: 46px; height: 46px; margin-bottom: 14px; border-radius: 13px;
  display: grid; place-items: center;
  /* ★ 2026-10-06 第三轮：从「实心紫渐变 + 光晕」改成「淡紫底 + 紫字」。
     这个类同时被「对接流程」和「为什么是交大学桥」两处用（一共 7 个块），
     全站点着一样的实心紫，读起来就是「一直在喊」，而且和两个登记按钮撞衫——
     喊的人和干活的人穿一样的衣服，按钮就不突出了。
     淡底紫字（--brand on --brand-soft）对比度 9.08:1，比原来白字还稳。
     ★ 对接流程那四格在下面用 .flow-grid 单独覆盖成实心（它是有顺序的四步，
       颜色在那里承担了信息；「为什么是交大学桥」三格是并列的，不该染色）。 */
  background: var(--brand-soft); color: var(--brand);
  font-family: var(--font-display); font-size: 19px;
}

/* 「对接流程」四步：紫 → 玫红的进度渐变。背景色本身就是「第几步」的提示，
   所以数字用白字（四格的对比度见 --step-* 的注释，全部 ≥4.5:1）。 */
.flow-grid .feature:nth-child(1) .feature-icon { background: var(--step-1); color: #fff; }
.flow-grid .feature:nth-child(2) .feature-icon { background: var(--step-2); color: #fff; }
.flow-grid .feature:nth-child(3) .feature-icon { background: var(--step-3); color: #fff; }
.flow-grid .feature:nth-child(4) .feature-icon { background: var(--step-4); color: #fff; }
.feature h4 { font-size: 16px; }
.feature p  { margin-top: 8px; font-size: 14px; color: var(--muted); }

/* ---------------------------------------------------------------- 表单 */

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.field-full { grid-column: 1 / -1; }

.field { margin-bottom: 22px; }

.field-label {
  display: block; margin-bottom: 8px;
  font-size: 14.5px; font-weight: 600; color: var(--text);
}
.field-label .req { color: var(--danger); margin-left: 3px; }
.field-hint { margin: -3px 0 9px; font-size: 13px; color: var(--muted); }

.input, .select, .textarea {
  width: 100%; padding: 11px 14px;
  font-family: inherit; font-size: 15px; color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  transition: border-color .15s, box-shadow .15s;
}
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--brand);
  box-shadow: 0 0 0 3px rgb(var(--brand-rgb) / .12);
}
.input::placeholder, .textarea::placeholder { color: #B7A79D; }
.textarea { min-height: 92px; resize: vertical; line-height: 1.65; }
.select { appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%237A8699' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center; padding-right: 38px;
}

/* 科目多选 */
.subject-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
/* 性别只有两项，用同一套胶囊样式排两列；列宽收窄，别拉成一整行 */
.gender-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; max-width: 220px; }
/* 上海 16 个区，4 列正好四行、2 列正好八行，两个断点都不留半行空位 */
.district-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
/* ★ 「线上」独占一行（2026-09-15，清单第 15 条）。上面那句「不留半行空位」
   说的是 16 个区；多出来的第 17 个如果照常占一格，最后一行就只剩它自己、
   右边空半格，看着像排版坏了。横跨整行把这个空位补上，
   顺带也让它在视觉上不像「第 17 个区」——它本来就不是。 */
.district-grid .chip-wide { grid-column: 1 / -1; }
/* 年级 8 档单选：4 列正好两行，窄屏两列（见下面 720px 断点） */
.grade-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
/* 期望薪资 6 档：3 列正好两行 */
.salary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; max-width: 480px; }

/* 首页反馈的类型单选：三个，一排放得下；窄屏折成一行一个（见下面 720px 断点） */
.type-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; max-width: 420px; }

/* 选项胶囊：科目多选（checkbox）和性别单选（radio）共用。
   结构固定为 label 包一个隐藏 input + span，选中态由 :checked + span 控制。 */
.subject-chip { position: relative; }
.subject-chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.subject-chip span {
  display: block; padding: 10px 6px; text-align: center;
  font-size: 14.5px; font-weight: 500; color: var(--text-soft);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); cursor: pointer;
  transition: all .14s; user-select: none;
}
.subject-chip span:hover { border-color: var(--brand); color: var(--brand); }
.subject-chip input:checked + span {
  background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600;
}
.subject-chip input:focus-visible + span { box-shadow: 0 0 0 3px rgb(var(--brand-rgb) / .25); }

/* 「在校大学生」（清单第 17 条）。它不是科目，是「这条需求要进哪个广场」的开关，
   所以**不能长得和科目一样**——虚线边 + 选中时换成青绿，和师资广场的「985/211」
   层次标签同一套视觉语言（那边也用过 is-tier 的虚线）。
   ★ 用青绿而不是暖色：青绿是「大学生需求」在全站的色（首页那张入口卡、广场本身
     都用它），勾中它就该预告「你会去那个广场」。用暖色会被读成「师资广场」，
     因为暖色在这个站里一直是师资那一边的。 */
.subject-chip.is-college span { border-style: dashed; }
.subject-chip.is-college input:checked + span {
  background: var(--teal); border-color: var(--teal); color: #fff;
}
/* 「兼职/实习」同样是标记（2026-11-02）。虚线边沿用「在校大学生」那套语言，
   选中态换成墨绿——墨绿是岗位在全站的色，勾中它就该预告「你会去那个广场」。 */
.subject-chip.is-job span { border-style: dashed; }
.subject-chip.is-job input:checked + span {
  background: var(--jade); border-color: var(--jade); color: #fff;
}
/* 禁用状态：勾了「在校大学生」之后九个科目会变成这样。
   要明确地「灰下去」，不能只是不能点——看不出被禁的话用户会一直点它 */
.subject-chip input:disabled + span {
  opacity: .45; cursor: not-allowed; background: var(--surface-alt);
}
.subject-chip input:disabled + span:hover { border-color: var(--border); color: var(--text-soft); }

/* 「在校大学生」那段提示。复用 .form-tip 的暖色底，只是它是整块出现的 */
.form-tip.is-college-tip { margin-top: 14px; }

/* 需求表单里和金额框并列的「待商议」（清单第 15 条）。
   胶囊样式跟科目/年级那些勾选共用一套，只是**不铺满整行**——
   它是个并列的备选，不是这一栏的主体；铺满会让人误以为它才是默认值。 */
.salary-negotiable { display: block; margin-top: 10px; max-width: 240px; }

/* 薪资 / 频次 组合输入 */
.inline-group { display: flex; gap: 10px; align-items: stretch; }
.inline-group .input { flex: 1 1 auto; min-width: 0; }
.inline-group .select { flex: 0 0 130px; }
.inline-group .unit { display: flex; align-items: center; padding: 0 4px; color: var(--muted); font-size: 14.5px; white-space: nowrap; }

/* 表单页脚与提示 */
.form-actions {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-top: 30px; padding-top: 24px; border-top: 1px solid var(--border-soft);
}

.form-note {
  display: flex; gap: 10px; align-items: flex-start;
  margin-bottom: 26px; padding: 15px 17px;
  background: var(--brand-soft); border-radius: var(--radius-sm);
  font-size: 13.5px; color: var(--brand-dark);
}
.form-note svg { flex: none; margin-top: 2px; }

/* 首页「两个需求方向」那条说明（2026-09-19 用户要「框变大一些」）。
   ★ 做成**修饰类**而不是改 .form-note 基类：基类全站还有 9 处在用
     （两个表单、三个广场页、本页反馈区），改基类等于把那 9 处一起改了。
   字号对齐 .notice-box（14.5px），留白也跟着放，让它看着和上面
   「平台使用说明」是同一档「要读的正文」，而不是夹在正文里的一条小提示。
   ⚠️ 它比上面那个 .notice-box（container-narrow）宽——这是**故意**的，
      原来就是这个宽度，没缩；而且用户要的就是「变大」，缩窄就反了。 */
.form-note.is-lead {
  padding: 20px 22px; margin-bottom: 0;
  font-size: 14.5px; line-height: 1.8;
}
.form-note.is-lead svg { width: 18px; height: 18px; margin-top: 3px; }
.form-note.is-lead strong { font-weight: 700; color: var(--brand); }

/* 师资问卷页「已经登记过的老师直接登录」那条（2026-10-14 用户要「单独成框放大」）。
   ★ 又一条**修饰类**，同样不动 .form-note 基类（全站 9 处）。
   ★ 它正上方就是「填这份问卷 = 注册师资账号」那条说明（.form-note-brand），
     两条说的是两个相反的选择（你是新来的 / 你已经登记过），所以不能长得一样：
     上一条是「左侧一条主色竖线」（说明类），这一条改成**四面都描边**
     ——看着像个独立的框，回访老师第一眼就能找到出口。
   ★ 字号 15px，比上一条（13.5px）明显大一档：用户原话是「让老师都能看到」，
     不是「再补一句说明」。全站最大的一档正文是 .notice-box 的 14.5px，
     这里再大一档是**有意**的——它在页面第一屏、总共只有一句话。
   ⚠️ 颜色仍走 --brand-dark on --brand-soft（和上一条同一对，对比度已经验过）。
     别为了更醒目换成实心主色底：那样要么得配白字、要么和 2026-10-06 定的
     「实心色只留给能点的东西」打架。
   ⚠️ 也别用玫红（--accent）——那套色相在这个站里是**社会需求广场/家长**，
     这一页是师资侧，紫才是它该有的颜色。 */
.form-note.is-return {
  padding: 18px 20px;
  font-size: 15px; line-height: 1.75;
  border: 1.5px solid var(--brand);
}
.form-note.is-return svg { width: 18px; height: 18px; margin-top: 4px; }
.form-note.is-return a {
  color: var(--brand); font-weight: 700;
  text-decoration: underline; text-underline-offset: 2px; white-space: nowrap;
}
/* 上面那条 `.form-note + .form-tip { margin-top: -14px }` 是给「隐私承诺 → 填写提示」
   贴成一组用的。现在这两条中间插进了登录框，再吃 -14px 就变成
   「登录框和填写提示是一组」——可那是两件事（前者是「你可能根本不用填」，
   后者是「要填的话请认真填」）。还原成正常的 26px 下边距。
   特异性 (0,3,0) > (0,2,0)，盖得住。 */
.form-note.is-return + .form-tip { margin-top: 0; }

/* 首页「平台使用说明」整块（2026-09-15 加）。**故意没有复用 .form-note**：
   那个是 flex 横排（图标 + 一句话），把三段话塞进去会并排成三列。
   这块是要人**读**的正文，靠 padding + 段间距，字号也用正文大小。
   颜色仍走同一套 --brand-soft/--brand-dark，让它看着还是「说明」那一类。 */
.notice-box {
  padding: 22px 24px;
  background: var(--brand-soft); border-radius: var(--radius);
  color: var(--brand-dark); font-size: 14.5px; line-height: 1.8;
}
.notice-box p + p { margin-top: 14px; }
.notice-box strong { font-weight: 700; color: var(--brand); }
.notice-box a { color: var(--brand); text-decoration: underline; text-underline-offset: 2px; }
/* 「问题反馈/联系我们」页上那块联系邮箱（2026-10-20）。和下面的表单卡隔开一段。 */
.feedback-contact { margin-bottom: 22px; }

/* 填写提示（清单第 12 条）。和上面那条隐私承诺**必须长得不一样**：
   那条是蓝底加锁的「保证书」，这条是暖底的「提醒」，同一个样式会让人
   以为是同一句话重复了两遍，直接划过去。 */
.form-tip {
  display: flex; gap: 10px; align-items: flex-start;
  margin-bottom: 26px; padding: 15px 17px;
  background: #FDF4E5; border-radius: var(--radius-sm);
  font-size: 13.5px; color: var(--warn-ink);
}
.form-tip svg { flex: none; margin-top: 2px; }
/* 紧跟隐私说明时收一点间距：两条都是「填之前先看一眼」的话，挨着才像一组，
   各自 26px 下边距叠起来隔 52px，会被当成两件不相干的事。 */
.form-note + .form-tip { margin-top: -14px; }

.alert {
  display: none; margin-bottom: 22px; padding: 13px 16px;
  border-radius: var(--radius-sm); font-size: 14px;
}
.alert.show { display: block; }
.alert-error   { background: var(--danger-soft);  color: var(--danger); }
.alert-success { background: var(--success-soft); color: var(--success); }
/* 弹框里跟在错误文案后面的「去我的申请」链接（名额已满时用）。
   必须继承错误色，不能变成蓝色链接——它和那句话是一个整体。 */
.alert-link {
  color: inherit; font-weight: 700;
  text-decoration: underline; text-underline-offset: 2px;
}
.alert-link:hover { color: inherit; }

/* 进行中对接超过名额上限时的置顶提醒（2026-10-21）。 */
.quota-alert { line-height: 1.65; }
.quota-alert strong { display: block; margin-bottom: 4px; font-size: 15px; }
.quota-alert p { margin: 0; }

/* ★★ [hidden] 的兜底规则（2026-10-20，同一个坑第四次）。
   作者样式里只要写了 `display:`，就会盖掉浏览器给 [hidden] 的 display:none。
   前三次分别是 .search-clear / .pager / .search-box，这次是 .form-tip：
   `#collegeTip` 明明带了 hidden，却一直显示在需求问卷里（零报错）。
   与其每加一个带 display 的类就补一条 `.xxx[hidden]`，不如在这里一次性收口。
   ⚠️ 断言显隐时必须量 getComputedStyle，只看 el.hidden 是量不出这个坑的。 */
[hidden] { display: none !important; }

/* 必填项报错时闪一下红框（2026-10-20 用户第 7 条，见 common.js 的 errorAt）。
   只亮三秒左右就撤，不常驻——常驻会让人以为这一栏坏了。 */
.field-error {
  outline: 2px solid var(--danger);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
  animation: field-error-pulse 1.2s ease-out;
}
@keyframes field-error-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(198, 40, 40, .35); }
  100% { box-shadow: 0 0 0 12px rgba(198, 40, 40, 0); }
}

/* 「其他科目/课程」胶囊（2026-10-20 用户第 8 条）：虚线框，和真科目区分开 */
.subject-chip.is-other span { border-style: dotted; }

/* ---------------------------------------------------------------- 列表卡片 */

/* 检索框（清单第 17 条）。四个广场共用，由 square.js 插在 .toolbar 前面。
   ★ 单独一行、不塞进 .toolbar：那一行是 space-between，多一个孩子会被推到中间，
     左边筛选、右边计数、中间杵个输入框，两边的留白比输入框还宽。 */
.search-box {
  display: flex; align-items: center; gap: 9px;
  /* 2026-11-03：右边多了一个「查询」按钮，420px 会把输入区挤得太窄，放宽一档。 */
  max-width: 480px; margin-bottom: 16px; padding: 0 12px;
  /* ★ 2026-10-09（清单：三个广场的搜索框太不显眼）：1px --border → 2px
     --border-strong，另加一层极淡的投影。两个动作缺一不可——
     只把 1px 描深，框还是"贴着底色的一条线"；只加到 2px 不换色，
     那条线本身就看不见（1.11:1），加粗只是把一条看不见的线画粗。
     ★ 投影不是装饰：检索框压在广场页的校门线稿上，底下是有花纹的，
       单靠描边时框内框外"看起来是同一片底"。一点点影把输入区**抬起来**，
       人才认得出"这里是个能打字的格子"。 */
  background: var(--surface); border: 2px solid var(--border-strong);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-sm);
  transition: border-color .15s, box-shadow .15s;
}
.search-box:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px rgb(var(--brand-rgb) / .12); }
.search-box > svg { flex: none; color: var(--muted); }
/* ★★ 不能省，而且**不是**可选的兜底 —— 上面那条 display:flex 会盖掉浏览器给
   [hidden] 的 display:none（作者样式一律优先于 UA 样式，跟选择器权重无关）。
   后台那个检索框只在「师资账号」「学习需求」两页出现，另外五页靠 `el.hidden`
   收起：少了这一条，那五页会一直挂着一个搜不了任何东西的输入框，
   而且是**一声不吭**地挂着（HTML 里写着 hidden、JS 也设了 hidden，就是不消失）。
   同一个坑在本文件里已经踩过两次：下面的 .search-clear[hidden]，以及 .pager
   （它干脆改用 :empty 而不是 hidden 属性）。 */
.search-box[hidden] { display: none; }
/* .input 带来的一圈边框和 11px 上下内边距在这层壳里是多余的（壳自己就是那个框），
   去掉；否则输入框和外壳之间会出现一道双线。 */
.search-input { flex: 1; min-width: 0; padding: 10px 0; border: none; background: none; }
.search-input:focus { box-shadow: none; }
/* type=search 自带一个叉，和我们的 .search-clear 功能重叠但样式不受控（各家浏览器
   长得都不一样）。藏掉它，只留我们自己那个。 */
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.search-clear {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; font-family: inherit;
  font-size: 17px; line-height: 1; color: var(--muted);
  background: var(--surface-alt); border: none; border-radius: 50%; cursor: pointer;
}
.search-clear:hover { color: var(--text); }
/* ★ 不能省：作者样式里的 display:flex 会盖掉浏览器给 [hidden] 的 display:none
   （作者样式一律优先于 UA 样式，跟选择器权重无关），那个叉会永远挂在那儿。 */
.search-clear[hidden] { display: none; }
/* 「查询」按钮（2026-11-03）。按钮本身复用 .btn.btn-primary.btn-sm，
   这里只收掉主按钮那圈光晕、把圆角缩到和检索框内壳一致：
   它嵌在 2px 描边的搜索框里，再加一圈光会像浮在框外。 */
.search-submit {
  flex: none; padding: 7px 15px; border-radius: 7px; box-shadow: none;
}

.toolbar {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
  justify-content: space-between; margin-bottom: 22px;
}
.toolbar-count { font-size: 14px; color: var(--muted); }

/* 筛选区分「科目」「区域」两排，各排各的；竖着摞起来才不会混成一片 */
.filters { display: flex; flex-direction: column; align-items: flex-start; gap: 9px; }
.filter-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.filter-label { font-size: 13px; color: var(--muted); margin-right: 2px; }

.filter-chip {
  padding: 6px 14px; font-size: 13.5px; font-family: inherit;
  color: var(--text-soft); background: var(--surface);
  border: 1px solid var(--border); border-radius: 999px;
  cursor: pointer; transition: all .14s;
}
.filter-chip:hover { border-color: var(--brand); color: var(--brand); }
.filter-chip.active { background: var(--brand); border-color: var(--brand); color: #fff; font-weight: 600; }
/* 办学层次（985／211／师范院校）和具体校名挤在「学校」这一排：长得一样的话，
   「985」看着就像一所叫这个名字的学校。虚线边 + 选中用暖色，一眼分得开 */
.filter-chip.is-tier { border-style: dashed; }
/* 选中态用 --accent-ink（更深的那支橙）而不是悬停底色那支：
   这上面是**白字**，白字压在中明度的橙上只有 3:1，压深的才有 5.3:1。 */
.filter-chip.is-tier.active { background: var(--accent-ink); border-color: var(--accent-ink); }

.list-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; }

.item-card {
  display: flex; flex-direction: column;
  padding: 22px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: box-shadow .18s, border-color .18s, transform .18s;
  /* ★ 给下面那个浮在右上角的标号当定位参照。
     少了这一行，标号会飘到整个页面的右上角去（离它最近的非 static 祖先就变成 body 了）。 */
  position: relative;
}
.item-card:hover { box-shadow: var(--shadow); border-color: rgb(var(--brand-rgb) / .30); transform: translateY(-2px); }

/* 标号的右边留出位置，名字/学校再长也不会钻到标号底下。
   两个数加起来要盖得住标号：标号右边缘离卡片 16px、宽约 40px。 */
.item-top { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding-right: 60px; }

/* 卡片右上角的标号（#25）。用户报号时跟后台的编号对上用的。
   用中性灰而不是品牌色：它是给人对照的编号，不该抢姓名和按钮的注意力。 */
.item-no {
  position: absolute; top: 13px; right: 16px;
  padding: 1px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; letter-spacing: .3px;
  color: var(--muted); background: var(--surface-alt);
  border: 1px solid var(--border-soft);
}

/* ★★ 2026-10-11：两个头像徽章一起改，根因是同一件事——**渐变的浅色那一头**。
   为什么一直没发现：量对比度的探针原来只取渐变的**第一个色标**，
   也就是深紫那头（11:1），等于只量了好看的那半边；浅蓝那头从来没被量过。
   改成「取最不利的一端」之后，两处同一天一起露出来。
   ★ `.avatar` 原来的第二站 #4A8AD4，白字压上去只有 **3.57:1**；
     2026-10-21 D 套直接绑 var(--brand-light) #9C3045，白字 7.19:1，深端 #7A1B2E 10.38:1。
     2026-10-31 F 套浅端 #3F7360 白字 5.48:1；同日换 G 套后浅端 #AD5230 白字 5.22:1、
     深端 #93391B 7.39:1，仍过 AA；11-02 回到 D 套后两端 10.38:1 / 7.19:1。 */
.avatar {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 15px; font-weight: 700; color: #fff;
  background: linear-gradient(135deg, var(--brand), var(--brand-light));
}
/* 需求卡的头像是「对方姓名首字」。★ 旧写法 `color:#4A340A`（深褐）是**琥珀时代**留下的，
   跟 .btn-accent 当年那个 `#4A2E00` 是同一批货，浅色字压在这头只有 2.50:1。
   ★ 结论照旧：**白字 + 两头都压深**。2026-10-21 D 套改成藏青渐变
     #1E3A5F → #3E6190，白字 11.50:1 / 6.33:1，整条渐变都过 AA。
     2026-10-31 F 套改成赭石渐变 #8A4416 → #A55C2E；同日 G 套改成孔雀青渐变
     #17554F → #2F7C74，白字 8.58:1 / 4.93:1。
     ⚠️ 浅端那一档是**算着选的**：F 套原选的 #B0693A 白字只有 4.26:1，掉出 AA；
     G 套的 #2F7C74 白字 4.93:1，同样是量过才敢用。
     11-02 回到 D 套的藏青渐变 #1E3A5F → #3E6190，白字 11.50:1 / 6.33:1。 */
.item-card.is-demand .avatar { background: linear-gradient(135deg, var(--accent), var(--accent-light)); color: #fff; }
/* 岗位卡整张换成墨绿系（2026-11-02）：头像、悬停边、单号都跟着走，
   不这么做的话它会和两个需求广场的藏青卡一模一样，只能靠文字区分。 */
.item-card.is-job .avatar {
  background: linear-gradient(135deg, var(--jade-dark), var(--jade-light)); color: #fff;
}
.item-card.is-job:hover { border-color: rgb(var(--jade-rgb) / .45); }
.item-card.is-job .item-no { color: var(--jade-dark); background: var(--jade-soft); border-color: rgb(var(--jade-rgb) / .18); }
/* 岗位名称是主标题，可能是「周末少儿编程助教」这种二十多字的长句；
   允许在词内换行，别把卡片撑出横向滚动。 */
.item-card.is-job .item-name { overflow-wrap: anywhere; }
/* 社会需求卡的主标题现在是问卷里的「所在区域」（2026-11-08），
   「杨浦新江湾城长江南路地铁站附近」这种二十来字的地址很常见，同样允许词内换行。
   ★ 只挂在 .is-demand 上：师资卡的主标题是人名、大学生需求卡仍是打码姓名，
     那两个广场这次不动（需求卡和岗位卡共用 .item-name，别写成全局规则）。 */
.item-card.is-demand .item-name { overflow-wrap: anywhere; }
/* 薪资是自由文本（「3000元/月，另有绩效」也合法），同样允许换行。 */
.fact-val-salary { overflow-wrap: anywhere; }

.item-who { min-width: 0; }
.item-name { font-size: 15.5px; font-weight: 700; }
/* ★ 2026-10-11：副标题里加了「· 学历 · 专业」之后，320px 的卡片上它会折成两行
   （学校名本身就可能二十来个字）。原来没有 line-height，两行的行距会贴在一起，
   紧得像同一行断错了地方。1.45 是让两行读起来仍是两行、又不把卡片撑高。 */
.item-sub  { font-size: 13px; line-height: 1.45; color: var(--muted); }

.item-tags { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 15px; }

.tag {
  display: inline-block; padding: 4px 11px;
  font-size: 12.5px; font-weight: 500;
  background: var(--brand-soft); color: var(--brand);
  border-radius: 999px;
}
.tag-accent { background: var(--accent-soft); color: var(--accent-ink); }
.tag-plain  { background: var(--surface-alt); color: var(--text-soft); border: 1px solid var(--border); }
/* 岗位卡上的「兼职/实习」类型标签（2026-11-02）。用墨绿，和门类标签分开。 */
.tag-job    { background: var(--jade-soft); color: var(--jade-dark); }
/* 需求卡片上的「所在区」标签。和科目标签同排出现，所以要一眼分得清：
   科目是实色胶囊，区做成描边的方形一点的样子，靠形状区分而不是只靠颜色 */
.tag-area {
  background: transparent; color: var(--muted);
  border: 1px dashed var(--border); border-radius: var(--radius-sm);
}

.item-facts { display: grid; gap: 9px; margin-bottom: 18px; }

.fact { display: flex; gap: 10px; font-size: 13.5px; }
.fact-key { flex: none; width: 68px; color: var(--muted); }
.fact-val { color: var(--text-soft); }
.fact-val strong { color: var(--text); font-weight: 700; }

/* 需求卡片「位置」那一栏里既有区标签又有原文，勾了四五个区就会折行——
   用 flex 让标签和文字自己排版，别挤成一条长线 */
.fact-val-area { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

.item-note {
  margin-bottom: 18px; padding: 11px 13px;
  background: var(--surface-alt); border-radius: var(--radius-sm);
  font-size: 13.5px; color: var(--text-soft);
  /* ★ 2026-10-11：这一段是**个人简历 / 需求备注**，用户在 textarea 里分好的段
     得原样显示。⚠️ 光靠这一条不够——服务端原来用 `clip()` 把换行压掉了，
     数据到这儿早就没有换行（见 lib/util.js 的 clipMulti）。**两头都得对**：
     CSS 保留换行 + 服务端别洗掉换行，缺一条都还是「显示成一坨」。
     `overflow-wrap: anywhere` 和 .msg-text 同理：长邮箱/长链接不能撑破卡片。 */
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* 岗位备注的小标题（「岗位描述」）。单独占一行，和正文分开，
   否则一句话会读成「岗位描述：可以写……」连在一起。 */
.item-note-key {
  display: block; margin-bottom: 4px;
  font-size: 12px; font-weight: 700; color: var(--jade-dark);
}

.item-foot {
  margin-top: auto; display: flex; align-items: center;
  justify-content: space-between; gap: 12px;
  /* 窄屏上「有 N 人申请对接 + 申请对接按钮」一行放不下时会顶破卡片，允许换行兜底 */
  flex-wrap: wrap;
}
/* 需求卡的「尾巴」：填写时间 + 页脚，一起钉在卡片底部（用户 2026-09-19 提的）。
   ★ margin-top: auto 是从 .item-foot 那儿**挪上来**的。卡片是 flex column，
     谁带这个 auto，富余空间就在谁的上方。留在 .item-foot 上的话，富余会全吃在
     「时间和页脚之间」，日期就孤零零飘在卡片中间了。
   ★ 时间单独占一行、不塞进 .item-foot：foot 是 space-between 且允许换行，
     卡片宽 320px 时 foot 只有 309px，而「徽标 + 时间 + 按钮」一行要 310px——
     就差 1px，1440/1280/1100/760 四档常见宽度下页脚会整个折成两行。 */
.item-tail { margin-top: auto; }

/* 「填写于 2026-09-14」。只到日；日期做等宽数字，几张卡并排时右端不会因为
   1 和 8 的宽度差而参差不齐。★ 这个类本来就躺在样式表里没人用，这次是复用。 */
.item-time {
  font-size: 12.5px; color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  margin-bottom: 9px;
}

/* 卡片底部「填写时间 + 举报」那一行（2026-11-04）。
   ★ 举报是小字链接，不是按钮：申请对接才是主流程，兜底动作不能和它抢。
   ★ .item-time 原来的下边距挪到这一层，否则时间左边距会让举报链接上下错位。 */
.item-meta {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; margin-bottom: 9px;
}
.item-meta .item-time { margin-bottom: 0; }
.item-report {
  font-size: 11.5px; color: var(--muted); text-decoration: none;
  white-space: nowrap; opacity: .72;
}
.item-report:hover { color: var(--danger); opacity: 1; text-decoration: none; }
/* 师资卡没有「填写时间」那一行，举报单独占一行、靠右 */
.item-report-row { display: flex; justify-content: flex-end; margin-top: 2px; }

/* 被举报核实的需求：**仍然留在广场上**，但必须一眼看出它不正常
   （2026-11-04 用户原话：「标注『此需求异常，已被举报』」）。
   用告警色而不是直接撤掉：撤掉的话家长会以为是自己看错了，
   同一个人换个号还能再发一遍。 */
.item-suspended {
  display: flex; align-items: center; gap: 8px;
  margin: 12px 0 2px; padding: 9px 12px;
  background: var(--danger-soft); color: var(--danger);
  border: 1px solid rgba(198, 40, 40, .18);
  border-radius: var(--radius-sm);
  font-size: 12.5px; line-height: 1.45;
}
.item-suspended svg { flex: none; }

/* 右侧一组：申请人数 + 按钮（清单第 12 条）。包一层是为了让 .item-foot 仍然是
   两个子元素——它是 space-between，多塞一个孩子会把隐私徽标挤到中间去。 */
.item-foot-right {
  display: inline-flex; align-items: center; justify-content: flex-end;
  flex-wrap: wrap; gap: 10px; margin-left: auto;
}
.intent-count {
  font-size: 12.5px; color: var(--accent-ink); font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: var(--accent-soft); border-radius: 999px; padding: 3px 10px;
}

/* ---------------------------------------------------------------- 手机需求紧凑行（2026-10-24） */

/* 两个需求广场在 ≤720px 时把卡片换成一位一行（用户选定方案三后定的版式）。
   ★ 行高实测约 88px（390px 宽、三排都有内容时），比原来的计划多 9px，
     仍比卡片（平均 625px）省一个数量级的纵向空间。断点在 JS 那边用
     matchMedia 的同一个 720px 控制。
   ★ 一行里三排的顺序是用户逐字点的：
       ① 姓名 → 有 x 人申请对接 → 申请对接（按钮靠最右）
       ② 年级 ←→ 薪资
       ③ 地区 · 时间段 */
.demand-list { display: flex; flex-direction: column; gap: 8px; }

.demand-row {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: border-color .16s, box-shadow .16s;
}
.demand-row:hover { border-color: rgb(var(--accent-rgb) / .32); box-shadow: var(--shadow); }
/* 键盘聚焦时画一圈实色轮廓；鼠标点不画（:focus-visible），
   否则点完一行会一直挂着一圈框，像没点掉。 */
.demand-row:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

.demand-row-avatar {
  flex: none; width: 36px; height: 36px; border-radius: 50%;
  display: grid; place-items: center;
  background: linear-gradient(135deg, var(--accent), var(--accent-light));
  color: #fff; font-size: 14px; font-weight: 700;
}
/* 岗位紧凑行（2026-11-02）：墨绿头像和悬停边，与两个需求广场分开。
   薪资原文可能比数字长得多，所以这里放开截断；普通需求行仍是 nowrap。 */
.demand-row.is-job .demand-row-avatar {
  background: linear-gradient(135deg, var(--jade-dark), var(--jade-light));
}
.demand-row.is-job:hover { border-color: rgb(var(--jade-rgb) / .40); }
.demand-row.is-job .demand-row-salary {
  flex: 0 1 auto; min-width: 0; color: var(--jade-dark);
  overflow: hidden; text-overflow: ellipsis;
}
.demand-row.is-job .demand-row-grade { flex: 0 1 auto; }

.demand-row-main {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; gap: 1px;
}

/* 第一排：姓名 + 人数 + 按钮。★ 姓名用 flex: 0 1 auto 而不是 1：
   人数要**紧贴**在姓名右边；按钮单独 margin-left:auto 顶到最右。
   姓名太长时自己截断，人数和按钮都不让位（它们是这一行最该看全的）。 */
.demand-row-line1 { display: flex; align-items: center; gap: 6px; min-width: 0; }
.demand-row-name {
  flex: 0 1 auto; min-width: 0;
  font-size: 14.5px; font-weight: 700; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.demand-row-count {
  flex: none;
  font-size: 10.5px; line-height: 1.55; padding: 2px 7px;
}
.demand-row-line1 .btn { flex: none; margin-left: auto; }

/* 第二排：年级在左、薪资在右。用户要的是「中间年级的正右方放薪资」，
   所以这一排是两端对齐，而不是把薪资跟在年级后面挤成一串。 */
.demand-row-line2 {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; min-width: 0;
}
.demand-row-grade {
  font-size: 12.5px; color: var(--text-soft);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.demand-row-salary {
  flex: none; font-size: 12.5px; font-weight: 600;
  color: var(--accent-ink); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.demand-row-label { margin-right: 3px; font-weight: 500; color: var(--muted); }

/* 第三排：地区 · 时间段。两段都可能长，各自截断，别把行撑成两排。 */
.demand-row-line3 {
  display: flex; align-items: center; gap: 5px;
  min-width: 0; font-size: 12px; line-height: 1.4; color: var(--muted);
  white-space: nowrap;
}
.demand-row-area, .demand-row-time { overflow: hidden; text-overflow: ellipsis; }
.demand-row-area { flex: 0 1 auto; }
.demand-row-time { flex: 1 1 auto; }
.demand-row-sep { flex: none; color: var(--border-strong); }
/* 右下角的单号（2026-10-25 用户要求）。样式照着卡片右上角的 .item-no 收小：
   中性灰、不抢姓名和按钮；margin-left:auto 把它顶到这一行的最右。 */
.demand-row-no {
  flex: none; margin-left: auto;
  padding: 0 6px; border-radius: 999px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .3px; line-height: 1.6;
  color: var(--muted); background: var(--surface-alt);
  border: 1px solid var(--border-soft);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* 点开一行后的完整需求卡。★ 卡自己的那圈白底/边框/内边距在弹窗里全是重复的：
   外面已经有一层 .modal-body，再套一层卡框就是「框中框」，手机上正文会被挤窄。 */
.demand-peek { max-width: 520px; }
.demand-peek .modal-body { padding: 20px; background: var(--bg); }
.demand-peek .item-card {
  padding: 4px 0 0;
  background: transparent; border: 0; box-shadow: none;
}
.demand-peek .item-card:hover {
  transform: none; border-color: transparent; box-shadow: none;
}
/* 手机全局把 .fact 改成了上下排（见样式表末尾的档案版式那一节）；
   弹窗里改成横排，六项事实才不会把弹窗撑成一条要滚很久的长条。 */
.demand-peek .item-facts .fact { flex-direction: row; align-items: baseline; gap: 10px; }
.demand-peek .item-facts .fact-key { flex: none; width: 56px; font-size: 12.5px; }
.demand-peek .item-facts .fact-val { font-size: 13.5px; }
.demand-peek .item-note { margin-bottom: 14px; }

.privacy-badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12.5px; color: var(--muted);
}

.empty {
  padding: 70px 24px; text-align: center;
  background: var(--surface); border: 1px dashed var(--border);
  border-radius: var(--radius);
}
.empty-icon {
  width: 60px; height: 60px; margin: 0 auto 18px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--brand-soft); color: var(--brand);
}
.empty h3 { font-size: 17px; }
.empty p  { margin-top: 8px; color: var(--muted); font-size: 14px; }

.loading { padding: 70px 20px; text-align: center; color: var(--muted); font-size: 14.5px; }

/* ---------------------------------------------------------------- 弹窗 */

.modal-mask {
  position: fixed; inset: 0; z-index: 100;
  display: none; align-items: center; justify-content: center;
  padding: 20px;
  background: rgba(20, 28, 40, .5);
  backdrop-filter: blur(3px);
}
.modal-mask.show { display: flex; }

.modal {
  width: 100%; max-width: 500px; max-height: 88vh; overflow-y: auto;
  background: var(--surface); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  animation: modal-in .2s ease;
}
@keyframes modal-in { from { opacity: 0; transform: translateY(12px) scale(.98); } }

.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 20px 24px; border-bottom: 1px solid var(--border-soft);
}
.modal-head h3 { font-size: 17px; }
.modal-close {
  flex: none; width: 32px; height: 32px; border: none; border-radius: 8px;
  background: transparent; color: var(--muted); font-size: 20px; line-height: 1;
  cursor: pointer; transition: background .14s;
}
.modal-close:hover { background: var(--surface-alt); color: var(--text); }

.modal-body { padding: 24px; }

/* 窄弹窗（改密码那种只有两个输入框的）。
   ★ 上一版是写在 admin.html / my.html 的 style="" 里，两处各写一遍；
     这里收成一个类。上面那两个后台弹窗（老师档案、批量建号结果）
     比 500px 还**宽**，所以它们继续用行内 style 写 max-width ——
     那是有意为之，不是漏搬。 */
.modal-sm { max-width: 420px; }
.modal-foot {
  display: flex; justify-content: flex-end; gap: 10px;
  padding: 18px 24px; border-top: 1px solid var(--border-soft);
}

/* ---------------------------------------------------------------- 免责声明 */

/* 声明弹窗比普通弹窗宽一点、正文能滚：这份文字比「申请对接」长得多，
   挤在 500px 里会变成一个要滚很久的窄条 */
.modal-notice { max-width: 620px; }

/* 还没同意的时候不给 ×：这个弹窗必须通过「同意 / 不同意」两个按钮之一离开。
   Esc 和点遮罩已经在 bindModalDismiss 里按 data-modal-locked 挡住了，
   这里把视觉上的出口也收掉，免得用户找不到出路。 */
.modal-mask[data-modal-locked] .modal-close { display: none; }

.notice-body { font-size: 14px; line-height: 1.75; color: var(--text-soft); }
.notice-body h4 {
  margin: 22px 0 8px; font-size: 14.5px; font-weight: 700; color: var(--text);
}
.notice-body h4:first-child { margin-top: 0; }
.notice-body p { margin-bottom: 10px; }
.notice-body strong { color: var(--text); font-weight: 700; }

/* 开头的定位那句，稍微大一点、颜色深一点，让人先看清「这是什么平台」 */
.notice-lead { font-size: 14.5px; color: var(--text); }

/* 「本平台概不负责」——最要紧的一句，给一点警示色。
   只改样式不改字：免责条款的措辞不能由我润色 */
.notice-warn { color: var(--danger); }

.notice-version {
  margin-top: 20px; padding-top: 14px;
  border-top: 1px solid var(--border-soft);
  font-size: 12.5px; color: var(--muted);
}

/* 表单提交按钮下面那行「提交即表示已阅读并同意《…》」 */
.notice-hint {
  margin-top: 14px; text-align: center;
  font-size: 13px; color: var(--muted);
}
/* ⚠️ 原来写的是 var(--primary)——**这个变量根本不存在**（全站叫 --brand），
   所以这行一直静默失效、链接在继承色里躺着。2026-10-06 顺手改正。 */
.notice-hint a { color: var(--brand); text-decoration: underline; }

/* ---------------------------------------------------------------- 提交成功弹窗 */

/* 比声明弹窗窄：就四句话，用不着 620px 那么宽 */
.modal-done { max-width: 480px; }
/* 底下接了「为您推荐合适的老师」那一次（2026-10-16）：宽一档。
   ★ 那 5 位老师每人是「头像 + 姓名 + 认证徽标 + 学校·学历·专业」一行，
     480px 里那一行会折成两行，五个人就是十行。560px 刚好够。
   ★ 只在真有推荐时才宽 —— 平时那张就四句话，拉宽了反而空。
     类名由 showDoneModal 按 extraHtml 加，不是写死在这。 */
.modal-done.has-extra { max-width: 560px; }
.modal-done .modal-body { font-size: 14px; line-height: 1.75; color: var(--text-soft); }
.modal-done .modal-body p { margin-bottom: 10px; }
.modal-done .modal-body p:last-child { margin-bottom: 0; }
.modal-done .modal-body strong { color: var(--text); font-weight: 700; }

/* 「感谢您的填写…」——和声明开头那句同样处理，先让人看清发生了什么 */
.done-lead { font-size: 14.5px; color: var(--text); }

/* ---------------------------------------------------------------- 提示条 */

.toast-wrap {
  position: fixed; left: 50%; bottom: 34px; z-index: 200;
  transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 10px; align-items: center;
  pointer-events: none;
}

.toast {
  padding: 12px 22px; border-radius: 999px;
  font-size: 14.5px; font-weight: 500; color: #fff;
  background: #2A3646; box-shadow: var(--shadow-lg);
  animation: toast-in .22s ease;
}
.toast.is-error   { background: var(--danger); }
.toast.is-success { background: var(--success); }
@keyframes toast-in { from { opacity: 0; transform: translateY(14px); } }

/* ---------------------------------------------------------------- 表格（管理后台） */

.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }

table.data { width: 100%; border-collapse: collapse; font-size: 13.5px; }

table.data th, table.data td {
  padding: 12px 14px; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--border-soft); white-space: nowrap;
  /* 表格里的数字要竖向对齐，用等宽数字 */
  font-variant-numeric: tabular-nums;
}
table.data th {
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-alt); font-weight: 600; color: var(--text-soft);
  font-size: 13px; border-bottom: 1px solid var(--border);
}
table.data tr:last-child td { border-bottom: none; }
table.data tbody tr:hover { background: var(--surface-alt); }
/* ★ 2026-10-11 从 `white-space: normal` 改成 pre-wrap：这几列装的是
   **反馈正文 / 对接留言 / 备注**，用户在 textarea 里分好的段要原样显示。
   normal 会把换行当空格吃掉。（服务端那边同步换成了 clipMulti，
   只改 CSS 不改服务端 = 数据到这儿本来就没换行，白改。） */
table.data td.wrap {
  white-space: pre-wrap; overflow-wrap: anywhere;
  min-width: 180px; max-width: 320px;
}

/* 备注 / 留言这类**用户自己打的字**（2026-10-12 用户清单第 1 条）：
   列表里只占一行，放不下收成「…」，鼠标悬停看全文（走 title 属性）。

   ★ 为什么套一层 span 而不是直接在 td 上做：td 是 table-layout:auto，列宽由
     最长内容决定，td 上的 max-width 管不住它，省略号永远轮不到出现；
     span 是 inline-block，它自己的 max-width 才是真约束。
   ★ 也**不能**沿用上面的 .wrap：那是 pre-wrap，正是「备注一长就把一行撑高」
     的来头 —— 用户要治的就是这个。两列的行高现在一律一样。
   ★ 全文有两条路：悬停（不进详情页也能看），以及点开这一行看详情（正经读全文
     的地方，段落原样保留）。 */
.note-ellipsis {
  display: inline-block; max-width: 16em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  vertical-align: bottom;
}

/* ---------------------------------------------------------------- 操作列钉左

   用户 2026-09-19 提的：「把『标为已得到对接 / 标为已处理 / 删除』这几个按钮
   放到左面去，要不然我还得滑动屏幕找。」四张表原来都把操作列放在**最后**，
   而需求表有 13 列、好几个 wrap 列还带 180px 最小宽，横向拖到底才点得到。

   ★★ 光挪到第一列是不够的：管理员往右看数据的时候它照样跟着走出视野。
      所以这一列是 `position: sticky; left: 0`——**滚到哪儿它都在**。
      `.table-wrap` 的 `overflow-x: auto` 就是它的滚动容器，不用另外包一层。

   ★★ 三个必须一起做的配套，少一个都会出洋相：

   ① **必须自己刷背景色**。sticky 单元格是浮在别的单元格上面的，
      不刷底色的话横向滚动时下面的字会**透过来**，两行字叠在一起。
      底色分两种：普通行用 `--surface`，表头那格用 `--surface-alt`。
   ② **行悬停色要单独跟一下**。`tbody tr:hover` 刷的是 `tr` 的底色，
      而这一格有自己的背景，不写这一条的话**鼠标划过时只有它不变色**。
   ③ **z-index 得排三层**，否则竖向滚动时操作格会**盖住表头**：
        td.col-actions (1)  <  th 表头 (2)  <  th.col-actions (3)
      （表头本身就是 sticky top，两个方向都钉的那一格在最上面。）
      为此上面 `table.data th` 那一条补了 `z-index: 2`——原来只有它一个
      sticky 元素，不写也不出问题；现在多了个 sticky 的 td，就必须排了。

   ⚠️ 分隔线用 `box-shadow` 而不是 `border-right`：表格是 `border-collapse: collapse`，
      这种表格里 sticky 单元格的 border 在 Chrome/Edge 下**不跟着格子走**
      （横滚时线留在原地）。box-shadow 画在格子自己身上，跟得稳稳的。 */
table.data th.col-actions,
table.data td.col-actions {
  position: sticky; left: 0; z-index: 1;
  background: var(--surface);
  box-shadow: inset -1px 0 0 var(--border);
}
table.data th.col-actions {
  z-index: 3;
  background: var(--surface-alt);
}
table.data tbody tr:hover td.col-actions { background: var(--surface-alt); }

.mono { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 13px; }

.contact-line { display: block; line-height: 1.9; }
.contact-line .k { color: var(--muted); }

/* 对接申请表里「被对接那条」的联系方式（清单第 4 条）。
   和上面那行灰色机构名拉开：加一条左侧细线 + 缩进，读起来是
   「这条申请挂在谁身上 → 那个人怎么联系」的从属关系，
   而不是又一条并列的信息。字号收半档，免得抢了申请人那一列。 */
.target-contact {
  margin-top: 6px; padding-left: 8px;
  border-left: 2px solid var(--border);
  font-size: 12.5px;
}

.pill {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: 12px; font-weight: 600;
}
.pill-ok      { background: var(--success-soft); color: var(--success); }
.pill-pending { background: var(--accent-soft); color: var(--accent-ink); }
/* 后台需求表里的类型徽标（2026-11-02）。塞在编号格里，和 #P 并排，
   让管理员一眼分清「学习需求」和「兼职/实习岗位」。 */
.badge-job {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  border-radius: 999px; font-size: 11px; font-weight: 600;
  background: var(--jade-soft); color: var(--jade-dark);
  white-space: nowrap;
}

/* 分页条（管理后台，清单第 11 条）。不满一页时由 renderPager 清空 innerHTML，
   靠 .pager:empty 整个收起——别改成给容器加 hidden 属性，
   display:flex 会盖掉浏览器给 hidden 的默认 display:none，反而一直显示。 */
.pager {
  display: flex; justify-content: center; align-items: center;
  flex-wrap: wrap; gap: 10px; margin-top: 16px;
}
.pager:empty { display: none; }
/* 表格里的页码/条数要竖向对齐，用等宽数字（和 table.data 一个路子） */
.pager-info { font-size: 13.5px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* 登录框 */
.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.login-card { width: 100%; max-width: 400px; padding: 38px 34px; }

/* 后台标签页 */
.tabs { display: flex; gap: 4px; margin-bottom: 22px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.tab {
  padding: 11px 18px; font-family: inherit; font-size: 14.5px; font-weight: 500;
  color: var(--text-soft); background: none; border: none;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  cursor: pointer; transition: color .14s, border-color .14s;
}
.tab:hover { color: var(--brand); }
.tab.active { color: var(--brand); border-bottom-color: var(--brand); font-weight: 600; }
.tab .count {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  font-size: 12px; font-weight: 600;
  background: var(--brand-soft); color: var(--brand); border-radius: 999px;
}

.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin-bottom: 24px; }
.stat {
  padding: 18px 20px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.stat-label { font-size: 13px; color: var(--muted); }
/* 数值用正文墨色（比例数字，不用 tabular-nums——大字号下等宽会显得松散）；
   只有"待处理"这类需要注意的指标才换告警色，且它本身带了文字标签，不靠颜色单独表意。 */
.stat-num { margin-top: 4px; font-size: 27px; font-weight: 700; color: var(--text); line-height: 1.2; }
.stat-num.is-warn { color: var(--warn-ink); }

/* ---------------------------------------------------------------- 页脚 */

.site-footer {
  margin-top: 56px; padding: 38px 0 44px;
  background: var(--surface); border-top: 1px solid var(--border);
}
.footer-inner { display: flex; flex-wrap: wrap; gap: 24px; justify-content: space-between; align-items: flex-start; }
.footer-brand { max-width: 340px; }
.footer-brand p { margin-top: 12px; font-size: 13.5px; color: var(--muted); }
.footer-links { display: flex; flex-wrap: wrap; gap: 26px; }
.footer-col h5 { margin: 0 0 11px; font-size: 13px; color: var(--text); letter-spacing: .03em; }
.footer-col a { display: block; margin-bottom: 8px; font-size: 13.5px; color: var(--muted); }
.footer-col a:hover { color: var(--brand); text-decoration: none; }
.footer-bottom {
  margin-top: 30px; padding-top: 22px; border-top: 1px solid var(--border-soft);
  font-size: 12.5px; color: var(--muted);
}

/* ICP 备案号（工信部要求挂首页底部并链接到 beian.miit.gov.cn）。
   用和 .footer-bottom 同一档字号/颜色，别再调淡——备案号必须看得清。 */
.footer-filing {
  margin-top: 8px; font-size: 12.5px; color: var(--muted);
}
.footer-filing a {
  color: inherit; text-decoration: underline; text-underline-offset: 2px;
}
.footer-filing a:hover { color: var(--brand); }

/* ★ 2026-10-07：首屏校门图的 CC BY-SA 署名（协议要求，见 common.js 页脚那段注释）。
   比 .footer-bottom 再小一号、再淡一档——它是**法律声明**，不是给人读的内容，
   但不能小到看不见：署名要「合理可见」才算数。
   ⚠️ 颜色用 --muted（已经是全局加深过的那档），别再往下调淡——
      淡到读不出来的署名等于没署名。 */
.footer-credit {
  margin-top: 10px; font-size: 11.5px; line-height: 1.7;
  color: var(--muted); opacity: .85;
}
.footer-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.footer-credit a:hover { color: var(--brand); }
.footer-credit b { font-weight: 600; }

/* 禁止抄袭声明（2026-10-21）：全站页脚最后一个元素。
   比署名再清楚一点——署名是授权条件，这句是权利声明；两者都不该被调淡到读不出来。 */
.footer-copyright {
  margin-top: 14px; padding-top: 12px;
  border-top: 1px solid var(--border-soft);
  font-size: 12px; line-height: 1.7;
  color: var(--muted);
}

/* ---------------------------------------------------------------- 响应式 */

@media (max-width: 980px) {
  .hero-grid    { grid-template-columns: 1fr; gap: 38px; }
  .hero h1      { font-size: 46px; }
  .feature-grid { grid-template-columns: 1fr; }
  .subject-grid { grid-template-columns: repeat(3, 1fr); }
  /* 三张入口卡在 980px 往下改成两列：再挤下去每张只剩 240px 左右，
     标题会拆成三四行，反而不如让第三张另起一行 */
  .entry-grid   { grid-template-columns: 1fr 1fr; }
  /* 两个登记入口在 980px 往下改成上下排列：并排时每张只剩 ~330px，
     「我是大学生，我要提供教学」这种长标题会拆成两行，按钮也跟着变窄。
     改版要的就是「一眼看见」，宁可让它占满整行。 */
  .pick-grid    { grid-template-columns: 1fr; gap: 16px; }
}

@media (max-width: 720px) {
  body { font-size: 14.5px; }

  .container { padding: 0 16px; }
  .header-inner { height: auto; padding: 10px 0; flex-wrap: wrap; }
  /* 收紧导航。2026-09-15 起是 6 个入口（加了「大学生需求广场」），
     12.5px 下六项约 430px、可用宽度 358px，**排不下一行，会折成两行**——
     .site-nav 本来就是 flex-wrap，折行不会溢出，只是 header 高一点。
     要压回一行只能砍「我是大学生」「我要找老师」这两句，而它们正是新用户
     唯一能凭字面看懂的入口，不值得为一行的整齐牺牲掉。
     ★ 用户 2026-09-15 看过折行的实测，明确说「先不动，就这样」——别再主动改。 */
  .site-nav { width: 100%; gap: 2px; }
  .site-nav a { padding: 6px 8px; font-size: 12.5px; }

  .hero { padding: 46px 0 42px; }
  .hero h1 { font-size: 40px; }
  .hero-lead { font-size: 15px; }
  /* 四个词在手机上收一档（25 → 21px），但仍要明显大于正文的 15px */
  .hero-keys { font-size: 21px; letter-spacing: 1px; gap: 0 12px; margin-top: 15px; }
  .hero-note { font-size: 13px; }
  .hero-actions .btn { flex: 1 1 100%; }
  .hero-stats {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px 14px; margin-top: 30px; padding-top: 20px;
    text-align: center;
  }
  .hero-stat-num { font-size: 22px; }
  .hero-stat-label { line-height: 1.4; }

  .section { padding: 38px 0; }
  .page-head { padding: 28px 0 20px; }
  .page-head h1 { font-size: 23px; }

  .entry-grid   { grid-template-columns: 1fr; gap: 14px; }
  .entry-card   { padding: 22px 20px 20px 24px; }
  .entry-card h3 { font-size: 18.5px; }

  /* 登记入口在手机上依然是第一屏之后的第一件事，字号只降到刚放得下。
     ★ 方案 D 的留白在 .pick-cover / .pick-body 上，卡片本身不带 padding。 */
  .pick-cover   { padding: 18px 20px 19px; }
  .pick-body    { padding: 20px 20px 22px; }
  .pick-card h3 { font-size: 23px; }
  .pick-btn     { margin-top: 20px; padding: 13px 20px; font-size: 16px; }
  .trust-strip  { gap: 8px 20px; margin-top: 20px; font-size: 13px; }

  .form-grid    { grid-template-columns: 1fr; gap: 0; }
  .subject-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; }
  /* 手机上两列：16 个区正好排满 8 行，不留孤零零的半个空位；
     每个胶囊也更宽，手指点得中。性别两列本来就窄，不用动 */
  .district-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  /* 年级 8 档、期望薪资 6 档在手机上排两列：「社会人士」「100元及以下」这类
     四五个字的标签挤三列会换行，两列刚好 */
  .grade-grid    { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .salary-grid   { grid-template-columns: repeat(2, 1fr); gap: 8px; max-width: none; }
  /* 反馈类型三个挤一行会窄到「对接问题」换行，手机上改成一行一个 */
  .type-grid     { grid-template-columns: 1fr; gap: 8px; max-width: none; }
  .card-pad     { padding: 20px 17px; }
  .inline-group .select { flex: 0 0 108px; }

  .list-grid { grid-template-columns: 1fr; gap: 14px; }
  .item-card { padding: 19px; }

  .form-actions { flex-direction: column-reverse; align-items: stretch; }
  .form-actions .btn { width: 100%; }

  .modal { max-width: 100%; }
  .modal-body { padding: 20px; }
  .modal-head, .modal-foot { padding: 16px 20px; }
}

/* ==================================================================
   动效层（2026-10-06 加）
   ==================================================================
   这个站是**多页**的（每个导航项都是一个 .html），所以「切换页面」=
   浏览器真跳转一次。下面是三件事凑出来的「像 SPA」：

   ① 新页面进来时淡入（纯 CSS，不依赖 JS 也能跑）；
   ② 点站内链接时先把当前页淡出，再跳（common.js 拦的，见 initMotion）；
   ③ 长页面上，滚到哪儿哪儿的卡片淡入上浮。

   ★★ 三条纪律，改这块之前先读：
   ① **动画一律只碰 opacity / transform**。碰 width/height/top 会触发重排，
      一个 26s 的背景动画能让整站滚动掉帧；只碰这两个属性走的是合成层，
      主线程完全不参与。
   ② **进场动画结束后必须"落地"**（JS 摘掉 is-entering，见 initMotion）。
      动画的 fill 阶段在 CSS 层叠里**压过普通声明**——不摘的话，
      后面想靠 `body.is-leaving main { opacity: 0 }` 淡出根本压不动它，
      表现是"点了链接页面不淡出、愣一下才跳"。
   ③ 所有动效都要能被 prefers-reduced-motion 一次性关掉（文件最末尾那段）。
      前庭功能敏感的人会因为页面滑动而眩晕，这不是审美问题。 */

/* ★★★ 这四条 keyframes **必须把 to 写全**，只写 from 会出人命。
   只写 from 时，`to` 是"元素当前的样式"（隐式关键帧），
   而 .reveal 元素当前的 opacity 恰恰就是 0（下面那条 :is 规则压着）——
   于是动画从 0 演到 0，元素**永远不出现**，而 class 该加的都加了、
   控制台一声不吭。2026-10-06 第一版就栽在这儿：
   首页首屏以下整片空白，DOM 里 18 个元素全带着 is-in，看着完全正常。 */
@keyframes page-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes head-in {
  from { opacity: 0; transform: translate3d(0, -10px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes rise-in {
  from { opacity: 0; transform: translate3d(0, 22px, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes reveal-in {
  from { opacity: 0; transform: translate3d(0, 16px, 0); }
  to   { opacity: 1; transform: none; }
}

/* ---- ① 进场 ---- */

main { animation: page-in .45s ease both; }
.site-header { animation: head-in .5s cubic-bezier(.2, .8, .3, 1) both; }

/* 落地：动画播完由 initMotion 挂上 is-settled，把上面两条整个摘掉。
   摘掉之后元素回到自己的静态样式（opacity 1 / 无 transform），
   和动画最后一帧长得一模一样，所以肉眼看不出这一下切换。 */
html.is-settled main,
html.is-settled .site-header { animation: none; }

/* ② 更晚的一道保险：把首屏那几件的进场动画也摘掉（is-resting，1.8s 时挂）。
   ★ 为什么不是并进上面那条：首屏最后一件的动画要到 1.12s 才播完
     （延迟 0.44s + 时长 0.68s），is-settled 在 0.45s 就来了，
     合并的话会把错落的节奏拦腰砍断。 */
html.is-resting main,
html.is-resting .site-header,
html.is-resting .hero-inner > * { animation: none; }

/* 首屏内容一件一件升起来。★ 用 nth-child 排延迟而不是给每个元素写类：
   首屏是会改的（2026-09-29 刚改过一轮），排在这里新加的元素自动跟上节奏。
   ★ 只对 .hero-inner 的直接子元素生效——首屏之外的东西走 ③ 那套滚动淡入。 */
.hero-inner > * { animation: rise-in .68s cubic-bezier(.2, .8, .3, 1) both; }
.hero-inner > *:nth-child(1) { animation-delay: .05s; }
.hero-inner > *:nth-child(2) { animation-delay: .13s; }
.hero-inner > *:nth-child(3) { animation-delay: .22s; }
.hero-inner > *:nth-child(4) { animation-delay: .30s; }
.hero-inner > *:nth-child(5) { animation-delay: .38s; }
.hero-inner > *:nth-child(n+6) { animation-delay: .44s; }

/* ---- ② 离场（点站内链接时） ----
   ★ 只有 JS 挂上 .is-leaving 才走这里；JS 没跑（禁用/报错）时链接照常跳转，
     只是没有淡出——**降级路径必须是"少一个动画"，不是"点不动"**。 */
body.is-leaving main,
body.is-leaving .site-footer { opacity: 0; transition: opacity .24s ease; }

/* ---- ③ 滚动淡入 ----
   ★ 初始的"藏起来"挂在 html.motion-ready 底下，而不是 .reveal 本身：
     motion-ready 是 common.js **同步**挂上的（脚本在 </body> 前、先于首次绘制），
     万一 JS 根本没加载、或者被浏览器拦了，这个类不存在，
     所有 .reveal 元素就是普通可见元素——不会留下一页空白。 */
.motion-ready .reveal { opacity: 0; }
.motion-ready .reveal.is-in { animation: reveal-in .6s cubic-bezier(.2, .8, .3, 1) both; }

/* ---- 小件 ---- */

/* 文本选中色跟着品牌走。默认那抹蓝是系统的，和整站配色对不上，
   而选中态在表单页出现得极频繁（用户要复制粘贴自己的信息）。 */
::selection { background: rgb(var(--brand-rgb) / .17); color: var(--text); }

/* 锚点跳转不要"啪"地一下到位 */
html { scroll-behavior: smooth; }

/* 滚动条（Chromium/Edge）：细一点、灰一点，别用系统那条带按钮的粗条。
   ★ 只改 WebKit 那套，Firefox 走 scrollbar-width，两条都写但互不冲突。 */
* { scrollbar-width: thin; scrollbar-color: #CDBFB4 transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: #CDBFB4; border-radius: 999px;
  border: 2px solid transparent; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: #B3A093; background-clip: padding-box; }

/* ================================================================
   ★★★ 2026-10-10：师资账号制度 —— 登录 / 消息栏 / 资料卡 / 后台账号审核
   ================================================================

   这一批加了「老师注册账号」这件事，界面上多出四类新东西：
     ① 登录页与三个「请你先登录」的空态（.auth-*）
     ② 消息栏（.msg-*）和导航上那个未读红点（.nav-badge）
     ③ 「我的资料卡」与后台详情弹窗共用的那套「档案」版式（.mine-*）
     ④ 后台的新徽章和可点行（.pill-bad / .row-click / .acct-kind）

   ★ 配色**一个新变量都没加**：能点的一律 --brand（全站规矩：实心色只留给
     能点的东西），状态色一律走已有的 --success / --accent / --danger 三档。
     加第四个紫或第五个红，这一页就会开始「一直在喊」。 */

/* ---------------------------------------------------------------- 登录态页面 */

/* 登录页 / 消息栏 / 资料卡 / 反馈页共用的窄容器。
   ★ 上边距比普通页少一点：这几页的头一行都是「你要么登录、要么走开」的
     决断，隔太远会让人以为下面还有别的内容要读。 */
.auth-page { max-width: 560px; }
.auth-head { padding-bottom: 22px; }

.auth-h3 { font-size: 19px; margin-bottom: 8px; }
.auth-note { font-size: 14px; line-height: 1.8; color: var(--text-soft); }
.auth-note a { color: var(--brand); font-weight: 600; }

.auth-actions {
  display: flex; gap: 12px; flex-wrap: wrap; margin-top: 22px;
}

/* 登录页脚注：注册入口 / 忘记密码。
   ★ 这两句必须**分开摆**（一行一句），不能并成一句「没有账号或忘记密码请联系」 ——
     前者是自己能办的事（去填问卷），后者是要等管理员的事，混在一起
     会让人以为注册也要等管理员批准。 */
.auth-foot {
  margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border-soft);
  font-size: 13.5px; color: var(--text-soft); line-height: 1.9;
}
.auth-foot strong { color: var(--text); }

/* 密码输入：4 位数字，字距拉开一点读得出自己输了几位 */
.pin-input {
  letter-spacing: .5em; font-size: 20px; text-align: center;
  font-family: var(--font-num);
}
.pin-input::placeholder { letter-spacing: normal; font-size: 14px; }

/* 广场页顶部那条「想申请对接？先登记」的引导（未登录时才画）。
   ★ 用 .form-note 那一套色（浅紫底 + 深紫字）而不是告警色：
     它不是错误，是这个页面的**正常入口**。
   ★ 摆成一行：左边一句说明、右边两个按钮，中间用 margin-left:auto 顶开。
     窄屏时按钮换行到下面（flex-wrap），不会把说明挤成一列字。 */
.guest-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-bottom: 20px; padding: 14px 18px;
  background: var(--brand-soft); border-radius: var(--radius-sm);
  font-size: 13.5px; color: var(--brand-dark); line-height: 1.7;
}
.guest-bar strong { font-weight: 700; }
.guest-bar .btn { flex: none; }
.guest-bar .btn:first-of-type { margin-left: auto; }

/* 问卷页顶部那条注册说明。**和上面那条隐私承诺长得像但要说清是两件事**，
   所以只加一条左侧粗线 + 略深的底，不改字号、不改颜色——
   靠「这一条被框出来了」来提示「这条要读完」。 */
.form-note-brand {
  background: var(--brand-soft);
  border-left: 3px solid var(--brand);
  color: var(--brand-dark);
}

/* ---------------------------------------------------------------- 消息栏 */

.msg-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 14px;
}
.msg-count { font-size: 13.5px; color: var(--text-soft); }

.msg-item { padding: 0; overflow: hidden; margin-bottom: 12px; }

/* 未读的那条整体加粗 + 左边一条主色竖线。
   ★ 不用背景色整块染：消息栏里未读可能有七八条，全染上就成了「一片颜色」，
     反而看不出哪条是真新的。竖线 + 加粗只在**该看的地方**加重量。 */
.msg-item.is-unread { border-left: 3px solid var(--brand); }

.msg-head {
  display: flex; align-items: center; gap: 10px;
  padding: 15px 18px; cursor: pointer;
}
.msg-head:hover { background: var(--surface-alt); }

/* 未读圆点。★ 已读时**留位置**（visibility 而不是 display:none）：
   一个会左右移动的文字列表，读起来像在跳。 */
.msg-dot {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--brand);
}
.msg-item:not(.is-unread) .msg-dot { visibility: hidden; }

.msg-kind {
  flex: none; padding: 2px 9px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600;
}
/* 三种消息各一个色，但**都不是新的色相**：
   对接通过 = 成功（这是老师最关心的那条，给绿），
   账号通知 = 主色（平台自己说的话），
   平台公告 = 强调色（要人去看的）。 */
.msg-kind-match     { background: var(--success-soft); color: var(--success); }
/* 对接未通过（2026-10-12）。★ **绝不能复用 .msg-kind-match**：那条是绿的、
   写着「对接通过」，挂在一条标题为「对接申请未通过」的消息上，用户会以为发错了。
   用 danger 那一档，和 .pill-bad 同一套配色。 */
.msg-kind-reject    { background: var(--danger-soft);  color: var(--danger); }
.msg-kind-system    { background: var(--brand-soft);   color: var(--brand-dark); }
.msg-kind-broadcast { background: var(--accent-soft);  color: var(--accent-ink); }
/* 管理员私信（2026-10-12）。★ 用**主色**那一档，和「平台公告」的玫红区分开：
   管理员一眼要能分出「这条是单独发给我的」还是「群发的」。
   同档位的 .msg-kind-system（账号通知）也是主色系，但两者标题文案不同
   （「账号通知」vs「管理员私信」），11.5px 的标签撞色不构成误导。 */
.msg-kind-private   { background: var(--brand-soft);   color: var(--brand-dark); }

.msg-title {
  flex: 1 1 auto; min-width: 0;
  font-size: 14.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-item.is-unread .msg-title { font-weight: 700; }

.msg-time { flex: none; font-size: 12.5px; color: var(--muted); }

/* 展开箭头。★ 用 CSS 过渡而不是换字符（▾/▴）：
   换字符会让那一格的宽度变一下，整行文字跟着抖。 */
.msg-arrow { flex: none; color: var(--muted); transition: transform .18s; }
.msg-item.is-open .msg-arrow { transform: rotate(180deg); }

.msg-body {
  padding: 0 18px 18px 36px;
  border-top: 1px solid var(--border-soft);
}

/* ★ 正文必须是 pre-wrap：对接消息是**多行**的（微信一行、手机一行），
   HTML 默认把换行当空格吃掉，三条联系方式会挤成一行。
   ⚠️ 光有 `white-space: pre-wrap` 不够，还得 `overflow-wrap: anywhere` ——
      长邮箱是一个不能断的「词」，pre-wrap 不会在词内折行，窄屏上直接横向溢出。 */
.msg-text {
  margin-top: 14px; font: inherit; font-size: 14px; line-height: 1.85;
  white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text);
}
/* 「查看对应需求」按钮（2026-10-20 用户第 3 条）：正文下面单独一行 */
.msg-body .msg-demand-btn { margin-top: 14px; }

/* 导航上的未读红点。★ position:relative 挂在**链接**上（见 common.js 的
   navLinksHtml），不是导航容器上——挂容器上所有项共用一个红点，位置是错的。 */
.site-nav a { position: relative; }
.nav-badge {
  position: absolute; top: -4px; right: -10px;
  min-width: 17px; height: 17px; padding: 0 5px;
  display: inline-grid; place-items: center;
  border-radius: 999px; background: var(--accent); color: #fff;
  font-size: 10.5px; font-weight: 700; line-height: 1;
  font-family: var(--font-num);
}
.nav-badge[hidden] { display: none; }
/* ★ 上面那句必须写：`.nav-badge` 自己带 display:inline-grid，
   会盖掉浏览器给 hidden 属性的 display:none —— 不成对写的话，
   「隐藏的红点」会一直显示着，而且显示的是**上一次的数字**。 */

/* ---------------------------------------------------------------- 档案版式
   「我的资料卡」和后台的详情弹窗**共用**这一套（.mine-*）。
   两边显示的是同一份问卷、同一批字段，各写一套 CSS 早晚会出现
   「资料卡上是圆角的、弹窗里是方的」这种谁也说不清的差异。 */

.mine-title {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 6px;
}
.mine-title h3 { font-size: 21px; }

.mine-meta { font-size: 12.5px; color: var(--muted); margin-bottom: 20px; }

/* 字段网格。★ auto-fit + minmax 而不是固定列数：
   后台弹窗比资料卡宽得多，固定 2 列的话弹窗里会空出一大片。 */
.mine-facts {
  display: grid; gap: 12px 20px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin-bottom: 22px;
}
.fact { display: flex; flex-direction: column; gap: 3px; }
.fact-key { font-size: 12.5px; color: var(--muted); }
.fact-val { font-size: 14px; }

.mine-block {
  margin-top: 20px; padding-top: 18px;
  border-top: 1px solid var(--border-soft);
}
.mine-block-title {
  font-size: 12.5px; font-weight: 700; letter-spacing: .04em;
  color: var(--muted); margin-bottom: 12px;
}

.contact-row { display: flex; gap: 12px; align-items: baseline; line-height: 2; }
.contact-key { flex: none; width: 62px; font-size: 13px; color: var(--muted); }
.contact-val { font-size: 14px; word-break: break-all; }
.contact-val.is-empty { color: var(--muted); }

.mine-note {
  font-size: 14px; line-height: 1.85; color: var(--text-soft);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.mine-tip { margin-top: 10px; font-size: 13px; color: var(--text-soft); line-height: 1.8; }
.mine-tip a { color: var(--brand); font-weight: 600; }

/* 我的申请（2026-10-20 用户第 1 条）。
   ★ .item-no 在广场卡上是**右上角绝对定位**的；这里的卡片标题是普通文档流，
     不覆盖的话标号会飞到视口右上角（最近的定位祖先是 body）。 */
.mine-title .item-no { position: static; }
#intentList { display: flex; flex-direction: column; gap: 18px; }
.my-intent-card .mine-facts { margin-bottom: 18px; }
.my-intent-card .mine-block:last-of-type { margin-bottom: 0; }
.intent-actions {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 22px; padding-top: 18px;
  border-top: 1px solid var(--border-soft);
}

/* 资料卡顶上那张「当前登录账号」卡。
   ★ 左边账号、右边两个按钮，窄屏换行——用 auto 顶开而不是 space-between，
     账号名很长（邮箱）时 space-between 会把它挤到换行后仍然贴边。 */
.mine-account { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.mine-account-main { min-width: 0; }
.mine-label { font-size: 12.5px; color: var(--muted); margin-bottom: 3px; }
.mine-account-name { font-size: 15px; word-break: break-all; }
.mine-account .auth-actions { margin-top: 0; margin-left: auto; }

/* 资料卡只读视图下面那两个按钮。margin-top 归零是因为上面 .mine-block
   已经带了 20px 的上边距，再加一层会空出一大块。 */
.mine-view-actions { margin-top: 4px; }

/* 「我的资料卡」这一节的小标题（复用 .section-head 的排版，只是把上边距
   收一点——它上面就是账号卡，隔太远会像换了一页）。 */
.mine-head { margin-top: 0; }

/* ---------------------------------------------------------------- 后台：账号审核 */

/* 「已驳回」徽章。★ 必须和 pill-pending 拉开：驳回是**终态之一**
   （他得重新填），待审核是**中间态**（什么都不用做，等就行）。
   两态用同一个颜色的话，管理员扫一眼分不出「哪些要我动手」。 */
.pill-bad { background: var(--danger-soft); color: var(--danger); }

/* 「已修改」那行小字（决策 11）。★ 它挂在状态徽章**下面**，
   不是并排——两者正交（待审核的人也能改资料）。 */
/* 「是否已注册」下面那行小字（2026-10-22 起是最近登录时间，不再是「已修改」）。
   ★ 别缩成 10px 以下：它虽然只是注解，但管理员要读得出具体时间。 */
.reg-login { margin-top: 4px; font-size: 11.5px; color: var(--muted); white-space: nowrap; }

/* 账号名前面那个「手机 / 微信 / 邮箱」标签。★ 这个标签不是装饰：
   老师当初可能是用微信注册的，管理员把微信号当手机号念给他听，
   他会说「这不是我手机号」，然后两个人一起怀疑平台坏了。 */
.acct-kind {
  display: inline-block; padding: 1px 6px; margin-left: 2px;
  border-radius: 4px; background: var(--surface-alt);
  border: 1px solid var(--border-soft);
  font-size: 11px; color: var(--muted);
}

/* 后台里「点这一行看详情」的行。★ 只在能点的行上放 cursor:pointer，
   hover 的底色也用**比表头更淡**的一档——表格里已经有一条 hover 规则了，
   再压一层重色会让整行看起来像被选中了。 */
table.data tbody tr.row-click { cursor: pointer; }
table.data tbody tr.row-click:hover td { background: var(--surface-alt); }
/* ⚠️ 上面那条会盖掉 col-actions 那一格自己的 hover 底色（同权重、后写的赢），
   把钉住的操作列重新声明一遍——不写的话横滚时那一格的 hover 会不一样。 */
table.data tbody tr.row-click:hover td.col-actions { background: var(--brand-soft); }

/* 轻提示的第四种：中性提示（如「申请对接需要登录」）。
   ★ 用主色而不是灰：它多半跟着一个动作（去登录），灰底会让人以为
     是条无关紧要的旁白，扫一眼就划过去了。 */
.toast.is-info { background: var(--brand); }

/* 后台「操作」列现在最多塞三个按钮（通过 / 驳回 / 重置密码），
   比原来的「一个开关 + 删除」宽。给一档最小宽度，免得列被压成两行。 */
table.data th.col-actions,
table.data td.col-actions { min-width: 180px; }

/* ★★★ 一次性关掉全部动效。放在最后，靠层叠顺序盖住上面所有 animation/transition
   （同权重下后写的赢；这里还额外用了 !important，因为 .hero::before 那条
   是伪元素，光靠顺序不保险）。
   ⚠️ 关的是"动"，**不是"显示"**——opacity/transform 必须归位，
      否则 .reveal 会永远停在 opacity:0，整页空白。 */
/* ================================================================
   2026-10-11 新一批：头像 / 高校学历认证 / 资料卡展开 / 认证材料
   ================================================================ */

/* ---------------------------------------------------------- 师资广场的卡 */

/* 有头像时 .avatar 里装的是 <img> 而不是姓氏。
   ★ 尺寸/圆角/thickness 全部沿用 .avatar 自己那份（44px 圆、渐变底），
     这里只补两件 <img> 才需要的事：裁切方式，和加载不出来时的兜底底色。
   ★ object-fit: cover 不能省：用户传的多半是竖着的手机照，
     不裁的话会把人脸压扁成一张饼。 */
.avatar-img { object-fit: cover; background: var(--surface-alt); }

/* 默认头像（2026-10-11）：没传照片的师资卡用 /img/avatar-male.svg · avatar-female.svg 顶上。
   ★ 为什么不复用 .avatar-img：那个类把 background 写成了 --surface-alt（浅灰），是拿来
     给真照片垫底的；垫在**线稿**下面就是「白线画在浅灰上」，等于没画。这里让 .avatar
     自带的那条品牌渐变透出来就对了（线稿是透明底的白形状，换主色不用重画图）。
   ★ 需求卡的姓氏方块不吃这条：那两个类是分开的，.item-card.is-demand .avatar 仍是暖色渐变。 */
.avatar-portrait { object-fit: cover; }

/* 姓名 + 绿框一行。★ 包一层 flex 而不是把 <span> 塞进 .item-name 里：
   .item-name 是 15.5px/700，徽章继承过去就变成又大又粗的绿字，
   得反过来一条条覆盖字号字重（早晚漏一条）。 */
.item-name-row { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }

/* 高校学历认证的绿框（用户原话：「在名字旁边加一个绿色的框框写上高校学历认证」）。
   ★ 底和字都用 --success 那一对：浅绿底 #E4F7EF 上压 #0A7A4E 是 **4.83:1**，
     过 AA（这一条是 2026-10-10 专门加深过的，见 :root 里那段注释）。
   ⚠️ 别改成白字压深绿——那样徽章会变得比姓名还重，而它是个**标记**不是标题。
   ★ 描边用的是 30% 的主绿字面量而不是新开一个 --success-rgb 三元组：
     全站的 RGB 三元组只服务于「要跟着主色变」的那几个色（brand/accent/teal/shadow），
     状态色（success/danger/warn）从来不需要变，加三元组等于多一个要同步的地方。 */
.verify-badge {
  flex: none;
  display: inline-flex; align-items: center;
  padding: 1px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; line-height: 1.7;
  white-space: nowrap;
  color: var(--success); background: var(--success-soft);
  border: 1px solid rgb(10 122 78 / .3);
}

/* 整张师资卡可以点开（用户第 3 条）。★ 手型只给**有 data-card 的**师资卡：
   需求卡上没挂这个属性，鼠标移上去不该显得能点。 */
.item-card[data-card] { cursor: pointer; }

/* 「展开资料卡」那一行。★ 它是卡片里的一整条，不是一个小胶囊：
   它的作用是让人发现「这张卡还能点」，做小了就等于没做。
   ★ 用虚线上边框和上面的内容分开，而不是加底色——底色一加，
   它会和 .item-note 那个灰块抢注意力，看着像两个平行的区块。 */
.item-toggle {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; margin: 0 0 14px; padding: 8px 0 0;
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--brand); background: none; cursor: pointer;
  border: none; border-top: 1px dashed var(--border);
}
.item-toggle:hover { color: var(--brand-dark); }
.item-toggle-caret { transition: transform .18s; }
.item-card.is-open .item-toggle-caret { transform: rotate(180deg); }

/* 师资卡上的「给老师私信留言」（2026-10-12 清单第 3 条）。
   ★ 在 .item-foot **下面**单独占一行，不并进那一行：foot 是 space-between，
     左右两组本来只剩 1px 余量（见 square.js 里量过的账），再塞一个必然折行。
   ★ 只补一条间距，按钮本身用现成的 .btn.btn-ghost.btn-sm.btn-block，
     颜色跟着全局走 —— 以后换主色不用回来改这里。
   ★ **不加分隔线**：卡里 .item-toggle 已经有一条虚线（那条是必要的，它要说
     「这是一整条能点的带子」），再来一条就把卡片切成两截了。留白的间距足够。
   ★ 也不加 hover 以外的任何强调：它不该和「申请联系老师」抢注意力。 */
.item-guest { margin-top: 12px; }

/* 点开才显示的三项（第 3 条）。★ 只在 .item-more 里覆盖 .fact-key 的 width：
   那条 68px 是给「性别」「期望薪资」这种四字以内的标签定的，
   而这里的「目前学习/工作区域」「所获荣誉 / 证书」在 68px 里会折成两行。
   不直接改 .fact-key 那一处，是因为它还被另外几个广场的表用着，
   动它等于顺手改了别人已经看惯的排版。 */
.item-more { display: grid; gap: 10px; margin: 0 0 16px; }
.item-more[hidden] { display: none; }
.item-more .fact-key { width: auto; }

/* 荣誉/证书是**一段话**而不是一个短值，所以它自己一栏：
   .fact 在卡里本来就是「标签在上、值在下」（见档案版式那一节的 .fact），
   这里只需要让它顶到上面去。 */
/* ★ 2026-10-11：荣誉栏是 rows="2" 的 textarea，老师会一行一条地写，
   所以这块也要 pre-wrap。它是**唯一**一个用 .fact-stack 的地方（见 square.js），
   旁边那些单行的 .fact-val（学校/时间/区域）不需要，别顺手加成一整片。 */
.item-more .fact-stack .fact-val {
  line-height: 1.7;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* --------------------------------------------------- 「我的资料卡」的头像 */

/* 头像 + 右侧说明。★ 这一块是**写死的 HTML**（见 my.html 里那段注释）：
   它不能被 renderView 的重渲染冲掉，否则上传按钮的监听会一起没。 */
.mine-hero {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  margin-bottom: 22px; padding-bottom: 20px;
  border-bottom: 1px solid var(--border-soft);
}
/* 和广场上那张卡的 .avatar 同款（圆形渐变 + 姓氏），只是大一号：
   他自己看到的，就是别人在广场上看到的样子。 */
.mine-avatar {
  flex: none; width: 84px; height: 84px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  font-size: 30px; font-weight: 700; color: #fff;
  background: linear-gradient(135deg, var(--brand), var(--brand-light));
}
.mine-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mine-hero-main { flex: 1 1 260px; min-width: 0; }
.mine-hero-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.mine-avatar-hint {
  margin: 10px 0 0; font-size: 12.5px; line-height: 1.75; color: var(--muted);
}
.mine-avatar-hint strong { color: var(--text-soft); }

/* --------------------------------------------------- 高校学历认证那一张卡 */

/* 四种状态共用的一行：左边一个标记，右边一段话。
   ★ 四种状态的原话都写在 JS 里（见 my.js 的 renderVerify），
     这里只管版式——文案和状态是绑死的，版式不是。 */
.verify-state { display: flex; align-items: flex-start; gap: 14px; }
.verify-state h3 { font-size: 16px; margin-bottom: 6px; }
.verify-state p { font-size: 13.5px; line-height: 1.8; color: var(--text-soft); margin: 0; }
.verify-state a { color: var(--brand); font-weight: 600; }
/* 通过 / 待审 / 驳回：左边那一竖条是**唯一**区分三态的颜色，
   底纹都留白——资料卡这一页同时有好几块彩色的东西，
   再来三块彩色底会分不清哪块是状态、哪块是提示。 */
.verify-state.is-ok,
.verify-state.is-pending,
.verify-state.is-rejected { padding-left: 14px; border-left: 3px solid var(--border); }
.verify-state.is-ok { border-left-color: var(--success); }
.verify-state.is-pending { border-left-color: var(--accent); }
.verify-state.is-rejected { border-left-color: var(--danger); }
/* 被驳回时左边那个标记会换行到下面去（它是一个 11.5px 的胶囊，
   和标题挤在一行会显得比标题还靠上），所以那一种改成竖排。 */
.verify-state.is-rejected { flex-direction: column; gap: 8px; }
.verify-reason {
  margin: 0 0 8px; padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--danger-soft); color: var(--danger);
  font-size: 13.5px; line-height: 1.7;
}
.verify-reason strong { font-weight: 700; }

/* 认证方式那三个单选（复用 .subject-chip 的样子）。
   ★ 用 grid 三列而不是 .subject-grid 的五列：这里只有三个选项，
     五列会空出两格，看着像少画了东西。 */
.verify-methods {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 10px;
  max-width: 520px;
}

/* 待提交的材料缩略图。 */
.verify-picks { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
.verify-pick {
  position: relative; width: 104px; height: 104px;
  border-radius: var(--radius-sm); overflow: hidden;
  border: 1px solid var(--border); background: var(--surface-alt);
}
.verify-pick img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 右上角那个删除按钮。★ 32px 是**手指能点到的下限**（WCAG 2.5.8 的 24px 是底线，
   手机上 24px 太容易点偏，而这里点偏的后果是删掉另一张）。
   ★ 半透明黑底 + 白叉：材料本身什么颜色都有，只有黑白这一对在哪儿都看得清。 */
.verify-pick-del {
  position: absolute; top: 4px; right: 4px;
  width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 18px; line-height: 1; color: #fff; cursor: pointer;
  background: rgb(0 0 0 / .55); border: none;
}
.verify-pick-del:hover { background: rgb(0 0 0 / .75); }

/* 「选填」那种小字标（.req 的孪生兄弟：一个是必须，一个是可填可不填）。
   ★ 用中性灰而不是彩色：它不是提醒，是**免罪符**——让人知道这栏可以空着。 */
.field-label .hint-soft,
.hint-soft {
  margin-left: 6px; padding: 1px 7px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; vertical-align: 1px;
  color: var(--muted); background: var(--surface-alt);
  border: 1px solid var(--border);
}

/* 就读学校那几栏（2026-10-11 用户追加）：本科/硕士/博士**叠在同一个 .field 里**，
   跟着学历一栏一栏长出来。★ 栏与栏之间要留出间距——.field 本身是 grid 的一格，
   它内部的几块之间没有任何默认间隔，不给的话「本科就读学校」的输入框会
   紧贴着「硕士就读学校」的标签，看着像同一栏的两行。 */
.school-slot + .school-slot { margin-top: 14px; }
/* 每栏底下那句小字（「这一栏的学校会显示在师资广场上」）。空的时候 JS 直接
   hidden，不留空行——见 common.js 的 applySchoolFields。 */
.school-slot .field-hint { margin: 5px 0 0; }

/* ------------------------------------------------- 后台：认证材料与理由 */

/* 材料弹窗顶部那块「拿这个去对材料」的信息。 */
.verify-meta {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-bottom: 12px;
}
/* 三张材料。★ 每张给一个上限高度、点开才看原图：
   手机拍的学生证原图是 1600px 宽，三张直接铺满弹窗的话，
   管理员得滚半天才能看到下面那颗「通过」按钮。 */
.verify-shots { display: flex; flex-wrap: wrap; gap: 12px; margin: 12px 0; }
.verify-shot {
  display: block; width: 200px; height: 200px;
  border-radius: var(--radius-sm); overflow: hidden;
  border: 1px solid var(--border); background: var(--surface-alt);
}
.verify-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.verify-shot.is-missing {
  display: grid; place-items: center; padding: 10px;
  font-size: 12.5px; color: var(--muted); text-align: center;
}

/* 驳回理由（后台列表和详情里那一行小字）。★ 用告警色但字号更小：
   它是**已经发生过的事**，不是待办；管理员扫列表时不该被它抢走注意力，
   但翻到这一行时要立刻看得见。 */
.verify-reason-cell {
  margin-top: 4px; font-size: 11.5px; line-height: 1.6;
  color: var(--danger); max-width: 260px; overflow-wrap: anywhere;
}

/* 发过的公告（后台「群发公告」页的历史列表，2026-10-11）。
   ★ 正文用 pre-wrap：公告是多行的，而这块和消息栏一样是**读全文**的地方
     （见 lib/util.js 的 clipMulti 那段——同一批改动里已经栽过一次换行被吃）。
   ★ 正文限高 + 滚动，不整段展开：管理员要看的是「我发过哪几条」，
     十条各占半屏的话，撤回按钮会被推到看不见的地方。 */
.bc-list { list-style: none; margin: 0; padding: 0; }
.bc-item {
  border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px; margin-bottom: 10px;
}
.bc-item-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-bottom: 6px;
}
.bc-item-title { font-size: 14.5px; flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.bc-item-meta { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.bc-item-del { flex: none; }
.bc-item-body {
  margin: 0; font-family: inherit; font-size: 13.5px; line-height: 1.7;
  color: var(--text-soft); white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 9em; overflow-y: auto;
}
.bc-empty { font-size: 13.5px; color: var(--muted); padding: 6px 0; }

/* ---------------- 群发 / 私发 切换 + 收件人勾选（2026-10-12） ----------------
   用户原话：「可以给用户私发功能，直接放到原来的群发公告里，就是我可以选择群发，
   也可以选择私发，私发的话我可以通过联系方式/姓名/编号来搜索给谁发」。

   ★ 切换用两个按钮、不用 <select>：这是两种**做法**，不是同一个参数的两个取值，
     摊开摆着管理员才知道还有私发这条路（藏在下拉里等于没有）。
   ★ 选中态用**淡底 + 主色字**（--brand-soft/--brand-dark），不用实心主色：
     全站规矩是实心色只留给「会产生外部效果的那个按钮」，
     而这一页真正干活的是下面那个「发送」，切换器不能跟它抢。 */
.mode-switch {
  display: inline-flex; gap: 4px; padding: 3px;
  background: var(--surface-alt); border-radius: 10px; margin-bottom: 16px;
}
.mode-btn {
  border: 0; background: transparent; cursor: pointer;
  font: inherit; font-size: 13.5px; color: var(--text-soft);
  padding: 7px 14px; border-radius: 8px;
}
.mode-btn:hover { color: var(--brand-dark); }
.mode-btn.is-on {
  background: var(--surface); color: var(--brand-dark);
  font-weight: 600; box-shadow: 0 1px 2px rgb(15 23 42 / .08);
}

/* 收件人候选列表。★ 限高 + 滚动：账号上百个时它会把「发送」按钮推到几屏之外。 */
.pick-list {
  border: 1px solid var(--border); border-radius: 10px;
  max-height: 260px; overflow-y: auto; margin-top: 8px;
}
.pick-item {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 12px; cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.pick-item:last-child { border-bottom: 0; }
.pick-item:hover { background: var(--brand-soft); }
.pick-item input { flex: none; margin: 0; cursor: pointer; }
.pick-name { font-size: 13.5px; font-weight: 600; }
.pick-meta { font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; }
.pick-empty, .pick-more {
  font-size: 13px; color: var(--muted); padding: 12px; margin: 0;
}
.pick-more { border-top: 1px solid var(--border); }

/* ==================================================================
   「智能匹配」（2026-10-16）
   ------------------------------------------------------------------
   四块东西，都只服务这一个功能：
     .rec-bar       推荐页顶部「正在按 XX 区推荐」+ 改区入口
     .rec-entry     我的资料卡页上那个入口卡片
     .rec-teachers  发布成功弹窗里的「为您推荐合适的老师」
     .match-strip   推荐卡上「为什么推给你」那一行
   ================================================================== */

/* 顶部说明栏。
   ★ 用品牌浅色底（--brand-soft）而不是灰底：它说的是「下面这批单子是**按你的
     条件**挑的」，是这个页面成立的前提，不该长得像一条可有可无的提示。
   ★ flex-wrap：区多的时候（有人会把 17 个全勾上）那串区名会很长，
     不让它换行的话「修改可授课区」按钮会被挤出屏幕。 */
.rec-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-bottom: 22px; padding: 12px 16px;
  background: var(--brand-soft); border: 1px solid rgb(var(--brand-rgb) / .18);
  border-radius: 12px;
}
.rec-bar-icon { display: flex; flex: none; color: var(--brand); }
.rec-bar-text { flex: 1 1 240px; font-size: 13.5px; color: var(--text-soft); }
.rec-bar-text strong { color: var(--brand); font-weight: 700; overflow-wrap: anywhere; }
/* ★ 不能省：作者样式里的 display:flex 会盖掉浏览器给 [hidden] 的 display:none
   （作者样式一律优先于 UA 样式，跟选择器权重无关）。同一个坑这个文件里
   已经栽过三次（.search-clear / .pager / .search-box），别再栽第四次。 */
.rec-bar[hidden] { display: none; }

/* 我的资料卡页上那个入口。
   ★ 整块是 <a>：点哪儿都能进，不用瞄准一个按钮。
   ★ 用品牌描边 + 悬停浅底，不用实心色：这一页的主流程是「看/改我的资料卡」，
     实心色（按这个项目的老规矩）只留给能点的正经事，而这里有两个正经事。 */
.rec-entry {
  display: flex; align-items: center; gap: 14px;
  margin-bottom: 22px; padding: 16px 18px;
  background: var(--surface); border: 1px solid rgb(var(--brand-rgb) / .25);
  border-radius: 14px; text-decoration: none; color: inherit;
  transition: background .18s, border-color .18s;
}
.rec-entry:hover { background: var(--brand-soft); border-color: rgb(var(--brand-rgb) / .45); }
.rec-entry-icon {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 40px; height: 40px; border-radius: 11px;
  color: var(--brand); background: var(--brand-soft);
}
.rec-entry:hover .rec-entry-icon { background: var(--surface); }
.rec-entry-text { display: flex; flex-direction: column; gap: 3px; flex: 1 1 auto; min-width: 0; }
.rec-entry-text strong { font-size: 15px; color: var(--text); }
.rec-entry-text span { font-size: 13px; color: var(--muted); line-height: 1.6; }
.rec-entry-go { flex: none; font-size: 13.5px; font-weight: 600; color: var(--brand); }

/* 发布成功弹窗里那块「为您推荐合适的老师」。
   ★ 上面加一条虚线分隔：它接在三句「联系方式不公开…」后面，不隔一下会
     读成同一段。 */
.rec-teachers {
  margin-top: 18px; padding-top: 16px;
  border-top: 1px dashed var(--border);
}
.rec-teachers-head { font-size: 15px; font-weight: 700; color: var(--text); }
.rec-teachers-note { margin-top: 6px; font-size: 13px; color: var(--muted); line-height: 1.7; }
.rec-teachers-note strong { color: var(--text-soft); }

/* 一位老师一行。
   ★ 把它做成**一张小卡**（白底 + 描边）而不是继续用 <p> 堆：
     弹窗正文那一段是 14px/1.75 的行距，几行字连排下来人和人会糊成一片，
     家长根本分不清哪几行说的是同一个老师。
   ★★ position: relative **不能省**。cardHeader 里的 .item-no（#S12 那个标号）是
      `position: absolute; top:13px; right:16px`，它的定位参照是**最近的定位祖先**，
     而弹窗这条链上一路都是 static —— 少了这一行，标号会飘到整个弹窗的右上角去
     （压在关闭按钮上）。.item-card 里写这一行就是为了同一件事，见那边的注释。
   ★ 标号**留着**（不隐藏）：家长跟管理员反馈时能报个号，和广场上是同一个号。
   ★ .item-top 自带 padding-right:60px 是给那个标号让位的，这里**照留** ——
     名字/学校再长也不会钻到标号底下。
     ⚠️ 压的是这一行的间距，外面那个 .modal-done 的字号行高不碰。 */
.rec-teacher {
  position: relative;
  margin-top: 12px; padding: 12px 14px;
  background: var(--surface-alt); border: 1px solid var(--border-soft);
  border-radius: 11px;
}
.rec-teacher .item-top { margin-bottom: 10px; }
/* 标号在这个尺寸下要收一点：广场卡片宽 320px，这一行只有弹窗的一半 */
.rec-teacher .item-no { top: 10px; right: 12px; font-size: 11px; }
.rec-teacher .item-tags { margin-bottom: 0; }
.rec-teacher .item-name { font-size: 14.5px; }
.rec-teacher .item-sub { font-size: 12.5px; }
.rec-teacher .avatar { width: 38px; height: 38px; font-size: 15px; }
/* 期望薪资单独一行。★ 不并进上面的 item-sub（那是「学校·学历·专业」），
   两个都是「关于这个人的事实」，但薪资是**家长最关心的那个数**，得让它自己站一行。 */
.rec-teacher-salary { margin-top: 8px; font-size: 12.5px; color: var(--muted); }
.rec-teacher-salary strong { color: var(--text); font-weight: 700; }
/* 小简历（2026-10-17）：自我介绍截到**两行**，剩下的点这一行看完整资料卡。
   ★ 用 -webkit-line-clamp 截，不是 JS 数着字数 cut——老师写的中文/英文/数字
     混排，按字数截出来的长度差得远；这里由浏览器按实际排版算，多宽都不会溢出。
   ★ overflow-wrap: anywhere 兜住一长串没有空格的英文/网址，否则它会把
     整行撑宽，两行截断就形同虚设。 */
.rec-teacher-note {
  margin-top: 8px; font-size: 12.5px; line-height: 1.6;
  color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}

/* 整行可点（2026-10-16）：点开是这位老师的完整资料卡。
   ★ 判据挂在 [data-rec-card] 上而不是 .rec-teacher 本身：bindRecommend 那个
     资料弹窗顶上也会摆一个 .rec-teacher 壳子（里面是 teacherBrief，没有这个属性），
     那个壳子不可点——同一个类名两种行为，靠属性分，不靠位置猜。
   ★ hover 只挪边框色，**不改高度**（不加粗、不加 padding）：这几行是列表，
     鼠标扫过去时整块跳动比没有反馈更烦人。 */
.rec-teacher[data-rec-card] { cursor: pointer; transition: border-color .15s, background .15s; }
/* ★ hover 底色用品牌浅色（--brand-soft）而不是 --surface：
   这几行躺在**白底的弹窗**里，而卡自己的底色本来就是 --surface-alt（#FBF7F1），
   换成 --surface 是白压白、肉眼几乎看不出来，等于只有边框在动。 */
.rec-teacher[data-rec-card]:hover { border-color: rgb(var(--brand-rgb) / .45); background: var(--brand-soft); }
.rec-teacher[data-rec-card]:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* 「联系这位老师」那一行。★ 按钮**不要** btn-block：
   整行宽的实心按钮会把这一行变成一个巨大的 CTA，而家长在这一屏要看的是
   5 位老师分别是谁——按钮占满一行时，视线会被它牵着跳过上面的信息。 */
.rec-teacher-actions { display: flex; justify-content: flex-end; margin-top: 10px; }

/* ------------------------------------------------------------------ *
 * 从「发布成功」弹窗里开出来的那几个框（2026-10-16）
 *
 * ★★ 抬 z-index 是必须的，不是保险：那个成功弹窗是 JS 动态建、append 到
 *    body **最后**的（common.js 的 showDoneModal），mask 又都是 z-index:100，
 *    同层级按 DOM 顺序分高低——它在最后，就压在上面。不抬这一档的话，
 *    从推荐行点出来的框会藏在成功弹窗底下，表现是「点了没反应、按钮坏了」。
 * ★ 只抬这三个（.modal-raised）而不是改 .modal-mask 的全局层级：
 *    全站别的弹窗都按 DOM 顺序排得好好的，那条规矩不动。
 * ------------------------------------------------------------------ */
.modal-mask.modal-raised { z-index: 110; }

/* 「联系这位老师」里那两条路（申请 / 私信留言）。
   ★ 两个都是**整块可点的按钮**，不是两个并排的小按钮：这两条路的区别要靠
     一句说明才讲得清（一条走核验、一条直达消息栏），并排的小按钮没地方写字，
     用户只能靠猜。整块可点也更好按（手机上尤其）。
   ★ 都是描边、都不填色：这里是「选一条路」，不是「确认」。填色的那个应该是
     下一步表单里的「提交申请」——一屏里只能有一个实心主按钮（这条规矩
     2026-10-06 换配色时定过）。 */
.rec-choices { display: grid; gap: 10px; margin-top: 16px; }
.choice {
  display: block; width: 100%; text-align: left;
  padding: 14px 16px; cursor: pointer;
  font: inherit; color: inherit;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  transition: border-color .15s, background .15s;
}
.choice:hover { border-color: rgb(var(--brand-rgb) / .5); background: var(--surface-alt); }
.choice-title { display: block; font-size: 15px; font-weight: 700; color: var(--text); }
.choice-desc { display: block; margin-top: 5px; font-size: 12.5px; line-height: 1.65; color: var(--muted); }

/* 资料卡弹窗里那张只读的卡。
   ★ 必须**掐掉 .item-card 的 hover**：那条规则里有个 transform: translateY(-2px)，
     是给广场上「点得动」的卡做的反馈。弹窗里这张卡不可点，浮起来就是在骗人。 */
.rec-profile .item-card:hover {
  box-shadow: var(--shadow-sm); border-color: var(--border); transform: none;
}
.rec-profile-foot { margin-top: 12px; font-size: 12.5px; line-height: 1.7; color: var(--muted); }
/* 资料卡那张卡自己已经有一圈 22px 内边距，外面 .modal-body 还有 24px。
   两层叠起来手机上只剩两百来像素给正文（弹窗 500 封顶、窄屏按 100% 算），
   卡里那张脸和名字会被挤到折行。这里把**卡自己**那圈收掉一半：
   外面那圈负责留白，里面这圈只需要把内容和边框分开一点。 */
.rec-profile .item-card { padding: 16px; }

.rec-teachers-foot { margin-top: 16px; }

/* 推荐卡上「为什么推给你」那一行。
   ★ 只有推荐接口下发的卡片才有（见 square.js 的 matchStrip），广场上没有。
   ★ 底色用面白里最浅的那一档 + 一条左边框收口，做成「系统附注」的样子：
     它和卡上别的字不是一回事——那些是家长填的，这行是平台算的。 */
.match-strip {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin-bottom: 13px; padding: 7px 10px;
  background: var(--surface-alt); border-left: 3px solid rgb(var(--brand-rgb) / .28);
  border-radius: 0 8px 8px 0;
}
.match-tag {
  padding: 1px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; line-height: 1.7; white-space: nowrap;
  color: var(--text-soft); background: rgb(var(--brand-rgb) / .09);
}
/* 「较远」那一档要**一眼看得出来**：老师扫一屏的时候得先知道哪些是系统硬凑的，
   不然他会以为「推荐就这水平」，而其实是他选的区本来就没那么多单。
   ★ 用 --warn-ink 那一对（#FDF4E5 暖底上 6.02:1，过 AA），不是 --danger：
     这不是错误，只是一条距离提示，喊成红色会让人以为出事了。 */
.match-tag.is-warn { color: var(--warn-ink); background: #FDF4E5; }
.match-beyond { flex: 1 1 100%; font-size: 11.5px; color: var(--warn-ink); }

@media (max-width: 640px) {
  /* 手机上「去看看 →」会把这行挤到两行，而且它本来就是个辅助文案，藏掉最干净 */
  .rec-entry-go { display: none; }
  .rec-entry { gap: 11px; padding: 14px; }
  .rec-bar { padding: 11px 13px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .motion-ready .reveal { opacity: 1 !important; }
  .hero::before { animation: none !important; }
  html { scroll-behavior: auto !important; }
}

/* ==================================================================
   管理后台 · 发展统计（2026-10-24）
   ★ 只复用现有 --brand / --accent / --teal 与文字色，图表外不新增色值。
   ★ 图表容器在小屏横向滚动：把整张折线图缩进手机宽度会让坐标字看不清。
   ================================================================== */

.growth-wrap { display: flex; flex-direction: column; gap: 16px; }
.growth-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 16px; flex-wrap: wrap; padding: 4px 2px 0;
}
.growth-title { margin: 0; font-size: 24px; line-height: 1.3; color: var(--text); letter-spacing: .02em; }
.growth-sub { margin: 6px 0 0; font-size: 12.5px; line-height: 1.7; color: var(--muted); }
.growth-pill {
  display: inline-flex; align-items: center; min-height: 28px; padding: 3px 11px;
  border-radius: 999px; border: 1px solid rgb(var(--brand-rgb) / .20);
  background: rgb(var(--brand-rgb) / .06); color: var(--brand);
  font-size: 12px; font-weight: 700; white-space: nowrap;
}

.growth-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.growth-kpi {
  min-width: 0; padding: 16px; border: 1px solid var(--border);
  border-radius: 16px; background: var(--surface);
}
.growth-kpi-pv {
  border-color: rgb(var(--brand-rgb) / .24);
  background: linear-gradient(180deg, rgb(var(--brand-rgb) / .045), var(--surface) 68%);
}
.growth-kpi-label { font-size: 12.5px; font-weight: 700; line-height: 1.5; color: var(--muted); }
.growth-kpi-split { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px 13px; margin: 9px 0 8px; }
.growth-kpi-part { display: inline-flex; align-items: baseline; gap: 3px; font-size: 12px; color: var(--muted); white-space: nowrap; }
.growth-kpi-part strong {
  font-size: 24px; line-height: 1.1; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.growth-kpi-value {
  margin: 8px 0 7px; font-size: 32px; line-height: 1.1; font-weight: 800;
  color: var(--brand); font-variant-numeric: tabular-nums;
}
.growth-kpi-sub { font-size: 11.5px; line-height: 1.65; color: var(--muted); overflow-wrap: anywhere; }

.growth-card {
  min-width: 0; padding: 20px; border: 1px solid var(--border);
  border-radius: 18px; background: var(--surface);
}
.growth-card-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 12px 20px; flex-wrap: wrap; margin-bottom: 12px;
}
.growth-card-head h3 { margin: 0; font-size: 16px; line-height: 1.5; color: var(--text); }
.growth-card-head p { margin: 5px 0 0; max-width: 74ch; font-size: 12.5px; line-height: 1.7; color: var(--muted); }
.growth-head-note { font-size: 12.5px; line-height: 1.7; color: var(--muted); white-space: nowrap; }
.growth-head-note strong { color: var(--text); font-variant-numeric: tabular-nums; }

.growth-chart-scroll { overflow-x: auto; padding: 2px 0 4px; }
.growth-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 10px; font-size: 12px; color: var(--muted); }
.growth-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.growth-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; flex: none; }
.growth-series-0 { --growth-bar-color: var(--teal); }
.growth-series-1 { --growth-bar-color: var(--brand); }
.growth-series-2 { --growth-bar-color: var(--accent); }
.growth-swatch.growth-series-0, .growth-bar.growth-series-0 { background: var(--teal); }
.growth-swatch.growth-series-1, .growth-bar.growth-series-1 { background: var(--brand); }
.growth-swatch.growth-series-2, .growth-bar.growth-series-2 { background: var(--accent); }

.growth-bar-chart {
  display: grid; grid-template-columns: repeat(7, minmax(72px, 1fr));
  gap: 10px; align-items: end; min-width: 620px; padding: 4px 2px 0;
}
.growth-bars {
  display: flex; align-items: flex-end; justify-content: center; gap: 6px;
  height: 176px; padding: 0 4px; border-bottom: 1px solid var(--border-strong);
}
.growth-bar-col {
  display: flex; flex-direction: column; align-items: center;
  justify-content: flex-end; gap: 4px; flex: 1 1 0; min-width: 0; height: 100%;
}
.growth-bar-value {
  height: 14px; font-size: 11px; line-height: 14px; color: var(--text-soft);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.growth-bar-track { display: flex; align-items: flex-end; justify-content: center; width: 100%; height: 158px; }
.growth-bar {
  display: block; width: 14px; min-height: 2px; border-radius: 5px 5px 0 0;
  background: var(--brand);
}
.growth-bar-axis { padding-top: 8px; text-align: center; line-height: 1.45; color: var(--muted); }
.growth-bar-date { display: block; font-size: 12.5px; font-weight: 700; color: var(--text-soft); font-variant-numeric: tabular-nums; }
.growth-bar-wd { display: block; font-size: 11px; }
.growth-bar-axis.is-today .growth-bar-date, .growth-bar-axis.is-today .growth-bar-wd { color: var(--brand); }
.growth-foot-note { margin: 10px 0 0; font-size: 11.5px; line-height: 1.7; color: var(--muted); }

.growth-svg { display: block; width: 100%; min-width: 760px; height: auto; }
.growth-grid-line { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 5; }
.growth-axis-line { stroke: var(--border-strong); stroke-width: 1; }
.growth-axis-text { fill: var(--muted); font-family: inherit; font-size: 11.5px; }
.growth-axis-text.is-today { fill: var(--brand); font-weight: 700; }
.growth-base-line { stroke: var(--border-strong); stroke-width: 1; stroke-dasharray: 2 4; }
.growth-today-line { stroke: var(--brand); stroke-width: 1.2; stroke-dasharray: 4 4; opacity: .60; }
.growth-area { fill: rgb(var(--accent-rgb) / .08); }
/* 实际线用正文墨色，避免和「乐观」预测线同色（两者只差实线/虚线，
   图例里两个藏青色块并排时很难一眼分清）。 */
.growth-actual { stroke: var(--text); stroke-width: 2.8; stroke-linejoin: round; stroke-linecap: round; }
.growth-marker { fill: var(--text); stroke: var(--surface); stroke-width: 2; }
.growth-scenario-0 { --growth-line-color: var(--teal); }
.growth-scenario-1 { --growth-line-color: var(--brand); }
.growth-scenario-2 { --growth-line-color: var(--accent); }
.growth-forecast {
  stroke: var(--growth-line-color, var(--brand)); stroke-width: 1.8;
  stroke-dasharray: 7 5; stroke-linejoin: round; stroke-linecap: round; opacity: .70;
}
.growth-forecast.growth-scenario-1 { stroke-width: 2.4; opacity: 1; }
.growth-line-swatch { display: inline-block; width: 28px; border-top: 2px dashed var(--growth-line-color, var(--brand)); align-self: center; flex: none; }
.growth-line-swatch.is-actual { border-top: 3px solid var(--text); }
.growth-line-summary { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px 15px; font-size: 12px; color: var(--muted); }
.growth-line-summary strong { font-size: 15px; color: var(--text); font-variant-numeric: tabular-nums; }
.growth-inline-note { display: block; margin-top: 8px; font-size: 11.5px; line-height: 1.7; color: var(--warn-ink); }
.growth-delta { font-size: 11.5px; font-weight: 700; white-space: nowrap; }
.growth-delta.is-up { color: var(--success); }
.growth-delta.is-down { color: var(--danger); }
.growth-delta.is-flat { color: var(--muted); }

.growth-week-scroll { overflow-x: auto; }
.growth-table { width: 100%; min-width: 780px; border-collapse: collapse; font-size: 13px; line-height: 1.55; }
.growth-table th, .growth-table td { padding: 10px 12px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; }
.growth-table thead th { background: var(--surface-alt); color: var(--text-soft); font-size: 12px; font-weight: 700; }
.growth-table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.growth-th-sub { color: var(--muted); font-size: 11px; font-weight: 500; }
.growth-week-range { color: var(--text-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
.growth-week-state {
  display: inline-block; padding: 1px 8px; border-radius: 999px;
  background: var(--surface-alt); color: var(--text-soft); font-size: 11.5px; font-weight: 700;
}
.growth-week-state.is-current { background: rgb(var(--brand-rgb) / .10); color: var(--brand); }
.growth-week-days { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; }
.growth-week-count { display: block; color: var(--text); font-size: 15px; font-weight: 700; }
.growth-table tr.is-current { background: rgb(var(--brand-rgb) / .035); }

.growth-details { display: flex; flex-direction: column; gap: 10px; }
.growth-details details {
  padding: 14px 16px; border: 1px solid var(--border);
  border-radius: 14px; background: var(--surface);
}
.growth-details summary { cursor: pointer; color: var(--text-soft); font-size: 13px; font-weight: 700; }
.growth-details details[open] summary { margin-bottom: 10px; }
.growth-notes { margin: 0; padding-left: 20px; color: var(--text-soft); font-size: 12.5px; line-height: 1.8; }
.growth-notes li + li { margin-top: 4px; }
.growth-source { margin: 10px 0 0; color: var(--muted); font-size: 11.5px; line-height: 1.7; }

.growth-stale {
  display: flex; justify-content: space-between; align-items: center; gap: 10px 16px; flex-wrap: wrap;
  padding: 10px 14px; border: 1px solid rgb(var(--teal-rgb) / .30); border-radius: 12px;
  background: #FDF4E5; color: var(--warn-ink); font-size: 12.5px; line-height: 1.7;
}
.growth-error {
  padding: 26px 22px; border: 1px solid var(--border); border-radius: 18px;
  background: var(--surface); text-align: center;
}
.growth-error h2 { margin: 0 0 8px; font-size: 20px; color: var(--text); }
.growth-error p { margin: 0 0 12px; color: var(--text-soft); font-size: 13px; line-height: 1.7; }

@media (max-width: 1100px) {
  .growth-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .growth-kpis { grid-template-columns: 1fr; }
  .growth-head { padding-top: 0; }
  .growth-title { font-size: 21px; }
  .growth-card { padding: 15px; border-radius: 15px; }
  .growth-kpi { padding: 14px; }
  .growth-kpi-part strong { font-size: 22px; }
  .growth-kpi-value { font-size: 29px; }
  .growth-card-head { margin-bottom: 10px; }
  .growth-legend { gap: 7px 12px; }
}

/* ==================================================================
   举报页（2026-11-04）
   卡片上的小字入口 + 这一页的对象选择器/原因。颜色沿用告警色——
   举报是「出事了才用」的路径，不该和主流程抢品牌色。
   ================================================================== */
.report-step {
  display: flex; align-items: center; gap: 9px;
  margin: 20px 0 12px; font-size: 15px; font-weight: 700; color: var(--text);
}
.report-step-no {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--brand); color: #fff;
  font-size: 12.5px; font-weight: 800;
}
.report-search { margin: 14px 0 12px; }
.report-list {
  display: flex; flex-direction: column; gap: 8px;
  max-height: 340px; overflow-y: auto;
  padding: 2px;
}
.report-item {
  display: flex; align-items: center; gap: 12px;
  width: 100%; padding: 11px 14px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); cursor: pointer; text-align: left;
  font: inherit; color: var(--text);
  transition: border-color .16s, background .16s;
}
.report-item:hover { border-color: var(--border-strong); background: var(--surface-alt); }
.report-item.is-picked { border-color: var(--danger); background: var(--danger-soft); }
.report-item-no {
  flex: none; min-width: 52px;
  font-size: 12px; font-weight: 700; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.report-item-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.report-item-main strong { font-size: 14px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.report-item-main span { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.report-item-act { flex: none; font-size: 12.5px; font-weight: 700; color: var(--brand); }
.report-item.is-picked .report-item-act { color: var(--danger); }
.report-empty, .report-more { font-size: 12.5px; color: var(--muted); line-height: 1.7; margin: 6px 2px; }
.report-picked {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin-top: 12px; padding: 10px 14px;
  background: var(--surface-alt); border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
  font-size: 13.5px;
}
.report-fineprint { margin-top: 14px; font-size: 12px; color: var(--muted); line-height: 1.6; }

/* 手机紧凑行上的「异常」小标（2026-11-04 举报）。和卡片上那条红条同一个意思，
   只是一行里放不下整句，缩成两个字；完整说明点开详情卡还能看到。 */
.demand-row-flag {
  flex: none; padding: 1px 7px; border-radius: 999px;
  background: var(--danger-soft); color: var(--danger);
  font-size: 11px; font-weight: 700;
}
