:root {
  --sidebar-width: 200px;
}

body.has-modal-open {
  overflow: hidden;
}

/* Form controls do not inherit colour from their parent — the UA gives them its own
   near-black. That left unstyled buttons, inputs and selects rendering at pure #000
   next to body text at #20242c, two blacks that read as a printing error rather than
   a choice. Placed first so anything with a colour of its own still wins. */
button,
input,
select,
textarea {
  color: inherit;
}

/* Scrim behind the mobile slide-in nav. Hidden on desktop (where the sidebar is
   a static column); the mobile @media block turns it on. It also captures taps
   to close the menu and stops the background from scrolling under the fixed
   overlay — on iOS Safari that background scroll detaches fixed elements and
   garbles the layout (Android keeps fixed elements stable, so it never showed
   there). */
.sidebar-backdrop {
  display: none;
}

.sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 30;
  width: var(--sidebar-width);
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 0 14px 18px;
  background: var(--sidebar-brand);
  border-right: 1px solid var(--sidebar-brand-deep);
  color: var(--sidebar-ink);
  transition: width 180ms ease, padding 180ms ease;
}


/* quick task (260828-pl): 品牌块现在是 <a>, 链到多校导航总站(PORTAL_URL)。
   标签换了但 class 没换, 所以原有的 .brand-block 规则(含折叠态、monash 的两条特异性
   覆盖、以及 darkReaderBoot 的 CHROME_SELECTORS)全部原样适用 —— 这正是选择改标签而
   不是外面再套一层 <a> 的原因: 套一层会在 .sidebar 和 .brand-block 之间插进一个新的
   flex 子项, 折叠态那几条按位置写的规则就得跟着改。
   只需要中和 <a> 自带的三样: 下划线、链接色、以及它不是 flex 容器这件事(下面那条
   基础规则已经写了 display, 这里不重复)。 */
.brand-block {
  text-decoration: none;
  color: inherit;
}

.brand-block {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  height: var(--topbar-height);
  /* -15px on the right, not -14px: the extra pixel covers the sidebar's own
     border-right, which otherwise slices a 1px line straight through the top
     band between this block and the topbar. Children paint above their parent's
     border, so the block hides it here while the border still divides the
     column below. Invisible on unimelb only because that border, this block and
     the topbar are all the same navy. */
  margin: 0 -15px 0 -14px;
  padding: 0 14px;
  /* Part of the topbar band, not the sidebar: it sits at topbar height in the
     top-left corner, so a school with an accent topbar gets one continuous
     colour across the whole top row. Defaults to the brand chrome + white. */
  background: var(--topbar-bg);
  color: var(--topbar-ink);
  /* Same underline as .topbar — one band, one line. */
  border-bottom: 1px solid var(--topbar-line);
}

.brand-mark {
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  /* Filled with the school's identity colour. For a dark-brand school that is
     the same colour as the .brand-block behind it, so this reads exactly like
     the transparent tile it replaced; for an accent-brand school it is the only
     place the real colour appears in the chrome. */
  border: 2px solid var(--brand-mark-border);
  border-radius: var(--radius-ui);
  color: var(--brand-mark-ink);
  background: var(--brand-mark);
  font-weight: 800;
  letter-spacing: 0;
  /* The mark is always a CJK character and Georgia has none, so each school fell
     through to whatever system CJK face came first and the glyph sat at a
     slightly different height on every one. Name the CJK faces first so all
     eight share one set of metrics; the serifs stay as the last resort. */
  font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC",
    "Source Han Sans SC", Georgia, "Times New Roman", serif;
  font-size: 24px;
  /* place-items centres the LINE BOX; pinning line-height makes that box the
     glyph's own em, so the two centres coincide. */
  line-height: 1;
}

.brand-block strong,
.brand-block span {
  display: block;
}

.brand-block > div:not(.brand-mark) {
  min-width: 0;
  /* The text block's centre is the mark's centre — one shared midline. Stated
     explicitly rather than left to block flow, so there is one rule to read
     when a school looks off. Monash used to carry a translateY(4px) hand-nudge
     here; it is gone, and all eight now measure 0. */
  /* Optical, not geometric: the pair IS centred, but the eye anchors on the big
     name line, which sits above the midpoint, so the lockup reads high. Nudge it
     down 3px — under half the 6.6px it would take to centre the name itself,
     which came out bottom-heavy. Offset, not padding: padding grows the box and
     justify-content re-centres it, so only half of it would land. */
  position: relative;
  top: 3px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.brand-block span,
.section-label,
.card-meta,
.result-count,
.form-note,
.notification-item span,
.log-row span {
  color: var(--muted);
  font-size: 12px;
}

.brand-block strong {
  color: var(--topbar-ink);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 22px;
  line-height: 1;
}

.sidebar-toggle {
  position: absolute;
  top: 50%;
  right: -11px;
  z-index: 2;
  transform: translateY(-50%);
  width: 22px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--sidebar-ink) 16%, transparent);
  border-radius: var(--radius-ui);
  background: rgba(0, 0, 0, 0.18);
  color: color-mix(in srgb, var(--sidebar-ink) 68%, transparent);
  font-size: 14px;
  line-height: 1;
  opacity: 0.52;
}

.sidebar-toggle:hover {
  background: color-mix(in srgb, var(--sidebar-ink) 12%, transparent);
  color: var(--sidebar-ink);
  opacity: 1;
}

/* Split: the logo subtitle rides the topbar band (accent-coloured for some
   schools), the section label stays on the dark sidebar. */
.brand-block span {
  color: color-mix(in srgb, var(--topbar-ink) 74%, transparent);
}

.sidebar .section-label {
  color: color-mix(in srgb, var(--sidebar-ink) 74%, transparent);
}

.brand-block span {
  white-space: nowrap;
  font-size: 11px;
  line-height: 1.2;
}

.nav-list,
.board-list,
.followed-tags {
  display: grid;
  gap: 4px;
}

.nav-list {
  padding-top: 2px;
}

.nav-item,
.board-link,
.tag-link,
.admin-link {
  width: 100%;
  min-height: 38px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  color: color-mix(in srgb, var(--sidebar-ink) 88%, transparent);
  /* Heavier than the body default. The 88% above is a resting state that lets
     the active item stand out, but it costs contrast — on a LIGHT sidebar
     (阿德, 西澳, ANU) dark ink at 88% lands at 4.23:1, under AA. Weight cannot
     raise a contrast ratio, but it is what actually makes small text hold up at
     a given one; the dark-sidebar schools are unaffected (white stays ~15:1). */
  font-weight: 800;
  text-align: left;
}

.nav-item > span:first-child {
  width: 18px;
  display: inline-grid;
  place-items: center;
  flex: 0 0 18px;
  font-size: 13px;
}

.nav-item:hover,
.board-link:hover,
.tag-link:hover,
.admin-link:hover,
.sort-button:hover,
.tab-pill:hover,
.card-actions button:hover,
.detail-actions button:hover,
.comment-actions button:hover {
  background: var(--surface-muted);
}

.nav-item.is-active,
.board-link.is-active,
.tag-link.is-active,
.tab-pill.is-active,
.sort-button.is-active,
.admin-tabs .is-active {
  background: var(--brand-weak);
  color: var(--brand);
}

.sidebar .nav-item.is-active,
.sidebar .board-link.is-active,
.sidebar .tag-link.is-active {
  background: var(--sidebar-active-bg);
  color: var(--sidebar-active-ink);
  box-shadow: none;
  font-weight: 800;
}

.sidebar .nav-item:hover,
.sidebar .board-link:hover,
.sidebar .tag-link:hover,
.sidebar .admin-link:hover {
  background: color-mix(in srgb, var(--sidebar-ink) 12%, transparent);
  color: var(--sidebar-ink);
}

/* Monash-only: white sidebar. Everything below flips the default dark-tinted
   sidebar (white text on brand fill) to a light one (dark text on white). The
   brand-block logo header keeps its Monash-blue fill as a visual anchor, so it
   is intentionally left untouched. Scoped to [data-tenant="monash"] so unimelb's
   dark sidebar is unaffected. */
/* Independent layout: a full-width blue topbar with the white sidebar as a
   standalone column BELOW it. The logo block is lifted out of the sidebar flow
   and pinned to the top-left so it becomes the topbar's blue left segment. */
[data-tenant="monash"] .topbar {
  left: 0;
}

/* 这条描述的是"品牌块当顶栏的左端"那套布局, 只在侧栏是**一根真实的列**时才成立 ——
   它把自己 fixed 到视口左上角, 宽度直接取 var(--sidebar-width)。
   而 --sidebar-width 在 @media (max-width: 720px) 里是 0px(窄屏侧栏改成滑入式面板,
   那 290px 是面板自己的宽度, 不走这支变量)。没有这层 min-width 的话, 手机上这块宽度
   就只剩左右内边距的 28px, 品牌名被挤成一个字一行竖着排, 而且因为是 fixed 定位, 还
   盖在顶栏的搜索框上 —— 用户报的"左上角 UI 错乱"就是它。
   紧邻那条 [data-tenant="monash"] .sidebar 不用一起关进来: 它调的是侧栏自身的
   top/背景/描边, 窄屏下的滑入面板同样需要。 */
@media (min-width: 721px) {
  [data-tenant="monash"] .brand-block {
    position: fixed;
    inset: 0 auto auto 0;
    width: var(--sidebar-width);
    height: var(--topbar-height);
    margin: 0;
    padding: 0 14px;
    z-index: 21;
    background: var(--brand);
    border-bottom: 1px solid var(--brand-deep);
  }
}

[data-tenant="monash"] .sidebar {
  top: var(--topbar-height);
  padding-top: 8px;
  background: var(--surface);
  border-right-color: transparent;
  color: #1b2433;
}

/* Collapsed: the logo block stays blue (it is now the topbar's left end), so the
   「莫」mark keeps its white outline/text against that blue. */
[data-tenant="monash"] .sidebar.is-collapsed .brand-block {
  background: var(--brand);
  justify-content: center;
  align-items: center;
  width: var(--sidebar-width);
  height: var(--topbar-height);
}

[data-tenant="monash"] .sidebar.is-collapsed .brand-mark {
  border-color: rgba(255, 255, 255, 0.88);
  color: #ffffff;
}

[data-tenant="monash"] .sidebar .nav-item,
[data-tenant="monash"] .sidebar .board-link,
[data-tenant="monash"] .sidebar .tag-link,
[data-tenant="monash"] .sidebar .admin-link {
  color: #2a3346;
}

[data-tenant="monash"] .sidebar .section-label {
  color: #8a93a6;
}

[data-tenant="monash"] .sidebar .nav-item:hover,
[data-tenant="monash"] .sidebar .board-link:hover,
[data-tenant="monash"] .sidebar .tag-link:hover,
[data-tenant="monash"] .sidebar .admin-link:hover {
  background: #eef1f7;
  color: #111a2b;
}

[data-tenant="monash"] .sidebar .nav-item.is-active,
[data-tenant="monash"] .sidebar .board-link.is-active,
[data-tenant="monash"] .sidebar .tag-link.is-active {
  background: var(--sidebar-brand);
  color: #ffffff;
}

[data-tenant="monash"] .sidebar .tag-follow-section {
  border-top-color: #e5e8ef;
}

[data-tenant="monash"] .sidebar-toggle {
  background: var(--surface);
  border-color: #d6dbe6;
  color: #6b7688;
}

[data-tenant="monash"] .sidebar-toggle:hover {
  background: #eef1f7;
  color: #1b2433;
}

.sidebar-section,
.tag-follow-section {
  min-height: 0;
}

.tag-follow-section {
  margin-top: 8px;
  padding-top: 12px;
  border-top: 1px solid color-mix(in srgb, var(--sidebar-ink) 14%, transparent);
}

.followed-tag-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  padding: 0 0 0 10px;
}

.followed-tag-head .section-label {
  margin: 0;
}

.tag-link {
  min-height: 30px;
  padding: 5px 10px;
  border-radius: var(--radius-ui);
  color: color-mix(in srgb, var(--sidebar-ink) 78%, transparent);
  font-size: 12px;
  font-weight: 700;
}

.followed-tag-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 26px;
  gap: 5px;
  align-items: center;
}

.followed-tag-row .tag-link {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.tag-remove-button,
.followed-tag-add button {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--sidebar-ink) 16%, transparent);
  border-radius: var(--radius-ui);
  background: color-mix(in srgb, var(--sidebar-ink) 8%, transparent);
  color: color-mix(in srgb, var(--sidebar-ink) 74%, transparent);
  font-weight: 900;
  line-height: 1;
}

.tag-remove-button:hover,
.followed-tag-add button:hover {
  border-color: color-mix(in srgb, var(--sidebar-ink) 38%, transparent);
  background: color-mix(in srgb, var(--sidebar-ink) 16%, transparent);
  color: var(--sidebar-ink);
}

/* Keep the remove (×) discreet: hidden until the tag row is hovered or a control
   inside it is focused. Its 26px column stays reserved so nothing shifts. */
.tag-remove-button {
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
}

.followed-tag-row:hover .tag-remove-button,
.followed-tag-row:focus-within .tag-remove-button {
  opacity: 1;
  pointer-events: auto;
}

/* 意见反馈 entry, pinned to the bottom of the sidebar (margin-top:auto pushes it
   past the tag section) so it is always the last thing in the rail regardless of
   how many followed tags there are. */
/* 同一组里只有第一个吃掉剩余空间；后面紧跟的那个（「意见反馈」在「下载 App」
   之后）不能再 auto，否则 flex 把空隙平分给两者，中间拉开一大片。 */
.sidebar-feedback-link + .sidebar-feedback-link {
  margin-top: 0;
}

.sidebar-feedback-link {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 34px;
  border: 1px solid color-mix(in srgb, var(--sidebar-ink) 14%, transparent);
  border-radius: var(--radius-ui);
  padding: 0 10px;
  background: color-mix(in srgb, var(--sidebar-ink) 6%, transparent);
  color: color-mix(in srgb, var(--sidebar-ink) 78%, transparent);
  font-size: 12px;
  font-weight: 800;
  cursor: pointer;
}

/* Account cluster (avatar + bell, or the log-in button) is mirrored at the TOP of
   the sidebar (above the nav) and hidden by default. Below the breakpoint the
   centred topbar search needs the room, so CSS hides the topbar copy and reveals
   this one. data-action delegation means either copy fires the same handler.
   quick task (260827): the hand-off width was raised from 1280px to 1600px — the
   topbar copy was still crowding the centred search bar well above the old
   threshold. */
.sidebar-account {
  display: none;
}

/* The bell (stroke: currentColor) and the log-in ghost-button default to dark ink
   (they normally get their light colour from .topbar-scoped rules that don't reach
   the sidebar). On the dark sidebar they'd be invisible, so light them here. */
.sidebar-account .notification-button,
.sidebar-account .ghost-button {
  color: color-mix(in srgb, var(--sidebar-ink) 88%, transparent);
}

.sidebar-account .ghost-button {
  border-color: color-mix(in srgb, var(--sidebar-ink) 28%, transparent);
}

.sidebar-account .notification-button:hover,
.sidebar-account .ghost-button:hover {
  background: color-mix(in srgb, var(--sidebar-ink) 12%, transparent);
}

/* Rail-only red dot standing in for the (hidden) bell — see the rail block below. */
.account-unread-dot {
  display: none;
}

@media (min-width: 721px) and (max-width: 1599px) {
  .header-actions .notification-button,
  .header-actions .profile-chip {
    display: none;
  }

  /* Pin the trio account → 网站导航(site-links) → feedback together at the bottom.
     The account's auto top-margin pushes it and both siblings down as one group.
     site-links normally has its OWN auto top-margin (it used to pin itself); with
     the account above it that would be a SECOND auto margin and the free space
     would split between them, leaving a gap — so neutralise site-links' here and
     let the account be the sole pinner. */
  .sidebar-account {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin-top: auto;
    margin-bottom: 8px;
  }

  .sidebar .sidebar-links-link {
    margin-top: 0;
  }

  /* quick task (260828-lgn): 意见反馈也必须一起中和 —— 上面那段注释说"让 account
     独自负责吊底", 但 .sidebar-feedback-link 的 margin-top: auto 从来没被解掉。
     site-links 这个元素只有部分租户会渲染(西澳就没有), 所以在那些租户上剩下的两个
     auto margin 是 account 和 feedback, flex 把剩余空间**平分**给它们 —— 实测西澳
     1536×900 下两者各拿 185.5px, 于是登录按钮吊在侧栏中间、离底部还有一大截。
     这是"位置也不对"的全部原因, 与断点无关。 */
  .sidebar .sidebar-feedback-link {
    margin-top: 0;
  }

  .sidebar-account .profile-chip {
    flex: 1 1 auto;
    min-width: 0;
  }

  .sidebar-account .profile-chip > span:last-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* 登录按钮（未登录时）跟着账号簇一起下到侧栏底部。
   下界原本是 981px，理由是"折叠成图标条时按钮放不下，所以留在顶栏"——那条假设依赖
   侧栏在 ≤980px 时按宽度自动收窄，而折叠早已改由 .is-collapsed 类驱动、与宽度脱钩，
   于是 721~980 这一段成了唯一还把登录按钮留在顶栏的区间：用户在 976px 看到右上角
   突然冒出"登录 / 注册"，就是撞进了这条缝。
   现在下界与上面那块对齐（721px），整个 721~1599 区间行为一致，登录一律在左下角。
   放不下的问题改由文案解决：折叠态只显示"登录"，见 .sidebar.is-collapsed 那条。 */
@media (min-width: 721px) and (max-width: 1599px) {
  .header-actions .desktop-action {
    display: none;
  }

  .sidebar-account .ghost-button {
    flex: 1 1 auto;
  }
}

/* quick task (260828-lgn): 1360–1599 且**未登录**时, 把登录入口还给右上角。
   实测(西澳, 展开侧栏, 强制显示顶栏那份后量搜索栏右缘到操作区左缘的余量):

     视口 1280px -> 余量  47px    视口 1536px -> 余量 170px

   未登录的账号簇只多占 100px, 所以 1360 往上余量 ≥ ~87px, 宽裕。1280 那档只剩 47px,
   所以原来的 1280 下界是有道理的, 这里不下探到那个数, 取 1360 留一档安全余量。
   登录态继续沿用 1599 上界: 头像 + 用户名(已限宽 120px) + 铃铛比一颗按钮宽得多,
   同样的余量要更晚才够。两种状态各按自己的宽度决定, 而不是都迁就宽的那个。 */
@media (min-width: 1360px) and (max-width: 1599px) {
  body:not(.is-authed) .header-actions .desktop-action {
    display: flex;
  }

  body:not(.is-authed) .sidebar-account {
    display: none;
  }

  /* quick fix (260829-pin)：把 account 藏回顶栏之后，侧栏就一个 margin-top: auto 都
     不剩了 —— 上面 721–1599 那块把 site-links / feedback 的 auto 解成 0，前提是
     "account 独自负责吊底"，而这块恰好把 account 关掉。于是 1360–1599 未登录时，
     网站导航和意见反馈紧贴关注标签往上飘（实测 1480×950：意见反馈底边 537，侧栏
     底边 950）。这里把 base 那套原样还回来：site-links 吊底，feedback 跟在它下面。
     site-links 不是每个租户都渲染（西澳就没有），所以 feedback 也得拿回 auto，再用
     与 base 同款的相邻选择器把"两个 auto 平分剩余空间"那种情况压掉。
     admin-link 那两条不用跟着抄：只有管理员会渲染，而管理员必然是登录态，进不来
     body:not(.is-authed) 这个分支。 */
  body:not(.is-authed) .sidebar .sidebar-links-link,
  body:not(.is-authed) .sidebar .sidebar-feedback-link {
    margin-top: auto;
  }

  body:not(.is-authed) .sidebar .sidebar-links-link ~ .sidebar-feedback-link {
    margin-top: -10px;
  }
}

/* Collapsed (icon-only) sidebar: MERGE the bell into the avatar — hide the separate
   bell button and the username, leaving just the avatar with a red dot on its
   corner when there's anything unread. Keyed off the collapse toggle
   (.sidebar.is-collapsed), NOT a width, since the sidebar no longer auto-rails by
   viewport width. */
.sidebar.is-collapsed .sidebar-account {
  justify-content: center;
}

.sidebar.is-collapsed .sidebar-account .notification-button {
  display: none;
}

.sidebar.is-collapsed .sidebar-account .profile-chip {
  position: relative;
  flex: none;
  width: auto;
  padding: 0;
  background: transparent;
  border: 0;
}

.sidebar.is-collapsed .sidebar-account .profile-chip > span:last-child {
  display: none;
}

/* 折叠态的登录按钮：以前是 display:none —— 藏起来之后未登录用户在图标条里根本没有
   登录入口，只能靠顶栏那份补上，而那正是"976px 右上角突然冒出登录 / 注册"的来源。
   现在改成显示：main.ts 在折叠时把文案换成短的"登录"（完整的"登录 / 注册"在 78px
   宽的列里会一字一行折断），配上和 .nav-item 同一套的 44px 居中盒子，读起来就是
   图标条里的又一个条目。 */
.sidebar.is-collapsed .sidebar-account .ghost-button {
  flex: none;
  justify-content: center;
  width: 44px;
  padding: 0;
  font-size: 11px;
  white-space: nowrap;
}

.sidebar.is-collapsed .account-unread-dot {
  display: block;
  position: absolute;
  top: -1px;
  right: -1px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #e5484d;
  border: 2px solid var(--sidebar-brand);
}

.sidebar-feedback-link:hover,
.sidebar-feedback-link:focus-visible {
  border-color: color-mix(in srgb, var(--sidebar-ink) 34%, transparent);
  background: color-mix(in srgb, var(--sidebar-ink) 14%, transparent);
  color: var(--sidebar-ink);
}

/* 网站导航 entry — a second pinned item that sits directly ON TOP of 意见反馈,
   so it copies the feedback button's look rather than the nav-list's. */
.sidebar-links-link {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 34px;
  border: 1px solid color-mix(in srgb, var(--sidebar-ink) 14%, transparent);
  border-radius: var(--radius-ui);
  padding: 0 10px;
  background: color-mix(in srgb, var(--sidebar-ink) 6%, transparent);
  color: color-mix(in srgb, var(--sidebar-ink) 78%, transparent);
  font-size: 12px;
  font-weight: 800;
  text-align: left;
  cursor: pointer;
}

.sidebar-links-link > span:first-child,
.sidebar-feedback-link > span:first-child {
  width: 14px;
  flex: 0 0 14px;
  display: inline-grid;
  place-items: center;
}

.sidebar-links-link:hover,
.sidebar-links-link:focus-visible {
  border-color: color-mix(in srgb, var(--sidebar-ink) 34%, transparent);
  background: color-mix(in srgb, var(--sidebar-ink) 14%, transparent);
  color: var(--sidebar-ink);
}

/* Active state matches the nav-list's selected row (white pill, brand ink) so the
   rail reads as ONE navigation surface even though this button lives apart. */
.sidebar-links-link.is-active {
  border-color: var(--sidebar-active-bg);
  background: var(--sidebar-active-bg);
  color: var(--sidebar-active-ink);
}

/* Only the FIRST bottom-pinned item may claim the free space: a second
   margin-top:auto splits it and floats the pair apart. The sidebar's 16px column
   gap is also too airy for what should read as one stacked pair, so pull the
   follower up to a ~6px gap. Covers the admin-only 设置 link too, which had the
   same double-auto problem before this button existed. */
.sidebar-links-link ~ .sidebar-feedback-link,
.admin-link ~ .sidebar-links-link,
.admin-link ~ .sidebar-feedback-link {
  margin-top: -10px;
}

.sidebar.is-collapsed .sidebar-links-link span:last-child,
.sidebar.is-collapsed .sidebar-feedback-link span:last-child {
  display: none;
}

.sidebar.is-collapsed .sidebar-links-link,
.sidebar.is-collapsed .sidebar-feedback-link {
  justify-content: center;
  width: 44px;
  padding: 0;
}

.feedback-sent {
  display: grid;
  gap: 8px;
  padding: 6px 0 2px;
}

.feedback-sent strong {
  font-size: 16px;
}

.feedback-sent p {
  margin: 0;
  color: var(--muted);
}

.followed-tag-add {
  display: grid;
  grid-template-columns: 22px 0fr;
  gap: 3px;
  width: 22px;
  margin: 6px 0 0;
  padding-left: 10px;
  transition: width 140ms ease, grid-template-columns 140ms ease;
}

/* .is-open is the STICKY state set by clicking + (state.followedTagInputOpen).
   :hover alone was the whole reveal before, which meant any re-render — or the
   pointer drifting a few px off the 22px button — collapsed the box mid-typing.
   Hover is kept as a preview affordance on desktop. */
.followed-tag-add:hover,
.followed-tag-add:focus-within,
.followed-tag-add.is-open {
  grid-template-columns: 22px minmax(0, 1fr);
  width: 132px;
}

.followed-tag-add input {
  width: 100%;
  min-width: 0;
  height: 22px;
  border: 1px solid color-mix(in srgb, var(--sidebar-ink) 16%, transparent);
  border-radius: var(--radius-ui);
  padding: 0;
  background: color-mix(in srgb, var(--sidebar-ink) 6%, transparent);
  color: var(--sidebar-ink);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms ease, padding 140ms ease;
}

.followed-tag-add:hover input,
.followed-tag-add:focus-within input,
.followed-tag-add.is-open input {
  padding: 0 7px;
  opacity: 1;
  pointer-events: auto;
}

.followed-tag-add input::placeholder {
  color: color-mix(in srgb, var(--sidebar-ink) 46%, transparent);
}

.followed-tag-add button {
  width: 22px;
  height: 22px;
  border-color: transparent;
  background: color-mix(in srgb, var(--sidebar-ink) 7%, transparent);
  color: color-mix(in srgb, var(--sidebar-ink) 48%, transparent);
  font-size: 12px;
}

.section-label {
  margin: 0 0 8px 10px;
  font-weight: 700;
  text-transform: uppercase;
}

.board-link {
  justify-content: space-between;
}

.admin-link {
  margin-top: auto;
  color: var(--sidebar-ink);
  font-weight: 700;
}

body.is-sidebar-collapsed {
  --sidebar-width: 78px;
}

.sidebar.is-collapsed {
  align-items: center;
  padding: 14px 9px;
}

.sidebar.is-collapsed .brand-block {
  justify-content: center;
  width: 100%;
  height: auto;
  margin: 0;
  padding: 0;
  background: transparent;
  border-bottom: 0;
}

/* 悉大 collapsed: the block goes transparent here, so the tile sits straight on
   the dark column and its white frame reads as a ring around the red rather
   than as part of the mark. Painted in the tile's own colour, not transparent —
   transparent would let the column show through those 2px and shrink the tile
   by 4px. */
[data-tenant="usyd"] .sidebar.is-collapsed .brand-mark {
  border-color: var(--brand-mark);
}

.sidebar.is-collapsed .brand-block div:not(.brand-mark),
.sidebar.is-collapsed .nav-item span:last-child,
/* .board-link span 以前只写在 980px 媒体查询里, 现在两条折叠路径共用这一套规则,
   所以并进来 —— 否则窄屏自动折叠时版块名会在 44px 宽的列里一字一行。 */
.sidebar.is-collapsed .board-link span,
.sidebar.is-collapsed .section-label,
.sidebar.is-collapsed .tag-follow-section,
.sidebar.is-collapsed .admin-link {
  display: none;
}

.sidebar.is-collapsed .nav-item,
.sidebar.is-collapsed .board-link {
  justify-content: center;
  width: 44px;
}

/* 版块名藏起来之后, 用一个小圆点代替它占位, 选中时点亮 —— 同样来自那段媒体查询。 */
.sidebar.is-collapsed .board-link::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--soft);
}

.sidebar.is-collapsed .board-link.is-active::before {
  background: var(--sidebar-brand);
}

.sidebar.is-collapsed .sidebar-toggle {
  position: absolute;
  top: 50%;
  right: -11px;
  transform: translateY(-50%);
  width: 22px;
  height: 34px;
  background: rgba(0, 0, 0, 0.18);
  color: color-mix(in srgb, var(--sidebar-ink) 68%, transparent);
  border-color: color-mix(in srgb, var(--sidebar-ink) 16%, transparent);
}

.sidebar.is-collapsed .sidebar-toggle:hover {
  background: color-mix(in srgb, var(--sidebar-ink) 12%, transparent);
  color: var(--sidebar-ink);
}

/* The sidebar and topbar used to repaint to #101725 on the course view, because
   that page was a dark immersive theme. It is a light content page now, so the
   chrome stays exactly what it is everywhere else — one less per-view colour
   switch standing between us and a site-wide theme (docs/THEME-COLOR-SWITCHES.md).
   The monash counter-rules that restored dark ink for this view went with it. */

.topbar {
  position: fixed;
  inset: 0 0 auto var(--sidebar-width);
  z-index: 20;
  height: var(--topbar-height);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  padding: 10px 28px;
  background: var(--topbar-bg);
  border-bottom: 1px solid var(--topbar-line);
  color: var(--topbar-ink);
  /* No backdrop-filter: the background is opaque so a blur is never visible,
     yet a fixed bar re-blurring everything scrolled under it costs a repaint
     every frame — a major source of scroll jank. */
  transition: inset 180ms ease;
}

/* 基线是流式: flex 撑开 + max-width 封顶 + 自动外边距将剩余空间平分到两侧。
   它在窄屏下会自己收缩, 永远不会压到右侧控件上; 代价是"居中"是相对
   剩余空间而言的 —— 右侧那块宽度一变它就横移。宽屏下改用三列网格
   把这个依赖断掉, 见文件末尾 @media (min-width: 1360px) 那块。 */
.search-shell {
  position: static;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 720px;
  margin-inline: auto;
  height: 42px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 15px;
  border: 1px solid transparent;
  border-radius: var(--radius-ui);
  /* quick task (260828-h3n, 第四轮): 曾是 rgba(255, 255, 255, 0.96)。那 4% 的透明度
     是为了让下面的品牌色顶栏透上来一点点, 但代价是这条声明成了**纯字面量**, 于是
     暗色下引擎独立推导出 rgba(24, 26, 27, 0.96) —— 一支和其它面板都不一样的灰,
     用户看到的"搜索框没有一同变色"就是这个。改读 token 之后它汇入
     var(--darkreader-bg--surface), 和全站面板同色。
     浅色下的位移: 96% 白 → 纯白, 顶栏那点透色没了。上面那条注释本来就把它描述成
     "a WHITE pill sitting on the dark bar", 所以纯白反而更贴合原意。 */
  background: var(--surface);
}

.search-shell-icon {
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  color: var(--muted);
}

.search-shell-icon svg {
  width: 18px;
  height: 18px;
}

.search-shell:focus-within {
  border-color: color-mix(in srgb, var(--brand) 28%, transparent);
  box-shadow: var(--focus);
}

.search-shell.is-faculty-search:focus-within {
  border-color: color-mix(in srgb, var(--topbar-search-theme, var(--brand)) 42%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--topbar-search-theme, var(--brand)) 24%, transparent);
}

.search-shell input {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  height: 40px;
  border: 0;
  outline: 0;
  background: transparent;
  /* The shell is a WHITE pill sitting on the dark bar, so the field paints its
     own ink. Inheriting --topbar-ink made every typed character (and the caret)
     white-on-white — invisible; only the placeholder showed, because that colour
     comes from the UA sheet, not from us. */
  color: var(--text);
  caret-color: var(--ink-strong);
  line-height: 40px;
}

.search-shell input::placeholder {
  color: var(--soft);
}

/* ::selection inherits the field's colour too — a white-on-brand selection was
   the same disappearing act while dragging over what you just typed. */
.search-shell input::selection {
  background: color-mix(in srgb, var(--brand) 26%, transparent);
  color: var(--ink-strong);
}

/* Same fix .ch-searchbar carries: tokens.css paints --focus on ANY focus-visible
   input, which drew a second ring inside the shell's own :focus-within ring —
   two boxes around one field. The shell is the focus indicator here. */
.search-shell input:focus,
.search-shell input:focus-visible {
  outline: none;
  box-shadow: none;
}

.topbar-subject-suggestions {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  z-index: 30;
  display: grid;
  gap: 4px;
  border: 1px solid #d8e0ec;
  border-radius: var(--radius-card);
  padding: 7px;
  background: var(--surface);
  box-shadow: 0 18px 45px color-mix(in srgb, var(--brand) 18%, transparent);
}

.topbar-subject-suggestions button {
  display: grid;
  grid-template-columns: 94px minmax(0, 1fr);
  gap: 3px 10px;
  border: 0;
  border-radius: var(--radius-ui);
  padding: 10px;
  background: transparent;
  color: var(--ink-strong);
  text-align: left;
}

.topbar-subject-suggestions button:hover,
.topbar-subject-suggestions button:focus-visible {
  /* 底色走令牌, 不写字面量 —— 同 .subject-review-summary 的理由: 含 var() 的
     声明 Dark Reader 整条跳过, 字面量那一半会在暗色下原样留成浅色。 */
  background: color-mix(in srgb, var(--topbar-search-theme, var(--brand)) 14%, var(--surface-2));
}

.topbar-subject-suggestions strong {
  grid-row: span 2;
  align-self: center;
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 950;
}

.topbar-subject-suggestions span {
  overflow: hidden;
  color: #111111;
  font-size: 13px;
  font-weight: 900;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.topbar-subject-suggestions em,
.topbar-subject-empty {
  overflow: hidden;
  color: #657186;
  font-size: 12px;
  font-style: normal;
  font-weight: 750;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.topbar-subject-empty {
  padding: 10px;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  /* 没有搜索框的页面(海报/课程评价首页/学院详情/游戏组队)靠这条把自己顶到最右。 */
  margin-left: auto;
}

/* Grid already pins the right actions to column 3, so this just neutralises the
   no-search auto margin on pages that render a search shell. */
.search-shell ~ .header-actions {
  margin-left: 0;
}

/* 尺寸不再写死 —— 这个占位现在就是那颗登录按钮本身(同类同文案, 见 main.ts 的
   authActions), 宽高由它自己的内容和内边距决定, 于是水合前后逐像素一致, 居中的搜索栏
   不会再横移。写死 110px 的老版本在 721~1599 那一档还会**凭空多出一个 110px 的盒子**
   (那一档真实按钮是 display:none), 搜索栏实测被挤开 59px。 */
.header-auth-skeleton {
  pointer-events: none;
  border-radius: var(--radius-ui);
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.12));
  background-size: 180% 100%;
  animation: skeleton-pulse 1.2s ease-in-out infinite;
}

/* 文案只用来撑出正确的宽度, 不该被读到 —— 已登录的用户在水合完成前不该看见一句
   "登录 / 注册"。用 visibility 藏子元素而不是给按钮 color: transparent: 后者
   (0,1,0) 压不过 .topbar .ghost-button 的 color: var(--topbar-ink) (0,2,0), 实测
   文字照样是白的; 这条选到的是这个 span, 顶栏那批规则够不着它。 */
.header-auth-skeleton > span {
  visibility: hidden;
}

/* Language switcher — a dropdown that scales to N languages (中 / EN / 日本語 /
   한국어) instead of a widening row of pills. Two variants: "bar" (web top bar,
   dark bg) and "settings" (native Me page, light bg). See renderLanguageSwitcher. */
.lang-switch {
  position: relative;
  display: inline-flex;
}

.lang-switch-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 8px 0 10px;
  /* The "bar" variant only ever renders inside .topbar (main.ts), so it follows
     the topbar's ink — otherwise a school with an accent topbar gets a white
     language pill on yellow. The "settings" variant overrides these below. */
  border: 1px solid color-mix(in srgb, var(--topbar-ink) 32%, transparent);
  border-radius: var(--radius-ui);
  background: transparent;
  color: color-mix(in srgb, var(--topbar-ink) 90%, transparent);
  font-size: 12px;
  font-weight: 800;
  cursor: pointer;
  white-space: nowrap;
}
.lang-switch-trigger:hover {
  border-color: color-mix(in srgb, var(--topbar-ink) 60%, transparent);
}
.lang-switch.is-open .lang-switch-trigger {
  border-color: var(--topbar-ink);
  background: color-mix(in srgb, var(--topbar-ink) 12%, transparent);
}
.lang-switch-globe {
  width: 15px;
  height: 15px;
  opacity: 0.85;
}
.lang-switch-current {
  line-height: 1;
}
.lang-switch-caret {
  width: 12px;
  height: 12px;
  opacity: 0.8;
  transition: transform 0.15s ease;
}
.lang-switch.is-open .lang-switch-caret {
  transform: rotate(180deg);
}

/* Theme switch — an icon-only sibling of .lang-switch-trigger just above,
   same recipe (topbar-ink-following border/colour via color-mix, same 32px
   height) but square, since there is one glyph and no label to make room for. */
.theme-switch-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid color-mix(in srgb, var(--topbar-ink) 32%, transparent);
  border-radius: var(--radius-ui);
  background: transparent;
  color: color-mix(in srgb, var(--topbar-ink) 90%, transparent);
  cursor: pointer;
}
.theme-switch-trigger:hover {
  border-color: color-mix(in srgb, var(--topbar-ink) 60%, transparent);
  background: color-mix(in srgb, var(--topbar-ink) 12%, transparent);
}
.theme-switch-trigger svg {
  width: 15px;
  height: 15px;
}

.lang-switch-backdrop {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: transparent;
}
.lang-switch-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 31;
  min-width: 172px;
  padding: 6px;
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
}
.lang-menu-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-height: 38px;
  padding: 0 12px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}
.lang-menu-option:hover {
  background: var(--surface-muted, rgba(0, 0, 0, 0.05));
}
.lang-menu-label {
  font-weight: 600;
}
.lang-menu-name {
  color: var(--muted);
  font-size: 12px;
}
/* Active row = a full blue highlight (replaces the old ✓), so every row shares
   one shape and the menu stays tidy with no gutter reserved for a checkmark. */
.lang-menu-option.is-active,
.lang-menu-option.is-active:hover {
  background: var(--brand);
  color: #ffffff;
}
.lang-menu-option.is-active .lang-menu-name {
  color: rgba(255, 255, 255, 0.72);
}

/* Settings variant — sits on the light "Me" page, so the trigger needs dark ink. */
.lang-switch--settings .lang-switch-trigger {
  border-color: var(--soft, rgba(0, 0, 0, 0.14));
  color: var(--text);
}
.lang-switch--settings .lang-switch-trigger:hover {
  border-color: var(--brand);
}
.lang-switch--settings.is-open .lang-switch-trigger {
  background: rgba(0, 0, 0, 0.04);
  /* Pinned so the open state never inherits the topbar ink from the shared
     .lang-switch.is-open rule — this variant sits on a light page. */
  border-color: var(--brand);
}
.lang-switch--settings .lang-switch-globe {
  opacity: 0.6;
}

.ghost-button,
.primary-button,
.icon-button,
.profile-chip,
.sort-button,
.tab-pill,
.card-actions button,
.detail-actions button,
.comment-actions button,
.admin-tabs button,
.admin-actions button,
.admin-row button {
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  color: var(--text);
}

.ghost-button,
.primary-button {
  min-height: 32px;
  padding: 0 12px;
  font-weight: 700;
}

.primary-button {
  background: var(--button-bg);
  color: var(--button-ink);
}

/* .secondary-button had NO rule at all — measured on the 转发 tab it was a raw UA
   button: border 2px outset, radius 0, 400/16px, 26px tall beside a 38px field.
   In dark mode the engine turns that default bevel into the harsh light frame the
   composer was reported for. Three places use the class (转发链接, the auth
   modal's 发送验证码, the bookmark modal's 解析), so all three were unstyled.

   Tonal, not outlined: this button sits inside a dashed row next to a bordered
   input, and a third outline there is exactly the stacked-hairline noise the
   composer already had too much of. Filling it instead removes an outline.

   Built on --surface-2/-3, so it needs no dark-mode pin of its own: those tokens
   are already redirected by darkReaderBoot's surfaceVarFix() and follow the
   school's own neutral ladder. */
.secondary-button {
  min-height: 34px;
  padding: 0 14px;
  border: 0;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.15s ease, opacity 0.15s ease;
}

.secondary-button:hover:not(:disabled) {
  background: var(--surface-3);
}

.secondary-button:disabled {
  opacity: 0.55;
  cursor: default;
}

.topbar .ghost-button,
.topbar .icon-button {
  color: var(--topbar-ink);
}

.topbar .icon-button {
  font-size: 14px;
}

.topbar .ghost-button {
  border: 1px solid color-mix(in srgb, var(--topbar-ink) 42%, transparent);
}

.topbar .ghost-button {
  height: 32px;
  font-size: 13px;
}

.topbar .ghost-button:hover,
.topbar .icon-button:hover {
  background: color-mix(in srgb, var(--topbar-ink) 13%, transparent);
}

/* 顶栏右侧那两枚(通知铃、账号胶囊)和左边两枚(主题、语言)读同一个描边配方。
   之前不一致有两个不同的原因:
   - .notification-button 写的是裸字面量 rgba(255, 255, 255, 0.34)。浅色下它和
     color-mix(var(--topbar-ink) 32%) 几乎等值(0.34 vs 0.32), 所以从来没人发现;
     但暗色下这是两条走不同通道的声明 —— 引擎对字面量自己重新推导, 对读了
     --topbar-ink 的那条则按该 token 的极点换算, 落点不同。左边两枚实测
     rgba(48, 52, 54, 0.32), 铃铛不是。
   - .profile-chip 压根没有 border, 只有一层 rgba(255,255,255,.14) 的底。
   统一成同一条声明, 于是两个主题下都是按构造一致, 不需要任何 pin —— 同一条
   声明经过同一个转换, 只能得到同一个结果。
   .topbar 前缀是必须的: .notification-button 在侧栏 (.sidebar-account) 也渲染
   一份, 那里的正确墨色是 --sidebar-ink 而不是 --topbar-ink。特异度 (0,2,0) 压过
   原来那条 (0,1,0), 侧栏那份保持不变。 */
.topbar .notification-button,
.topbar .profile-chip {
  border: 1px solid color-mix(in srgb, var(--topbar-ink) 32%, transparent);
  border-radius: var(--radius-ui);
}

/* 底色去掉(2026-08-29):那层 12% 的墨洗让胶囊在四枚顶栏控件里独自偏白 —— 主题、
   语言、铃铛都是"描边+透明底",只有它是个填充块,并排看像是被选中或被禁用了。
   原来留底的理由是"以示已登录",但那件事已经由头像和用户名本身说清楚了,不需要再
   加一层背景重复说一遍。必须显式写 transparent 而不是删掉整条:基类 .profile-chip
   仍带着 rgba(255,255,255,.14),删掉这条只会让那层更白的底浮上来。 */
/* 墨色也要跟着顶栏走，不能写死白色。基类 .profile-chip 的 color: white 是照着
   "顶栏是深色"写的，八所里七所确实如此（topbarInk 都是 #FFFFFF）；新南是唯一
   一个反过来的 —— 它的品牌黄 #FFDB00 扛得住大面积，所以顶栏就是黄的，topbarInk
   是 #111111。于是白色用户名落在黄底上，对比度 1.07:1，等于没有。
   同一条顶栏里，主题/语言/铃铛三枚早就读 --topbar-ink 了，只有这枚漏了。 */
.topbar .profile-chip {
  background: transparent;
  color: var(--topbar-ink);
}

.topbar .notification-button:hover,
.topbar .profile-chip:hover {
  border-color: color-mix(in srgb, var(--topbar-ink) 60%, transparent);
}

.icon-button {
  position: relative;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  /* iOS Safari keeps the native push-button appearance even when a background is
     set, and that native chrome ignores grid centering — the glyph (e.g. the "+"
     publish button) renders off-centre/crooked. Chromium (Android) drops native
     styling once display is set, so it only shows there. Reset it explicitly. */
  -webkit-appearance: none;
  appearance: none;
  padding: 0;
  line-height: 1;
  font-size: 19px;
}

.notification-button {
  width: 32px;
  min-width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.34);
}

.bell-icon {
  width: 17px;
  height: 17px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.badge {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 17px;
  height: 17px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--brand);
  color: white;
  font-size: 10px;
}

/* quick task (260828-lgn): 顶栏这份的用户名必须限宽 + 省略号。侧栏那份早就有
   (.sidebar-account .profile-chip > span:last-child), 顶栏这份没有, 于是用户名越长
   .header-actions 越宽, 把居中的搜索栏往里挤 —— 260827 把交接宽度从 1280px 一路抬到
   1600px, 治的就是这个症状而不是病根。限住之后账号簇宽度可预期, 断点才谈得上有依据。 */
.header-actions .profile-chip > span:last-child {
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.profile-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 4px;
  background: rgba(255, 255, 255, 0.14);
  color: white;
}

.avatar {
  overflow: hidden;
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  border: 0;
  border-radius: var(--radius-ui);
  background: linear-gradient(135deg, var(--brand-deep), var(--brand-bright));
  color: white;
  font-weight: 800;
  letter-spacing: 0;
}

.avatar img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  transform-origin: center;
}

.avatar-small {
  width: 26px;
  height: 26px;
  font-size: 11px;
}

.avatar-normal {
  width: 34px;
  height: 34px;
  font-size: 13px;
}

.avatar-large {
  width: 82px;
  height: 82px;
  font-size: 24px;
}

.avatar-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.avatar-button:hover {
  background: transparent;
}

.avatar-button:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--brand) 55%, transparent);
  outline-offset: 3px;
}

.thread-author-button {
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  font-weight: inherit;
  cursor: pointer;
}

.thread-author-button:hover {
  color: var(--ink-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.main-content {
  margin-left: var(--sidebar-width);
  padding: calc(var(--topbar-height) + 12px) 24px 48px;
  transition: margin-left 180ms ease;
}

.feed-top-row {
  position: sticky;
  top: var(--topbar-height);
  z-index: 12;
  display: grid;
  grid-template-columns: 260px minmax(720px, 860px) 260px;
  justify-content: center;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.feed-tool-rail {
  justify-self: end;
}

.board-tabs {
  min-width: 0;
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 6px;
  overflow-x: auto;
  width: 100%;
  margin: 0;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  /* Opaque, no blur: this bar is sticky, so a backdrop blur re-computed on every
     scroll frame is expensive for a barely-visible effect. */
  background: var(--surface);
  scrollbar-width: none;
}

.board-tabs::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* No visible scrollbar on any platform — the strip scrolls by swipe on touch and by
   wheel on desktop (see bindHorizontalWheel), so the bar was only taking up room
   under the tabs. ::-webkit-scrollbar above covers Chrome/Safari; this covers Firefox,
   which would otherwise draw its own. */
.board-tabs {
  scrollbar-width: none;
}

.tab-pill,
.sort-button {
  flex: 0 0 auto;
  min-height: 28px;
  padding: 0 11px;
  border-radius: var(--radius-ui);
  font-size: 13px;
}

/* App-only Weibo-style top bar (markup only rendered inside the native app):
   a centered "关注 ▾" filter trigger + a collapsed search icon, with a dropdown
   of followed tags. The website top bar is unaffected. */
.topbar-native .feed-source-trigger {
  grid-column: 2;
  justify-self: center;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin: 0 auto; /* flex 基线下靠它居中; 宽屏网格下由 justify-self 接手 */
  padding: 6px 8px;
  border: 0;
  background: transparent;
  color: var(--topbar-ink);
  font-size: 17px;
  font-weight: 700;
}
.topbar-native .feed-source-caret {
  font-size: 12px;
  opacity: 0.85;
  transition: transform 140ms var(--ease-out);
}
.topbar-native .feed-source-trigger.is-open .feed-source-caret {
  transform: rotate(180deg);
}
.topbar-native .topbar-search-icon,
.topbar-native .topbar-compose-icon {
  color: var(--topbar-ink);
}
.topbar-native .topbar-search-icon svg,
.topbar-native .topbar-compose-icon svg {
  width: 21px;
  height: 21px;
  display: block;
}
.tag-filter-backdrop {
  position: fixed;
  inset: 0;
  z-index: 30;
  background: transparent;
}
.tag-filter-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 31;
  min-width: 210px;
  max-width: min(84vw, 320px);
  max-height: 60vh;
  overflow-y: auto;
  padding: 6px;
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
}
.tag-filter-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-height: 42px;
  padding: 0 12px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font-size: 15px;
  text-align: left;
}
.tag-filter-item.is-active {
  color: var(--brand);
  font-weight: 600;
}
.tag-filter-check {
  color: var(--brand);
}
.tag-filter-empty {
  padding: 12px;
  color: var(--muted);
  font-size: 13px;
}

/* "Me" page language row (moved here out of the top bar). */
.profile-setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 12px;
}
.profile-setting-label {
  color: var(--muted);
  font-size: 14px;
}

.feed-intro {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 42px;
  height: 42px;
  margin: 0;
  color: var(--text);
  font-size: initial;
}

.eyebrow {
  margin: 0 0 6px;
  color: var(--brand);
  font-size: 12px;
  font-weight: 800;
  text-transform: uppercase;
}

.profile-hero h1 {
  margin: 0;
  font-size: clamp(26px, 3vw, 42px);
  line-height: 1.05;
  letter-spacing: 0;
}

.profile-hero p {
  max-width: 620px;
  margin: 8px 0 0;
  color: var(--muted);
}

.feed-layout {
  display: grid;
  grid-template-columns: 260px minmax(720px, 860px) 260px;
  justify-content: center;
  align-items: start;
  gap: 12px;
  margin-top: 0;
}

.feed-column {
  min-width: 0;
  grid-column: 2;
}

.feed-risk-spotlight {
  display: grid;
  grid-template-columns: minmax(0, 230px) minmax(0, 1fr);
  gap: 12px;
  margin-bottom: 12px;
  padding: 12px;
  min-height: 0;
  overflow: hidden;
  border: 1px solid #d9e0ec;
  border-top: 4px solid #ff8f95;
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--ink-strong);
}

.feed-risk-copy {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* 这条是整块面板里唯一说明"数字是什么、哪头是坏"的地方。没有它, 读者要自己从
   "榜是升序排的"反推出低分才是该躲的 —— 而通常的直觉恰恰相反(数字大 = 问题大)。 */
.feed-risk-lede {
  margin: 0;
  color: #5a6480;
  font-size: 11px;
  font-weight: 800;
  line-height: 1.45;
}

.feed-risk-faculties-label {
  color: #7c8799;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.04em;
}

.feed-risk-copy h2 {
  margin: 0;
  color: #07133d;
  font-size: 20px;
  line-height: 1.18;
  font-weight: 950;
}

.feed-risk-copy h2 span {
  margin-right: 6px;
}

.feed-risk-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* 吊底: 空白留在最后, 而不是夹在筛选器和按钮中间。 */
  margin-top: auto;
  padding-top: 4px;
}

.feed-risk-faculties {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 5px;
}

.feed-risk-faculties button {
  min-width: 0;
  min-height: 28px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--risk-fc, #29328f) 58%, #d8dfec);
  border-radius: var(--radius-ui);
  padding: 0 6px;
  background: var(--surface);
  color: #26324d;
  font-family: inherit;
  font-size: 10px;
  font-weight: 900;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}

.feed-risk-faculties button.is-active {
  border-color: var(--risk-fc, #29328f);
  box-shadow: inset 0 0 0 1px var(--risk-fc, #29328f);
}

.feed-risk-primary,
.feed-risk-secondary,
.feed-risk-row {
  font-family: inherit;
  cursor: pointer;
}

.feed-risk-primary,
.feed-risk-secondary {
  min-height: 34px;
  padding: 0 12px;
  border-radius: var(--radius-ui);
  font-size: 13px;
  font-weight: 900;
}

.feed-risk-primary {
  border: 1px solid #29328f;
  background: #29328f;
  color: #ffffff;
}

.feed-risk-secondary {
  border: 1px solid #ccd5e6;
  background: rgba(255, 255, 255, 0.76);
  color: #243055;
}

.feed-risk-board {
  min-width: 0;
  display: grid;
  gap: 8px;
  padding: 8px;
  border: 1px solid #d9e0ec;
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.feed-risk-board-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  color: #273057;
  font-size: 12px;
}

.feed-risk-board-meta {
  margin-left: auto;
}

.feed-risk-mobile-vote {
  display: none;
}

.feed-risk-board-head strong {
  font-size: 13px;
  font-weight: 950;
}

.feed-risk-board-head span {
  color: #6c7488;
  font-weight: 800;
}

.feed-risk-list {
  display: grid;
  gap: 4px;
}

.feed-risk-row {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 4px 6px;
  border: 1px solid #e2e7f0;
  border-radius: var(--radius-ui);
  background: #ffffff;
  text-align: left;
}

.feed-risk-row:hover,
.feed-risk-row:focus-visible {
  border-color: #b8c4d9;
  outline: 0;
}

.feed-risk-row .ch-score {
  min-width: 40px;
  min-height: 38px;
  padding: 2px 6px;
  border: 0;
  color: #111111;
}

.feed-risk-row .ch-score b {
  font-size: 17px;
}

.feed-risk-row .ch-score i {
  display: block;
  font-size: 8px;
  line-height: 1;
}

.feed-risk-rank {
  color: #7c8799;
  font-size: 13px;
  font-weight: 950;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.feed-risk-rank.is-top {
  color: #e05a47;
}

.feed-risk-subject {
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.feed-risk-subject b {
  flex: 0 0 auto;
  border: 1px solid var(--risk-fc, #ccd5e6);
  border-radius: 999px;
  padding: 2px 6px;
  color: #3e4a62;
  font-size: 9px;
  line-height: 1;
  letter-spacing: 0.04em;
}

.feed-risk-subject span {
  overflow: hidden;
  color: #18213f;
  font-size: 11px;
  font-weight: 850;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 独立一列, 而且 nowrap 没有 max-width: 条数是这张榜的可信度依据, 不能再被别的
   文本挤掉。学院用短名, 和左边筛选器同一套词, 本来就短。 */
.feed-risk-evidence {
  display: flex;
  align-items: baseline;
  gap: 6px;
  color: #778296;
  font-size: 10px;
  font-style: normal;
  font-weight: 800;
  white-space: nowrap;
}

.feed-risk-faculty-tag {
  color: #8a93a6;
}

.feed-risk-count {
  color: #3e4a62;
  font-variant-numeric: tabular-nums;
}

.feed-risk-skeleton {
  height: 28px;
  border-radius: var(--radius-ui);
  background: linear-gradient(90deg, #eef2f8, #ffffff, #eef2f8);
  animation: skeleton-pulse 1.2s ease-in-out infinite;
}

[data-theme="dark"] .feed-risk-spotlight,
[data-theme="black"] .feed-risk-spotlight {
  border-color: color-mix(in srgb, var(--text) 18%, transparent);
  background: var(--surface);
}

[data-theme="dark"] .feed-risk-copy h2,
[data-theme="black"] .feed-risk-copy h2 {
  color: var(--text);
}

[data-theme="dark"] .feed-risk-faculties button,
[data-theme="black"] .feed-risk-faculties button {
  border-color: color-mix(in srgb, var(--risk-fc, var(--text)) 54%, var(--line));
  background: color-mix(in srgb, var(--surface) 92%, var(--text) 8%);
  color: var(--text);
}

[data-theme="dark"] .feed-risk-board-head span,
[data-theme="black"] .feed-risk-board-head span {
  color: var(--muted);
}

[data-theme="dark"] .feed-risk-board,
[data-theme="black"] .feed-risk-board {
  border-color: color-mix(in srgb, var(--text) 16%, transparent);
  background: color-mix(in srgb, var(--surface) 94%, var(--text) 6%);
}

[data-theme="dark"] .feed-risk-row,
[data-theme="black"] .feed-risk-row {
  border-color: color-mix(in srgb, var(--text) 14%, transparent);
  background: color-mix(in srgb, var(--surface) 96%, var(--text) 4%);
}

[data-theme="dark"] .feed-risk-row:hover,
[data-theme="dark"] .feed-risk-row:focus-visible,
[data-theme="black"] .feed-risk-row:hover,
[data-theme="black"] .feed-risk-row:focus-visible {
  border-color: color-mix(in srgb, var(--text) 28%, transparent);
}

[data-theme="dark"] .feed-risk-lede,
[data-theme="black"] .feed-risk-lede,
[data-theme="dark"] .feed-risk-faculties-label,
[data-theme="black"] .feed-risk-faculties-label,
[data-theme="dark"] .feed-risk-faculty-tag,
[data-theme="black"] .feed-risk-faculty-tag {
  color: var(--muted);
}

[data-theme="dark"] .feed-risk-count,
[data-theme="black"] .feed-risk-count {
  color: var(--text);
}

[data-theme="dark"] .feed-risk-subject b,
[data-theme="black"] .feed-risk-subject b,
[data-theme="dark"] .feed-risk-evidence,
[data-theme="black"] .feed-risk-evidence {
  color: var(--muted);
}

[data-theme="dark"] .feed-risk-subject b,
[data-theme="black"] .feed-risk-subject b {
  border-color: color-mix(in srgb, var(--risk-fc, var(--text)) 70%, var(--line));
}

[data-theme="dark"] .feed-risk-subject span,
[data-theme="dark"] .feed-risk-board-head strong,
[data-theme="black"] .feed-risk-subject span,
[data-theme="black"] .feed-risk-board-head strong {
  color: var(--text);
}

[data-theme="dark"] .feed-risk-skeleton,
[data-theme="black"] .feed-risk-skeleton {
  background: linear-gradient(90deg, var(--surface-3), var(--surface-2), var(--surface-3));
}

@media (max-width: 720px) {
  .feed-risk-spotlight {
    grid-template-columns: 1fr;
    padding: 13px;
  }

  .feed-risk-copy h2 {
    font-size: 19px;
  }

  .feed-risk-actions {
    display: none;
  }

  .feed-risk-faculties {
    order: 3;
    align-self: start;
  }

  /* 窄屏下四列挤不下。舍学院短名而不是条数: 学院就在上面那排筛选器里,
     而"几条评价撑着"是这张榜可不可信的唯一依据。 */
  .feed-risk-faculty-tag {
    display: none;
  }

  .feed-risk-board-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .feed-risk-board-meta {
    grid-column: 1;
    grid-row: 2;
    margin-left: 0;
  }

  .feed-risk-mobile-vote {
    grid-column: 2;
    grid-row: 1 / span 2;
    display: inline-flex;
    align-items: center;
    min-height: 30px;
    padding: 0 10px;
    white-space: nowrap;
  }

  .feed-risk-board {
    padding: 8px;
  }
}

/* Left rail suppressed (both its widgets are behind hidden-flags — see
   renderFeed's hotRailContent). Without this the rail's column stays reserved
   and the feed sits visibly off-centre. .feed-top-row carries its OWN copy of
   the same multi-column template, so both grids have to move together or the
   board-tab strip stays centred on the old geometry while the feed below it
   shifts — measured 136px apart when only .feed-layout was collapsed.
   The tool rail is the complication: unlike the hot rail it still has content
   (the sort control), and it lived in the very gutter being removed. It is
   stacked into the same grid cell as the tabs and pulled back out leftwards
   with a transform — transforms do not affect layout, so the tabs keep the
   full column width and stay aligned with the feed, while the control still
   renders just left of them.

   MUST stay inside a min-width query. These selectors are (0,2,0) and the
   narrow-viewport layouts below (1280 / 1280-short / 980 / phone) redefine
   .feed-layout and .feed-top-row at (0,1,0), so an unguarded version of this
   block outranks every one of them and forces the wide two-column template
   onto small screens — which is exactly what happened: a 720px minimum column
   plus a 260px sidebar on an ~860px viewport left a large dead band down the
   right-hand side. Those breakpoints already collapse to a single column and
   need no rail handling at all, so the fix is to not reach them, rather than
   to add an override inside each. */
@media (min-width: 1681px) {
  .feed-layout.is-rail-empty,
  .feed-top-row.is-rail-empty {
    grid-template-columns: minmax(720px, 860px) 260px;
  }

  .feed-top-row.is-rail-empty .feed-tool-rail {
    /* This grid's own gap. */
    transform: translateX(calc(-100% - 12px));
  }
}

/* Same treatment for the band between the two wide breakpoints. Separate block
   rather than one shared rule because this range deliberately drops the column
   floor to 0 and widens the gap to 14px (see the max-width: 1680px block). */
@media (min-width: 1281px) and (max-width: 1680px) {
  .feed-layout.is-rail-empty,
  .feed-top-row.is-rail-empty {
    grid-template-columns: minmax(0, 860px) 260px;
    gap: 14px;
  }

  .feed-top-row.is-rail-empty .feed-tool-rail {
    transform: translateX(calc(-100% - 14px));
  }
}

/* Child placement is shared by both ranges above and is inert at narrower
   widths: the single-column layouts there put every child in column 1 anyway,
   and .feed-sidebar is display:none, so naming column 2 has no effect. */
@media (min-width: 1281px) {
  .feed-top-row.is-rail-empty .feed-tool-rail,
  .feed-top-row.is-rail-empty .board-tabs {
    grid-row: 1;
    grid-column: 1;
  }

  .feed-top-row.is-rail-empty .feed-tool-rail {
    justify-self: start;
  }

  .feed-top-row.is-rail-empty .feed-create-rail {
    grid-row: 1;
    grid-column: 2;
  }

  .feed-layout.is-rail-empty .feed-column {
    grid-column: 1;
  }

  .feed-layout.is-rail-empty .feed-sidebar {
    grid-column: 2;
  }
}

.sort-menu {
  position: relative;
}

.sort-menu summary {
  position: relative;
  width: 42px;
  min-width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--brand);
  font-size: 0;
  line-height: 0;
  padding: 0;
  cursor: pointer;
  list-style: none;
}

.sort-menu summary::-webkit-details-marker {
  display: none;
}

.sort-menu summary::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 14px;
  height: 14px;
  transform: translate(-50%, -50%);
  /* currentColor, 不是 var(--brand)。两者在浅色下等价 —— summary 自己就是
     `color: var(--brand)` —— 但暗色下差别是这个图标可见与否:
     --brand 是租户品牌色(墨大 #00092a 深藏青), tokens.css 的 [data-theme="dark"]
     块刻意不覆盖它(见那个块结尾的 MAINTENANCE 注释: brand 系归 Dark Reader 管),
     而这条声明是"背景图里套 var()", 引擎整条跳过 —— 于是四个方块在深色底上仍是
     深藏青, 基本看不见。summary 自身的 color 走的是普通 color 通道, 引擎会把它
     转成浅灰(实测 rgb(232,232,232)), 所以让方块跟着 currentColor 走就自动对了。 */
  background:
    linear-gradient(currentColor 0 0) left top / 5px 5px no-repeat,
    linear-gradient(currentColor 0 0) right top / 5px 5px no-repeat,
    linear-gradient(currentColor 0 0) left bottom / 5px 5px no-repeat,
    linear-gradient(currentColor 0 0) right bottom / 5px 5px no-repeat;
}

.sort-popover {
  position: absolute;
  top: 48px;
  left: 0;
  z-index: 15;
  width: 118px;
  display: grid;
  gap: 4px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.sort-popover .sort-button {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
}

.sort-menu:not([open]) .sort-popover {
  display: none;
}

.result-count {
  display: none;
}

.masonry-feed {
  max-width: 860px;
  margin: 0 auto;
  column-count: initial;
  column-gap: 0;
  border: 1px solid var(--line-card);
  border-radius: var(--radius-ui);
  background: var(--surface);
  overflow: hidden;
}

.feed-sidebar {
  position: sticky;
  grid-column: 3;
  top: calc(var(--topbar-height) + 58px);
  display: grid;
  gap: 14px;
  /* Cap the sticky sidebar to the viewport and let it scroll internally. Without
     this, opening the "add date" form grows the sidebar past the viewport and, being
     sticky-pinned, its lower content (校园活动 + the save button) becomes unreachable. */
  max-height: calc(100vh - var(--topbar-height) - 74px); /* fallback */
  max-height: calc(100dvh - var(--topbar-height) - 74px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Keep it scrollable (so opening the add-date form doesn't hide content) but
     hide the scrollbar itself. */
  scrollbar-width: none;
}

.feed-sidebar::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

.calendar-sidebar-block {
  display: grid;
  gap: 6px;
}

/* Publish button mirrors the four-squares filter button on the opposite side
   of the tab row: same box treatment/height, hugging the tabs from the right
   (justify-self: start) just as .feed-tool-rail hugs them from the left. */
.feed-create-rail {
  justify-self: start;
}

/* Solid brand fill like the course modal's "+ 评价课程" — the outlined version read
   as secondary next to the board pills, which is backwards for the feed's primary
   action. */
.feed-create-button {
  height: 42px;
  min-height: 42px;
  display: inline-grid;
  place-items: center;
  border: 1px solid var(--button-bg);
  border-radius: var(--radius-ui);
  background: var(--button-bg);
  color: var(--button-ink);
  padding: 0 16px;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0.01em;
  line-height: 1.2;
  cursor: pointer;
}

.feed-create-button:hover,
.feed-create-button:focus-visible {
  background: var(--button-bg-hover);
  border-color: var(--button-bg-hover);
}

.feed-hot-rail {
  position: sticky;
  grid-column: 1;
  top: calc(var(--topbar-height) + 58px);
  width: 100%;
  min-width: 0;
}

.side-widget {
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  padding: 14px 14px 16px;
}

.hot-posts-widget h2,
.events-widget h2 {
  margin: 0 0 12px;
  color: var(--ink-strong);
  font-size: 15px;
  font-weight: 800;
}

.calendar-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}

.calendar-title-row h2 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 15px;
  font-weight: 800;
}

.calendar-title-row a {
  color: var(--brand);
  font-size: 11px;
  font-weight: 850;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.feed-hot-rail .hot-posts-widget {
  display: grid;
  gap: 10px;
  margin-top: 0;
  padding: 14px 14px 16px;
  background: var(--surface);
}

.widget-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.widget-title-row h2 {
  margin: 0;
}

.widget-title-row button {
  border: 0;
  border-radius: var(--radius-ui);
  background: #f0f3f8;
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 800;
}

.hot-post-list {
  display: grid;
  gap: 0;
}

.hot-post-list.is-loading {
  gap: 10px;
  padding-top: 10px;
}

.hot-post-skeleton {
  display: block;
  height: 58px;
  border-radius: var(--radius-ui);
  background: linear-gradient(90deg, var(--surface-muted), var(--surface-2), var(--surface-muted));
  background-size: 180% 100%;
  animation: skeleton-pulse 1.2s ease-in-out infinite;
}

.hot-post-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 54px;
  gap: 10px;
  width: 100%;
  padding: 10px 0;
  border: 0;
  border-top: 1px solid #e8ebf1;
  background: transparent;
  color: var(--ink-strong);
  text-align: left;
}

.hot-post-copy {
  min-width: 0;
  display: grid;
  gap: 3px;
}

.hot-post-copy small,
.hot-post-copy em {
  overflow: hidden;
  color: #737c8d;
  font-size: 12px;
  font-style: normal;
  line-height: 1.25;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.hot-post-copy strong {
  display: -webkit-box;
  overflow: hidden;
  color: var(--ink-strong);
  font-size: 13px;
  line-height: 1.35;
  font-weight: 800;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.hot-post-thumb {
  width: 54px;
  height: 54px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border: 1px solid #dce2ed;
  border-radius: var(--radius-ui);
  background: var(--surface-muted);
}

.hot-post-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hot-post-initial {
  color: var(--brand);
  font-size: 12px;
  font-weight: 900;
}

/* Text-only hot post (normal post, no photo): drop the empty thumb column. */
.hot-post-item.is-textonly {
  grid-template-columns: minmax(0, 1fr);
}

.hot-post-excerpt {
  display: -webkit-box;
  overflow: hidden;
  color: #526079;
  font-size: 12px;
  font-weight: 650;
  line-height: 1.35;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

/* Course-review promo card below the hot-posts widget. */
.course-review-promo {
  position: relative;
  display: grid;
  gap: 8px;
  width: 100%;
  margin-top: 12px;
  padding: 14px;
  overflow: hidden;
  text-align: left;
  cursor: pointer;
  border: 1px solid #d9e0ec;
  background:
    linear-gradient(135deg, rgba(240, 244, 250, 0.92), rgba(255, 255, 255, 0.98)),
    #ffffff;
  color: var(--ink-strong);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.course-review-promo:hover,
.course-review-promo:focus-visible {
  transform: translateY(-2px);
  border-color: #9fb1d0;
  box-shadow: 0 12px 26px color-mix(in srgb, var(--ink-strong-bg) 12%, transparent);
}

.course-review-promo .promo-badge {
  position: relative;
  justify-self: start;
  padding: 3px 7px;
  border-radius: var(--radius-ui);
  background: #eef3fb;
  color: #30405f;
  font-size: 11px;
  font-weight: 900;
}

.course-review-promo strong {
  position: relative;
  color: var(--ink-strong);
  font-size: 16px;
  line-height: 1.25;
  font-weight: 900;
}

.course-review-promo p {
  position: relative;
  margin: 0;
  color: #59677d;
  font-size: 12px;
  line-height: 1.55;
}

.course-review-promo .promo-cta {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-radius: var(--radius-ui);
  border: 1px solid #cfd9ea;
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 850;
}

.course-review-promo .promo-cta::after {
  content: "→";
}

.course-review-promo:hover .promo-cta,
.course-review-promo:focus-visible .promo-cta {
  border-color: #a9b9d6;
  background: #eef4ff;
}

.events-widget {
  display: grid;
  gap: 8px;
}

.events-stack-widget {
  display: grid;
  gap: 0;
  overflow: hidden;
  padding: 0;
}

.events-stack-list {
  display: grid;
  gap: 0;
}

.events-stack-list .events-widget {
  position: relative;
  padding: 10px 12px 10px;
  border-top: 1px solid #e9edf4;
}

.events-stack-list .events-widget h2 {
  margin: 0;
}

.events-stack-list .events-widget::after {
  content: "";
  position: absolute;
  right: 12px;
  bottom: 0;
  left: 12px;
  height: 28px;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--surface) 82%);
}

.events-day-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 2px;
  border-top: 1px solid #eef1f6;
  border-bottom: 1px solid #eef1f6;
}

.events-day-nav-shared {
  padding: 8px 10px;
  border: 0;
  border-bottom: 1px solid #e9edf4;
  border-radius: 0;
  background: var(--surface);
}

.events-day-label {
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 800;
}

.events-day-arrow {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 15px;
  cursor: pointer;
}

.events-day-arrow:hover:not(:disabled) {
  background: #f0f4fb;
  border-color: var(--ink-strong-deep-bg);
}

.events-day-arrow:disabled {
  opacity: 0.35;
  cursor: default;
}

.events-day-list {
  display: grid;
  gap: 6px;
  max-height: 279px;
  overflow-y: auto;
  padding: 0 0 2px;
  scrollbar-width: none;
}

.events-day-list::-webkit-scrollbar {
  display: none;
}

.event-link {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  min-height: 72px;
  padding: 4px 3px;
  border-radius: var(--radius-ui);
  color: var(--ink-strong);
  text-decoration: none;
}

.event-link:hover,
.event-link:focus-visible {
  background: #f6f9fd;
}

.event-tag {
  justify-self: start;
  max-width: 100%;
  width: max-content;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 2px 5px;
  border: 1px solid #dce5f1;
  border-radius: 5px;
  background: var(--canvas);
  color: #64728a;
  font-size: 9px;
  font-weight: 900;
  letter-spacing: 0;
}

.event-thumb {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-ui);
  border: 1px solid #d5dbe8;
  background-color: var(--surface-muted);
  background-size: cover;
  background-position: center;
}

.event-thumb-stone {
  background-image: linear-gradient(120deg, #b8d6f2 0 40%, #f6f1e8 40% 62%, #7d8eb4 62% 100%);
}

/* SEO 静态正文({{SEO_BODY}}, 见 index.html)是塞进 #app 里面的, 在 main.js 把它
   innerHTML 掉之前会被完整绘制一帧 —— 而它在这份样式表里一条规则都没有, 于是
   进站先闪一屏没排版的纯文字。has-js 由 index.html <head> 里的同步脚本在首屏
   绘制前打上, 所以这条从第一帧就生效; 不跑 JS 的客户端拿不到那个类, 正文照常
   可读。 */
.has-js .seo-shell {
  display: none;
}

.event-copy {
  min-width: 0;
  display: grid;
  align-content: center;
  gap: 3px;
}

.event-copy strong {
  display: -webkit-box;
  overflow: hidden;
  font-size: 12px;
  /* 900 -> 400。这一块里 body / .event-link / .event-copy small 全都是 400, 900 是
     整条右栏上唯一一处特粗, 两列活动标题读起来像在喊。层级不靠字重也够: 标题 12px
     的正文墨色, 时间 11px 的 --muted。 */
  font-weight: 400;
  line-height: 1.24;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.event-copy small {
  color: var(--muted);
  font-size: 11px;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.event-link:hover .event-copy strong,
.event-link:focus-visible .event-copy strong {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.events-empty {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
}

.calendar-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}

.calendar-head strong {
  color: var(--brand);
  font-size: 14px;
  /* 没写过字重, 吃的是 <strong> 的 UA 默认 700。 */
  font-weight: 400;
}

.calendar-head span {
  color: var(--muted);
  font-size: 12px;
}

.calendar-widget {
  padding-bottom: 10px;
}

.calendar-month-nav {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-content: space-between;
  margin-bottom: 12px;
}

.calendar-month-nav strong {
  min-width: 0;
  flex: 1;
  padding: 5px 10px;
  border-radius: var(--radius-ui);
  /* quick task (260828-h3n, 第四轮): 曾是写死的 #f3f5f9(墨大浅蓝灰), 和
     .calendar-key-dates 那两支是同一类漏网字面量 —— 绕开了 neutralScale 的色相旋转,
     也绕开了引擎的 --darkreader-bg--surface-muted。 */
  background: var(--surface-muted);
  color: var(--brand);
  text-align: center;
  font-size: 11px;
  font-weight: 400;
}

.calendar-month-nav button {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--brand);
  font-size: 17px;
  font-weight: 900;
  line-height: 1;
}

.calendar-month-nav button:hover,
.calendar-month-nav button:focus-visible {
  border-color: var(--brand);
  background: var(--brand-weak);
}

.calendar-week,
.calendar-row {
  display: grid;
  grid-template-columns: 16px repeat(7, 1fr);
  gap: 2px;
  text-align: center;
}

.calendar-week {
  padding: 10px 0 6px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 11px;
  font-weight: 400;
}

.calendar-grid {
  display: grid;
  gap: 2px;
  margin-bottom: 4px;
}

.calendar-row {
  align-items: center;
  position: relative;
}

.calendar-day {
  min-height: 27px;
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  align-content: center;
  border-radius: var(--radius-ui);
  color: #4b5563;
  font-size: 12px;
  font-weight: 400;
}

.calendar-day b {
  /* <b> 的 UA 默认是 font-weight: bolder —— **相对**父级再顶一档(父 700 时它是
     900, 父 400 时是 700)。所以只把 .calendar-day 降到 400 不够, 日期数字还会是
     700; 这里钉死。 */
  font-weight: 400;
  position: relative;
  z-index: 3;
  line-height: 1;
}

.calendar-week-label {
  min-height: 25px;
  align-self: center;
  justify-self: end;
  padding-right: 1px;
  color: #6f8794;
  font-size: 8px;
  font-weight: 400;
  line-height: 1.05;
  text-align: right;
  white-space: nowrap;
  writing-mode: vertical-rl;
}

.calendar-grid .is-muted {
  color: #a7adb7;
}

/* 日历常态"黑白、悬停显色"这件事没有留在这里 —— 先后试过 opacity 和
   filter: saturate() 两条路都不够好(前者把中性的文字和 hairline 一起洗淡、整块发虚;
   后者按亮度映射, #ff0961 的亮度只有 68/255, 压到 rgb(68,68,68) 之后 deadline 那行
   对比度只剩 1.9:1)。最终做法是在 darkReaderBoot.js 的 calendarToneFix() 里发射
   两套规则: 常态钉中性墨色, 再由 .calendar-widget:hover / :focus-within 前缀的一套
   盖回各自的 tone。见那里的注释。

   过渡留在这里。那两套规则是通过 fixes.css 以 !important 钉的, 只负责"终点是什么
   颜色", 不带任何时间信息 —— 于是灰↔彩是一帧切换, 很硬。transition 不是颜色属性,
   引擎不碰它, 所以写在普通样式表里就行, 也不需要为每支 tone 各写一遍。
   必须挂在这些子元素自己身上而不是组件上: 变的是它们各自的 color / border-color /
   background-color, 而 transition 只对声明它的那个元素生效。
   只列这三个属性、不写 all: 组件里还有 transform 和 opacity 在动(单行 hover 的淡洗、
   月份切换), 用 all 会把它们也拖进这条 260ms 的曲线里。
   减少动效的偏好由 app.css 顶部那条全局 @media (prefers-reduced-motion) 统一接管。 */
[data-theme="dark"] .calendar-key-date-row,
[data-theme="dark"] .calendar-key-date-row .calendar-key-date-main,
[data-theme="dark"] .calendar-key-date-row .calendar-date-badge,
[data-theme="dark"] .calendar-key-date-row .calendar-date-badge small,
[data-theme="dark"] .calendar-day.has-event,
[data-theme="dark"] .calendar-day.has-event b,
[data-theme="dark"] .calendar-range-bar {
  transition:
    color 260ms var(--ease-out, ease),
    border-color 260ms var(--ease-out, ease),
    background-color 260ms var(--ease-out, ease);
}

/* 不属于当前月份的日期。浅色下靠 #a7adb7 这个浅灰就够, 深色下灰阶被压缩, 同样的
   "灰"分不出前后景, 所以这里用透明度 —— 这一处要的确实是"退到背景里", 与上面那条
   刻意用 saturate 而非 opacity 是两回事, 别把两者合并。
   两者不冲突也不相乘: 上面动的是彩度, 这里动的是透明度。 */
[data-theme="dark"] .calendar-grid .is-muted {
  opacity: 0.45;
}

.calendar-grid .is-today {
  background: var(--brand);
  color: #ffffff;
}

.calendar-grid .has-event.is-deadline:not(.is-today) {
  background: #fff4f4;
}

.calendar-grid .has-event.is-break:not(.is-today) {
  background: #fff8e9;
}

.calendar-grid .has-event.is-exam:not(.is-today) {
  background: #f4f5ff;
}

.calendar-grid .has-event.is-result:not(.is-today) {
  background: #eef3ff;
}

.calendar-grid .has-event.is-orientation:not(.is-today) {
  background: #eaf7fa;
}

.calendar-grid .has-event.is-term:not(.is-today) {
  background: #eef9f2;
}

.calendar-grid .has-event.is-timetable-plan:not(.is-today) {
  background: #eef6f8;
}

.calendar-grid .has-event.is-timetable-preference:not(.is-today) {
  background: #f4f8ef;
}

.calendar-grid .has-event.is-timetable-creation:not(.is-today) {
  background: #fff8e8;
}

.calendar-grid .has-event.is-timetable-review:not(.is-today) {
  background: #f5f3ff;
}

.calendar-grid .has-event.is-custom:not(.is-today) {
  background: #f1f6ff;
}

.calendar-grid .has-event.tone-deadline:not(.is-today) {
  background: #ffe8e8;
}

.calendar-grid .has-event.tone-census:not(.is-today) {
  background: #efe9ff;
}

.calendar-grid .has-event.tone-results:not(.is-today) {
  background: #edf1ff;
}

.calendar-grid .has-event.tone-orientation:not(.is-today) {
  background: #e7f8f8;
}

.calendar-grid .has-event.tone-break:not(.is-today) {
  background: #fff0df;
}

.calendar-grid .has-event.tone-exam:not(.is-today) {
  background: #f3ebff;
}

.calendar-grid .has-event.tone-custom:not(.is-today) {
  background: #eef4ff;
}

.calendar-row-bars {
  position: absolute;
  left: 18px;
  right: 2px;
  bottom: 1px;
  z-index: 3;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  grid-auto-rows: 3px;
  gap: 0;
  pointer-events: none;
}

.calendar-range-bar {
  min-width: 0;
  border-radius: 0;
  pointer-events: auto;
}

.calendar-range-bar:first-child {
  border-radius: 999px 0 0 999px;
}

.calendar-range-bar:last-child {
  border-radius: 0 999px 999px 0;
}

.calendar-range-bar.is-deadline {
  background: #f0b9b9;
}

.calendar-range-bar.is-break {
  background: #ead49c;
}

.calendar-range-bar.is-exam {
  background: #c9cef4;
}

.calendar-range-bar.is-result {
  background: #c9d4f1;
}

.calendar-range-bar.is-orientation {
  background: #b9dbe2;
}

.calendar-range-bar.is-term {
  background: #bfdec9;
}

.calendar-range-bar.is-timetable-plan {
  background: #b9d4dc;
}

.calendar-range-bar.is-timetable-preference {
  background: #cfe0b9;
}

.calendar-range-bar.is-timetable-creation {
  background: #ead49c;
}

.calendar-range-bar.is-timetable-review {
  background: #cdc7ec;
}

.calendar-range-bar.is-custom {
  background: #b9cbe8;
}

.calendar-range-bar.tone-deadline {
  background: #e76f6f;
}

.calendar-range-bar.tone-census {
  background: #9277e8;
}

.calendar-range-bar.tone-results {
  background: #7185d6;
}

.calendar-range-bar.tone-career {
  background: #d6e2d8;
}

.calendar-range-bar.tone-plan {
  background: #56acd0;
}

.calendar-range-bar.tone-preference {
  background: #caa934;
}

.calendar-range-bar.tone-create {
  background: #dc8740;
}

.calendar-range-bar.tone-orientation {
  background: #44b8b5;
}

.calendar-range-bar.tone-break {
  background: #d08b46;
}

.calendar-range-bar.tone-exam {
  background: #9f82dd;
}

.calendar-range-bar.tone-custom {
  background: #5f8fd8;
}

.calendar-custom-form button {
  border: 1px solid #dce2ed;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--brand);
  font-size: 11px;
  font-weight: 900;
}

.calendar-custom-form {
  display: grid;
  gap: 7px;
  margin-top: 8px;
  padding: 9px;
  border: 1px solid #dfe6f1;
  border-radius: var(--radius-ui);
  background: #fbfcff;
}

.calendar-custom-form label {
  display: grid;
  gap: 4px;
  margin: 0;
  color: #536176;
  font-size: 10px;
  font-weight: 850;
}

.calendar-custom-form input,
.calendar-custom-form select {
  min-width: 0;
  height: 29px;
  padding: 0 8px;
  border: 1px solid #d9e0eb;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--brand);
  font-size: 11px;
  font-weight: 750;
}

.calendar-custom-date-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7px;
}

.calendar-custom-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.calendar-custom-head span {
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 850;
}

.calendar-custom-close {
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius-ui);
  background: color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
  color: #536176;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.calendar-custom-close:hover {
  background: color-mix(in srgb, var(--ink-strong-bg) 12%, transparent);
  color: var(--ink-strong);
}

.calendar-color-field {
  display: grid;
  gap: 5px;
  color: #536176;
  font-size: 10px;
  font-weight: 850;
}

.calendar-color-row {
  display: flex;
  gap: 9px;
  padding-top: 1px;
}

.calendar-color-swatch {
  position: relative;
  width: 22px;
  height: 22px;
  cursor: pointer;
}

.calendar-color-swatch input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.calendar-color-swatch span {
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--swatch);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink-strong-bg) 10%, transparent);
}

.calendar-color-swatch input:checked + span,
.calendar-color-swatch input:focus-visible + span {
  box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px var(--swatch);
}

/* User-picked colours for custom calendar dates (row tint + date-badge tint). */
.calendar-key-date-row.cal-c-blue { border-color: #cdd9f4; background: #eef3ff; }
.cal-c-blue .calendar-date-badge { background: #dfe8ff; }
.calendar-key-date-row.cal-c-green { border-color: #cde9d8; background: #eefaf2; }
.cal-c-green .calendar-date-badge { background: #e0f5e8; }
.calendar-key-date-row.cal-c-amber { border-color: #ecdcb4; background: #fdf7ea; }
.cal-c-amber .calendar-date-badge { background: #fbeecb; }
.calendar-key-date-row.cal-c-orange { border-color: #f2d3bf; background: #fdf2ec; }
.cal-c-orange .calendar-date-badge { background: #fbe2d3; }
.calendar-key-date-row.cal-c-red { border-color: #eecdd1; background: #fdf1f2; }
.cal-c-red .calendar-date-badge { background: #fbdfe2; }
.calendar-key-date-row.cal-c-purple { border-color: #ddd4f2; background: #f5f2fd; }
.cal-c-purple .calendar-date-badge { background: #e9e2fb; }

.calendar-custom-form button {
  min-height: 30px;
  margin-top: 1px;
}


/* 关键日期收起态。BASE(这一段之外)保持原样 —— 列表常开、摘要不出现 —— 所以在没有
   悬停的设备上什么都没变。收起只在 @media (hover: hover) 里生效: 触屏上没有"放上去"
   这个状态, 收起就等于把这几个日期永远藏起来。
   :focus-within 一起写, 否则键盘走进去时列表是合着的。 */
.calendar-key-dates-summary {
  display: none;
}

@media (hover: hover) {
  /* 分隔线挪到 shell 上: 列表收起时 border-width 归 0, 线得由一个不参与折叠的
     元素来画。暗色下这条线的 pin 也跟着挪到了 shell(见 darkReaderBoot.js)。 */
  .calendar-key-dates-shell {
    margin-top: 5px;
    padding-top: 5px;
    border-top: 1px solid color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
  }

  .calendar-key-dates-summary {
    display: block;
    overflow: hidden;
    max-height: 22px;
    color: var(--muted);
    font-size: 12px;
    font-weight: 400;
    opacity: 1;
    transition: max-height 0.22s ease, opacity 0.16s ease;
  }

  .calendar-key-dates-shell .calendar-key-dates {
    /* 折叠用 max-height 而不是 display/height:auto —— 后者没法过渡。600px 够放
       ~13 行, 比任何一个月的关键日期都多。 */
    max-height: 0;
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-height 0.24s ease, opacity 0.18s ease;
  }

  .calendar-widget:hover .calendar-key-dates-summary,
  .calendar-widget:focus-within .calendar-key-dates-summary {
    max-height: 0;
    opacity: 0;
  }

  .calendar-widget:hover .calendar-key-dates-shell .calendar-key-dates,
  .calendar-widget:focus-within .calendar-key-dates-shell .calendar-key-dates {
    max-height: 600px;
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .calendar-key-dates-summary,
  .calendar-key-dates-shell .calendar-key-dates {
    transition: none;
  }
}

.calendar-key-dates {
  display: grid;
  gap: 6px;
  margin-top: 5px;
  padding: 5px 0 0;
  border-top: 1px solid color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
}

.calendar-key-dates a,
.calendar-key-date-row,
.calendar-key-dates > span {
  display: grid;
  gap: 7px;
  align-items: center;
  min-height: 30px;
  padding: 2px 6px;
  /* quick task (260828-h3n): 这两支曾是写死的 #e1e6ef / #fafbfe —— 墨大中性阶里那
     两支带蓝的浅色, 是这条规则写下时直接抄的字面量。build.mjs 的 neutralScale 会把
     整个中性阶按各校 brand 的色相旋转(所以 unsw 落在真中性灰、anu 落在暖褐), 但字
     面量绕开了它: 八所学校的关键日期条都还是墨大的蓝, 暗色下引擎把它算成
     rgb(40, 53, 76) —— 一条在黄色学校页面里格外扎眼的蓝边。改读 token 就自动跟上
     每校自己的色相, 不需要为任何一所写例外。
     浅色模式下墨大的位移: 边框 #e1e6ef → --line #e4e8ef(差 3/255, 看不出);
     底色 #fafbfe → --surface-muted #f0f2f6, 比原来灰一档 —— 这个角色本来就是
     "嵌在卡片里的浅底", 与同页其他 --surface-muted 面板对齐反而更整齐。 */
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface-muted);
  color: var(--ink-strong);
  font-size: 11px;
  font-weight: 850;
  line-height: 1.18;
  text-decoration: none;
}

.calendar-key-dates a,
.calendar-key-dates > span {
  grid-template-columns: 58px minmax(0, 1fr);
}

.calendar-key-date-row {
  position: relative;
  grid-template-columns: 58px minmax(0, 1fr);
  padding-right: 24px;
  overflow: hidden;
}

.calendar-key-date-main {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 58px minmax(0, 1fr);
  gap: 7px;
  align-items: center;
  min-width: 0;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: inherit;
  font-size: inherit;
  font-weight: inherit;
  line-height: inherit;
  text-decoration: none;
}

.calendar-key-dates .calendar-key-date-main:hover,
.calendar-key-dates .calendar-key-date-main:focus-visible {
  background: transparent;
}

.calendar-key-dates a:hover,
.calendar-key-date-row:hover {
  border-color: #b9c5d6;
  background: var(--surface);
}

.calendar-key-date-delete {
  position: absolute;
  top: 3px;
  right: 3px;
  z-index: 2;
  width: 17px;
  height: 17px;
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
  color: color-mix(in srgb, var(--ink-strong) 58%, transparent);
  font-size: 13px;
  font-weight: 900;
  line-height: 1;
  opacity: 0;
  transition: opacity 140ms ease, background 140ms ease, color 140ms ease;
}

.calendar-key-date-row:hover .calendar-key-date-delete,
.calendar-key-date-row:focus-within .calendar-key-date-delete {
  opacity: 1;
}

.calendar-key-date-delete:hover,
.calendar-key-date-delete:focus-visible {
  border-color: #cbd5e1;
  background: var(--surface);
  color: var(--brand);
}

.calendar-date-badge {
  width: 50px;
  min-height: 23px;
  justify-self: center;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.72);
  color: var(--ink-strong);
  font-size: 11px;
  font-weight: 950;
  line-height: 1.05;
}

.calendar-date-badge small {
  color: #5f6d81;
  font-size: 9px;
  font-weight: 850;
}

.calendar-date-range-text {
  width: 50px;
  min-height: 23px;
  justify-self: center;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 1px;
  color: var(--ink-strong);
  font-size: 11px;
  font-weight: 950;
  line-height: 1.05;
  text-align: center;
}

.calendar-date-range-text small {
  display: block;
  width: 100%;
  color: #5f6d81;
  font-size: 9px;
  font-weight: 850;
}

.calendar-date-range-text strong {
  display: block;
  width: 100%;
}

.calendar-key-dates a.is-deadline,
.calendar-key-date-row.is-deadline {
  border-color: #edd2d5;
  background: #fff6f6;
}

.calendar-key-dates .is-deadline .calendar-date-badge {
  background: #fff4f4;
}

.calendar-key-dates a.is-break,
.calendar-key-date-row.is-break {
  border-color: #eadbb4;
  background: #fffaf0;
}

.calendar-key-dates .is-break .calendar-date-badge {
  background: #fff8e9;
}

.calendar-key-dates a.is-exam,
.calendar-key-date-row.is-exam {
  border-color: #d7dcf2;
  background: #f6f7ff;
}

.calendar-key-dates .is-exam .calendar-date-badge {
  background: #f4f5ff;
}

.calendar-key-dates a.is-result,
.calendar-key-date-row.is-result {
  border-color: #d6ddf1;
  background: #f5f7ff;
}

.calendar-key-dates .is-result .calendar-date-badge {
  background: #eef3ff;
}

.calendar-key-dates a.is-orientation,
.calendar-key-date-row.is-orientation {
  border-color: #cfe9ee;
  background: #f2fbfd;
}

.calendar-key-dates .is-orientation .calendar-date-badge {
  background: #eaf7fa;
}

.calendar-key-dates a.is-term,
.calendar-key-date-row.is-term {
  border-color: #d4eadc;
  background: #f3fbf6;
}

.calendar-key-dates .is-term .calendar-date-badge {
  background: #eef9f2;
}

.calendar-key-dates a.is-timetable-plan,
.calendar-key-date-row.is-timetable-plan {
  border-color: #cfe3e9;
  background: #f5fbfc;
}

.calendar-key-dates .is-timetable-plan .calendar-date-badge {
  background: #eef6f8;
}

.calendar-key-dates a.is-timetable-preference,
.calendar-key-date-row.is-timetable-preference {
  border-color: #dae8ca;
  background: #f8fbf4;
}

.calendar-key-dates .is-timetable-preference .calendar-date-badge {
  background: #f4f8ef;
}

.calendar-key-dates a.is-timetable-creation,
.calendar-key-date-row.is-timetable-creation {
  border-color: #eadbb4;
  background: #fffaf0;
}

.calendar-key-dates .is-timetable-creation .calendar-date-badge {
  background: #fff8e8;
}

.calendar-key-dates a.is-timetable-review,
.calendar-key-date-row.is-timetable-review {
  border-color: #dedbf4;
  background: #f8f7ff;
}

.calendar-key-dates .is-timetable-review .calendar-date-badge {
  background: #f5f3ff;
}

.calendar-key-dates a.is-custom,
.calendar-key-date-row.is-custom {
  border-color: #cfdcf2;
  background: #f7faff;
}

.calendar-key-dates .is-custom .calendar-date-badge {
  background: #f1f6ff;
}

.calendar-key-dates a.tone-deadline,
.calendar-key-date-row.tone-deadline {
  border-color: #efb7b7;
  background: #fff1f1;
}

.calendar-key-dates .tone-deadline .calendar-date-badge,
.calendar-key-dates .tone-deadline .calendar-date-range-text {
  background: #ffe8e8;
}

.calendar-key-dates a.tone-census,
.calendar-key-date-row.tone-census {
  border-color: #c7b6f5;
  background: #f5f1ff;
}

.calendar-key-dates .tone-census .calendar-date-badge,
.calendar-key-dates .tone-census .calendar-date-range-text {
  background: #efe9ff;
}

.calendar-key-dates a.tone-career,
.calendar-key-date-row.tone-career {
  border-color: #9bdcac;
  background: #f0fbf3;
}

.calendar-key-dates .tone-career .calendar-date-badge,
.calendar-key-dates .tone-career .calendar-date-range-text {
  background: #e4f8e9;
}

.calendar-key-dates a.tone-plan,
.calendar-key-date-row.tone-plan {
  border-color: #91cfe9;
  background: #f0faff;
}

.calendar-key-dates .tone-plan .calendar-date-badge,
.calendar-key-dates .tone-plan .calendar-date-range-text {
  background: #e4f5ff;
}

.calendar-key-dates a.tone-preference,
.calendar-key-date-row.tone-preference {
  border-color: #dcc46a;
  background: #fff9df;
}

.calendar-key-dates .tone-preference .calendar-date-badge,
.calendar-key-dates .tone-preference .calendar-date-range-text {
  background: #fff3bf;
}

.calendar-key-dates a.tone-results,
.calendar-key-date-row.tone-results {
  border-color: #a9b5eb;
  background: #f1f4ff;
}

.calendar-key-dates .tone-results .calendar-date-badge,
.calendar-key-dates .tone-results .calendar-date-range-text {
  background: #e7ecff;
}

.calendar-key-dates a.tone-orientation,
.calendar-key-date-row.tone-orientation {
  border-color: #89d8d5;
  background: #eefafa;
}

.calendar-key-dates .tone-orientation .calendar-date-badge,
.calendar-key-dates .tone-orientation .calendar-date-range-text {
  background: #ddf5f4;
}

.calendar-key-dates a.tone-create,
.calendar-key-date-row.tone-create {
  border-color: #e9b579;
  background: #fff3e7;
}

.calendar-key-dates .tone-create .calendar-date-badge,
.calendar-key-dates .tone-create .calendar-date-range-text {
  background: #ffe8cd;
}

.calendar-key-dates a.tone-break,
.calendar-key-date-row.tone-break {
  border-color: #e3ca8a;
  background: #fff9ea;
}

.calendar-key-dates .tone-break .calendar-date-badge,
.calendar-key-dates .tone-break .calendar-date-range-text {
  background: #fff0c7;
}

.calendar-key-dates a.tone-exam,
.calendar-key-date-row.tone-exam {
  border-color: #bca9e8;
  background: #f6f1ff;
}

.calendar-key-dates .tone-exam .calendar-date-badge,
.calendar-key-dates .tone-exam .calendar-date-range-text {
  background: #eee6ff;
}

.calendar-key-dates a.tone-custom,
.calendar-key-date-row.tone-custom {
  border-color: #a9bee9;
  background: #f2f6ff;
}

.calendar-key-dates .tone-custom .calendar-date-badge,
.calendar-key-dates .tone-custom .calendar-date-range-text {
  background: #e7efff;
}

.calendar-key-dates .calendar-key-date-row .calendar-date-badge,
.calendar-key-dates .calendar-key-date-row .calendar-date-range-text {
  background: transparent;
}

.calendar-key-dates time {
  color: var(--brand);
  font-weight: 900;
}

.calendar-add-bottom {
  display: grid;
  justify-items: center;
  gap: 2px;
  position: relative;
  height: 0;
  margin: 0;
  padding: 0;
}

/* When the add-date form is open it must occupy real height (the "+" button is
   absolutely positioned, so the container is 0-height by default). Otherwise the
   form overflows the zero-height box and overlaps the events list below it. */
.calendar-add-bottom.is-editing {
  height: auto;
  justify-items: stretch;
  margin-top: 8px;
}

.calendar-add-subtle {
  width: 18px;
  height: 18px;
  position: absolute;
  top: 2px;
  left: 50%;
  transform: translateX(-50%);
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
  color: color-mix(in srgb, var(--ink-strong) 42%, transparent);
  font-size: 14px;
  font-weight: 800;
  line-height: 1;
  opacity: 0.42;
  transition: opacity 140ms ease, background 140ms ease, color 140ms ease;
}

.calendar-widget:hover .calendar-add-subtle,
.calendar-widget:focus-within .calendar-add-subtle {
  opacity: 1;
}

.calendar-add-subtle:hover,
.calendar-add-subtle:focus-visible {
  background: color-mix(in srgb, var(--ink-strong-bg) 10%, transparent);
  color: color-mix(in srgb, var(--ink-strong) 72%, transparent);
}

.calendar-add-message {
  display: none;
}

.post-card {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: 14px;
  width: 100%;
  margin: 0;
  padding: 20px 24px 18px;
  break-inside: avoid;
  border-radius: 0;
  border-bottom: 1px solid var(--line-card);
  background: transparent;
  overflow: hidden;
  transition:
    background 160ms ease;
  content-visibility: auto;
  contain-intrinsic-size: 360px;
}

.post-card:hover {
  background: transparent;
}

.post-card:last-child {
  border-bottom: 0;
}

.thread-avatar {
  padding-top: 2px;
}

.thread-avatar .avatar {
  border: 1px solid #d5d5d5;
  border-radius: 1000px;
  background: var(--surface-muted);
  color: #20242c;
}

.is-hidden-post {
  opacity: 0.62;
}

/* Anonymous ("匿名") posts read as identity-hidden: a muted gray-tinted card
   with a dimmed title and a neutral placeholder avatar, so they are visually
   distinct from normal (authored) posts at a glance. */
.post-card.is-anonymous {
  background: rgba(110, 116, 128, 0.09);
}
.post-card.is-anonymous:hover {
  background: rgba(110, 116, 128, 0.13);
}
.post-card.is-anonymous .post-card-body > h2 {
  color: var(--muted);
}
.thread-avatar .avatar.anon-avatar,
.detail-author-row .avatar.anon-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  background: #6b7280;
  color: #fff;
  border-color: #6b7280;
}
.thread-author-button.is-anon {
  color: var(--muted);
  font-weight: 700;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: default;
}

.media-strip {
  position: relative;
  /* Full content width so more than ~3 images are visible before scrolling, and
     the right edge lines up with the review score box (both bounded by the card
     content padding). */
  width: 100%;
  margin-top: 14px;
}

.media-track {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  cursor: grab;
  /* pan-y 是补上去的。原来只写 pan-x, 意思是「这块区域只接受横向手势」—— 手指落在
     媒体条上往下拖, 浏览器直接把手势丢掉, 页面不动。横滑照样能用: 浏览器按手指
     起始方向选轴, 两个都允许才是横向滚动条该有的写法。 */
  touch-action: pan-x pan-y;
}

/* 只有一件媒体的时候整条不是滚动容器 —— 它没东西可滚, 却仍旧铺满卡片宽度。
   手机上一条 9:16 的转载视频只占 158px, 右边 171px 的空白仍然属于这条轨道,
   连带 cursor: grab 和 scroll-snap 一起挂在一块什么都没有的地方。收成 fit-content
   之后那片空白回归卡片本身, 上下拖动和点击都按普通页面处理。 */
.media-strip.is-single .media-track {
  width: fit-content;
  max-width: 100%;
  overflow: visible;
  scroll-snap-type: none;
  cursor: default;
  touch-action: auto;
}

.media-track::-webkit-scrollbar {
  display: none;
}

.post-media {
  width: 150px;
  height: 112px;
  flex: 0 0 150px;
  display: block;
  border: 1px solid #d5d5d5;
  border-radius: var(--radius-ui);
  object-fit: cover;
  object-position: center;
  background: #efefef;
  scroll-snap-align: start;
  cursor: zoom-in;
}

/* Full-screen image lightbox */
.media-viewer {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 24px;
  background: rgba(8, 12, 20, 0.92);
  overscroll-behavior: contain;
}

.media-viewer-stage {
  grid-column: 2;
  margin: 0;
  display: grid;
  place-items: center;
  gap: 12px;
  min-width: 0;
}

.media-viewer-stage img {
  max-width: min(92vw, 1120px);
  max-height: 84vh;
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: #0b0f18;
}

.media-viewer-count {
  color: rgba(255, 255, 255, 0.82);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
}

.media-viewer-close {
  position: absolute;
  top: 16px;
  right: 18px;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: #ffffff;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}

.media-viewer-nav {
  width: 52px;
  height: 52px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  color: #ffffff;
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
}

.media-viewer-prev {
  grid-column: 1;
}

.media-viewer-next {
  grid-column: 3;
}

.media-viewer-close:hover,
.media-viewer-nav:hover {
  background: rgba(255, 255, 255, 0.24);
}

@media (max-width: 640px) {
  .media-viewer {
    padding: 12px;
    gap: 4px;
  }
  .media-viewer-nav {
    width: 42px;
    height: 42px;
    font-size: 24px;
  }
}

.media-nav {
  display: none;
}

.media-prev {
  left: 0;
}

.media-next {
  right: 0;
}

.text-art {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 16px;
  background:
    radial-gradient(circle at 80% 16%, rgba(0, 32, 91, 0.14), transparent 30%),
    linear-gradient(135deg, #f9fafc, #e9efff);
}

.text-art span {
  color: #969696;
  font-size: 12px;
  font-weight: 700;
}

.text-art strong {
  color: var(--brand);
  font-size: 26px;
  line-height: 1;
}

.post-card-body {
  position: relative;
  padding: 0;
  /* Grid item: without min-width:0 the automatic minimum size lets the media
     carousel (a flex overflow-x:auto track of fixed-width thumbnails) balloon
     this body past its 338px track. The card's overflow-x:hidden then clips the
     right side — posts with images looked cut off on mobile. min-width:0 pins the
     body to the track so the carousel scrolls internally instead. */
  min-width: 0;
}

.card-top-actions {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  align-items: center;
  gap: 6px;
}

.post-time,
.card-top-actions .flag {
  white-space: nowrap;
}

.post-more-menu {
  position: relative;
}

.post-more-menu summary {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0;
  list-style: none;
  line-height: 1;
}

.post-more-menu summary::-webkit-details-marker {
  display: none;
}

.card-top-actions button {
  min-height: 30px;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  color: var(--muted);
  font-size: 13px;
}

.post-more-menu summary:hover,
.card-top-actions button:hover {
  background: var(--surface-muted);
  color: #385898;
}

.post-more-popover {
  position: absolute;
  top: 30px;
  right: 0;
  z-index: 12;
  min-width: 84px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  box-shadow: 0 12px 24px rgba(12, 19, 36, 0.12);
}

.post-more-popover button {
  width: 100%;
  justify-content: flex-start;
  padding: 0 10px;
  text-align: left;
  /* 这个菜单里的按钮（编辑 / 删除 / 举报 / 屏蔽）全是裸 <button>，既不在上面
     那张共享基础表（.ghost-button, .primary-button, … 那一长串）里，也没有全局
     button 复位可依靠 —— 文件开头那条只给了 color: inherit。所以它们在这张弹层
     里渲染成 UA 默认按钮：2px outset 边框、直角、400 字重。和下面 .secondary-button
     那条注释记下的是同一个毛病，这里补齐同一套复位。 */
  min-height: 30px;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  font-weight: 600;
}

.post-more-popover button:hover {
  background: var(--surface-muted);
}

/* .is-danger 在 10 处标记「破坏性操作」——删除帖子、删除匿名帖、屏蔽用户、解除
   全部屏蔽、清除本地数据、清除草稿、**注销账号** —— 而整份样式表里从来没有它的
   规则。profile.js:131 的注释甚至把它和 ghost-button / primary-button 并列，当成
   站内既有的约定在用。结果是注销账号的按钮和一个普通按钮长得一模一样，破坏性
   操作在视觉上没有任何提示。 */
.is-danger {
  color: var(--danger);
}

.is-danger:hover {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  color: var(--danger);
}

.thread-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  /* Only the top author/board row needs to clear the absolutely-positioned
     time + ••• menu; the title/body/review below use the full card width. */
  padding-right: 132px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.4;
}

.thread-meta strong {
  color: #000000;
  font-weight: 600;
}

/* 认证蓝勾。勾是画出来的几何图形, 不是字形 —— U+2713 不在 Inter 里, 每台机器都
   逐字回退到各自的系统字体, 而那些字体的 ✓ 墨迹足有 1.25em 高、只有 0.68em 宽:
   在一个直径 1.4em 的圆里, 那一撇的右上端正好顶到圆外, 白勾落在浅色卡片上就
   看不见了 —— 也就是「钩没显示完全, 被颜色盖住」。实测 18px 那档墨迹 15px 高,
   离圆边只剩半个像素。

   换成 mask 之后勾的比例、位置和留白都由 viewBox 定死, 三档尺寸(14/18/20)只是
   等比放大, 跟字体再无关系。ink 用 currentColor, 所以暗色引擎照旧调得动它。 */
.verified-dot {
  position: relative;
  display: inline-grid;
  place-items: center;
  /* 名字行是 flex, 而 grid 容器的 min-width:auto 只有勾那么宽 —— 名字一长这颗
     圆就被压成竖椭圆。这里钉死不许伸缩。 */
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  border-radius: 1000px;
  background: #385898;
  color: #ffffff;
  /* 把 ✓ 那个字符本身藏掉(可访问名走 aria-label, 不受影响)。 */
  font-size: 0;
  line-height: 1;
}

/* 勾本身。用 inset 而不是 width/height 的百分比: ::before 是 grid item, 而这一格
   的行高是被内容撑出来的(内容为空), 于是 height:66% 循环解析 —— 实测解成 1.97px,
   勾被压成一条横线。inset 的百分比对着 padding box 算, 没有这个循环, 而且同一条
   规则就管住了 14 / 18 / 20 三档。 */
.verified-dot::before {
  content: "";
  position: absolute;
  inset: 17%;
  background-color: currentColor;
  -webkit-mask: var(--verified-tick) center / contain no-repeat;
  mask: var(--verified-tick) center / contain no-repeat;
}

.post-flags,
.tag-row,
/* Attribution on an imported post. Deliberately quiet but always present — it sits
   directly under the byline on a card and under the title in the detail view, so the
   original author is visible without competing with the post's own text. */
.post-source {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 6px 0 0;
  color: #667085;
  font-size: 12px;
  line-height: 1.35;
}

/* Rides along on the byline row, so it must not introduce its own box or margin. */
.post-source.is-card {
  display: inline;
  margin: 0;
  gap: 0;
}

.post-source a {
  color: var(--brand);
  font-weight: 750;
  text-decoration: none;
}

.post-source a:hover {
  text-decoration: underline;
}

/* 转发语(migration 0067) —— 转发者自己写的那几句话, 压在原文之上。
   这就是这条帖子的正文, 所以按正文排, 不带任何装饰。原来它底下有一条分隔线来告诉
   读者「线以下不是他说的」; 现在原文自己装进 .post-quote 那个带底色的框里, 边界比
   一条线清楚得多, 线就多余了(留着会变成框上方一道悬空的横杠)。 */
.repost-note {
  margin: 8px 0 10px;
  color: var(--text);
  font-size: 15px;
  line-height: 1.65;
}

/* 引用块 —— 照 App 的 PostCard(quoted 分支)。

   底色直接用 --surface-3 这一支纯色令牌, 不用 color-mix 兑。第一版写的是
   color-mix(var(--ink-strong) 7%, transparent), 想着亮色下是本校墨色的薄雾、暗色下
   --ink-strong 变纯白就自动成了 App 的 rgba(255,255,255,0.06) —— 实测不成立: 暗色
   引擎不认这层意图, 它把**兑出来的结果**当普通颜色压暗, 量到 srgb(0.07 0.07 0.07)
   / 0.07 压在 rgb(18,18,19) 的卡片上, 框整个消失了。

   这正是本文件 18294 行那段注释讲的事: 引擎会重写 :root 上的自定义属性, 所以每个
   var() 引用自动跟着走 —— 覆盖值只准写 token。--surface-3 亮色 #eef1f6(几乎就是
   App 的 rgba(120,120,128,0.09) 压在白底上的样子), 暗色 #2d2d2d, 两头都比卡片亮
   一档, 一条声明管两种主题。 */
.post-quote {
  margin: 0 0 10px;
  border-radius: var(--radius-ui);
  padding: 11px 12px 3px;
  background: var(--surface-3);
}

/* 框里的标题降成正文字号、不加粗 —— 这条帖子的标题是上面那段转发语, 框里的只是
   被引用那条的题目。App 那边同样是 15px / weight 400。 */
.post-card .post-quote h2,
.detail-post .post-quote h1 {
  margin: 0 0 6px;
  color: var(--text);
  font-size: 15px;
  font-weight: 400;
  line-height: 1.5;
}

/* @原作者 —— 可点, 去原文。 */
.post-quote-handle {
  color: var(--brand);
  font-weight: 750;
  text-decoration: none;
}

a.post-quote-handle:hover {
  text-decoration: underline;
}

/* 落款「来自抖音 ↗」。@原作者说的是谁写的, 这一行说的是从哪儿搬来的。 */
.post-quote-foot {
  margin-top: 2px;
  padding-bottom: 8px;
  color: var(--muted);
  font-size: 12.5px;
}

.post-quote-foot a {
  color: inherit;
  text-decoration: none;
}

.post-quote-foot a:hover {
  text-decoration: underline;
}

/* 框内最后一件东西的下边距交给框自己的 padding, 免得框底空出一大截。 */
.post-quote > :last-child {
  margin-bottom: 0;
}

/* 详情页的引用块。那边的媒体框和正文框各有自己的边框、底色和 16px 内边距 —— 原样
   套进引用块里就是三层框套框。框内一律脱壳, 只留内容平铺, 和 App 的详情页一致。 */
.detail-quote {
  padding: 14px 16px 4px;
}

.detail-quote .detail-media-wrap,
.detail-quote .detail-post-body {
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
  overflow: visible;
}

/* 媒体排在正文之后, 正文最后一段自带 20px 下边距, 这里不用再加。 */
.detail-quote .detail-media-wrap {
  margin: 0 0 4px;
}

.repost-note p {
  margin: 0 0 6px;
  word-break: break-word;
}

.repost-note p:last-child {
  margin-bottom: 0;
}

.translation-meta,
.card-actions,
.detail-actions,
.comment-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

/* Inside a tag row the indicator rides along on THAT line (pushed right by the
   auto margin) instead of claiming a row of its own — as its own full-width row
   it added an empty-looking band to the bottom of every translated feed card. */
.tag-row .translation-meta {
  width: auto;
  margin-left: auto;
}

/* 同样的搭便车写法, 这次是操作行(点赞/评论/收藏/分享)。默认的 width:100% 会让它
   独占一行, 那正是要消掉的那条空带子; auto 宽度 + margin-left:auto 把它顶到同一行
   的右端。margin-top 清零, 因为 .card-actions 自己已经有 margin-top。
   窄屏下 .card-action-group 变成 width:100%(见 ~12460 的断点), 于是这条提示自然
   折到下一行右对齐 —— 和它挪动之前的位置一致, 不需要额外的断点规则。 */
.card-actions .translation-meta {
  width: auto;
  margin: 0 0 0 auto;
}

.translation-meta {
  justify-content: flex-end;
  width: 100%;
  gap: 6px;
  margin: 6px 0 0;
  color: #667085;
  font-size: 12px;
  line-height: 1.35;
}

.translation-meta button {
  min-height: 0;
  padding: 2px 0;
  border: 0;
  background: transparent;
  color: #667085;
  font-size: 12px;
  font-weight: 750;
  cursor: pointer;
}

.translation-meta button:hover {
  color: var(--ink-strong);
}

.translation-meta.is-loading {
  color: #8a94a6;
}

.flag,
.tag-row span,
.edited {
  border-radius: var(--radius-ui);
  padding: 2px 7px;
  background: var(--surface-muted);
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
}

.flag.danger {
  color: var(--danger);
  background: #fff0f2;
}

.post-card h2 {
  margin: 7px 0 6px;
  overflow: visible;
  font-size: 19px;
  line-height: 1.35;
  font-weight: 800;
  letter-spacing: 0;
  color: var(--ink-strong);
}

/* App-only (Apple typography): tighten bold headings a touch. */
.is-native-app .post-card h2 {
  letter-spacing: -0.01em;
}

.post-card p {
  display: block;
  margin: 0 0 12px;
  overflow: visible;
  color: #000000;
  font-size: 16px;
  line-height: 1.55;
}

/* Preserve the line breaks authors enter in the post composer. Keep this on
   plain post bodies (rather than every paragraph in a card) so structured
   review cards retain their own layout. */
.post-card-text p,
.detail-post-body > p {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Feed "展开全文 / 收起" toggle. The collapse only engages on narrow screens;
   on desktop the body stays fully expanded and this toggle is hidden (the
   mobile @media block below flips it on and clamps the collapsed text). */
/* Clamp long post bodies and reveal the "展开全文 / 收起" toggle. This used to be
   mobile-only (inside the 720px query), but a wall of text is a wall of text on a
   desktop feed too — a single long post pushed everything after it below the fold. */
.post-card-text.is-collapsible:not(.is-expanded) {
  position: relative;
}

.post-card-text.is-collapsible:not(.is-expanded) p,
/* Course-review comments render their own markup, so they need the rule by name. */
.review-post-comment.is-collapsible:not(.is-expanded) {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
}

.post-expand-toggle {
  display: inline-flex;
  margin: -6px 0 12px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--brand);
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
}

/* An image the article placed between two paragraphs (post_media.paragraph_index).
   Full column width, its own vertical rhythm, natural aspect — a 公众号 screenshot is
   often tall, and cropping it to a strip thumbnail would lose what it is showing. */
.post-inline-media {
  margin: 22px 0;
}

/* max-width, never width: a 公众号 article is written at ~677px, so its pictures come
   down at 614–1080px. Forcing them to fill a 1032px column upscaled the small ones —
   the follow QR was being stretched from 614 to 998 and going soft. Capped at 720 so
   a big photo does not tower over the text either, and centred like the original. */
/* The .detail-post prefix is not decoration: `.detail-post .post-media` already sets
   width: 100% for the strip, same specificity as a bare `.post-inline-media
   .post-media`, and it wins on order. width: auto is what actually releases it. */
.post-inline-media .post-media,
.detail-post .post-inline-media .post-media {
  display: block;
  width: auto;
  max-width: min(100%, 720px);
  height: auto;
  margin: 0 auto;
  border-radius: var(--radius-ui);
  background: #eef2f8;
  cursor: zoom-in;
}

.post-inline-media .post-media.is-video {
  cursor: default;
}

/* ---- 专栏 card: posts long enough that unfolding them in the feed is hostile ----
   The body does not appear at all — it becomes one clickable row: two lines of
   summary beside the article's own cover, then 长文 · 时长 · 阅读全文. Showing five
   clipped lines of the real text instead (the first attempt) put a ghost paragraph
   and a floating bar in the middle of the card; a 专栏 card describes the piece, it
   does not print a torn-off piece of it. */
.post-article {
  display: flex;
  gap: 14px;
  align-items: stretch;
  width: 100%;
  margin: 2px 0 12px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: #f7f9fd;
  text-align: left;
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease;
}

.post-article:hover,
.post-article:focus-visible {
  border-color: var(--ink-strong-bg);
  background: var(--brand-weak);
  outline: 0;
}

.post-article-main {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  align-content: center;
}

/* Three lines, taking the room the 阅读全文 row used to hold: the whole card is the
   link, so a label saying so was spending a line to repeat the obvious. */
.post-article-summary {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 650;
  line-height: 1.55;
}

/* Fixed box, cropped: article covers are every aspect ratio there is (a poster, a
   QR code, a screenshot) and the row has to keep one height whichever turns up. */
.post-article-thumb {
  flex: 0 0 auto;
  width: 104px;
  height: 78px;
  border-radius: var(--radius-ui);
  background: #e9eef6;
  object-fit: cover;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  margin-top: 10px;
}

.card-actions {
  justify-content: flex-start;
  gap: 14px;
  margin-top: 10px;
}

.card-action-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 18px;
}

.post-time {
  color: var(--muted);
  font-size: 12px;
  white-space: nowrap;
}

/* 发布时间暂时不在卡片右上角显示——只保留在帖子详情页（board · time 那一行）。
   恢复：删除下面这条规则即可。 */
.post-card .post-time {
  display: none;
}

.media-strip + .tag-row {
  margin-top: 12px;
}

.card-actions button,
.detail-actions button,
.comment-actions button {
  min-height: 30px;
  padding: 0;
  /* Was #000. Being a class+element selector it outranked .post-action-button's own
     #303642, so every like/save/share control on the page — and the SVG icons that
     inherit from them — rendered pure black against body text at #20242c. That is
     three near-blacks in one card. */
  color: var(--text);
}

.post-action-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: #303642;
  font-size: 14px;
  line-height: 1;
}

.post-action-button:hover {
  color: var(--ink-strong);
}

.action-icon {
  width: 19px;
  height: 19px;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.action-icon-send {
  width: 20px;
  height: 20px;
}

.post-action-button span {
  color: #303642;
  font-size: 13px;
  line-height: 1;
}

.card-actions button:hover,
.detail-actions button:hover,
.comment-actions button:hover {
  color: var(--ink-strong);
  background: transparent;
}

.card-actions .is-active,
.detail-actions .is-active {
  color: #303642;
  background: transparent;
}

.card-actions .is-active .action-icon,
.detail-actions .is-active .action-icon {
  fill: var(--ink-strong);
  stroke: var(--ink-strong);
}

.card-actions .is-active span,
.detail-actions .is-active span {
  color: #303642;
}

.empty-state,
.load-state {
  max-width: 720px;
  margin: 24px auto;
  padding: 28px;
  border-radius: var(--radius-md);
  background: var(--surface);
  text-align: center;
}

.empty-state span,
.load-state p {
  display: block;
  margin-top: 6px;
  color: var(--muted);
}

.empty-state .primary-button {
  margin-top: 16px;
}

.skeleton-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}

.skeleton-grid span {
  height: 70px;
  border-radius: var(--radius-sm);
  /* 中间那道高光同样必须是令牌, 否则暗色下骨架屏是一条白光扫过黑底(含 var()
     的声明 Dark Reader 不重写, 见 .subject-review-summary)。暗色里 --surface-2
     比 --surface-3 略暗, 高光方向反过来了, 但扫动的对比还在, 观感无碍。 */
  background: linear-gradient(90deg, var(--surface-3), var(--surface-2), var(--surface-3));
}

.skeleton-grid span:nth-child(2) {
  height: 96px;
}

.skeleton-grid span:nth-child(3) {
  height: 118px;
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(18, 23, 33, 0.34);
  overscroll-behavior: contain;
}

/* 暗色遮罩这里曾经是毛玻璃(backdrop-filter: blur(16px) saturate(140%), 外加
   .modal-overlay-layer 上第二层 blur(6px))。**已经去掉, 别加回来**, 原因:

   .modal-backdrop 是 position: fixed; inset: 0 —— 铺满整个视口; 而滚动条在它的
   **子元素** .modal-card 上(max-height + overflow: auto)。于是每拖一下, 卡片在这
   个毛玻璃的层叠上下文里重绘, 浏览器就要把整屏重新模糊+饱和一遍。窗口越大越贵:
   用户那台 1784×1431 = 255 万像素, 拖详情页明显发钝, 而且只在暗色下复现。

   不是猜的。用户在自己机器上分两段各拖 6 秒量过帧间隔, 开毛玻璃 vs 关毛玻璃,
   关掉之后立刻顺 —— 这条注释就是那次结论。

   代价刚好全付在最需要流畅的那一刻: 毛玻璃是静止时才看得见的质感, 而它只在你拖
   的时候收费。所以留半透明遮罩, 不留模糊。

   遮罩底色仍然要靠 darkReaderBoot.js 的 scrimFix() 用 !important 钉死, 这一条没
   变: 上面那句 rgba(18, 23, 33, 0.34) 是纯字面量, 正是 Dark Reader 引擎"会"重写
   的那一类, 实测被换成不透明的 rgb(24, 26, 27) —— alpha 整个没了, 弹窗一开背景
   就被一块死灰铺平。没有那个 pin, 这里就不是半透明遮罩而是一块死灰。

   如果哪天还想要这个质感: 别直接加回 .modal-backdrop, 那样滚动容器又会落在模糊
   元素**内部**, 同一个问题原样重现。可行的方向是给 .modal-backdrop 加一个只负责
   模糊的兄弟层(absolute + inset: 0, 压在卡片下面), 让卡片不再是模糊元素的后代 ——
   卡片重绘就不该再让那层失效。没做是因为没验证过 Chrome 实际会不会这么优化。 */

/* 链式滚动挡在真正的滚动容器上。.modal-backdrop 上也写了一句 overscroll-behavior，
   但那是空转的——它 display:grid、没有 overflow，压根不是滚动容器，这个属性只对滚动
   容器生效。卡片滚到头之后手势不该接着推背景页。 */
.modal-card,
.drawer-card {
  overscroll-behavior: contain;
  width: min(560px, 100%);
  max-height: min(86vh, 920px);
  overflow: auto;
  border-radius: 16px;
  background: var(--surface);
}

.modal-wide {
  width: min(720px, 100%);
}

.create-modal {
  width: min(900px, calc(100vw - 40px));
  overflow: auto;
}

/* Keep the publish button pinned to the bottom of the (scrolling) modal so it is
   always reachable, even on short screens where the form overflows. */
.create-form-footer {
  position: sticky;
  bottom: 0;
  z-index: 1;
  margin: 8px -18px -18px;
  padding: 14px 18px;
  background: var(--surface);
  border-top: 1px solid var(--line);
}

.create-submit-button {
  width: 100%;
  min-height: 46px;
  border-radius: var(--radius-sm);
  font-size: 15px;
}

/* Two-button send row: 发布 (grows) + 匿名发送 (auto), with a note beneath. */
.create-form-actions {
  display: flex;
  gap: 10px;
  align-items: stretch;
}
.create-form-actions .create-submit-button {
  flex: 1 1 auto;
  width: auto;
}
.create-anon-button {
  flex: 0 0 auto;
  min-height: 46px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
}
.create-anon-hint {
  margin: 8px 2px 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}
@media (max-width: 520px) {
  .create-form-actions {
    flex-direction: column;
  }
}

.detail-modal,
.admin-modal {
  width: min(980px, 100%);
}

.detail-modal {
  /* 放大一档。1080px 是这条规则最早跟着 .modal-wide(720px) 一路加宽时停下的地方,
     不是量出来的上限 —— 在 1696px 视口上实测只占 1080/1696, 两侧各空 300px, 而帖子
     正文常常是长中文段落, 行宽不够反而读得累。1440 在 1512/1600/1920 三档常见宽度上
     都还留得住外边距, 再宽就该考虑行长上限而不是窗口宽度了。 */
  width: min(1440px, calc(100vw - 48px));
  max-height: min(92vh, 1100px);
  overflow: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  background: #f7f9fc;
}

.detail-modal::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.auth-modal {
  width: min(880px, calc(100vw - 32px));
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(330px, 1fr);
  overflow: hidden;
  border: 1px solid #d8e1ef;
  background: var(--surface);
  color: var(--ink-strong);
  box-shadow: 0 24px 70px rgba(5, 14, 34, 0.22);
}

.auth-register-modal {
  width: min(940px, calc(100vw - 32px));
  grid-template-columns: minmax(320px, 0.9fr) minmax(360px, 1fr);
  background: var(--canvas);
}

.auth-info-panel,
.auth-form-panel,
.auth-register-profile-panel {
  min-width: 0;
  padding: 34px;
}

.auth-qr-heading {
  margin: 0;
  font-size: 19px;
  font-weight: 800;
}

/* 252px：面板本身 421 宽、左右各 34 内边距，可用约 353，252 留出的两侧余白仍然
   宽裕。二维码是这块面板存在的唯一理由，之前 208 显得拘谨；放大之后手机在稍远一点
   的距离也能一次对上焦。 */
.auth-qr-frame {
  width: 252px;
  height: 252px;
  display: grid;
  place-items: center;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #ffffff;
}

/* uqr 出的是一段 <svg>，它自带一层白色 rect 铺底；撑满这个白框即可。 */
.auth-qr-frame svg {
  display: block;
  width: 100%;
  height: 100%;
}

.auth-qr-frame.is-loading,
.auth-qr-frame.is-expired {
  gap: 10px;
  color: #4a5568;
  font-size: 13px;
  font-weight: 700;
}

.auth-qr-blurb {
  margin: 0;
  max-width: 260px;
  color: rgba(255, 255, 255, 0.72);
  font-size: 12.5px;
  line-height: 1.9;
}

/* 产品名高亮 —— 参考图里「哔哩哔哩客户端」那一段是彩色的，它是这两行里唯一需要
   被认出来的信息（用哪个 App 扫），其余是说明。
   但这里不能照抄它的品牌色：面板底就是 --brand，品牌色写在品牌底上等于隐形。
   改用纯白 —— 在这块深藏青上，白就是最高的一档对比，正好承担「高亮」这个角色。 */
/* 这个从 <span> 改成了指向 /app 的 <a>：没装 App 的人看到「请使用 X App 扫码
   登录」时，需要一条去装它的路。保留原来的白色粗体，只补上"可点"的信号——
   不加下划线的话它和旁边的普通文字长得一模一样，没人会去点。 */
.auth-qr-app {
  color: #ffffff;
  font-weight: 800;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  cursor: pointer;
}

.auth-qr-app:hover,
.auth-qr-app:focus-visible {
  text-decoration-thickness: 2px;
}

.auth-qr-approved {
  margin: 0;
  font-size: 13px;
  font-weight: 800;
}

.auth-register-profile-panel {
  min-width: 0;
  padding: 34px;
}

/* Two anchored groups instead of one centred stack: the brand lockup + headline
   sit at the TOP, the blurb + proof chips at the BOTTOM, with the empty space
   between them. `margin-top: auto` on the blurb is what splits the four flat
   siblings into those two groups — no wrapper divs needed. */
.auth-info-panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 20px;
  border-right: 1px solid #d8e1ef;
  background: var(--brand);
}

/* 登录弹窗左侧从「产品介绍」换成二维码（参考 B 站那块）。介绍文案对着一个已经点开
   登录框的人是多余的 —— 他已经决定要登录了；那块面积换成一个能直接用的入口更值。
   面板底色是 --brand（深色），所以二维码必须自带白底：QR 的容错依赖黑白对比，直接
   把黑模块画在深蓝上，多数扫码器识别不出来。
   必须是独立一条：上面那条 min-width/padding 是三个面板共享的，把声明并进去会连
   右侧表单面板一起改（实测把它的 padding 清成 0、OAuth 块压成 160px 居中）。 */
.auth-qr-panel {
  align-items: center;
  justify-content: center;
  gap: 22px;
  text-align: center;
  /* 底色沿用 .auth-info-panel 的 --brand 深藏青，不覆盖。
     深底下二维码那圈白框是必需品而不是装饰：QR 的容错依赖黑白对比，而且规范要求
     码四周留一圈浅色静区，直接把码画在深蓝上多数扫码器识别不出来。所以参考图那种
     「只有一条细边框」的做法在白底成立、在这里不成立 —— 这里保留白框，只把它做薄
     （8px 内边距 + 小圆角），让它读起来仍像一个框而不是一张卡。 */
}

.auth-qr-panel .auth-qr-heading {
  margin: 0;
  /* 覆盖 .auth-info-panel h2 的 clamp(30px,4vw,42px)：那个尺寸是给品牌口号写的，
     压在一张二维码上面会把码变成配图。 */
  color: #ffffff;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: 0.01em;
}

/* 「产品介绍 → 二维码」那一次交接（时序在 main.ts 的 scheduleAuthQrReveal）。
   为什么是 animation 而不是 transition：renderApp() 是全量重建 DOM，二维码那一屏
   的元素是**新造出来**的，一出生就已经是终态，transition 没有起点可言。animation
   相反，元素一插进文档就自己播一遍——正是这里要的。

   挂在直接子元素而不是 .auth-qr-panel 本身：挂在 section 上会把面板底色一起淡进来，
   那一下读起来像弹窗左边缺了一块，而不是内容换了。 */
@keyframes authQrReveal {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* 只在真正"出场"的那一次播。上面那段注释说对了一半: 全量重建 DOM 确实让 transition
   没有起点、只能用 animation —— 但同一个理由也意味着**此后每一次重渲染**都会重新造
   一遍这些元素, 动画于是一遍遍重播, 面板一闪一闪(用户报的"闪屏")。.is-revealing 由
   main.ts 的揭示定时器只挂这一帧(见 state.authQrRevealAnim), 之后的渲染不带它。 */
.auth-qr-panel.is-revealing > * {
  animation: authQrReveal 360ms ease both;
}

@media (prefers-reduced-motion: reduce) {
  .auth-qr-panel.is-revealing > * {
    animation: none;
  }
}



.auth-register-profile-panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 18px;
  border-right: 1px solid #d8e1ef;
  background: #eef4fb;
  color: var(--ink-strong);
}

.auth-register-panel-heading {
  display: flex;
  align-items: center;
  gap: 12px;
}

.auth-register-panel-heading div {
  display: grid;
  gap: 3px;
}

.auth-register-panel-heading span {
  color: #647084;
  font-size: 12px;
  font-weight: 850;
}

.auth-register-panel-heading strong {
  color: var(--ink-strong);
  font-size: 22px;
  line-height: 1.2;
}

.auth-register-preview-card,
.auth-register-edit-card {
  display: grid;
  gap: 14px;
  padding: 16px;
  border: 1px solid #d8e1ef;
  border-radius: var(--radius-card);
  background: var(--surface);
}

.auth-register-preview-card {
  align-items: center;
  justify-items: center;
  text-align: center;
}

.auth-register-preview-card strong {
  display: block;
  color: var(--ink-strong);
  font-size: 18px;
}

.auth-register-preview-card span {
  color: #647084;
  font-size: 12px;
  font-weight: 800;
}

.auth-register-edit-card {
  gap: 12px;
}

.auth-avatar-dropzone {
  display: grid;
  gap: 8px;
  align-items: center;
  border: 1px dashed #a9b8cc;
  border-radius: var(--radius-card);
  padding: 12px;
  background: var(--surface-2);
  color: var(--ink-strong);
  cursor: pointer;
}

.auth-avatar-dropzone:hover,
.auth-avatar-dropzone:focus-within {
  border-color: var(--ink-strong-deep-bg);
  background: var(--surface-2);
}

.auth-avatar-preview {
  display: grid;
  width: 92px;
  aspect-ratio: 1;
  place-items: center;
  overflow: hidden;
  border-radius: var(--radius-ui);
  background: var(--ink-strong-deep-bg);
  color: #ffffff;
  font-size: 36px;
  font-weight: 950;
}

.auth-avatar-preview-large {
  width: 128px;
  font-size: 42px;
}

.auth-avatar-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform-origin: center;
}

.auth-avatar-copy {
  display: grid;
  gap: 6px;
}

.auth-avatar-copy strong {
  color: var(--ink-strong);
  font-size: 16px;
}

.auth-avatar-copy small,
.auth-register-profile-panel label {
  color: #5f6b7c;
  line-height: 1.5;
}

.auth-register-edit-card > label:not(.auth-avatar-dropzone):not(.auth-avatar-range) {
  display: grid;
  gap: 7px;
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
}

.auth-register-profile-panel input:not([type="range"]):not([type="file"]),
.auth-register-profile-panel select {
  width: 100%;
  border: 1px solid #d8e1ef;
  border-radius: var(--radius-sm);
  padding: 10px 11px;
  background: var(--surface-2);
  color: var(--ink-strong);
  font: inherit;
}

.auth-register-profile-panel input:not([type="range"]):not([type="file"]):focus,
.auth-register-profile-panel select:focus {
  border-color: var(--ink-strong-deep-bg);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ink-strong-deep-bg) 12%, transparent);
  outline: none;
}

.auth-register-profile-panel input::placeholder {
  color: #8a95a8;
}

/* Square drag-to-crop avatar stage (replaces the old zoom slider). */
.avatar-crop-stage {
  position: relative;
  width: 148px;
  height: 148px;
  overflow: hidden;
  border-radius: var(--radius-ui);
  background: var(--ink-strong-bg);
  cursor: grab;
  user-select: none;
  touch-action: none;
}

.avatar-crop-stage.is-dragging {
  cursor: grabbing;
}

.avatar-crop-stage img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  transform-origin: center;
  pointer-events: none;
}

.avatar-crop-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: var(--radius-ui);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
  pointer-events: none;
}

.avatar-crop-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 5px 8px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  color: #ffffff;
  font-size: 11px;
  font-weight: 700;
  text-align: center;
  pointer-events: none;
}

.auth-avatar-range {
  display: grid;
  gap: 8px;
}

/* Fixed-suffix school-email field: [local input][@domain]. The wrapper looks like a
   single control; the domain is a locked suffix (dropdown when several are allowed). */
.email-input-group {
  display: flex;
  align-items: stretch;
  border: 1px solid #d7deea;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
}

.email-input-group:focus-within {
  border-color: var(--brand, #1652f0);
}

.email-input-group input[name="emailLocal"] {
  flex: 1;
  min-width: 0;
  border: 0;
  border-radius: 0;
  padding: 10px 12px;
  outline: none;
}

.email-domain-select,
.email-domain-fixed {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding: 0 10px;
  background: #f1f4f9;
  border-left: 1px solid #e2e7f0;
  color: var(--muted, #5b6577);
  font-size: 13px;
  font-weight: 700;
  max-width: 60%;
}

.email-domain-select {
  border: 0;
  border-left: 1px solid #e2e7f0;
  cursor: pointer;
}

.auth-code-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: end;
}

.auth-code-row label {
  min-width: 0;
}

.auth-code-row input {
  width: 100%;
}

.auth-send-code-button {
  height: 42px;
  min-height: 0;
  padding: 0 18px;
  border: 0;
  border-radius: var(--radius-sm);
  background: #e7edfb;
  color: var(--ink-strong-deep);
  font-weight: 800;
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}

.auth-send-code-button:hover,
.auth-send-code-button:focus-visible {
  background: var(--ink-strong-deep-bg);
  color: #ffffff;
}

.auth-send-code-button:disabled {
  cursor: not-allowed;
  background: var(--surface-3);
  color: #94a0b4;
}

.auth-brand-lockup {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: #ffffff;
  font-weight: 900;
}

.auth-info-panel h2 {
  max-width: 360px;
  margin: 0;
  color: #ffffff;
  font-size: clamp(30px, 4vw, 42px);
  line-height: 1.12;
  letter-spacing: 0;
  /* Balance the line lengths so a heading like 墨大学生认证社区 doesn't orphan its
     last glyph on a second line. */
  text-wrap: balance;
}

.auth-info-panel p {
  max-width: 390px;
  margin: 0;
  color: rgba(255, 255, 255, 0.76);
  line-height: 1.65;
}

/* The three trust points read as a checklist (✓ + text) rather than a wrapped row
   of chips crammed at the panel's bottom edge. */
.auth-proof-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 4px;
}

.auth-proof-list span {
  display: flex;
  align-items: center;
  gap: 11px;
  color: rgba(255, 255, 255, 0.82);
  font-size: 13.5px;
  font-weight: 600;
}

.auth-proof-list span::before {
  content: "✓";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  color: #ffffff;
  font-size: 12px;
  font-weight: 800;
}

.auth-info-panel .auth-small-note {
  color: rgba(255, 255, 255, 0.56);
  font-size: 12px;
}

.auth-form-panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 18px;
  background: var(--canvas);
  color: var(--ink-strong);
}

.auth-modal-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 0;
}

.auth-modal-top.is-close-only {
  justify-content: flex-end;
}

.auth-modal-top h2 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 22px;
}

.auth-modal .icon-button {
  color: var(--ink-strong);
}

.auth-form {
  padding: 20px;
  border: 1px solid #dfe6f1;
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
}

.auth-form p,
.auth-modal .form-note {
  color: #5f6b7c;
  line-height: 1.5;
}

.auth-form label {
  color: #344054;
}

.auth-form input {
  border: 1px solid #d8e1ef;
  background: var(--surface-2);
  color: var(--ink-strong);
}

.auth-form input:focus {
  border-color: var(--ink-strong-deep-bg);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ink-strong-deep-bg) 12%, transparent);
}

.auth-form input::placeholder {
  color: #8a95a8;
}

.auth-modal .primary-button {
  min-height: 46px;
  justify-content: center;
  border-color: var(--ink-strong-deep-bg);
  background: var(--ink-strong-deep-bg);
  color: #ffffff;
}

.auth-link-button {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--ink-strong-deep);
  text-align: left;
  font-weight: 850;
}

/* 密码框 + "忘记密码？"。链接平时收起, 点进密码框(或用键盘 Tab 到链接本身)才展开。
   :focus-within 挂在这个包裹层上, 而不是只写 label:focus 兄弟选择器 —— 后者在焦点
   从输入框移到链接的那一刻就失效了, 链接会在被点中之前先消失。
   收起用 max-height + opacity 而不是 display:none: 链接始终在 DOM 与无障碍树里,
   屏幕阅读器和键盘用户不依赖"先点一下密码框"才能发现它; visibility 在收起态置为
   hidden 是为了让它同时退出 Tab 序列, 展开后再回来。 */
.auth-password-field {
  display: grid;
  gap: 6px;
}

.auth-password-field .auth-forgot {
  justify-self: end;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  visibility: hidden;
  font-size: 12px;
  font-weight: 800;
  transition:
    max-height 180ms var(--ease-out, ease),
    opacity 180ms var(--ease-out, ease);
}

.auth-password-field:focus-within .auth-forgot {
  max-height: 24px;
  opacity: 1;
  visibility: visible;
}

.auth-link-button:hover,
.auth-link-button:focus-visible {
  color: var(--brand);
  text-decoration: underline;
}

/* Third-party (Google / Discord) sign-in */
.auth-oauth {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

.auth-oauth-divider {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--soft);
  font-size: 12px;
  font-weight: 800;
}

.auth-oauth-divider::before,
.auth-oauth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

.oauth-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 46px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.oauth-button:hover,
.oauth-button:focus-visible {
  background: var(--canvas);
  border-color: var(--line-strong, var(--line));
}

.oauth-button .oauth-icon {
  width: 18px;
  height: 18px;
  flex: none;
}

.oauth-button.oauth-discord {
  border-color: #5865f2;
  background: #5865f2;
  color: #ffffff;
}

.oauth-button.oauth-discord:hover,
.oauth-button.oauth-discord:focus-visible {
  background: #4752c4;
  border-color: #4752c4;
}

.auth-modal .success-state {
  padding: 18px;
  border: 1px solid #dfe6f1;
  border-radius: var(--radius-card);
  background: var(--surface);
  color: #5f6b7c;
}

.auth-modal .success-state strong {
  color: var(--ink-strong);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.drawer-card {
  position: fixed;
  top: 80px;
  right: 22px;
  width: min(420px, calc(100vw - 32px));
}

.modal-header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.modal-header h2 {
  margin: 0;
  font-size: 20px;
}

.modal-header p {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 13px;
}

.stack-form,
.profile-editor {
  display: grid;
  gap: 14px;
  padding: 18px;
}

.create-modal .stack-form {
  gap: 16px;
  padding: 18px;
  background: #f7f9fc;
}

/* Tabs and the close button share the modal's top row — there is no header above
   them any more, so this row is the top of the dialog. */
.create-head-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.create-head-row .create-type-tabs {
  flex: 1 1 auto;
  min-width: 0;
}

.create-head-row .create-close {
  flex: 0 0 auto;
}

.create-type-tabs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.create-type-tabs button {
  /* Was 68px tall and left-aligned, sized around a caption under each label. With the
     caption gone that left a single word stranded in a large empty box. */
  min-height: 44px;
  display: grid;
  align-content: center;
  gap: 4px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  padding: 8px 10px;
  background: var(--surface);
  color: var(--ink-strong);
  text-align: center;
}

.create-type-tabs button strong {
  font-size: 14px;
  font-weight: 950;
}

.create-type-tabs button span {
  color: #647084;
  font-size: 11px;
  font-weight: 800;
  line-height: 1.35;
}

.create-type-tabs button:hover,
.create-type-tabs button.is-active {
  border-color: var(--ink-strong-bg);
  background: var(--ink-strong-bg);
  color: #ffffff;
}

.create-type-tabs button:hover span,
.create-type-tabs button.is-active span {
  color: rgba(255, 255, 255, 0.74);
}


/* 课程评价页签的课程选择器。.create-subject-field is the positioning context for
   .topbar-subject-suggestions (absolute, top: 100%) — the course page gives it
   .ch-search-field for the same reason. */
.create-subject-picker {
  display: grid;
  gap: 7px;
}

.create-subject-caption {
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
}

.create-subject-field {
  position: relative;
}

/* The chosen course, shown instead of the search box. Reads as one filled field
   rather than a chip so it occupies the same slot the input did. */
.create-subject-picked {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  min-height: 38px;
  padding: 0 8px 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.create-subject-picked strong {
  font-size: 13px;
  font-weight: 800;
}

.create-subject-picked span {
  overflow: hidden;
  color: var(--muted);
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.create-subject-clear {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}

.create-subject-clear:hover {
  color: var(--danger, #e5484d);
  background: rgba(229, 72, 77, 0.1);
}

.create-panel {
  display: grid;
  gap: 14px;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.create-two-column,
.create-rating-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.create-rating-grid {
  grid-template-columns: repeat(5, minmax(0, 1fr));
}

/* Composer icon toolbar — replaced the boxed 图片上传 section. Quiet grey
   outline icons (emoji / image / poll); the popovers open below without a
   re-render so the typed body survives. */
.composer-attach {
  display: grid;
  gap: 8px;
}

.composer-toolbar {
  display: flex;
  align-items: center;
  gap: 4px;
}

.composer-tool {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #8a94a6;
  cursor: pointer;
}

.composer-tool svg {
  width: 20px;
  height: 20px;
}

.composer-tool:hover {
  background: var(--surface-2);
  color: #303a4d;
}

/* Dashed "+" tile in the preview grid — the picker itself, B站-style. */
.media-add-tile {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  min-height: 84px;
  border: 1.5px dashed #b9c3d3;
  border-radius: var(--radius-ui);
  background: transparent;
  color: #8a94a6;
  font-size: 30px;
  font-weight: 300;
  line-height: 1;
  cursor: pointer;
}

.media-add-tile:hover {
  border-color: var(--brand-bright, #7aa7ff);
  color: var(--brand-bright, #7aa7ff);
}

/* Body shell: textarea + upload area + icon toolbar in one bordered box. */
.body-input-shell {
  display: grid;
  gap: 0;
}

.body-shell-caption {
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  margin-bottom: 7px;
}

.body-input-shell textarea {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: 0;
  resize: vertical;
}

.body-input-shell textarea:focus {
  /* The shell reads as one control; keep the focus ring from splitting it. */
  outline-offset: -2px;
}

.composer-media-area {
  padding: 12px;
  border: 1px solid var(--line);
  border-top: 1px dashed color-mix(in srgb, var(--line) 70%, transparent);
  border-bottom: 0;
  background: var(--surface);
}

.body-input-shell .composer-toolbar {
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-top: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
  border-bottom-left-radius: var(--radius-sm);
  border-bottom-right-radius: var(--radius-sm);
  background: var(--surface);
}

.composer-tool.is-active {
  background: color-mix(in srgb, var(--brand-bright, #4d7cf5) 12%, transparent);
  color: var(--brand-bright, #4d7cf5);
}

/* Popover container: hosts the vendored <emoji-picker> once mounted, or the
   static fallback grid if the vendored import fails. */
.composer-emoji-pop {
  margin-top: 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  box-shadow: 0 10px 24px rgba(23, 32, 51, 0.08);
  overflow: hidden;
}

.composer-emoji-pop emoji-picker {
  width: 100%;
  height: 320px;
  --border-color: transparent;
  --background: var(--surface);
}

.composer-emoji-fallback {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
  gap: 2px;
  max-height: 220px;
  overflow-y: auto;
  padding: 10px;
}

.composer-emoji-fallback button {
  border: 0;
  border-radius: 8px;
  background: transparent;
  padding: 4px 0;
  font-size: 19px;
  line-height: 1.3;
  cursor: pointer;
}

.composer-emoji-fallback button:hover {
  background: var(--surface-2);
}

/* Poll summary chip shown once 完成 is pressed in the 发起投票 dialog. */
/* display:flex would override the UA's [hidden] → display:none, leaving a
   ghost "投票 · 0 个选项" chip that × can't visibly remove — keep it explicit. */
.composer-poll-chip[hidden] {
  display: none;
}

.composer-poll-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: #303a4d;
  font-size: 13px;
}

.composer-poll-chip svg {
  width: 15px;
  height: 15px;
  color: #8a94a6;
}

.composer-poll-chip button {
  border: 0;
  background: transparent;
  color: #647084;
  font-size: 13px;
  cursor: pointer;
  padding: 0 2px;
}

.composer-poll-chip button:hover {
  color: #172033;
}

/* 发起投票 dialog — an overlay of its own above the composer modal. */
.composer-poll-dialog {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  background: rgba(15, 22, 36, 0.4);
}

.composer-poll-dialog[hidden] {
  display: none;
}

.poll-dialog-card {
  display: grid;
  gap: 12px;
  width: min(440px, calc(100vw - 40px));
  max-height: min(560px, calc(100vh - 60px));
  overflow-y: auto;
  padding: 18px 20px;
  border-radius: var(--radius-md, 14px);
  background: var(--surface);
  box-shadow: 0 24px 60px rgba(10, 16, 30, 0.28);
}

.poll-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.poll-dialog-head strong {
  font-size: 16px;
}

.poll-dialog-head button {
  border: 0;
  background: transparent;
  color: #647084;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

.poll-dialog-hint {
  margin: 0;
  color: #8a94a6;
  font-size: 12.5px;
}

.poll-dialog-options {
  display: grid;
  gap: 10px;
}

.poll-dialog-row {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr) 26px;
  align-items: center;
  gap: 10px;
  color: #647084;
  font-size: 13px;
}

.poll-dialog-remove {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #98a2b3;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}

.poll-dialog-remove:hover {
  color: var(--danger, #e5484d);
  background: rgba(229, 72, 77, 0.1);
}

/* 两个选项是投票的下限(完成时也会拦), 所以只剩两行时把 ✕ 收起来, 而不是留一枚
   点了会被拒绝的按钮。用 visibility 而不是 display: 第三列的宽度保持不变, 于是
   加出第三行、✕ 重新出现时, 上面两行的输入框不会跟着横跳一下。 */
.poll-dialog-options.is-min .poll-dialog-remove {
  visibility: hidden;
  pointer-events: none;
}

.poll-dialog-row input {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: inherit;
  font: inherit;
}

.poll-dialog-add {
  justify-self: start;
  border: 0;
  background: transparent;
  color: var(--brand-bright, #4d7cf5);
  font-size: 13px;
  font-weight: 650;
  cursor: pointer;
  padding: 2px 0;
}

.poll-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.media-upload-entry {
  display: grid;
  gap: 8px;
  padding: 14px;
  border: 1px dashed #b9c3d3;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
}

.media-upload-entry span {
  color: #647084;
  font-size: 12px;
  font-weight: 750;
}

.media-upload-entry .media-upload-label {
  color: var(--muted);
  font-size: 13px;
  font-weight: 800;
}

.media-preview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: 8px;
}

.media-preview-item {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface-muted);
}

.media-preview-item img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.media-preview-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ink-strong-bg) 74%, transparent);
  color: #ffffff;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}

.media-add-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.media-upload-entry .media-add-button {
  display: inline-flex;
  align-items: center;
  margin: 0;
  cursor: pointer;
}

.media-add-button span {
  padding: 6px 12px;
  border: 1px solid #b9c3d3;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 800;
}

.media-add-button:hover span {
  border-color: var(--ink-strong-bg);
  background: #eef3fb;
}

/* Votable poll inside a post */
/* 整块投票的颜色都跟 --hero-accent 走, 也就是本校主题色。原来这里是一组写死的
   蓝: 描边 #d5dbe8、百分数 #385898、脚注 #647084 —— 十一所学校长一个样, 而且
   #d5dbe8 那个色相在暗色下被引擎忠实保留(它只压明度不动色相), 于是深色卡片上
   一圈发蓝的描边格外扎眼。

   用 --hero-accent 而不是 --brand 或 --brand-bright: 前者对白底会跟标题墨色撞
   (墨大 #000b34), 后者是给深色 chrome 调的, 新南那支 #FFDB00 压在白卡片上只有
   1.37:1。--hero-accent 的定义(tokens.css)正是「本校色, 但保证在浅色页面上读得
   出来」—— 默认取 --brand, 近黑的学校自己覆盖(墨大 #0a4db3)。 */
.post-poll {
  display: grid;
  gap: 8px;
  margin: 6px 0 12px;
}

.post-poll-option {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--hero-accent) 45%, var(--line-card));
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 14px;
  font-weight: 750;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
}

.post-poll-option:hover {
  border-color: var(--hero-accent);
}

.post-poll.is-voted .post-poll-option::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--poll-pct, 0%);
  background: color-mix(in srgb, var(--hero-accent) 16%, var(--surface-3));
  z-index: 0;
}

.post-poll-option.is-mine {
  border-color: var(--hero-accent);
  box-shadow: inset 0 0 0 1px var(--hero-accent);
}

.post-poll-label,
.post-poll-pct {
  position: relative;
  z-index: 1;
}

.post-poll-pct {
  font-weight: 900;
  color: var(--hero-accent);
}

.post-poll-meta {
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
}

.create-submit-button {
  min-height: 42px;
  font-weight: 900;
}

.stack-form label,
.profile-editor label {
  display: grid;
  gap: 7px;
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
}

.stack-form input,
.stack-form select,
.stack-form textarea,
.profile-editor input,
.profile-editor select,
.profile-editor textarea,
.admin-actions select {
  width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 11px;
  background: var(--surface);
  color: var(--text);
}

.form-note {
  margin: 0;
}

.edit-post-modal .stack-form {
  gap: 14px;
}

.edit-post-metrics {
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  line-height: 1.5;
  word-break: break-word;
}

.edit-post-note {
  margin: -6px 0 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

.edit-post-modal textarea {
  resize: vertical;
  min-height: 170px;
  line-height: 1.6;
  font: inherit;
}

.edit-post-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

/* Edit-post image manager: reorder (◀ ▶) + delete (×) per image, plus add. */
.edit-media-entry {
  display: grid;
  gap: 8px;
}

.edit-media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 10px;
}

.edit-media-item {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-muted);
  cursor: grab;
}

.edit-media-item:active {
  cursor: grabbing;
}

.edit-media-item.is-dragging {
  opacity: 0.45;
  outline: 2px dashed var(--brand);
  outline-offset: -2px;
}

.edit-media-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}

/* Drag affordance (top-left) + delete (top-right). */
.edit-media-grip {
  position: absolute;
  top: 4px;
  left: 4px;
  padding: 0 5px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.9);
  color: var(--ink-strong);
  font-size: 12px;
  line-height: 18px;
  font-weight: 800;
}

.edit-media-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(214, 40, 40, 0.92);
  color: #ffffff;
  font-size: 14px;
  font-weight: 800;
  line-height: 1;
  cursor: pointer;
}

.edit-media-empty {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

.error-text {
  color: var(--danger);
  margin: 0;
}

.success-text,
.success-state strong {
  color: var(--brand);
}

.success-state {
  padding: 18px;
}

.settings-body {
  display: grid;
  gap: 14px;
  padding: 18px;
}

.settings-body section {
  padding: 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.settings-body h3 {
  margin: 0 0 6px;
  font-size: 15px;
}

.settings-body p {
  margin: 0;
  color: var(--muted);
  line-height: 1.5;
}

.detail-post {
  display: grid;
  gap: 16px;
  padding: 22px 24px 18px;
  background: var(--surface);
}

.detail-post-hero {
  display: grid;
  gap: 12px;
}

.detail-author-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  color: var(--muted);
}

.detail-author-row .avatar-button {
  margin-top: 1px;
}

.detail-author-row > div {
  display: grid;
  gap: 2px;
}

.detail-author-row .thread-author-button {
  justify-self: start;
  color: var(--ink-strong);
  font-size: 15px;
  font-weight: 900;
  line-height: 1.2;
}

.detail-author-row span {
  color: #6a7280;
  font-size: 13px;
  line-height: 1.35;
}

.detail-post h1 {
  margin: 0;
  color: var(--ink-strong);
  font-size: clamp(24px, 2vw, 34px);
  line-height: 1.24;
  letter-spacing: 0;
}

.detail-media-wrap {
  overflow: hidden;
  border: 1px solid #dfe5ee;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
}

.detail-post .post-media {
  border-radius: var(--radius-ui);
  width: 100%;
  max-width: 100%;
  height: auto;
  max-height: 460px;
  object-fit: cover;
}

.detail-post-body {
  padding: 16px;
  border: 1px solid #e1e7f0;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
}

/* margin: 0 was right while the body was ONE <p> whose blank lines did the spacing.
   An article is split into a <p> per paragraph now, so with no margin the whole piece
   ran together as a single grey block — the thing that made the imported version
   tiring to read. 1.75/20px is close to the 1.8/24px a 公众号 is written at. */
.detail-post-body p {
  margin: 0 0 20px;
  color: var(--text);
  font-size: 17px;
  line-height: 1.75;
}

.detail-post-body > :last-child {
  margin-bottom: 0;
}

/* A section title the source published as <h1>, centred. Two of them in a row is the
   normal 公众号 shape ("4、" over "墨大又是全澳第一"), so the second tucks under the
   first instead of opening a second gap. */
.detail-post-body p.is-article-heading {
  margin: 30px 0 14px;
  /* --brand-bright, not --brand: on this tenant --brand is #000b34, near-black, and a
     heading that colour is only "bolder text". 公众号 titles carry real colour. */
  color: var(--brand-bright);
  font-size: 19px;
  font-weight: 900;
  line-height: 1.6;
  text-align: center;
}

.detail-post-body p.is-article-heading + p.is-article-heading {
  margin-top: -6px;
}

.detail-post-body p.is-article-center {
  text-align: center;
}

/* Live review-count token ({{REVIEW_COUNT}}) emphasis inside post bodies. */
.post-live-count {
  font-weight: 800;
  font-size: 1.12em;
  color: var(--brand);
}

/* Clickable links auto-detected in post bodies (feed + detail). */
.post-card-text a,
.detail-post-body a {
  color: var(--brand);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
}

.post-card-text a:hover,
.detail-post-body a:hover {
  opacity: 0.82;
}

.detail-post .tag-row span {
  background: #edf2f8;
  color: #43506a;
}

.detail-actions {
  padding-top: 2px;
  gap: 10px;
}

.detail-actions button:not(.post-action-button) {
  min-height: 32px;
  padding: 0 10px;
  border: 1px solid #d9e1ec;
  background: var(--surface);
  color: var(--ink-strong);
  font-weight: 800;
}

/* 弹层里的按钮必须逃出上面那条: 两条特异度相同 (0,2,1), 而这一行更靠后, 不重申
   的话菜单里的「举报 / 屏蔽此用户」会各自套上一圈边框, 变成弹层里的两颗药丸。 */
.detail-actions .post-more-popover button {
  padding: 0 10px;
  border: 0;
  background: transparent;
  font-weight: 600;
}

.detail-actions .post-more-popover button:hover {
  background: var(--surface-muted);
}

/* ••• 站在一排 32px 的操作按钮中间, 用 24px 的默认尺寸会矮一截。 */
.detail-more-menu summary {
  width: 32px;
  height: 32px;
}

.comment-section {
  display: grid;
  gap: 14px;
  padding: 18px 24px 24px;
  border-top: 1px solid #e1e7f0;
  background: #f7f9fc;
}

.comment-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.comment-section-head h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 22px;
}

.comment-section-head span {
  color: #6a7280;
  font-size: 13px;
  font-weight: 800;
}

.comment-form {
  display: grid;
  gap: 10px;
  padding: 14px;
  border: 1px solid #dfe5ee;
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.comment-form textarea {
  width: 100%;
  min-height: 92px;
  border: 1px solid #d9e1ec;
  border-radius: var(--radius-ui);
  padding: 12px;
  resize: vertical;
}

.comment-form button {
  justify-self: end;
  min-width: 110px;
}

/* Composer row: image picker on the left, submit on the right. */
.comment-form-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.comment-form-actions .comment-media-button {
  display: inline-flex;
  align-items: center;
  margin: 0;
  cursor: pointer;
}

.comment-form-actions button {
  justify-self: auto;
}

.comment-media-preview {
  margin: 2px 0;
}

/* Reply images rendered under a comment body. */
.comment-media {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 6px 0 2px;
}

.comment-media-img {
  max-width: 220px;
  max-height: 220px;
  width: auto;
  height: auto;
  border-radius: var(--radius-sm);
  border: 1px solid #e2e8f2;
  object-fit: cover;
}

.locked-note {
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--muted);
}

.comment-item {
  position: relative;
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr);
  margin-left: calc(var(--depth) * 16px);
  padding: 6px 0;
}

.comment-line {
  width: 2px;
  min-height: 100%;
  border-radius: 999px;
  background: linear-gradient(180deg, #d8e1ee, #eef3f8);
}

.comment-header {
  display: flex;
  align-items: flex-start;
  gap: 9px;
}

.comment-header > div {
  display: grid;
  gap: 2px;
}

.comment-header .thread-author-button {
  justify-self: start;
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 900;
  line-height: 1.2;
}

.comment-header span {
  color: #6a7280;
  font-size: 12px;
  line-height: 1.3;
}

.comment-body {
  margin: 10px 0 12px;
  color: #1d2430;
  font-size: 15px;
  line-height: 1.58;
  /* Comments are escaped text with no <br> of their own, same as a post body — the
     post got pre-wrap and the reply under it did not, so a comment written across
     three lines still landed as one paragraph. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.comment-content {
  padding: 14px 16px;
  border: 1px solid #dfe6f0;
  border-radius: var(--radius-ui);
  background: var(--surface);
  box-shadow: 0 8px 18px color-mix(in srgb, var(--ink-strong-bg) 4%, transparent);
}

.comment-item[style*="--depth:1"] .comment-content,
.comment-item[style*="--depth:2"] .comment-content,
.comment-item[style*="--depth:3"] .comment-content,
.comment-item[style*="--depth:4"] .comment-content,
.comment-item[style*="--depth:5"] .comment-content {
  background: #fbfcfe;
  box-shadow: none;
}

.comment-thread {
  display: grid;
  gap: 9px;
}

.comment-content > .comment-thread {
  margin-top: 10px;
  padding-top: 2px;
}

.comment-actions {
  gap: 8px;
}

.comment-actions button {
  min-height: 26px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-ui);
  color: #4d596d;
  font-size: 12px;
  font-weight: 850;
}

.comment-actions button:hover {
  border-color: #d7e0ec;
  background: #f5f8fc;
}

.empty-comments {
  margin: 0;
  padding: 18px;
  border: 1px dashed #cfd8e6;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: #6a7280;
}

.notification-list {
  display: grid;
  gap: 8px;
  padding: 12px;
}

.notification-item,
.admin-row,
.log-row,
.mini-post {
  display: block;
  width: 100%;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  text-align: left;
}

.notification-item.is-unread {
  border-color: color-mix(in srgb, var(--brand) 28%, var(--line));
  background: var(--brand-weak);
}

.profile-page {
  max-width: 1100px;
  margin: 0 auto;
  padding-top: 24px;
}

.profile-hero {
  position: relative;
  overflow: hidden;
  padding: 0 24px 22px;
  border: 1px solid #d9e1ee;
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 16px 36px color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
}

/* 封面色带, 和用户预览弹窗那条同款; 颜色跟本校主色走。 */
.profile-hero-cover {
  height: 116px;
  margin: 0 -24px;
  /* 和 profile.js 的 COVER_PRESETS.default 同一支渐变 —— 那边是行内样式, 永远
     盖过这里, 所以这条只是行内样式缺席时的兜底; 两边必须同步改。 */
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 64%),
    linear-gradient(135deg, var(--brand-deep), var(--brand) 45%, var(--brand-bright));
}

.profile-hero-logout {
  position: absolute;
  top: 14px;
  right: 16px;
  z-index: 2;
  padding: 6px 14px;
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: var(--radius-ui);
  color: #ffffff;
  background: color-mix(in srgb, var(--ink-strong-bg) 26%, transparent);
  font-weight: 800;
}

.profile-hero-logout:hover,
.profile-hero-logout:focus-visible {
  background: color-mix(in srgb, var(--ink-strong-bg) 44%, transparent);
}

/* Avatar overlaps the cover; headings sit beside it, bottom-aligned. */
.profile-hero-identity {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 18px;
  align-items: end;
  margin-top: -44px;
}

.profile-hero-headings {
  min-width: 0;
  padding-bottom: 4px;
}

.profile-avatar-shell {
  position: relative;
  width: 96px;
  height: 96px;
  flex: 0 0 auto;
}

.profile-hero .avatar-large {
  width: 96px;
  height: 96px;
  border: 4px solid #ffffff;
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink-strong-bg) 18%, transparent);
}

.profile-avatar-edit-button {
  position: absolute;
  right: -5px;
  bottom: -5px;
  display: grid;
  width: 28px;
  height: 28px;
  place-items: center;
  border: 1px solid #cbd7e8;
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink-strong-deep);
  font-size: 13px;
  font-weight: 950;
  box-shadow: 0 8px 18px color-mix(in srgb, var(--ink-strong-bg) 16%, transparent);
}

.profile-avatar-edit-button:hover,
.profile-avatar-edit-button:focus-visible {
  border-color: var(--ink-strong-deep-bg);
  background: #eef3fb;
}

.profile-name-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.profile-hero h1 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 32px;
  line-height: 1.06;
}

.profile-name-row .verified-dot {
  width: 20px;
  height: 20px;
  font-size: 13px;
}

.profile-hero-bio {
  max-width: 680px;
  margin: 14px 0 0;
  color: #59667b;
  font-size: 15px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Loading skeleton for the profile page (avoids flashing the mock identity). */
.profile-skel {
  display: block;
  box-sizing: border-box;
  border-radius: var(--radius-ui);
  background: linear-gradient(90deg, var(--surface-muted), var(--surface-2), var(--surface-muted));
  background-size: 180% 100%;
  animation: skeleton-pulse 1.2s ease-in-out infinite;
}

.profile-skel-avatar {
  width: 96px;
  height: 96px;
  border: 4px solid #ffffff;
  border-radius: 50%;
}

.profile-skel-line {
  height: 14px;
}

.profile-hero-headings .profile-skel-line {
  margin-bottom: 8px;
}

.profile-skel-name {
  width: 180px;
  height: 30px;
}

.profile-meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}

.profile-meta-row span {
  padding: 5px 9px;
  border-radius: var(--radius-ui);
  background: #eef3fb;
  color: #34435b;
  font-size: 12px;
  font-weight: 850;
}

/* "Edit profile" toggle — the whole editor form is hidden until this is clicked. */
/* 「修改个人资料」和「账号与安全」两颗 disclosure 并排, 各自的面板向下展开。 */
.profile-hero-edit {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
}

.profile-edit-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border: 1px solid var(--button-bg);
  border-radius: var(--radius-ui);
  background: var(--button-bg);
  color: var(--button-ink);
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
}

.profile-edit-toggle:hover,
.profile-edit-toggle:focus-visible {
  filter: brightness(1.06);
}

.profile-edit-toggle.is-open {
  background: var(--surface);
  color: var(--brand);
}

/* 两颗按钮的标签固定不变, 开合只靠这个箭头转 180° + .is-open 的反白来表示 —— 并排
   时都改成"收起"就分不清哪颗对应哪块面板了。 */
.profile-toggle-caret {
  font-size: 11px;
  font-style: normal;
  line-height: 1;
  transition: transform 160ms ease;
}

.profile-edit-toggle.is-open .profile-toggle-caret {
  transform: rotate(180deg);
}

/* 账号与安全的四个动作：整行可点 + 分隔线的设置列表。原先是四个裸 .ghost-button
   (border:0、背景透明) 排在 .profile-verify-row 里, 看上去就是浮在卡片里的几段文字,
   点击区域也只有文字那么宽。颜色一律走 token, 暗色跟着 :root 的重定义走。 */
.account-safety-list {
  display: grid;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}

/* 分隔线跟外框用同一支 --line, 不用 --line-soft: 后者在暗色下被定成 #181a1b, 和
   --surface 是同一个值(tokens.css 里明写「matches --surface's real floor」), 拿它画
   行间线在暗色下等于没画, 四条会糊成一块。 */
.account-safety-item {
  border-bottom: 1px solid var(--line);
}

.account-safety-item:last-child {
  border-bottom: 0;
}

.account-safety-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 46px;
  padding: 0 14px;
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: 14px;
  font-weight: 800;
  text-align: left;
  cursor: pointer;
}

.account-safety-row:hover,
.account-safety-row:focus-visible {
  background: var(--surface-muted);
}

.account-safety-row[disabled] {
  cursor: default;
  opacity: 0.55;
}

.account-safety-row-label {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.account-safety-row-value,
.account-safety-row-go {
  color: var(--muted);
  font-style: normal;
  font-weight: 750;
}

.account-safety-row-value {
  font-size: 12px;
}

.account-safety-row-go {
  font-size: 15px;
  line-height: 1;
}

/* .is-danger 只写了 color 和一条 hover 底色, 两条都和上面同特异度而位置更靠前, 不在
   这里重申就会被 .account-safety-row 那两条整个中和掉 —— 危险动作会退回成普通行。 */
.account-safety-row.is-danger {
  color: var(--danger);
}

.account-safety-row.is-danger:hover,
.account-safety-row.is-danger:focus-visible {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}

.account-safety-note {
  margin: 0;
  padding: 0 14px 12px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 750;
  line-height: 1.45;
}

.profile-editor {
  gap: 14px;
  margin: 16px 0;
  padding: 0;
  background: transparent;
}

.profile-editor-card {
  display: grid;
  gap: 14px;
  padding: 18px;
  border: 1px solid #d9e1ee;
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 10px 24px color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
}

.profile-avatar-card {
  grid-template-columns: 1fr;
  gap: 16px;
}

.profile-avatar-edit-preview {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
  padding: 14px;
  border: 1px solid #e1e8f3;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
}

.profile-avatar-edit-preview strong {
  display: block;
  color: var(--ink-strong);
  font-size: 16px;
}

.profile-avatar-edit-preview span {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.45;
}

.profile-avatar-edit-controls {
  display: grid;
  grid-template-columns: minmax(220px, 0.9fr) minmax(0, 1.1fr);
  gap: 14px;
  align-items: end;
}

.profile-avatar-dropzone {
  display: grid;
  gap: 5px;
  min-height: 74px;
  align-content: center;
  border: 1px dashed #a9b8cc;
  border-radius: var(--radius-ui);
  padding: 12px 14px;
  background: var(--surface-2);
  color: var(--ink-strong);
  cursor: pointer;
}

.profile-avatar-dropzone:hover,
.profile-avatar-dropzone:focus-within {
  border-color: var(--ink-strong-deep-bg);
  background: #eef3fb;
}

.profile-avatar-dropzone strong {
  font-size: 14px;
}

.profile-avatar-dropzone span {
  color: #647084;
  font-size: 12px;
  font-weight: 750;
}

.profile-field-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.profile-field-head strong {
  color: var(--ink-strong);
  font-size: 17px;
}

.profile-field-head span {
  max-width: 360px;
  color: #647084;
  text-align: right;
  font-size: 12px;
  font-weight: 750;
  line-height: 1.45;
}

.profile-form-grid {
  display: grid;
  grid-template-columns: minmax(180px, 1.2fr) minmax(130px, 0.8fr) minmax(110px, 0.7fr) minmax(150px, 0.9fr) minmax(170px, 1fr);
  gap: 12px;
  /* Top-align so every field's input lines up on the same row. The username
     cell is taller (it carries the change-limit note under its input); with
     end-alignment that note dragged the other inputs down to match. */
  align-items: start;
}

.profile-basic-card > label {
  margin-top: 2px;
}

/* Value box for the "当前阶段" field. The label now sits above it (like every
   other field), so this box lines up with the inputs on the same row. */
.profile-stage-card {
  display: grid;
  align-content: center;
  min-height: 40px;
  padding: 8px 12px;
  border: 1px solid #cbd7e8;
  border-radius: var(--radius-sm);
  background: #eef4fc;
}

.profile-stage-card strong {
  color: var(--ink-strong);
  font-size: 14px;
  font-weight: 850;
}

.profile-field-note {
  color: #69768a;
  font-size: 11px;
  font-weight: 750;
  line-height: 1.35;
}

/* 资料保存失败的回执。用 --danger 而不是自己挑一支红: 全站的错误色是同一支。 */
.profile-save-error {
  margin: 0 0 -4px;
  border-radius: var(--radius-ui);
  padding: 9px 12px;
  background: color-mix(in srgb, var(--danger) 10%, var(--surface));
  color: var(--danger);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.5;
}

.profile-save-button {
  min-height: 42px;
}

/* Cover background picker */
.cover-preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.cover-swatch {
  width: 64px;
  height: 40px;
  padding: 0;
  border-radius: var(--radius-ui);
  border: 2px solid transparent;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink-strong-bg) 14%, transparent);
  cursor: pointer;
}

.cover-swatch.is-active {
  border-color: var(--brand);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 35%, #ffffff);
}

.cover-upload {
  display: grid;
  place-items: center;
  background-color: #eef3fb;
  background-size: cover;
  background-position: center;
  color: #6a7788;
  font-size: 20px;
  font-weight: 800;
}

.cover-remove-button {
  width: fit-content;
  color: #b91c1c;
  font-size: 13px;
}

/* School-email verification card */
.profile-verify-card {
  border: 1px solid #cfe0c9;
  /* 这里的浅绿字面量刻意保留: 暗色由文件末尾"Dark Reader 引擎盲区补丁"里的
     [data-theme="dark"] .profile-verify-card 整条覆盖成 var(--surface), 所以
     基础规则不需要为暗色让步。 */
  background: linear-gradient(180deg, #f4faf1, var(--surface));
}

.profile-verify-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

.profile-verify-row input {
  flex: 1 1 220px;
  min-width: 0;
}

.profile-verify-note {
  margin: 0;
  color: #2f7d32;
  font-size: 13px;
  font-weight: 700;
}

.profile-verify-note.is-error {
  color: #b91c1c;
}

.profile-whiteboard-section {
  margin: 16px 0;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.whiteboard-heading {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: 14px;
}

.whiteboard-heading h2 {
  margin: 0 0 6px;
  color: var(--ink-strong);
  font-size: 22px;
  line-height: 1.2;
}

.whiteboard-heading p {
  margin: 0;
  color: var(--muted);
}

.whiteboard-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.whiteboard-toolbar .ghost-button {
  border: 1px solid var(--line);
  color: var(--ink-strong);
  background: var(--surface-2);
}

.user-whiteboard {
  position: relative;
  min-height: 430px;
  overflow: hidden;
  border: 1px solid #d8dee9;
  border-radius: var(--radius-md);
  background:
    linear-gradient(rgba(255, 255, 255, 0.86), rgba(255, 255, 255, 0.86)),
    linear-gradient(#eef3fb 1px, transparent 1px),
    linear-gradient(90deg, #eef3fb 1px, transparent 1px);
  background-size: auto, 32px 32px, 32px 32px;
}

.user-whiteboard.is-editable {
  cursor: default;
}

.whiteboard-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  color: var(--muted);
  text-align: center;
}

.whiteboard-item {
  position: absolute;
  z-index: 1;
  min-width: 140px;
  border: 1px solid color-mix(in srgb, var(--ink-strong-bg) 14%, transparent);
  border-radius: var(--radius-ui);
  background: rgba(255, 255, 255, 0.95);
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
}

.whiteboard-item.is-dragging {
  z-index: 3;
  opacity: 0.88;
}

.whiteboard-text-item {
  padding: 14px;
}

.whiteboard-text-item p {
  margin: 0;
  color: #172033;
  font-size: 15px;
  line-height: 1.55;
  white-space: pre-wrap;
}

.whiteboard-image-item {
  overflow: hidden;
}

.whiteboard-image-item img {
  display: block;
  width: 100%;
  max-height: 240px;
  object-fit: cover;
}

.whiteboard-image-item a {
  color: inherit;
}

.whiteboard-link-hint {
  display: block;
  padding: 7px 10px;
  border-top: 1px solid var(--line);
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 700;
  text-decoration: none;
}

.whiteboard-item-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  padding: 7px;
  border-bottom: 1px solid color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
  background: rgba(248, 250, 252, 0.92);
}

.whiteboard-item-controls button {
  min-height: 24px;
  padding: 0 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 700;
}

.whiteboard-item-controls button[data-action="whiteboard-drag"] {
  cursor: grab;
}

.user-profile-modal {
  width: min(980px, calc(100vw - 48px));
  overflow: hidden;
  background: #f7f9fc;
}

.user-profile-summary {
  position: relative;
  display: grid;
  gap: 16px;
  padding: 0 22px 20px;
  border-bottom: 1px solid #e0e6ef;
  background: var(--surface);
}

.user-profile-cover {
  height: 118px;
  margin: 0 -22px;
  /* 和 profile.js 的 COVER_PRESETS.default 同一支渐变 —— 那边是行内样式, 永远
     盖过这里, 所以这条只是行内样式缺席时的兜底; 两边必须同步改。 */
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0) 64%),
    linear-gradient(135deg, var(--brand-deep), var(--brand) 45%, var(--brand-bright));
}

.user-profile-identity {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 16px;
  align-items: end;
  margin-top: -46px;
}

.user-profile-identity .avatar-large {
  width: 96px;
  height: 96px;
  border: 4px solid #ffffff;
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink-strong-bg) 18%, transparent);
}

.user-profile-summary h3 {
  margin: 0 0 6px;
  color: var(--ink-strong);
  font-size: 30px;
  line-height: 1.12;
}

/* 认证徽章要排除掉: 这条选择器是 (0,2,0), 压得过 .verified-dot 自己那条 (0,1,0)
   的 color: #fff —— 结果蓝底上是一枚 rgb(104,112,130) 的灰勾, 几乎看不见。
   只有这个弹窗有这条 span 规则, 所以信息流和个人主页那两处徽章一直是白的。 */
.user-profile-summary p,
.user-profile-summary span:not(.verified-dot) {
  color: var(--muted);
}

.user-profile-summary .eyebrow {
  width: fit-content;
  margin: 0 0 6px;
  border-radius: var(--radius-ui);
  padding: 3px 8px;
  background: #edf2f8;
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 900;
}

.user-profile-summary p:not(.eyebrow) {
  max-width: 680px;
  margin: 0;
  font-size: 15px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Close button floats over the cover now that the top title bar is gone. */
/* 封面右上角的操作簇：••• 菜单 + 关闭。定位从 .user-profile-close 上移到这个容器,
   两颗按钮才排得成一行(原来是关闭键自己 absolute)。 */
.user-profile-top-actions {
  position: absolute;
  top: 12px;
  right: 14px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 8px;
}

.user-profile-close {
  width: 34px;
  height: 34px;
  color: #ffffff;
  background: color-mix(in srgb, var(--ink-strong-bg) 32%, transparent);
}

.user-profile-close:hover,
.user-profile-close:focus-visible {
  background: color-mix(in srgb, var(--ink-strong-bg) 50%, transparent);
}

/* ••• 落在深色封面上, 所以要和关闭键同一副打扮(白字 + 半透明深底), 而不是
   .post-more-menu summary 那份给浅色卡片用的灰字。hover 的蓝字同理要盖掉。 */
.user-profile-more summary {
  width: 34px;
  height: 34px;
  color: #ffffff;
  background: color-mix(in srgb, var(--ink-strong-bg) 32%, transparent);
  font-size: 14px;
}

.user-profile-more summary:hover,
.user-profile-more summary:focus-visible {
  color: #ffffff;
  background: color-mix(in srgb, var(--ink-strong-bg) 50%, transparent);
}

.user-profile-more .post-more-popover {
  min-width: 132px;
}

/* Name + academic-stage pill on one line. */
.user-profile-name-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}

.user-profile-summary .user-profile-name-row h3 {
  margin: 0;
}

/* Slightly larger verified badge to sit next to the big profile name. */
.user-profile-name-row .verified-dot {
  width: 18px;
  height: 18px;
  font-size: 12px;
}

.user-profile-stage {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding: 4px 11px;
  background: color-mix(in srgb, #385898 14%, var(--surface));
  border: 1px solid color-mix(in srgb, #385898 32%, #ffffff);
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 800;
  white-space: nowrap;
}

.user-profile-stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
}

.user-profile-stats span {
  display: grid;
  gap: 3px;
  padding: 12px;
  border: 1px solid #e0e6ef;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
  font-size: 12px;
  font-weight: 800;
}

.user-profile-stats strong {
  color: var(--ink-strong);
  font-size: 18px;
  line-height: 1.1;
}

.user-profile-posts {
  display: grid;
  gap: 12px;
  padding: 20px 22px 24px;
}

.user-profile-section-head {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 12px;
}

.user-profile-section-head h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 22px;
}

.user-profile-section-head span {
  color: #6a7280;
  font-size: 13px;
  font-weight: 850;
}

.user-profile-post-list {
  display: grid;
  gap: 10px;
}

.user-profile-empty {
  margin: 0;
  padding: 18px;
  border: 1px dashed #cbd5e3;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: #6a7280;
}

.profile-columns {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  margin-top: 32px;
}

/* Match the hero card style (border + shadow) so top and bottom read as one system. */
.profile-columns section {
  padding: 16px;
  border: 1px solid #d9e1ee;
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 16px 36px color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
}

/* Use the hero's navy ink for section titles (default --text is charcoal). */
.profile-columns section h2 {
  color: var(--ink-strong);
}

.campus-info-main {
  padding: var(--topbar-height) 0 16px;
  background:
    linear-gradient(180deg, rgba(245, 247, 251, 0.92), #f5f7fb 420px),
    radial-gradient(circle at 78% 8%, color-mix(in srgb, var(--brand) 16%, transparent), transparent 32%);
}

.campus-info-page {
  /* Originally a deliberately tiny 12px-a-side gutter, to maximise usable width
     — the board scrolls horizontally, so more width means more source columns
     visible before scrolling (school/UMSU/GSA/Melbourne/future sources).
     quick task (260827T): widened to 28px a side to match .clubs-page, which is
     the widest content page and sets the house gutter. Measured before the
     change: campus-info 12px vs clubs 28px, which read as this one page's
     content being pinned closer to the sidebar than every other. The board's
     own horizontal scroll absorbs the 32px lost, so the "more columns" intent
     costs at most a fraction of one column. */
  width: calc(100% - 56px);
  max-width: 2600px;
  margin: 0 auto;
  color: #111827;
}

.campus-info-hero {
  position: relative;
  min-height: 220px;
  display: grid;
  align-items: center;
  overflow: hidden;
  padding: 34px 44px;
  background:
    linear-gradient(90deg, rgba(255, 219, 22, 0.98), rgba(255, 207, 0, 0.86)),
    radial-gradient(circle at 74% 35%, color-mix(in srgb, var(--brand) 18%, transparent), transparent 34%);
}

.campus-info-hero::before,
.campus-info-hero::after {
  content: "";
  position: absolute;
  inset: auto;
  pointer-events: none;
}

.campus-info-hero::before {
  right: 9%;
  top: -36px;
  width: 300px;
  height: 300px;
  border-radius: 50%;
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--brand) 92%, transparent), rgba(39, 53, 249, 0.58)),
    repeating-linear-gradient(45deg, transparent 0 18px, rgba(255, 255, 255, 0.18) 18px 20px);
  clip-path: polygon(18% 0, 100% 22%, 73% 100%, 0 82%);
  transform: rotate(8deg);
}

.campus-info-hero::after {
  right: 4%;
  bottom: 22px;
  width: 240px;
  height: 78px;
  background:
    linear-gradient(110deg, transparent 0 18%, color-mix(in srgb, var(--brand) 90%, transparent) 18% 40%, transparent 40% 49%, rgba(255, 255, 255, 0.72) 49% 56%, transparent 56%),
    linear-gradient(90deg, color-mix(in srgb, var(--brand) 12%, transparent), color-mix(in srgb, var(--brand) 24%, transparent));
}

.campus-hero-copy {
  position: relative;
  z-index: 1;
  max-width: 720px;
}

.campus-hero-copy span {
  display: inline-block;
  margin-bottom: 10px;
  padding: 4px 9px;
  background: #111827;
  color: #ffffff;
  font-size: 12px;
  font-weight: 800;
}

.campus-hero-copy h1 {
  max-width: 760px;
  margin: 0;
  color: #0a0c10;
  font-size: clamp(36px, 4.8vw, 68px);
  line-height: 0.96;
  letter-spacing: 0;
  font-weight: 900;
}

.campus-hero-copy p {
  max-width: 560px;
  margin: 16px 0 0;
  color: rgba(17, 24, 39, 0.78);
  font-size: 15px;
  line-height: 1.55;
}

.campus-hero-tabs {
  position: absolute;
  z-index: 2;
  top: 14px;
  left: 50%;
  display: flex;
  gap: 3px;
  padding: 5px;
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.78);
  transform: translateX(-50%);
}

.campus-hero-tabs button {
  min-width: 78px;
  min-height: 30px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 800;
}

.campus-hero-tabs button.is-active {
  background: var(--surface);
  box-shadow: 0 1px 8px color-mix(in srgb, var(--brand) 12%, transparent);
}

.campus-feature-row {
  display: grid;
  grid-template-columns: 1.1fr 1.1fr 1fr;
  gap: 16px;
  margin-top: 24px;
}

.campus-feature-card,
.campus-pick-card,
.campus-news-card,
.campus-question-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.campus-feature-card {
  overflow: hidden;
}

.campus-feature-art {
  min-height: 142px;
  border-bottom: 1px solid var(--line);
}

.campus-feature-copy {
  padding: 13px 15px 16px;
}

.campus-feature-copy span,
.campus-pick-body span {
  color: #c00f10;
  font-size: 12px;
  font-weight: 900;
}

.campus-feature-copy h2,
.campus-pick-body h3,
.campus-news-card h3 {
  margin: 6px 0 0;
  color: var(--ink-strong);
  line-height: 1.22;
}

.campus-feature-copy h2 {
  font-size: 19px;
}

.campus-feature-copy p,
.campus-pick-body p,
.campus-news-card p {
  margin: 8px 0 0;
  color: #526079;
  line-height: 1.45;
}

.campus-section {
  margin-top: 30px;
}

.campus-section > h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
  color: #111827;
  font-size: 22px;
  line-height: 1;
}

.campus-events-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin: 0 0 14px;
}

.campus-events-head h2 {
  margin: 0;
  color: #111827;
  font-size: 22px;
  line-height: 1;
}

.campus-events-head span {
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
}

/* Two-pane 校园信息 layout (Phase 4 TIME-01): events board LEFT (narrower),
   weekly student timetable RIGHT. Hero removed — this is now the first thing
   under the page header. Stacks to a single column, timetable BELOW board,
   at the board's existing ≤640px breakpoint (see that block below). */
.campus-info-layout {
  display: flex;
  /* stretch: the timetable pane matches the board pane's height exactly, so the
     two sides line up bottom-to-bottom instead of the timetable sticking out. */
  align-items: stretch;
  gap: 24px;
  margin-top: 28px;
  /* Anchor for the absolutely-positioned calendar reveal tab (collapsed state). */
  position: relative;
  /* Bound the whole layout to the viewport so the page itself never scrolls and
     both panes reach the bottom edge (16px gutter) instead of floating. Each pane
     scrolls internally. Top offset = main pad-top (--topbar-height, 68) + this
     margin (28) = 96; plus the 16px bottom gutter = 112. */
  height: calc(100vh - 112px); /* fallback for browsers without dvh */
  height: calc(100dvh - 112px);
  min-height: 0;
  /* Never hard-clip: if the fixed-px timetable grid is taller than the
     viewport-bound box (short window OR browser zoom-in), scroll to reach it
     instead of cutting content off unreachably. The max-height media query
     below fully releases the height lock on short viewports. */
  overflow-y: auto;
}

/* Short-viewport fallback: when the window is too short (or the page is zoomed
   in, which shrinks the CSS-px viewport height), the fixed-px timetable grid can
   no longer fit inside a viewport-locked box. Release the height lock so the
   whole page scrolls normally and nothing is clipped. Mirrors the desktop
   two-pane layout collapsing to stacked at the ≤640px width breakpoint. */
@media (max-height: 760px) {
  .campus-info-layout {
    height: auto;
    min-height: 0;
    overflow-y: visible;
  }
}

/* Board sizes to its columns (does not stretch to fill), so when only a few
   columns exist the timetable sits right beside them with no empty gap. When
   columns exceed the width, the board shrinks (min-width:0) and its row scrolls
   horizontally. */
.campus-info-board-pane {
  min-width: 0;
  flex: 0 1 auto;
}

/* Timetable grows to absorb ALL leftover width so it hugs the right edge (no
   whitespace), never below 320px. Flex column so the panel fills the stretched
   height; the grid inside flex-grows to that height (no internal scrollbar). */
.campus-info-timetable-pane {
  min-width: 320px;
  flex: 1 1 340px;
  display: flex;
  flex-direction: column;
  /* Anchor for the in-pane collapse (›) button. */
  position: relative;
}

.campus-info-board-pane .campus-board {
  margin-top: 0;
}

/* ── 我的日程表 collapse / reveal (two-pane desktop-tablet view only) ──────────
   On narrower windows the calendar pane eats into the events board and covers a
   column. These controls let the user fold the calendar away to a thin vertical
   tab on the right edge (board reclaims the width), then bring it back. Hidden by
   default; only wired up at ≥641px, since the ≤640px layout already switches
   board/calendar through its own tab bar. */
.campus-calendar-collapse,
.campus-calendar-reveal {
  display: none;
}

@media (min-width: 641px) {
  .campus-calendar-collapse {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--surface);
    color: #3a4a7a;
    font-size: 18px;
    font-weight: 800;
    line-height: 1;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
  }

  .campus-calendar-collapse:hover {
    background: var(--surface-muted, var(--surface-3));
    color: #001b4b;
  }

  /* Collapsed: drop the calendar pane and surface the reveal tab instead. */
  .campus-info-layout.is-calendar-collapsed .campus-info-timetable-pane {
    display: none;
  }

  .campus-info-layout.is-calendar-collapsed .campus-calendar-reveal {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    z-index: 2;
    padding: 16px 8px;
    border: 1px solid var(--line);
    border-right: none;
    border-radius: 10px 0 0 10px;
    background: var(--surface-muted, var(--surface-3));
    color: #001b4b;
    font-size: 13px;
    font-weight: 800;
    cursor: pointer;
    box-shadow: -2px 0 8px color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
    transition: background 0.15s ease;
  }

  .campus-info-layout.is-calendar-collapsed .campus-calendar-reveal:hover {
    background: #e2e7f1;
  }

  /* Upright stacking is right for 日程 — two CJK glyphs read naturally down the
     tab. It is wrong for "Schedule", which becomes an eight-letter tower. */
  .campus-info-layout.is-calendar-collapsed .campus-calendar-reveal span {
    writing-mode: vertical-rl;
    text-orientation: upright;
    letter-spacing: 2px;
  }

  /* English: lay the word out normally and let the tab be a short horizontal pill
     on the edge instead of a tall vertical one. */
  body.is-lang-en .campus-info-layout.is-calendar-collapsed .campus-calendar-reveal {
    padding: 8px 12px;
  }

  body.is-lang-en .campus-info-layout.is-calendar-collapsed .campus-calendar-reveal span {
    writing-mode: horizontal-tb;
    text-orientation: mixed;
    letter-spacing: 0.02em;
    white-space: nowrap;
  }
}

/* Weekly timetable panel: header + week nav, guest/empty prompts, and the
   Mon-Fri hour-row grid. See src/utils/timetableWeek.js for the geometry
   math this grid's inline styles (top/height/left/width) are computed from. */
.timetable-panel {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-muted, var(--surface-3));
  /* Fill the stretched pane height so the week grid can flex-grow to the bottom. */
  flex: 1;
  min-height: 0;
}

.timetable-week-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.timetable-week-head h2 {
  margin: 0;
  color: #111827;
  font-size: 16px;
  font-weight: 800;
}

.timetable-week-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}

.timetable-week-controls button {
  width: 28px;
  height: 28px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: #3a4a7a;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}

.timetable-week-controls button:hover {
  background: var(--brand, #001b4b);
  border-color: var(--brand, #001b4b);
  color: #ffffff;
}

.timetable-week-controls strong {
  min-width: 108px;
  text-align: center;
  color: #111827;
  font-size: 13px;
  font-weight: 800;
}

.timetable-guest-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px 14px;
  border: 1px dashed var(--line);
  border-radius: 12px;
  background: var(--surface);
  color: var(--muted);
  font-size: 13px;
  line-height: 1.4;
}

.timetable-empty-prompt {
  padding: 14px;
  border-radius: 12px;
  background: var(--surface);
  text-align: center;
}

.timetable-empty-prompt p {
  margin: 0 0 10px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.4;
}

.timetable-empty-actions {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}

.timetable-week-grid {
  display: grid;
  grid-template-columns: 44px repeat(7, minmax(0, 1fr));
  /* Single row that fills the panel's remaining height; the hour rows/labels
     inside flex-grow to share it, so the grid stretches to the board's height. */
  grid-template-rows: minmax(0, 1fr);
  flex: 1;
  min-height: 0;
  overflow-x: auto;
}

.timetable-time-col {
  display: flex;
  flex-direction: column;
}

.timetable-day-header {
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: #3a4a7a;
  font-size: 11px;
  font-weight: 800;
}

.timetable-day-header span {
  color: var(--muted);
  font-weight: 600;
}

.timetable-hour-label {
  flex: 1 1 0;
  min-height: 30px;
  padding-top: 2px;
  padding-right: 6px;
  color: var(--muted);
  font-size: 10px;
  text-align: right;
}

.timetable-day-track {
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--line);
}

/* Anchors .timetable-block's absolute (percentage) positioning. Flex column so
   its in-flow .timetable-hour-row children grow to fill the stretched height,
   and flex:1 so the column fills the day track. */
.timetable-day-col {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.timetable-hour-row {
  flex: 1 1 0;
  min-height: 30px;
  border-top: 1px solid color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
}

.timetable-block {
  position: absolute;
  min-height: 16px;
  padding: 4px 6px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--brand, #001b4b);
  color: #ffffff;
  font-size: 11px;
  line-height: 1.25;
}

.timetable-block strong {
  display: block;
  font-size: 11px;
  font-weight: 800;
}

.timetable-block span {
  display: block;
  opacity: 0.85;
}

/* Manual class blocks (04-02): per-subject colour via --block-color (set
   inline from subjectColour()), a hoverable delete control, and a subdued
   time line alongside the location line. */
.timetable-class-block {
  background: var(--block-color, var(--brand));
  cursor: pointer;
}

/* Saved-event blocks (04-03): a board event favourited via ☆ (CONTEXT D4),
   coloured by --block-color (tag-derived hash via the same subjectColour()
   used for classes) so it reads as a distinct "from the board" block next to
   manual classes, with its own hoverable unsave control. */
.timetable-saved-block {
  background: var(--block-color, #2735f9);
}

.timetable-block-time {
  font-weight: 700;
  opacity: 0.92;
}

.timetable-class-remove,
.timetable-saved-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  z-index: 2;
  width: 15px;
  height: 15px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.28);
  color: #ffffff;
  font-size: 11px;
  font-weight: 900;
  line-height: 1;
  opacity: 0;
  cursor: pointer;
  transition: opacity 140ms ease, background 140ms ease;
}

.timetable-class-block:hover .timetable-class-remove,
.timetable-class-block:focus-within .timetable-class-remove,
.timetable-saved-block:hover .timetable-saved-remove,
.timetable-saved-block:focus-within .timetable-saved-remove {
  opacity: 1;
}

.timetable-class-remove:hover,
.timetable-class-remove:focus-visible,
.timetable-saved-remove:hover,
.timetable-saved-remove:focus-visible {
  background: var(--danger, #d64545);
}

.timetable-add-trigger {
  min-height: 28px;
  padding: 0 12px;
  font-size: 12px;
}

/* Add/edit class form — echoes .calendar-custom-form's aesthetic (inputs,
   selects, submit, open/close chrome). */
.timetable-add-form {
  display: grid;
  gap: 7px;
  margin: 4px 0 2px;
  padding: 10px;
  border: 1px solid #dfe6f1;
  border-radius: var(--radius-ui);
  background: #fbfcff;
}

.timetable-add-form label {
  display: grid;
  gap: 4px;
  margin: 0;
  color: #536176;
  font-size: 10px;
  font-weight: 850;
}

.timetable-add-form input,
.timetable-add-form select {
  min-width: 0;
  height: 29px;
  padding: 0 8px;
  border: 1px solid #d9e0eb;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--brand);
  font-size: 11px;
  font-weight: 750;
}

.timetable-add-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7px;
}

.timetable-add-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.timetable-add-head span {
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 850;
}

.timetable-add-close {
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius-ui);
  background: color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
  color: #536176;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.timetable-add-close:hover {
  background: color-mix(in srgb, var(--ink-strong-bg) 12%, transparent);
  color: var(--ink-strong);
}

.timetable-add-form button[type="submit"] {
  min-height: 30px;
  margin-top: 1px;
  border: 1px solid #dce2ed;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--brand);
  font-size: 11px;
  font-weight: 900;
  cursor: pointer;
}

.timetable-add-form button[type="submit"]:hover {
  background: var(--brand, #001b4b);
  border-color: var(--brand, #001b4b);
  color: #ffffff;
}

.timetable-add-message {
  margin: 0;
  padding: 0 2px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.4;
}

/* Weekend/undated saved events that don't get a Mon-Fri column (04-03/WR-02
   fix) — a compact list below the grid so a persisted save is never
   invisible. */
.timetable-other-saved {
  margin-top: 4px;
  padding: 10px 12px;
  border: 1px dashed var(--line);
  border-radius: 12px;
  background: var(--surface);
}

.timetable-other-saved h3 {
  margin: 0 0 6px;
  color: #3a4a7a;
  font-size: 11px;
  font-weight: 800;
}

.timetable-other-saved-list {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.timetable-other-saved-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 8px;
  background: var(--surface-muted, var(--surface-3));
}

.timetable-other-saved-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  color: #111827;
  font-size: 12px;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timetable-other-saved-date {
  color: var(--muted);
  font-size: 11px;
  white-space: nowrap;
}

.timetable-other-saved-remove {
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-strong-bg) 10%, transparent);
  color: #536176;
  font-size: 11px;
  font-weight: 900;
  line-height: 1;
  cursor: pointer;
}

.timetable-other-saved-remove:hover,
.timetable-other-saved-remove:focus-visible {
  background: var(--danger, #d64545);
  color: #ffffff;
}

.campus-board {
  margin-top: 28px;
  /* Fill the board pane's (viewport-bounded) height as a flex column so the
     timefilter sits on top and the columns row flex-grows to the bottom. */
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
}

/* Desktop hides the mobile source-switch tabs; the ≤640px block re-enables them. */
.campus-board-tabs {
  display: none;
}

/* Trello-style board: fixed-width "list" columns, ~5 per page, horizontal scroll
   for any beyond that (GSA/Melbourne columns land here in later phases). */
.campus-board-row {
  display: flex;
  align-items: stretch;
  gap: 18px;
  /* Small — the horizontal scrollbar is now thin (6px). Keeping this tight lets
     the columns' bottoms line up with the timetable pane's bottom. */
  padding-bottom: 4px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  /* Grow to fill the board pane's remaining height (below the timefilter) so the
     columns reach the bottom; min-height:0 lets the flex child actually shrink. */
  flex: 1 1 auto;
  min-height: 0;
}

.campus-board-column {
  flex: 0 0 320px;
  width: 320px;
  min-width: 0;
  min-height: 240px;
  /* The row (align-items:stretch) gives every column the same height as the
     viewport-bounded board, so columns reach the bottom edge; the body scrolls
     internally. max-height:100% keeps a column from exceeding the row. */
  max-height: 100%;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-muted, var(--surface-3));
  scroll-snap-align: start;
}

.campus-board-column-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0;
  padding: 14px 16px 12px;
  color: #111827;
  font-size: 18px;
  font-weight: 800;
  line-height: 1.1;
  background: var(--surface-muted, var(--surface-3));
  border-bottom: 1px solid color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
  border-radius: 16px 16px 0 0;
}

.campus-board-column-count {
  flex: 0 0 auto;
  min-width: 24px;
  padding: 1px 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-strong-bg) 10%, transparent);
  color: #3a4a7a;
  font-size: 12px;
  font-weight: 800;
  text-align: center;
}

/* Each column scrolls independently; the head stays pinned. */
.campus-board-column-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px;
}

/* Thin, invisible-until-touched scrollbars for the board.
   NOTE: `scrollbar-width`/`scrollbar-color` are INHERITED — an ancestor sets
   `scrollbar-width: thin` (grey thumb) which cascades in AND disables any
   `::-webkit-scrollbar` styling we'd try. So we override the standard properties
   here instead: transparent track+thumb at rest (scrollbar effectively invisible)
   and a faint navy thumb only while the column body / horizontal row is hovered
   or actively scrolled. Works in both Chrome (121+) and Firefox. */
.campus-board-column-body,
.campus-board-row {
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color 0.2s ease;
}

.campus-board-column-body:hover,
.campus-board-column-body:active,
.campus-board-row:hover,
.campus-board-row:active {
  scrollbar-color: color-mix(in srgb, var(--ink-strong) 32%, transparent) transparent;
}

.campus-board-column-loading {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.campus-board-column-loading-bar {
  display: block;
  height: 96px;
  border-radius: var(--radius-md);
  background: linear-gradient(90deg, var(--surface-muted), var(--surface-2), var(--surface-muted));
  background-size: 180% 100%;
  animation: skeleton-pulse 1.2s ease-in-out infinite;
}

.campus-board-column-loading p {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  text-align: center;
}

.campus-board-column-empty {
  padding: 20px;
  border-radius: var(--radius-md);
  background: var(--surface-muted);
  text-align: center;
}

.campus-board-column-empty strong {
  display: block;
  color: #111827;
  font-size: 13px;
  font-weight: 800;
}

.campus-board-column-empty span {
  display: block;
  margin-top: 6px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.4;
}

.campus-event-card {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.campus-event-card:hover,
.campus-event-card:focus-within {
  transform: translateY(-2px);
  box-shadow: 0 14px 30px color-mix(in srgb, var(--ink-strong-bg) 16%, transparent);
}

.campus-event-card.is-saved {
  border-color: #f2b807;
  box-shadow: 0 0 0 2px rgba(242, 184, 7, 0.4), 0 8px 20px color-mix(in srgb, var(--ink-strong-bg) 10%, transparent);
}

.campus-event-link {
  position: relative;
  display: grid;
  grid-template-rows: auto 1fr;
  overflow: hidden;
  border-radius: 14px;
  color: var(--ink-strong);
  text-decoration: none;
}

/* Star-to-save-to-timetable button — sibling of the card link (not nested, so the
   anchor stays valid). Repurposed from the old pin-to-top star (CONTEXT D4); the
   class name is kept for style continuity, the meaning changed. */
.campus-event-pin {
  position: absolute;
  right: 8px;
  top: 8px;
  z-index: 2;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.94);
  box-shadow: 0 1px 5px color-mix(in srgb, var(--ink-strong-bg) 20%, transparent);
  color: #9aa3b2;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: color 0.12s ease, transform 0.12s ease, opacity 0.12s ease;
}

/* Save star: revealed only on card hover/focus, or when saved/keyboard-focused (a11y). */
.campus-event-card:hover .campus-event-pin,
.campus-event-card:focus-within .campus-event-pin,
.campus-event-pin:focus-visible,
.campus-event-pin.is-saved {
  opacity: 1;
}

/* Touch devices have no hover state — keep the star reachable there. */
@media (hover: none) {
  .campus-event-pin {
    opacity: 1;
  }
}

.campus-event-pin:hover {
  color: #f2b807;
  transform: scale(1.08);
}

.campus-event-pin.is-saved {
  color: #f2b807;
}

.campus-event-image {
  display: block;
  width: 100%;
  /* Adaptive media: each image keeps its own aspect ratio — width fills the card,
     height follows naturally (no fixed frame, no cropping). */
  height: auto;
  background: var(--surface-muted);
}

.campus-event-tag {
  justify-self: start;
  width: fit-content;
  padding: 3px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ink-strong-deep-bg) 90%, transparent);
  color: #ffffff;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* Melbourne "What's On" cards show their full tag set — flow the chips in a
   wrapping row instead of stacking them as separate grid rows. */
.campus-event-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  justify-self: start;
}

.campus-event-card--no-image .campus-event-link {
  display: block;
}

.campus-event-card--no-image .campus-event-tag {
  position: static;
  justify-self: start;
  align-self: start;
  margin-bottom: 2px;
}

.campus-event-card--no-image .campus-event-body {
  padding-right: 46px;
}

.campus-event-body {
  display: grid;
  gap: 7px;
  padding: 14px 15px 15px;
}

.campus-event-body strong {
  display: -webkit-box;
  overflow: hidden;
  font-size: 16px;
  font-weight: 800;
  line-height: 1.35;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

.campus-event-when {
  color: #2a4bb0;
  font-size: 12px;
  font-weight: 800;
}

/* "by <club>" line on UMSU club cards */
.campus-event-org {
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

/* Board time-window filter pills */
.campus-board-timefilter {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 14px;
}

.campus-board-timefilter button {
  padding: 5px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}

.campus-board-timefilter button:hover {
  border-color: #2a4bb0;
  color: #2a4bb0;
}

.campus-board-timefilter button.is-active {
  background: var(--ink-strong-deep-bg);
  border-color: var(--ink-strong-deep-bg);
  color: #ffffff;
}

.campus-event-loc {
  display: -webkit-box;
  overflow: hidden;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.4;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* Mobile: collapse the board into one active column with a sticky source-switch
   tab bar (re-authored — restores the BORD-04 behaviour). */
@media (max-width: 640px) {
  /* Single column. The timetable is NOT stacked below the board on mobile —
     instead it's a tab in the source-switch bar (data-tab="timetable"), so the
     board and calendar never fight for space. The active tab drives which pane
     shows via [data-mobile-pane] on .campus-info-layout. */
  .campus-info-layout {
    display: block;
    /* Undo the desktop viewport-bounded height — the stacked mobile layout must
       size to its content and scroll the page normally. */
    height: auto;
    min-height: 0;
    /* ...and hand scrolling back to the page. Left at the desktop overflow-y:auto
       this pane stays a scroll container, which makes IT the sticky scrollport for
       .campus-board-tabs below: the tabs then get shifted a whole --topbar-height
       DOWN inside the pane (a dead band under the topbar, tabs overlapping the
       time filter) instead of sticking under the fixed topbar as the page scrolls. */
    overflow-y: visible;
    /* Desktop's breathing room under the header is dead space on a phone — the
       sticky tab bar already carries its own 8px padding. */
    margin-top: 0;
  }
  /* Match: the board is a normal block on mobile (columns use max-height:none
     below), not the desktop flex-fill column. */
  .campus-board {
    display: block;
    height: auto;
  }
  /* Timetable hidden unless its tab is selected; board hidden when it is.
     Reset the desktop flex + sticky sizing so the pane fills the phone width. */
  .campus-info-timetable-pane {
    display: none;
    margin-top: 0;
    min-width: 0;
    max-width: none;
    position: static;
    max-height: none;
    overflow: visible;
  }
  .campus-info-layout[data-mobile-pane="timetable"] .campus-info-timetable-pane {
    display: block;
  }
  .campus-info-layout[data-mobile-pane="timetable"] .campus-board-row,
  .campus-info-layout[data-mobile-pane="timetable"] .campus-board-timefilter {
    display: none;
  }
  /* On mobile the layout is block (no stretched height to fill), so give the
     grid fixed-height rows instead of flex-grow ones — blocks are % of the
     resulting definite day-column height, so they still position correctly. */
  .timetable-week-grid {
    grid-template-rows: none;
    flex: none;
  }
  .timetable-hour-label,
  .timetable-hour-row {
    flex: none;
    height: 56px;
    min-height: 0;
  }
  .timetable-day-col {
    flex: none;
    min-height: 0;
  }
  .campus-board-tabs {
    position: sticky;
    top: var(--topbar-height, 56px);
    z-index: 3;
    display: flex;
    gap: 6px;
    margin-bottom: 14px;
    padding: 8px 0;
    overflow-x: auto;
    background: var(--canvas, #f5f7fb);
    scrollbar-width: none;
  }
  .campus-board-tabs::-webkit-scrollbar {
    display: none;
  }
  .campus-board-tabs button {
    flex: 0 0 auto;
    padding: 8px 16px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--surface);
    color: #3a4a7a;
    font-size: 14px;
    font-weight: 800;
    white-space: nowrap;
  }
  .campus-board-tabs button.is-active {
    background: #001b4b;
    border-color: #001b4b;
    color: #ffffff;
  }
  .campus-board-row {
    display: block;
    overflow: visible;
    padding-bottom: 0;
  }
  .campus-board-column {
    display: none;
    flex: none;
    width: 100%;
    min-height: 0;
    max-height: none;
  }
  .campus-board-column.is-active {
    display: flex;
  }
  .campus-board-column-head {
    position: static;
  }
  .campus-board-column-body {
    overflow: visible;
  }
}

.campus-pick-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 16px;
}

.campus-pick-card {
  overflow: hidden;
}

.campus-pick-visual {
  aspect-ratio: 16 / 9;
  border-bottom: 1px solid var(--line);
}

.campus-pick-body {
  display: grid;
  gap: 8px;
  padding: 14px 16px 16px;
}

.campus-pick-body h3 {
  font-size: 17px;
}

.campus-pick-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 16px;
  color: #8a93a6;
  font-size: 12px;
}

.campus-pick-meta strong {
  color: var(--ink-strong);
}

.campus-news-strip {
  display: grid;
  grid-template-columns: repeat(6, minmax(180px, 1fr));
  gap: 14px;
}

.campus-news-card {
  overflow: hidden;
  background: transparent;
  border-color: transparent;
}

.campus-news-visual {
  aspect-ratio: 16 / 9;
  margin-bottom: 10px;
  border-radius: var(--radius-ui);
  border: 1px solid var(--line);
}

.campus-news-card h3 {
  min-height: 42px;
  font-size: 15px;
}

.campus-news-card p {
  color: #8a93a6;
  font-size: 12px;
}

.campus-question-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
}

.campus-question-card {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px 12px;
  min-height: 74px;
  padding: 16px 18px;
  color: var(--ink-strong);
  text-align: left;
}

.campus-question-card span {
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-ui);
  background: #dbe7f8;
  color: var(--ink-strong);
  font-weight: 900;
}

.campus-question-card strong {
  font-size: 15px;
}

.campus-question-card em {
  grid-column: 2;
  color: #8a93a6;
  font-size: 12px;
  font-style: normal;
}

.campus-visual-library .campus-feature-art,
.campus-visual-library.campus-pick-visual,
.campus-news-1 {
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--brand) 86%, transparent), rgba(13, 77, 139, 0.7)),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.16) 0 2px, transparent 2px 18px);
}

.campus-visual-calendar .campus-feature-art,
.campus-visual-calendar.campus-pick-visual,
.campus-news-2 {
  background:
    linear-gradient(135deg, #ffde17, #f6b800),
    radial-gradient(circle at 72% 28%, color-mix(in srgb, var(--brand) 28%, transparent), transparent 26%);
}

.campus-visual-campus .campus-feature-art,
.campus-visual-campus.campus-pick-visual,
.campus-news-3 {
  background:
    linear-gradient(120deg, color-mix(in srgb, var(--brand) 8%, transparent) 0 38%, color-mix(in srgb, var(--brand) 78%, transparent) 38% 58%, rgba(255, 255, 255, 0.78) 58% 66%, color-mix(in srgb, var(--ink-strong) 78%, transparent) 66%),
    #dfe7f3;
}

.campus-visual-study,
.campus-news-4 {
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--ink-strong) 84%, transparent), rgba(63, 99, 166, 0.74)),
    radial-gradient(circle at 18% 38%, rgba(255, 255, 255, 0.72), transparent 18%);
}

.campus-visual-rent {
  background:
    linear-gradient(120deg, #e9edf4, #ffffff),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--ink-strong) 18%, transparent) 0 1px, transparent 1px 18px);
}

.campus-visual-clubs {
  background:
    linear-gradient(135deg, var(--brand), var(--brand-bright)),
    radial-gradient(circle at 70% 28%, rgba(255, 219, 22, 0.78), transparent 16%);
}

.campus-visual-safety {
  background:
    linear-gradient(135deg, #f7f9fd, #dbe7f8),
    radial-gradient(circle at 78% 26%, rgba(192, 15, 16, 0.36), transparent 18%);
}

.discussion-main {
  min-height: 100vh; /* fallback */
  min-height: 100dvh;
  padding: calc(var(--topbar-height) + 18px) 16px 56px;
  background: linear-gradient(180deg, #eef3f8 0%, #f7f9fc 46%, #edf2f7 100%);
}

.discussion-page {
  width: min(1720px, 100%);
  margin: 0 auto;
  display: grid;
  gap: 14px;
}

.discussion-hero {
  display: grid;
  grid-template-columns: minmax(300px, 0.24fr) minmax(0, 1fr);
  align-items: stretch;
  gap: 14px;
  padding: 12px;
  border: 1px solid #d8e0eb;
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.discussion-copy {
  min-height: clamp(420px, 31vw, 520px);
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 24px;
  border-radius: var(--radius-ui);
  background: linear-gradient(150deg, var(--brand), var(--brand-deep));
  color: #ffffff;
}

.discussion-copy span {
  color: rgba(255, 255, 255, 0.7);
  font-size: 12px;
  font-weight: 800;
}

.discussion-copy h1 {
  margin: 10px 0 12px;
  color: #ffffff;
  font-size: clamp(42px, 5vw, 72px);
  line-height: 0.96;
  letter-spacing: 0;
}

.discussion-copy p {
  max-width: 330px;
  margin: 0 0 18px;
  color: rgba(255, 255, 255, 0.78);
  font-size: 14px;
  line-height: 1.65;
}

.discussion-pin-button,
.discussion-section-header button {
  width: fit-content;
  min-height: 32px;
  padding: 0 14px;
  border: 1px solid rgba(255, 255, 255, 0.46);
  border-radius: var(--radius-ui);
  background: rgba(255, 255, 255, 0.08);
  color: #ffffff;
  font-size: 13px;
  font-weight: 800;
}

.discussion-map-card {
  display: grid;
  grid-template-rows: 1fr auto;
  gap: 8px;
  padding: 8px;
  border: 1px solid #dbe3ee;
  border-radius: var(--radius-ui);
  background: linear-gradient(180deg, #e6f0fb, #f9fbfe);
}

.origin-map {
  position: relative;
  width: 100%;
  min-height: 0;
  height: auto;
  aspect-ratio: 2000 / 857;
  overflow: hidden;
  border-radius: var(--radius-ui);
  border: 1px solid #dbe3ee;
  background: var(--surface);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

.origin-map.is-dragging {
  cursor: grabbing;
}

.origin-map:focus-visible {
  outline: 2px solid var(--ink-strong);
  outline-offset: 3px;
}

.origin-map-stage {
  position: absolute;
  inset: 0;
}

.origin-map-loading {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  color: #526079;
  font-size: 13px;
  font-weight: 800;
}

.origin-map-image,
.origin-map-svg {
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  shape-rendering: geometricPrecision;
  text-rendering: geometricPrecision;
  fill: #a8adb2;
  stroke: rgba(255, 255, 255, 0.96);
  stroke-width: 0.55px;
}

.origin-map-svg path,
.origin-map-svg polygon,
.origin-map-svg polyline {
  vector-effect: non-scaling-stroke;
}

.origin-map-image {
  object-fit: fill;
}

.origin-map-pins {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.origin-image-pin {
  position: absolute;
  z-index: 1;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 2px solid #ffffff;
  border-radius: 999px 999px 999px 4px;
  background: var(--ink-strong-bg);
  color: #ffffff;
  font-size: 12px;
  font-weight: 900;
  box-shadow: 0 8px 18px color-mix(in srgb, var(--ink-strong-bg) 24%, transparent);
  transform: translate(-50%, -100%) rotate(-45deg);
  pointer-events: none;
  will-change: left, top;
}

.origin-image-pin span {
  transform: rotate(45deg);
}

.origin-image-pin.is-current {
  background: #c00f10;
  box-shadow: 0 8px 20px rgba(192, 15, 16, 0.28);
}

.origin-map-controls {
  position: absolute;
  z-index: 3;
  top: 12px;
  right: 12px;
  display: flex;
  gap: 6px;
}

.origin-map-controls button {
  min-width: 34px;
  min-height: 30px;
  padding: 0 10px;
  border: 1px solid #d6deea;
  border-radius: var(--radius-ui);
  background: rgba(255, 255, 255, 0.9);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 900;
}

.origin-map-controls button:hover {
  background: #eef3f8;
}

.discussion-map-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 28px;
  color: #526079;
  font-size: 13px;
}

.discussion-map-meta strong {
  color: var(--ink-strong);
  font-size: 16px;
}

.discussion-question-section {
  display: grid;
  gap: 12px;
  padding: 14px;
  border: 1px solid #d8e0eb;
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.discussion-content-grid {
  display: grid;
  grid-template-columns: minmax(340px, 0.9fr) minmax(0, 1.1fr);
  align-items: start;
  gap: 14px;
}

.discussion-column {
  min-width: 0;
  display: grid;
  gap: 12px;
}

.discussion-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.discussion-section-header span {
  color: var(--ink-strong);
  font-size: 18px;
  font-weight: 900;
}

.discussion-section-header button {
  border-color: #d5deea;
  background: var(--canvas);
  color: var(--ink-strong);
}

.discussion-poll-card {
  padding: 18px;
  border: 1px solid #d8e0eb;
  border-radius: var(--radius-ui);
  background: linear-gradient(180deg, #f7fbff, #eef5fc);
  color: var(--ink-strong);
}

.discussion-poll-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: 8px;
}

.discussion-poll-header h2 {
  margin: 0 0 10px;
  color: var(--ink-strong);
  font-size: 20px;
  line-height: 1.35;
}

.discussion-poll-header span,
.discussion-poll-prompt {
  color: #526079;
  font-size: 13px;
  font-weight: 800;
}

.discussion-poll-prompt {
  margin: 0 0 14px;
}

.discussion-thread-link {
  flex: 0 0 auto;
  min-height: 32px;
  padding: 0 12px;
  border: 1px solid #cfd9e7;
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 900;
}

.discussion-thread-link:hover {
  border-color: var(--ink-strong-bg);
  background: #eef3f8;
}

.discussion-poll-options {
  display: grid;
  gap: 8px;
}

/* 讨论页的投票和信息流里那份 (.post-poll-option) 是两个组件, 但对用户是同一件
   东西 —— 描边一起跟 --hero-accent 走, 免得两处颜色分叉。 */
.discussion-poll-option {
  position: relative;
  min-height: 46px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--hero-accent) 45%, var(--line-card));
  border-radius: var(--radius-ui);
  padding: 0 14px;
  background: var(--surface);
  color: var(--ink-strong);
  text-align: left;
  font-size: 15px;
  font-weight: 900;
}

.discussion-poll-option::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--poll-percent, 0%);
  background: color-mix(in srgb, var(--hero-accent) 14%, transparent);
}

.discussion-poll-option span,
.discussion-poll-option strong {
  position: relative;
  z-index: 1;
}

.discussion-poll-option:hover,
.discussion-poll-option.is-selected {
  border-color: var(--hero-accent);
  background: color-mix(in srgb, var(--hero-accent) 6%, var(--surface));
}

.discussion-question-list {
  display: grid;
  gap: 12px;
}

.discussion-question-card {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 12px;
  width: 100%;
  padding: 16px;
  border: 1px solid #e0e6ef;
  border-radius: var(--radius-ui);
  background: var(--surface-2);
  color: var(--ink-strong);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.discussion-question-card:hover {
  border-color: #b9c7d8;
  background: var(--surface);
}

.discussion-rank {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-ui);
  background: var(--ink-strong-bg);
  color: #ffffff;
  font-weight: 900;
}

.discussion-question-card h2 {
  margin: 0 0 7px;
  color: var(--ink-strong);
  font-size: 17px;
  line-height: 1.32;
}

.discussion-question-card p {
  margin: 0 0 10px;
  color: #526079;
  font-size: 13px;
  line-height: 1.55;
}

.discussion-question-card small {
  color: #7c8798;
  font-size: 12px;
}

/* Game-squad is a full-viewport embed that keeps the dark app topbar (like every
   other view) so the blue bar runs edge-to-edge across the top. The embed sits
   just below the fixed topbar and the whole area is locked to the viewport with
   no outer scroll — the embed scrolls internally. Backgrounds use the embed's own
   #f7f8fb so the chrome around it reads as one continuous surface. */
.game-squad-main {
  /* Native lobby (no iframe): a viewport-tall scroll container offset below the
     fixed topbar, so the SquadPool-style card grid scrolls inside it. */
  height: 100vh; /* fallback for browsers without dvh */
  height: 100dvh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--topbar-height) 0 0;
  background: #f7f8f5;
}

.game-squad-page {
  width: 100%;
  margin: 0;
  background: #f7f8f5;
}

.squadpool-frame-shell {
  position: relative;
  overflow: hidden;
  /* Edge-to-edge, so no rounded card corners that would reveal the page bg. */
  border-radius: 0;
  background: #f7f8fb;
}

/* The persistent embed host is a fixed body-child overlay (parked outside #app so
   re-renders never reload the iframe). Counter the phone breakpoint's root
   zoom:0.88 with the inverse here, so the host's NET zoom is 1.0: the SquadPool
   iframe then lives in a single, un-nested zoom layer (only the embed's own
   zoom:1.21), which is what iOS needs to stop miscomputing the embed viewport and
   leaving a right/bottom gutter. The wall's own chrome (topbar/bottom-nav) keeps
   the 0.88 zoom, so entering this view no longer makes the UI jump. On desktop
   --ui-zoom-inv is unset → 1 → net zoom stays 1, a no-op. positionSquadPoolHost()
   sizes/places this host in painted (visual) px to match its 1:1 rendering. */
.squadpool-persistent-host {
  zoom: var(--ui-zoom-inv, 1);
}

.squadpool-frame {
  width: 100%;
  height: 100%;
  border: 0;
  background: #f7f8fb;
}

/* On the game-squad view, keep the dark global topbar (same as every other view)
   and lock the page to the viewport so no outer scrollbar appears — the embed
   handles its own scrolling. Nav stays reachable via the sidebar, topbar, and the
   mobile bottom nav. */
body.is-game-squad-view {
  overflow: hidden;
}

.game-squad-auth {
  place-items: center;
  padding: 24px;
}

.game-squad-auth > div {
  width: min(520px, 100%);
  display: grid;
  gap: 14px;
  padding: 28px;
  border: 1px solid #d8e0eb;
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.game-squad-auth span {
  color: #526079;
  font-size: 13px;
  font-weight: 800;
}

.game-squad-auth h1 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 28px;
  line-height: 1.2;
}

.game-squad-auth p {
  margin: 0;
  color: #526079;
  font-size: 14px;
  line-height: 1.6;
}

.faculty-main {
  min-height: 100vh; /* fallback */
  min-height: 100dvh;
  padding: calc(var(--topbar-height) + 18px) 24px 34px;
  background:
    radial-gradient(circle at 18% 12%, rgba(98, 105, 118, 0.12), transparent 31%),
    radial-gradient(circle at 84% 18%, rgba(105, 112, 104, 0.08), transparent 28%),
    linear-gradient(135deg, #15171b 0%, #171b22 46%, #101216 100%);
}

.faculty-page {
  position: relative;
  width: min(1540px, 100%);
  margin: 0 auto;
  color: #f3f6ff;
}

.faculty-detail-main {
  background:
    radial-gradient(circle at 16% 10%, color-mix(in srgb, var(--ink-strong) 4.5%, transparent), transparent 32%),
    linear-gradient(180deg, #f4f6fa 0%, #eef2f7 100%);
}

.faculty-detail-main .faculty-page {
  color: var(--ink-strong);
}

.faculty-select-page {
  position: relative;
  min-height: calc(100vh - var(--topbar-height) - 52px); /* fallback */
  min-height: calc(100dvh - var(--topbar-height) - 52px);
  display: grid;
}

.faculty-select-page::before {
  content: "";
  position: fixed;
  inset: var(--topbar-height) 0 0 var(--sidebar-width);
  pointer-events: none;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
  background-size: 42px 42px;
  mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.72), transparent 86%);
}

.faculty-page-header {
  display: grid;
  gap: 8px;
  margin-bottom: 22px;
}

.faculty-page-header h1 {
  margin: 0;
  color: var(--ink-strong);
  font-size: clamp(34px, 4vw, 58px);
  line-height: 1.02;
  letter-spacing: 0;
}

.faculty-page-header p:not(.eyebrow) {
  max-width: 620px;
  margin: 0;
  color: #526079;
  font-size: 15px;
  line-height: 1.6;
}

.faculty-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(var(--fac-cols, 3), minmax(0, 1fr));
  grid-template-rows: repeat(var(--fac-rows, 3), var(--fac-row-size, minmax(0, 1fr)));
  gap: 20px;
  min-height: 100%;
  align-content: start;
}

.faculty-card {
  position: relative;
  z-index: 1;
  min-height: 240px;
  display: grid;
  grid-template-rows: auto 1fr auto auto;
  gap: 16px;
  padding: 28px;
  border: 1px solid color-mix(in srgb, var(--faculty-text, #f3f6ff) 34%, transparent);
  border-radius: var(--radius-ui);
  background: var(--faculty-theme, var(--surface));
  color: var(--faculty-text, var(--ink-strong));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 18px 40px rgba(0, 0, 0, 0.28);
  text-align: left;
  transition:
    background 140ms ease,
    border-color 140ms ease,
    color 140ms ease,
    transform 140ms ease;
}

.faculty-card:hover {
  border-color: color-mix(in srgb, var(--faculty-hover-text, #ffffff) 54%, transparent);
  background: var(--faculty-hover-theme, var(--surface));
  color: var(--faculty-hover-text, var(--ink-strong));
  transform: translateY(-2px);
}

.faculty-card:hover .faculty-index,
.faculty-card:hover strong,
.faculty-card:hover em,
.faculty-card:hover p,
.faculty-card:hover .faculty-review-count {
  color: var(--faculty-hover-text, var(--ink-strong));
}

.faculty-review-count {
  position: absolute;
  right: 22px;
  bottom: 18px;
  display: flex;
  align-items: baseline;
  gap: 8px;
  color: var(--faculty-text, var(--ink-strong));
  pointer-events: none;
}

.faculty-review-count .frc-num {
  font-size: clamp(52px, 5.2vw, 88px);
  font-weight: 950;
  line-height: 0.82;
  font-style: italic;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  /* Faint glow in the faculty accent so the numeral reads as a design accent,
     not just a counter. */
  text-shadow: 0 6px 26px color-mix(in srgb, var(--faculty-text, var(--ink-strong)) 45%, transparent);
}

.faculty-review-count .frc-label {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0.72;
}

/* Reserve corner room so long scope text never collides with the big numeral. */
.faculty-card p {
  padding-right: 96px;
}

.faculty-index {
  color: var(--faculty-text, var(--ink-strong));
  font-size: 15px;
  font-weight: 900;
}

.faculty-card strong {
  max-width: 96%;
  color: var(--faculty-text, var(--ink-strong));
  font-size: clamp(32px, 3.1vw, 54px);
  line-height: 1.04;
  font-weight: 950;
  overflow-wrap: break-word;
  word-break: break-word;
  hyphens: auto;
}

/* Wide grids (monash's 5 columns) have much narrower cards than unimelb's 3, so
   the big display type is dialled down and the corner room reserved for the
   review numeral is reclaimed, keeping long names inside the card. */
.faculty-grid--wide .faculty-card {
  min-height: 0;
  padding: 20px;
  gap: 10px;
}

.faculty-grid--wide .faculty-card strong {
  font-size: clamp(18px, 1.4vw, 24px);
  line-height: 1.14;
  max-width: 100%;
}

.faculty-grid--wide .faculty-card p {
  padding-right: 0;
}

.faculty-grid--wide .faculty-review-count .frc-num {
  font-size: clamp(30px, 2.6vw, 46px);
}

.faculty-card em {
  color: var(--faculty-text, var(--ink-strong));
  font-size: 16px;
  font-style: normal;
  font-weight: 900;
}

.faculty-card p {
  margin: 0;
  color: var(--faculty-text, var(--ink-strong));
  font-size: 16px;
  line-height: 1.45;
}

.faculty-detail-page {
  display: grid;
  gap: 18px;
}

.faculty-back-button {
  justify-self: start;
  min-height: 34px;
  border: 1px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 28%, var(--line-strong));
  border-radius: var(--radius-ui);
  padding: 0 12px;
  background: var(--surface);
  color: var(--ink-strong);
  font-weight: 800;
}

.faculty-detail-page .faculty-back-button:hover {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 12%, var(--surface));
}

.faculty-detail-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  padding: 28px 34px;
  border: 1px solid color-mix(in srgb, var(--faculty-detail-text, var(--ink-strong-bg)) 24%, transparent);
  border-left: 8px solid var(--faculty-detail-text, var(--ink-strong-bg));
  border-radius: var(--radius-ui);
  background: var(--faculty-detail-theme, var(--surface));
  color: var(--faculty-detail-text, var(--ink-strong));
}

.faculty-detail-hero > span {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border-radius: var(--radius-ui);
  border: 1px solid color-mix(in srgb, var(--faculty-detail-text, var(--ink-strong-bg)) 52%, transparent);
  background: var(--faculty-detail-text, var(--ink-strong-bg));
  color: var(--faculty-detail-theme, #ffffff);
  font-size: 28px;
  font-weight: 900;
}

.faculty-detail-hero h1 {
  margin: 0;
  color: var(--faculty-detail-text, var(--ink-strong));
  font-size: clamp(30px, 4vw, 52px);
  line-height: 1.05;
  letter-spacing: 0;
}

.faculty-detail-hero .eyebrow {
  color: var(--faculty-detail-text, var(--ink-strong));
}

.faculty-detail-hero p:not(.eyebrow) {
  margin: 10px 0 0;
  color: color-mix(in srgb, var(--faculty-detail-text, var(--ink-strong)) 84%, transparent);
}

.faculty-detail-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}

.faculty-detail-grid section {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 10px;
  padding: 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.faculty-detail-grid h2 {
  margin: 0 0 8px;
  color: var(--ink-strong);
  font-size: 18px;
}

.faculty-detail-grid p {
  margin: 0 0 14px;
  color: #5c6678;
  line-height: 1.55;
}

.faculty-search-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
}

.faculty-search-form input {
  min-height: 42px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  padding: 0 12px;
}

.faculty-search-form input:focus {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  outline: 2px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong)) 24%, transparent);
}

.faculty-review-entry {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.faculty-review-entry .ghost-button {
  min-height: 34px;
  padding: 0 12px;
  font-size: 12px;
}

.faculty-detail-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 24%, var(--line-strong));
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.faculty-detail-actions strong,
.faculty-detail-actions span {
  display: block;
}

.faculty-detail-actions strong {
  color: var(--ink-strong);
  font-size: 15px;
}

.faculty-detail-actions span {
  margin-top: 3px;
  color: #5c6678;
  font-size: 12px;
}

.faculty-detail-actions > div:last-child {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.faculty-detail-actions .ghost-button {
  min-height: 34px;
  padding: 0 12px;
  font-size: 12px;
}

.faculty-detail-page .primary-button {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  background: var(--faculty-detail-theme, var(--ink-strong-bg));
  color: var(--faculty-detail-text, #ffffff);
}

.faculty-detail-page .ghost-button {
  border: 1px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 30%, var(--line-strong));
  background: var(--surface);
  color: var(--ink-strong);
}

.faculty-detail-page .primary-button:hover,
.faculty-detail-page .primary-button:focus-visible,
.faculty-detail-page .ghost-button:hover,
.faculty-detail-page .ghost-button:focus-visible,
.faculty-detail-page .faculty-back-button:focus-visible {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 22%, transparent);
  outline: 0;
}

.faculty-detail-page .ghost-button:hover,
.faculty-detail-page .ghost-button:focus-visible {
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 12%, var(--surface));
}

.faculty-filter-panel {
  display: grid;
  gap: 12px;
  padding: 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.faculty-subject-filter-block {
  display: grid;
  gap: 8px;
}

.faculty-subject-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.faculty-area-toggle {
  display: none;
}

.faculty-area-chip {
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid rgba(217, 223, 235, 0.72);
  border-radius: var(--radius-ui);
  padding: 0 11px;
  background: rgba(255, 255, 255, 0.94);
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 850;
}

.faculty-area-chip:hover,
.faculty-area-chip:focus-visible {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 12%, var(--surface));
  outline: 0;
}

.faculty-area-chip span {
  color: #6a7280;
  font-weight: 750;
}

.faculty-area-chip.is-active {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  background: var(--faculty-detail-theme, var(--ink-strong-bg));
  color: var(--faculty-detail-text, #ffffff);
}

.faculty-area-chip.is-active span {
  color: currentColor;
}

.faculty-course-results {
  display: grid;
  gap: 18px;
}

.faculty-level-booklet {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  overflow: visible;
  border: 0;
  border-radius: 0;
  background: var(--surface);
}

.booklet-row {
  width: 100%;
  min-height: 40px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-ui);
  padding: 0 12px;
  background: var(--surface);
  color: var(--ink-strong);
  text-align: left;
}

.booklet-row:hover {
  border-color: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 42%, var(--line-soft));
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 10%, var(--surface));
}

.booklet-row:focus-visible {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 20%, transparent);
  outline: 0;
}

.booklet-row.is-active {
  border-color: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 62%, #ffffff);
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 18%, var(--surface));
  box-shadow: inset 4px 0 0 var(--faculty-detail-theme, var(--ink-strong-bg));
}

.booklet-row.is-active strong {
  color: var(--ink-strong);
}

.booklet-row.is-active em {
  color: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong)) 58%, var(--ink-strong));
}

.booklet-row.is-parent {
  font-weight: 900;
}

.booklet-row.is-open {
  background: #fbfcfe;
}

.booklet-row strong {
  min-width: 0;
  font-size: 14px;
  line-height: 1.2;
}

.booklet-row em {
  color: #6a7280;
  font-size: 12px;
  font-style: normal;
  font-weight: 900;
}

.booklet-caret {
  display: none;
}

.booklet-row.is-open .booklet-caret {
  transform: rotate(90deg);
}

.booklet-row:not(.is-parent) .booklet-caret {
  opacity: 0;
}

.booklet-children {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  border-bottom: 0;
  background: #fbfcfe;
}

.booklet-children .booklet-row {
  min-height: 56px;
  padding-left: 12px;
  background: #fbfcfe;
}

.booklet-children .booklet-row:hover {
  background: var(--canvas);
}

.undergraduate-level-filters .booklet-row {
  min-height: 40px;
}

.faculty-course-level {
  display: grid;
  gap: 12px;
}

.undergraduate-level-stack {
  gap: 18px;
}

.undergraduate-level-section {
  display: grid;
  gap: 10px;
}

.undergraduate-level-subheading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 12px;
  padding: 0 2px;
}

.undergraduate-level-subheading h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 18px;
}

.undergraduate-level-subheading span {
  color: #6a7280;
  font-size: 12px;
  font-weight: 850;
}

.faculty-course-level.is-expanded {
  animation: panel-in 140ms ease;
}

.faculty-level-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 14px;
  padding: 0 2px;
}



.faculty-level-heading h2 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 22px;
  line-height: 1.1;
}

.faculty-level-heading span {
  color: #6a7280;
  font-size: 12px;
  font-weight: 800;
}

/* Book-tab strip (课程 / 教授) at the head of the list. Styled like index tabs on
   a book: the active one is a raised page joined to the list below it, the
   inactive one sits back in the surface. */
.course-scope-tabs {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  min-width: 0;
}

.course-scope-tab {
  flex: 0 0 auto;
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-bottom: 0;
  border-radius: 10px 10px 0 0;
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 7%, var(--surface));
  color: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong)) 62%, #46506a);
  font-size: 14px;
  font-weight: 850;
  cursor: pointer;
}

.course-scope-tab.is-active {
  /* Sits 1px lower and paints over the strip's baseline, so the active tab reads
     as continuous with the cards underneath — the "page" of the book. */
  margin-bottom: -1px;
  padding-bottom: 1px;
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  background: var(--faculty-detail-theme, var(--ink-strong-bg));
  /* The faculty's OWN ink colour, not a hardcoded white — same pairing the hero
     uses. Education (yellow) and Engineering (bright green) are light themes, so
     white text on them was barely legible; those faculties carry black here. */
  color: var(--faculty-detail-text, #ffffff);
}

.course-scope-tab:not(.is-active):hover {
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 14%, var(--surface));
}

/* Sort chips (评价最多 / 好评优先 / 差评优先) on the right of the level heading.
   nowrap + horizontal scroll: the group must always read as ONE row of chips —
   letting it wrap split the last chip onto a line of its own on phones. */
.course-sort {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  margin-left: auto;
  overflow-x: auto;
  scrollbar-width: none;
}

.course-sort::-webkit-scrollbar {
  display: none;
}

.faculty-subject-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.faculty-subject-card {
  position: relative;
  display: grid;
  align-content: start;
  gap: 10px;
  min-height: 210px;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-top: 4px solid color-mix(in srgb, var(--faculty-theme, var(--ink-strong-bg)) 76%, var(--line-strong));
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  cursor: pointer;
  transition:
    border-color 140ms ease,
    box-shadow 140ms ease,
    transform 140ms ease;
  content-visibility: auto;
  contain-intrinsic-size: 300px;
}

/* Hover outlines the card in the owning faculty's colour. --faculty-line is set
   per card (the light-surface accent, so FBE's near-white becomes its black and
   the limes darken); --faculty-theme is the fallback for anything rendering a
   card without one. */
.faculty-subject-card:hover,
.faculty-subject-card:focus-visible {
  border-color: var(--faculty-line, color-mix(in srgb, var(--faculty-theme, var(--ink-strong-bg)) 45%, var(--line-strong)));
  box-shadow:
    0 0 0 1px var(--faculty-line, transparent),
    0 12px 28px color-mix(in srgb, var(--ink-strong) 10%, transparent);
  outline: 0;
  transform: translateY(-1px);
}

.load-more-wrap {
  display: grid;
  place-items: center;
  gap: 6px;
  margin: 10px 0 2px;
}

.load-more-button {
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 850;
  cursor: pointer;
}

.load-more-button:hover,
.load-more-button:focus-visible {
  border-color: #b9c3d4;
  background: #f6f8fb;
}

.load-more-sentinel {
  width: 100%;
  height: 1px;
}

.subject-card-cover {
  display: grid;
  /* Fixed score + offered columns so the title column always keeps the middle
     space; a long "Offered ..." string wraps inside its 96px column instead of
     starving the title and breaking the English name word-by-word. */
  grid-template-columns: 74px minmax(0, 1fr) 96px;
  gap: 12px;
  align-items: start;
}

.subject-score-tile {
  min-height: 82px;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 3px;
  border-radius: var(--radius-ui);
  background: #e7edf4;
  color: #111111;
}

.subject-score-tile.is-score-1,
.review-item-score.is-score-1,
.review-post-score.is-score-1 {
  background: #ff8f95;
}

.subject-score-tile.is-score-2,
.review-item-score.is-score-2,
.review-post-score.is-score-2 {
  background: #ffbf69;
}

.subject-score-tile.is-score-3,
.review-item-score.is-score-3,
.review-post-score.is-score-3 {
  background: #ffeb6a;
}

.subject-score-tile.is-score-4,
.review-item-score.is-score-4,
.review-post-score.is-score-4 {
  background: #a8e875;
}

.subject-score-tile.is-score-5,
.review-item-score.is-score-5,
.review-post-score.is-score-5 {
  background: #52d69d;
}

/* Clickable review-post title -> jumps to the course in the faculty view. */
.review-title-link {
  display: inline;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.review-title-link:hover,
.review-title-link:focus-visible {
  text-decoration: underline;
}

/* Bilingual title swap: the localized subject name shows by default, hover (or
   keyboard focus) reveals the original English title in its place. */
.review-title-link .rt-en {
  display: none;
}

.review-title-link:hover .rt-loc,
.review-title-link:focus-visible .rt-loc {
  display: none;
}

.review-title-link:hover .rt-en,
.review-title-link:focus-visible .rt-en {
  display: inline;
}

.review-title-arrow {
  font-size: 0.8em;
  color: var(--muted);
}

/* Auto-generated course-review post: metric chips row + comment + score box. */
.review-post {
  display: grid;
  gap: 12px;
  margin: 4px 0 2px;
}

.review-post-metrics {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.review-metric {
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px 10px;
  line-height: 1.5;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--canvas);
}

.review-metric small {
  color: var(--muted);
  font-size: 11px;
  font-weight: 800;
}

.review-metric b {
  color: var(--text);
  font-size: 14px;
  font-weight: 900;
}

.review-post-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 14px;
  align-items: stretch;
}

.review-post-comment {
  margin: 0;
  color: var(--text);
  line-height: 1.6;
  white-space: pre-wrap;
}

.review-post-score {
  align-self: start;
  min-width: 78px;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 2px;
  padding: 10px 8px;
  border-radius: var(--radius-ui);
  background: #e7edf4;
  color: #111111;
  text-align: center;
}

.review-post-score small {
  font-size: 10px;
  font-weight: 950;
  text-transform: uppercase;
}

.review-post-score strong {
  font-size: 28px;
  line-height: 1;
}

@media (max-width: 640px) {
  /* 作业负担 / 出勤 are context, not headline scores. On a phone the five chips
     wrapped to two rows and pushed the actual review text below the fold, so the
     feed card keeps only the three numbers — open the post to see the rest.
     Scoped to .is-feed so the detail view still shows everything. */
  .review-post.is-feed .review-metric-secondary {
    display: none;
  }

  /* Stacked, so this gap is now VERTICAL — at the desktop 14px it opened a 26px
     chasm above the SCORE tile (the comment's line-height leading adds to it)
     while only 2px sat below, which read as the tile belonging to the tag row
     rather than to the review. Tighten the top, pay it back at the bottom. */
  .review-post-main {
    grid-template-columns: 1fr;
    /* 0, not 4px: the comment's line-height already leaves ~12px of half-leading
       below its last line, which IS the visual gap. Anything added on top of it
       reads as lopsided against the tile's bottom margin. */
    gap: 0;
  }
  .review-post {
    margin-bottom: 12px;
  }
  .review-post-score {
    justify-self: start;
    min-width: 0;
    grid-auto-flow: column;
    gap: 8px;
    padding: 8px 14px;
  }
  .review-post-score strong {
    font-size: 22px;
  }
}

.subject-score-tile span {
  font-size: 10px;
  font-weight: 950;
  text-transform: uppercase;
}

.subject-score-tile strong {
  font-size: 30px;
  line-height: 1;
}

.subject-score-tile em {
  font-size: 10px;
  font-style: normal;
  font-weight: 800;
}

.subject-title-block {
  position: relative;
  min-width: 0;
  display: grid;
  align-content: start;
  gap: 4px;
}

.faculty-subject-card .subject-cn-name {
  color: #111111;
  font-size: 19px;
  font-weight: 950;
}

.review-scoreline {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.review-scoreline span {
  border-radius: var(--radius-ui);
  padding: 5px 7px;
  background: #f1f4f9;
  color: #33405c;
  font-size: 11px;
  font-weight: 900;
}

.review-preview {
  display: grid;
  grid-template-columns: 58px minmax(0, 1fr);
  gap: 10px;
  align-items: start;
  padding: 10px;
  border-radius: var(--radius-ui);
  background: #f7f8fa;
}

.review-preview div {
  min-height: 58px;
  display: grid;
  place-items: center;
  align-content: center;
  background: #ffe1e3;
  color: #111111;
}

.review-preview strong {
  font-size: 22px;
  line-height: 1;
}

.review-preview span {
  margin-top: 4px;
  font-size: 10px;
  font-weight: 900;
  text-transform: uppercase;
}

.review-preview p {
  margin: 0;
  color: #111111;
  font-size: 13px;
  line-height: 1.45;
}

.subject-review-modal {
  width: min(1200px, calc(100vw - 40px));
  max-width: 1200px;
  padding: 0;
  /* Card scrolls its own content: on short/mobile screens the review lists
     below the fold were clipped by overflow:hidden with no way to scroll. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.subject-review-modal .modal-header {
  padding: 18px 20px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
}

.subject-review-summary {
  display: grid;
  /* `auto`, not a hardcoded track: the column was pinned at 112px while the ring
     itself is 138px, so the ring overhung its own cell and the title stack read
     from underneath it. Sizing to the ring keeps the two in step whatever it
     grows to next. */
  grid-template-columns: auto minmax(0, 1fr);
  gap: 22px;
  align-items: center;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line-soft);
  /* 第二个色标必须是令牌, 不能是字面量: Dark Reader 引擎跳过任何含 var() 的
     颜色声明, 所以暗色下 var(--surface) 那端靠 tokens.css 正常变暗, 而写死的
     #f5f8fc 原封不动留在那儿 —— 实测计算值 linear-gradient(135deg,
     rgb(24,26,27), rgb(245,248,252)), 就是弹窗头部那片纸白。--surface-2 的亮色值
     #f8fafc 与原来的 #f5f8fc 相差 3/255, 亮色模式下看不出来。 */
  background: linear-gradient(135deg, var(--surface) 0%, var(--surface-2) 100%);
}

.subject-review-summary h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 24px;
  line-height: 1.18;
}

.subject-review-summary p {
  margin: 4px 0 10px;
  color: #4f596b;
}

.subject-review-summary .review-scoreline {
  gap: 8px;
}

.subject-review-summary .review-scoreline .is-key-stat {
  border: 1px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 38%, var(--line-strong));
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 16%, var(--surface));
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 950;
}

.subject-review-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
}

/* Source filter chips above the course-review list (站内评价 / 学生会 / StudentVIP). */
.review-source-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px 20px 4px;
}

.review-source-chip {
  border: 1px solid #d4dced;
  background: #f5f7fb;
  color: var(--muted, #7a8699);
  border-radius: 999px;
  padding: 5px 12px;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.review-source-chip.is-on {
  border-color: var(--brand, var(--ink-strong-bg));
  background: var(--brand, var(--ink-strong-bg));
  color: #ffffff;
}

.review-source-chip.is-off {
  opacity: 0.7;
  text-decoration: line-through;
}

/* Rating meter in the subject-review header: 推荐度 square + Usefulness/Difficulty/
   推荐度 percentage bars, each click-openable into a 1-5 distribution popover. */
.review-meter-summary {
  align-items: center;
  gap: 20px;
}

/* Title stack beside the 推荐度 ring: subject-code eyebrow, Chinese headline, English
   subtitle, then a light Handbook link. The code is a faculty-tinted chip; the negative
   left margin cancels its own left padding so the code TEXT left-aligns with the title
   below it (the chip's tint still hangs a touch further left). */
.review-subject-code {
  display: inline-block;
  /* -5px, not the chip's full -9px padding: cancelling the padding exactly left
     the code reading left of the title below it, because the chip's letter-spacing
     and the glyphs' own side bearing both push the visible ink inward. Tuned by
     eye against the headline. */
  margin: 0 0 8px -6px;
  padding: 3px 11px;
  border-radius: 7px;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0.08em;
  /* 两个颜色以前是 subjectReviews.js 直接写在 style="" 里的, 且写死了混向 #ffffff
     和 #1c2740 —— 也就是只对白卡成立。深色下那算式给出的是一枚近白的粉药丸压在
     近黑的卡上, 而它右边那圈推荐度描边是走 subjectFacultyThemeColor() 的、会跟着
     主题换端的, 于是同一个学院色在同一张卡上出现了两种完全不同的明度。
     现在 JS 只递一个 --code-accent(学院色), 明暗两端各自在这里配 —— 内联样式压不过
     媒体/主题规则, 想按主题分叉就必须把值搬进 CSS。 */
  background: color-mix(in srgb, var(--code-accent, var(--brand)) 16%, #ffffff);
  color: color-mix(in srgb, var(--code-accent, var(--brand)) 45%, #1c2740);
}

/* 深色: 卡是深的, 所以底色改成往 --surface 上薄薄地兑一层学院色, 字直接用学院色
   本身 —— 和旁边那圈描边同一个值, 两者终于是同一支色。 */
/* 学院色是按**浅底**调的。直接拿它当暗底上的文字色, 底色深的那几个学院会
   直接消失: 实测商学院(#111111)的药丸在暗色下对比度只有 1.11 —— 黑字黑底,
   ACCT10001 / MGMT20001 / FNCE10002 三颗胶囊肃然无存; 蓝色那几个也只有 2.96。
   所以暗色下把学院色往白里提: 色相保住, 亮度抬到读得出来。 */
/* 胶囊在暗色下的分工: **底色**说学院是谁, **白字**管读得清。
   上一版底和字同一支颜色(只差一个混入量), 理学院那颗就是深蓝底上的深蓝字。 */
/* 底色用 --code-accent(学院原色), 不用 --code-accent-dark。
   后者把商学院反色成了白 —— 那是为“圆点/文字”准备的, 拿来当底色就成了
   白字压浅灰底(实测对比 2.38)。底色不怕深: 白字压深底本来就是最清楚的。
   62% 是实测出来的量: 九个学院白字对比 8.0~17, 颜色又足够鲜艳能认出是哪个学院。 */
[data-theme="dark"] .review-subject-code {
  background: color-mix(in srgb, var(--code-accent, var(--brand)) 62%, #181a1b);
  color: #ffffff;
}

/* 暗色下读 --wt-fc-dark: 那是学院**自己的颜色**, 只有暗到埋进背景的那支
   (实测只有商学院 #111111)才已经在 JS 里反色成白。不在这里做混色 ——
   上一版把每支都往白里推, 九个学院出来是一排洗淡的灏灰。 */
[data-theme="dark"] .wt-chip::before {
  background: var(--wt-fc-dark, var(--wt-fc, #29328f));
}

[data-theme="dark"] .wt-chip {
  border-color: color-mix(in srgb, var(--wt-fc-dark, var(--wt-fc, #29328f)) 60%, transparent);
}

[data-theme="dark"] .wt-chip.is-active {
  background: var(--wt-fc-dark, var(--wt-fc, #29328f));
  border-color: var(--wt-fc-dark, var(--wt-fc, #29328f));
  color: #101114;
}

[data-theme="dark"] .wt-chip.is-active::before {
  background: #101114;
}

.review-meter-summary h3 {
  font-size: 28px;
  line-height: 1.2;
}

.review-meter-summary p {
  margin: 4px 0 0;
  color: #6b7688;
  font-size: 16px;
}

/* Right of the 推荐度 ring: the title stack + the three meter bars. On desktop the
   bars sit inline to the far right of the title (margin-left:auto). Mobile promotes
   these to grid cells (display:contents) so the ring pairs with the title and the
   bars get a full-width row below — see the ≤860px block. */
.review-summary-main {
  min-width: 0;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
}

.review-summary-title {
  min-width: 0;
}

.review-summary-main .review-meter-bars {
  margin: 0 0 0 auto;
}

.review-handbook-link.subject-link {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-top: 11px;
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: #5b6577;
  transition: color 0.15s ease;
}

.review-handbook-link.subject-link span {
  font-size: 11px;
  opacity: 0.75;
}

.review-handbook-link.subject-link:hover {
  color: color-mix(in srgb, var(--faculty-theme, var(--ink-strong)) 60%, #22303f);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* 推荐度 as a "hollow square" progress ring: a rounded rect whose perimeter stroke
   fills to the recommend %, with the number stacked in the middle. pathLength=100
   normalises the perimeter so stroke-dasharray can be the raw percentage. */
.review-meter-ring {
  position: relative;
  flex: 0 0 auto;
  width: 138px;
  height: 138px;
}

.rmr-svg {
  width: 100%;
  height: 100%;
  display: block;
}

.rmr-track {
  fill: none;
  stroke: #e6eaf2;
  stroke-width: 8;
}

.rmr-fill {
  fill: none;
  stroke: var(--brand, #1652f0);
  stroke-width: 8;
  stroke-linecap: round;
  transition: stroke-dasharray 0.4s ease;
}

.rmr-center {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  align-content: center;
  gap: 2px;
  text-align: center;
  color: var(--ink-strong);
}

.rmr-center span {
  font-size: 12.5px;
  font-weight: 900;
  letter-spacing: 0.03em;
}

.rmr-center strong {
  font-size: 32px;
  font-weight: 950;
  line-height: 1;
}

.rmr-center em {
  font-size: 11.5px;
  font-style: normal;
  opacity: 0.8;
}

.review-meter-bars {
  display: grid;
  gap: 11px;
  margin-top: 8px;
  max-width: 480px;
}

/* Which faculty owns the subject, in the modal's title bar, immediately after
   历史评价 — it labels the subject being read, so it belongs with the title
   rather than stranded beside the close button. `margin-right: auto` keeps the
   × parked at the far end. */
.review-faculty-line {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 13.5px;
  font-weight: 900;
}

/* A cross-listed subject renders one chip per faculty; they travel as a group so
   .modal-header's space-between cannot scatter them across the bar. The auto
   margin keeps the group with the title and leaves the × at the far end. */
.review-faculty-lines {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-right: auto;
}

.review-faculty-line:hover,
.review-faculty-line:focus-visible {
  border-color: color-mix(in srgb, var(--faculty-line, var(--soft)) 55%, var(--line));
  background: #f7f9fc;
  outline: 0;
}

.review-faculty-line i {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: var(--faculty-line, var(--soft));
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.14);
}

.review-faculty-line em {
  color: var(--muted);
  font-style: normal;
  font-size: 11px;
  font-weight: 850;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.review-meter-bar {
  position: relative;
}

.rmb-head {
  display: grid;
  grid-template-columns: 74px minmax(0, 1fr) 42px 22px;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
}

.rmb-label {
  font-size: 14px;
  font-weight: 800;
  color: var(--text, #22303f);
}

/* Distribution trigger: a mini bar-chart icon to the right of each dimension;
   hover or keyboard-focus reveals the 1-5 breakdown popover. */
.rmb-stat {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  color: var(--muted, #667085);
  cursor: pointer;
}

.rmb-stat:hover,
.rmb-stat:focus-visible {
  background: var(--surface-3);
  color: var(--text, #22303f);
  outline: none;
}

.rmb-stat.is-empty {
  visibility: hidden;
}

.rmb-stat-ico {
  width: 14px;
  height: 14px;
}

.rmb-stat-ico rect {
  fill: currentColor;
}

/* Per-dimension "五个正方形" scale: 5 track squares. At rest .rmb-sq-fill shows the
   community average as a partial fill; the row doubles as a quick-rating control. */
.rmb-squares {
  display: flex;
  align-items: center;
  gap: 5px;
}

/* Quick-rating buttons (one per square value). Hover fills whole squares up to the
   cursor (.rmb-sq-rate via :has()), hiding the average; click confirms. */
.rq {
  position: relative;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: transparent;
  line-height: 0;
  cursor: pointer;
}

/* Chunkier: at 16px the strip read as a row of dots rather than a gauge. */
.rmb-sq {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  background: #e6eaf2;
  overflow: hidden;
}

.rmb-sq-fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  background: var(--brand, #1652f0);
  transition: opacity 0.1s ease;
}

/* Whole-square hover fill, hidden until the row is hovered. */
.rmb-sq-rate {
  position: absolute;
  inset: 0;
  display: none;
}

.rmb-rate:hover .rmb-sq-fill,
.rmb-rate.is-dragging .rmb-sq-fill {
  opacity: 0;
}

.rmb-rate .rq:hover .rmb-sq-rate,
.rmb-rate .rq:has(~ .rq:hover) .rmb-sq-rate,
.rq.is-scored .rmb-sq-rate {
  display: block;
}

/* Press-and-slide rating (initRateStripDrag). Touch has no :hover, so the drag
   preview is driven by inline display on each .rmb-sq-rate instead — this class
   only has to suppress the resting average underneath and widen the touch target.
   touch-action:none so the gesture scrolls the score, not the page. */
.rmb-rate {
  touch-action: none;
}

.rmb-rate.is-dragging .rq.is-mine .rmb-sq {
  outline: 0;
}

/* "My rating" marker: a ring in the faculty theme colour on the square the user
   scored — drawn as an outline so it stays visible over a filled square, and hidden
   during the hover preview. */
.rq.is-mine .rmb-sq {
  outline: 2px solid var(--rate-color, #1652f0);
  outline-offset: 1px;
}

.rmb-rate:hover .rq.is-mine .rmb-sq {
  outline: 0;
}

.rmp-track {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}

.rmp-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--brand, #1652f0);
}

.rmb-pct {
  font-size: 15px;
  font-weight: 800;
  color: var(--muted, #667085);
  text-align: right;
}

.review-meter-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 6;
  width: min(260px, 72vw);
  background: var(--surface);
  border: 1px solid #e2e7f0;
  border-radius: 12px;
  box-shadow: 0 14px 32px rgba(16, 24, 40, 0.18);
  padding: 12px 14px;
  display: none;
  gap: 6px;
}

.rmb-stat:hover .review-meter-pop,
.rmb-stat:focus-within .review-meter-pop {
  display: grid;
}

.rmp-title {
  font-size: 12px;
  font-weight: 900;
  color: var(--text, #22303f);
  margin-bottom: 2px;
}

.rmp-row {
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) 42px;
  align-items: center;
  gap: 8px;
}

.rmp-star {
  font-size: 12px;
  font-weight: 800;
  color: var(--muted, #667085);
}

.rmp-n {
  font-size: 12px;
  color: var(--muted, #667085);
  text-align: right;
}

.subject-review-list {
  display: grid;
  gap: 10px;
}

.subject-review-section {
  display: grid;
  gap: 10px;
  padding: 16px 20px 18px;
}

.subject-review-section + .subject-review-section {
  border-top: 1px solid var(--line-soft);
}

.subject-review-section-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid #eef2f7;
}

.subject-review-section-heading h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 18px;
}

.subject-review-section-heading span {
  border-radius: var(--radius-ui);
  padding: 4px 8px;
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 12%, var(--surface-3));
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 950;
}

.subject-review-item,
.subject-review-empty {
  padding: 14px;
  border: 1px solid #e2e7f0;
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.subject-review-item {
  display: grid;
  grid-template-columns: 84px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

.review-item-score {
  min-height: 84px;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 3px;
  border-radius: var(--radius-ui);
  background: #e7edf4;
  color: #111111;
}

.review-item-score span {
  font-size: 10px;
  font-weight: 950;
  text-transform: uppercase;
}

.review-item-score strong {
  font-size: 32px;
  line-height: 1;
}

.review-item-score em {
  font-size: 10px;
  font-style: normal;
  font-weight: 850;
}

.subject-review-item p,
.subject-review-empty p {
  margin: 10px 0;
  color: #111111;
  line-height: 1.55;
}

.subject-review-empty {
  background: var(--surface-2);
}

.subject-review-item footer {
  color: #6a7280;
  font-size: 12px;
  font-weight: 800;
}

.review-thanks-modal {
  position: relative;
  max-width: 440px;
}

.review-thanks-modal.is-revealed {
  max-width: 470px;
}

/* Step 1 — thank you + ask */
.review-thanks-x {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
}

.review-thanks-intro {
  text-align: center;
  padding: 34px 30px 4px;
}

.review-thanks-badge {
  width: 68px;
  height: 68px;
  margin: 0 auto 16px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--brand-weak);
  font-size: 33px;
  line-height: 1;
}

.review-thanks-intro h2 {
  margin: 0 0 10px;
  font-size: 22px;
  color: var(--text);
}

.review-thanks-lede {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}

.review-thanks-cta {
  margin: 22px 30px 0;
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-muted);
  text-align: center;
}

.review-thanks-cta p {
  margin: 0 0 16px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--text);
}

.review-thanks-generate {
  width: 100%;
  min-height: 48px;
  padding: 0 20px;
  font-size: 15px;
  border-radius: var(--radius-sm);
}

/* 游客的登录提醒。与下面那块「生成分享卡」共用一套盒子尺寸，但底色取 --surface 而不是
   --surface-muted：两块紧挨着，同色会读成一块，而它们问的是两件不相干的事。 */
.review-thanks-guest {
  margin: 22px 30px 0;
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  text-align: center;
}

.review-thanks-guest p {
  margin: 0 0 16px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.6;
}

.review-thanks-login {
  width: 100%;
  min-height: 44px;
  padding: 0 20px;
  border-radius: var(--radius-sm);
  font-size: 14.5px;
}

/* 评价表单里「未登录会以匿名形式发出」那行小字。跟在提交按钮下面，是对「按了会发生
   什么」的说明，所以走 --muted 而不是警告色 —— 这不是一个错误状态。 */
.review-guest-note {
  margin: 10px 0 0;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.6;
}

.review-thanks-skip {
  display: block;
  width: 100%;
  margin: 0;
  padding: 18px 8px 22px;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 13px;
  cursor: pointer;
}

.review-thanks-skip:hover {
  color: var(--text);
  text-decoration: underline;
}

/* Step 2 — the generated card + share actions */
.review-share-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 20px;
}

.review-share-preview {
  align-self: center;
  width: 100%;
  max-width: 340px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  box-shadow: 0 12px 34px color-mix(in srgb, var(--brand) 16%, transparent);
}

.review-share-preview svg {
  display: block;
  width: 100%;
  height: auto;
}

.review-share-actions {
  display: flex;
  gap: 10px;
}

.review-share-actions .primary-button,
.review-share-actions .ghost-button {
  flex: 1 1 0;
  min-height: 44px;
  border-radius: var(--radius-sm);
  font-size: 14px;
}

.review-share-actions .ghost-button {
  border: 1px solid var(--line);
}

.review-share-status {
  margin: 0;
  min-height: 18px;
  font-size: 13px;
  font-weight: 600;
  color: var(--brand-bright);
  text-align: center;
}

.review-share-body .review-thanks-skip {
  padding: 2px 8px 4px;
}

.subject-tutor-list,
.subject-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.subject-tutor-list button {
  min-height: 28px;
  border: 1px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 28%, var(--line-strong));
  border-radius: var(--radius-ui);
  padding: 0 9px;
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 12px;
  font-weight: 850;
}

.subject-tutor-list button:hover,
.subject-tutor-list button:focus-visible {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 12%, var(--surface));
  outline: 0;
}

.subject-card-actions .ghost-button {
  min-height: 30px;
  padding: 0 9px;
  font-size: 12px;
}

.faculty-professor-panel {
  display: grid;
  gap: 14px;
}

.faculty-professor-toolbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, 0.9fr);
  gap: 14px;
  align-items: end;
  padding: 18px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.faculty-professor-toolbar h2,
.faculty-professor-toolbar p {
  margin: 0;
}

.faculty-professor-toolbar p {
  margin-top: 6px;
  color: #5c6678;
}

.professor-add-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: 8px;
}

.professor-add-form input {
  min-height: 40px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  padding: 0 10px;
}

.professor-add-form input:focus {
  border-color: var(--faculty-detail-theme, var(--ink-strong-bg));
  outline: 2px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong)) 22%, transparent);
}

/* Course-home variant: the professor-search empty state adds a faculty picker
   (no faculty context on that page), so the row is four cells, and the select
   dresses like the inputs. */
.ch-add-professor-form {
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1fr) auto;
}

.ch-add-professor-form select {
  min-height: 40px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  padding: 0 10px;
  background: var(--surface);
  color: inherit;
}

@media (max-width: 720px) {
  .ch-add-professor-form {
    grid-template-columns: 1fr;
  }
}

.professor-form-toggle {
  justify-self: end;
  min-height: 40px;
  padding: 0 14px;
}

.professor-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.professor-card,
.professor-empty-card {
  display: grid;
  gap: 12px;
  min-height: 150px;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.professor-card strong,
.professor-empty-card strong {
  display: block;
  color: var(--ink-strong);
  font-size: 18px;
}

.professor-card > div:first-child span,
.professor-empty-card p {
  display: block;
  margin-top: 4px;
  color: #5c6678;
  font-size: 13px;
}

.professor-empty-card .primary-button {
  justify-self: start;
  margin-top: 4px;
}

.graduate-subject-strip {
  display: grid;
  gap: 10px;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.graduate-subject-strip h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 16px;
}

.graduate-subject-strip div {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.graduate-subject-strip span {
  border-radius: var(--radius-ui);
  padding: 6px 8px;
  background: #f1f4f9;
  color: #33405c;
  font-size: 12px;
  font-weight: 800;
}

.review-modal {
  max-width: 820px;
}

.review-slider-grid,
.review-select-grid {
  display: grid;
  gap: 10px;
}

.review-slider-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.review-select-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.review-slider-field {
  gap: 12px;
  border: 1px solid #dce3ee;
  border-radius: var(--radius-ui);
  padding: 12px;
  background: var(--surface-2);
}

.review-slider-field > span {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.review-slider-field strong {
  color: var(--ink-strong);
  font-size: 13px;
}

.review-slider-field em {
  min-width: 42px;
  border-radius: var(--radius-ui);
  padding: 5px 8px;
  background: var(--ink-strong-deep-bg);
  color: #ffffff;
  font-style: normal;
  font-size: 13px;
  font-weight: 950;
  text-align: center;
}

.review-slider-field input[type="range"] {
  width: 100%;
  accent-color: var(--ink-strong-deep);
  cursor: pointer;
}

@keyframes panel-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes skeleton-pulse {
  0% {
    background-position: 100% 0;
  }

  100% {
    background-position: -100% 0;
  }
}

.subject-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  color: #697386;
  font-size: 12px;
  font-weight: 800;
}

.subject-card-side {
  display: grid;
  justify-items: end;
  gap: 8px;
  text-align: right;
  color: #697386;
  font-size: 12px;
  font-weight: 850;
  line-height: 1.25;
}

/* anywhere 而不是 break-word —— 这两个值只在一件事上不同, 而那件事正好是这里的病根:
   break-word 只在**已经排好的行**放不下时才断词, 它不参与 min-content 的计算; anywhere
   参与。这一列的轨道是 auto, 而 auto 轨道的下限就是 min-content, 于是
   "Camperdown/Darlington," 这种 155px 的不可断词(斜杠不构成断点)把轨道从 96px 顶到
   155px, span 直接越过卡片右边缘 42px —— 悉大商学院那一页 18 张卡全在溢出。
   换成 anywhere 之后 min-content 认得断点了, 轨道回到 96px, 文字正常折行, 卡片高度不变。 */
.subject-card-side span {
  white-space: normal;
  overflow-wrap: anywhere;
}

.subject-offered-inline {
  display: none;
  /* 和 .subject-card-side span 同一个理由: 这是同一段文字, 窄屏下换到这个元素来显示,
     而它所在的 1fr 列在手机上也就一百多像素, 同样顶不住那个不可断的校区名。 */
  overflow-wrap: anywhere;
  margin-top: 4px;
  color: #697386;
  font-size: 12px;
  font-weight: 850;
  line-height: 1.25;
}

.subject-card-review-button {
  min-height: 28px;
  padding: 0 9px;
  font-size: 11px;
  white-space: nowrap;
}

.subject-code {
  color: var(--faculty-theme, var(--ink-strong));
  font-size: 13px;
  letter-spacing: 0;
  line-height: 1.1;
}

/* 暗色下课程代码改读 --faculty-text。
   --faculty-theme 是学院的"卡片底色"那一支 —— 对 ABP/Arts/FBE 这些学校它就是
   #111111(黑卡片的底), 拿来当深色卡片上的文字等于隐形; 实测 ABP 的
   --faculty-theme 正是 #111111, 而卡片底是 rgb(18,18,19)。--faculty-text 是同一
   个学院"配这块底的字色"(ABP 的 #E04300), 正是这里该用的那一支。两支变量都由
   coursesPage.js 写在 .faculty-page 上, 这里只是挑对了读哪一支。
   注意这在浅色下不能这么改: 浅色卡片是白的, --faculty-text 是配黑卡片挑的, 会太亮。
   和 .ch-faculty-tile strong 的 --fc / --fc-line 是同一个"一支色两个用途"的取舍。 */
[data-theme="dark"] .subject-code {
  color: var(--faculty-text, var(--ink-strong));
}

/* 学院主页(.fh-page)上同一类问题的其余几处: 这些小字/链接读的是 --faculty-line,
   也就是 lightAccent —— 为白底挑的深色版。深色页面上 ABP 勉强能看, 而 FBE 的
   lightAccent 是 #111111, 直接隐形。同样改读 --faculty-text。 */
[data-theme="dark"] .fh-hero h1 .fh-dot,
[data-theme="dark"] .fh-fav-add,
[data-theme="dark"] .fh-fav-actions a,
[data-theme="dark"] .fh-col-more,
[data-theme="dark"] .fh-course-go,
[data-theme="dark"] .fh-major i {
  color: var(--faculty-text, var(--brand-bright));
}

.faculty-subject-card h3 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 18px;
  line-height: 1.16;
}

.faculty-subject-card h3.subject-en-name-only {
  font-size: 21px;
  line-height: 1.12;
  font-weight: 950;
}

.subject-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.subject-meta span {
  border-radius: var(--radius-ui);
  padding: 4px 7px;
  background: color-mix(in srgb, var(--faculty-theme, var(--ink-strong-bg)) 12%, var(--surface-3));
  color: #33405c;
  font-size: 11px;
  font-weight: 800;
}

.faculty-subject-card p {
  margin: 0;
  color: #4f596b;
  font-size: 13px;
  line-height: 1.5;
}

.subject-link {
  align-self: flex-start;
  justify-self: start;
  color: color-mix(in srgb, var(--faculty-theme, var(--ink-strong)) 70%, var(--ink-strong));
  font-size: 12px;
  font-weight: 900;
  text-decoration: none;
}

.faculty-course-empty {
  padding: 24px;
  border: 1px solid rgba(217, 223, 235, 0.82);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.faculty-course-empty strong {
  color: var(--ink-strong);
  font-size: 18px;
}

.faculty-course-empty p {
  margin: 8px 0 0;
  color: #5c6678;
}

.faculty-course-empty-search {
  display: grid;
  gap: 10px;
  justify-items: start;
  text-align: left;
}

.faculty-course-empty-search .primary-button,
.faculty-course-empty-search .ghost-button {
  min-height: 34px;
  margin-top: 2px;
}

.faculty-search-expanded-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  border: 1px solid color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 28%, var(--line-strong));
  border-radius: var(--radius-card);
  padding: 12px 14px;
  background: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong-bg)) 10%, var(--surface));
}

.faculty-search-expanded-note strong {
  display: block;
  margin-bottom: 3px;
  color: var(--ink-strong);
  font-size: 14px;
}

.faculty-search-expanded-note p {
  margin: 0;
  color: #596579;
  font-size: 13px;
}

.mini-post {
  display: grid;
  gap: 5px;
  margin-top: 0;
  padding: 14px 16px;
  border-color: #dfe5ee;
  border-radius: var(--radius-ui);
  background: var(--surface);
  transition:
    border-color 120ms ease,
    transform 120ms ease,
    box-shadow 120ms ease;
}

.mini-post:hover {
  border-color: #b9c7d8;
  box-shadow: 0 10px 24px color-mix(in srgb, var(--ink-strong-bg) 8%, transparent);
  transform: translateY(-1px);
}

.mini-post strong {
  color: var(--ink-strong);
  font-size: 16px;
  line-height: 1.35;
}

.mini-post span {
  display: block;
  color: var(--muted);
  font-size: 12px;
  font-weight: 800;
}

.clubs-page {
  --club-ink: #303030;
  --club-canvas: #dbdbdb;
  --club-card: #ffffff;
  --club-fog: var(--surface-muted);
  --club-ash: #ededed;
  --club-smoke: #c2c2c2;
  --club-graphite: #808080;
  --club-sand: #e2ceb8;
  --club-slate: #6a728e;
  --club-yellow: #f9f5a2;
  max-width: 1440px;
  min-height: calc(100vh - var(--topbar-height) - 60px); /* fallback */
  min-height: calc(100dvh - var(--topbar-height) - 60px);
  margin: 0 auto;
  padding: 24px;
  border-radius: 20px;
  background: var(--club-canvas);
  color: var(--club-ink);
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}

.clubs-hero-widget,
.clubs-index-widget,
.club-widget,
.clubs-state-widget {
  border: 0;
  border-radius: 20px;
  background: var(--club-card);
  box-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px 0;
}

.clubs-hero-widget {
  min-height: 210px;
  display: grid;
  align-content: center;
  gap: 10px;
  padding: 26px;
}

.clubs-hero-widget h1 {
  max-width: 760px;
  margin: 0;
  font-size: clamp(30px, 4vw, 52px);
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: 0;
}

.clubs-hero-widget h1 span {
  background: var(--club-yellow);
}

.clubs-hero-widget p {
  max-width: 680px;
  margin: 0;
  color: var(--club-ink);
  font-size: 15px;
  line-height: 1.55;
}

.club-caption {
  margin-bottom: 8px;
  color: var(--club-ink);
  font-size: 11px;
  line-height: 1.4;
  text-align: center;
}

.club-hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.club-hero-meta span,
.club-actions a,
.club-actions span,
.club-info-stack a,
.club-info-stack span,
.club-category-list span {
  border-radius: 20px;
  background: var(--club-fog);
  color: var(--club-ink);
}

.club-hero-meta span {
  padding: 8px 18px;
  font-size: 13px;
}

.clubs-index-widget {
  margin-top: 24px;
  padding: 18px;
}

.club-category-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.club-category-list span {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 38px;
  padding: 7px 12px;
  font-size: 12px;
}

.club-category-list b,
.club-category-list em {
  font: inherit;
}

.club-category-list em {
  color: var(--club-graphite);
}

.clubs-grid {
  display: grid;
  /* 列数由 JS 写进 --club-columns（见 appConstants.js 的 CLUBS_COLUMN_LADDER）。
     不能用 auto-fill 自己算：JS 还要按同样的列数生成 .club-masonry-column 容器，
     两边各算各的就会数不上，多出来的整列被挤到网格第二行。 */
  grid-template-columns: repeat(var(--club-columns, 5), minmax(0, 1fr));
  grid-auto-flow: dense;
  gap: 24px;
  margin-top: 24px;
}

.club-widget {
  min-height: 310px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px;
}

.club-widget:nth-child(9n + 1),
.club-widget:nth-child(9n + 5) {
  grid-row: span 2;
  min-height: 400px;
}

.club-widget.is-sand {
  background: var(--club-sand);
}

.club-widget.is-slate {
  background: var(--club-slate);
}

.club-widget.is-slate,
.club-widget.is-slate .club-category,
.club-widget.is-slate .club-description,
.club-widget.is-slate .club-actions span {
  color: #ffffff;
}

.club-card-head {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
}

.club-avatar {
  width: 62px;
  height: 62px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--club-fog);
  color: var(--club-ink);
  font-size: 17px;
  line-height: 1;
  box-shadow: rgba(0, 0, 0, 0.18) 0 1px 4px 0;
}

.club-category {
  margin: 0 0 4px;
  color: var(--club-graphite);
  font-size: 12px;
  line-height: 1.4;
}

.club-widget h2 {
  margin: 0;
  color: inherit;
  font-size: 20px;
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: 0;
}

.club-description {
  display: -webkit-box;
  margin: 0;
  overflow: hidden;
  color: var(--club-ink);
  font-size: 14px;
  line-height: 1.5;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 7;
}

.club-widget:nth-child(9n + 1) .club-description,
.club-widget:nth-child(9n + 5) .club-description {
  -webkit-line-clamp: 12;
}

.club-info-stack {
  display: grid;
  gap: 8px;
  margin-top: auto;
}

.club-info-stack a,
.club-info-stack span {
  display: block;
  min-width: 0;
  overflow: hidden;
  padding: 7px 12px;
  font-size: 12px;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.club-info-stack a:hover,
.club-actions a:hover {
  background: var(--club-ash);
}

.club-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 2px;
}

.club-actions a,
.club-actions span {
  padding: 7px 12px;
  font-size: 12px;
  text-decoration: none;
  white-space: nowrap;
}

.clubs-state-widget {
  margin-top: 24px;
  padding: 24px;
  text-align: center;
}

.clubs-state-widget strong {
  display: block;
  margin-bottom: 6px;
  font-weight: 400;
  font-size: 20px;
}

.clubs-state-widget p {
  margin: 0;
  color: var(--club-graphite);
}

.clubs-main {
  padding: var(--topbar-height) 0 0;
}

.clubs-main .clubs-page {
  max-width: none;
  min-height: calc(100vh - var(--topbar-height)); /* fallback */
  min-height: calc(100dvh - var(--topbar-height));
  margin: 0;
  padding: 24px 28px 28px;
  border-radius: 0;
  background:
    radial-gradient(circle at 88% 12%, rgba(255, 255, 255, 0.38), transparent 26%),
    radial-gradient(circle at 16% 6%, rgba(10, 77, 179, 0.35), transparent 24%),
    linear-gradient(135deg, color-mix(in srgb, var(--brand) 18%, #d6dce2), #c9d1d8 42%, color-mix(in srgb, var(--sidebar-brand) 22%, #d8d8d8));
}

.clubs-main .club-widget,
.clubs-main .clubs-state-widget {
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px 0;
}

.clubs-slogan-panel {
  width: 100%;
  display: inline-flex;
  flex-direction: column;
  justify-content: flex-start;
  break-inside: avoid;
  /* No bottom margin/padding: the masonry column's 24px gap alone separates the
     hero from the first card, so cards sit close instead of drifting far down. */
  padding: 0 16px 0 0;
  margin: 0;
  background: transparent;
  box-shadow: none;
  /* Speak the site's brand voice (Inter) rather than the board's mono zine face. */
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* Eyebrow chip, matching the Campus Info / profile heroes. */
.clubs-slogan-panel .club-caption {
  align-self: flex-start;
  width: fit-content;
  margin: 0 0 14px;
  padding: 5px 10px;
  border-radius: var(--radius-ui);
  background: var(--ink-strong-bg);
  color: #ffffff;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.04em;
  line-height: 1.2;
  text-transform: uppercase;
  text-align: left;
}

.clubs-slogan-panel h1 {
  /* Bigger now that the explanatory line under it is gone — the headline carries the
     panel on its own, and the old size left it floating in the space the paragraph
     used to fill. */
  max-width: 720px;
  margin: 0;
  color: var(--ink-strong);
  font-size: clamp(38px, 4.2vw, 64px);
  font-weight: 900;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

/* Brand-yellow highlighter, echoing the Campus Info hero's accent. */
.clubs-slogan-panel mark {
  padding: 0 0.06em;
  background: #ffdb16;
  color: var(--ink-strong);
}

.clubs-slogan-panel p:not(.club-caption) {
  max-width: 560px;
  margin: 16px 0 0;
  color: #526079;
  font-size: 15px;
  line-height: 1.6;
}

/* 网格, 不是 flex-wrap。这个面板只有 ~290px 宽(实测), 而标签长度从 "Tech"(64px)
   到 "Culture & Language"(165px) 差了两倍半 —— 自由换行的结果是 10 个标签排成 6 行,
   每行右边缘参差不齐, 中间三行还各自只站着一个标签。auto-fill + minmax 让每格等宽:
   同样的宽度下排成整齐的 2 列 5 行, 面板变宽时自动变 3 列、4 列。
   "Tech" 这类短标签会占满整格、两边留白, 这是等宽网格的正常代价, 换来的是一条干净
   的右边缘。 */
.club-filter-tags {
  display: grid;
  /* 下限 104px 而不是 120px。实测这个面板的内容区宽 240px, 而 auto-fill 排两列需要
     2 × 下限 + gap ≤ 容器宽 —— 120px 要 248px, 就差 8px, 于是整列塌回单列、十一个
     标签摞成十一行, 右边大片空白。104px 起两列在 216px 就成立, 留足余量; 三列要
     328px, 这个面板到不了, 所以实际就是稳定的两列。
     两列时每格 (240-8)/2 = 116px, 最长的中文标签"文化与语言"约 89px, 单行放得下。 */
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 8px;
  margin-top: 14px;
}

/* 居中 + 允许换行。中文标签(全部/文化与语言/运动与游戏…)在 126px 的格子里都是单行,
   但英文的 "Culture & Language" / "Community Service" 一行放不下 —— 等宽网格下这不
   可避免(两列已经是这个面板宽度的上限)。所以改成让它折行折得体面: grid 居中让 1 行
   和 2 行的标签都竖直居中, 上下 padding 取代原来的 0, 行高收到 1.25。 */
.club-filter-tags button {
  min-height: 34px;
  display: grid;
  place-items: center;
  text-align: center;
  line-height: 1.25;
  border: 1px solid color-mix(in srgb, var(--ink-strong-bg) 18%, transparent);
  border-radius: var(--radius-ui);
  padding: 6px 12px;
  background: rgba(255, 255, 255, 0.72);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 600;
  box-shadow: rgba(0, 0, 0, 0.08) 0 1px 2px 0;
}

.club-filter-tags button.is-active {
  border-color: var(--ink-strong-bg);
  background: var(--ink-strong-bg);
  color: #ffffff;
  box-shadow:
    rgba(0, 0, 0, 0.18) 0 1px 4px 0;
}

.club-filter-panel {
  margin-top: 24px;
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--brand) 18%, transparent);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.52);
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.08);
}

.club-filter-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  color: #8791a6;
  font-size: 12px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.club-filter-head strong {
  color: var(--ink-strong);
  font-size: 13px;
  letter-spacing: 0;
}

.clubs-main .clubs-grid {
  display: grid;
  /* quick task (260827R): the whole ladder was one to two steps too dense —
     at 1530px it packed 6 columns and at 761px still 4, leaving cards too
     narrow for a logo plus a two-line name. Every step below moved down one:
     6->5 here, a new 1600px step at 4, 1180px 5->3, 980px 4->3.
     那份阶梯现在住在 appConstants.js 的 CLUBS_COLUMN_LADDER 里 —— JS 要按同样的
     列数生成列容器，所以只能有一份。这里只读它写出来的 --club-columns。 */
  grid-template-columns: repeat(var(--club-columns, 5), minmax(0, 1fr));
  align-items: start;
  gap: 24px;
  margin-top: 0;
}

.club-masonry-column {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.clubs-main .club-widget {
  position: relative;
  min-height: 0;
  display: inline-flex;
  flex-direction: column;
  width: 100%;
  break-inside: avoid;
  margin: 0;
  padding: 0;
  overflow: hidden;
  color: var(--club-ink);
  cursor: pointer;
}

.clubs-main .club-widget.is-wide {
  width: 100%;
}

.club-image-wrap {
  width: 100%;
  min-height: 0;
  display: block;
  overflow: hidden;
  background: var(--surface);
}

.club-image-wrap img {
  width: 100%;
  min-height: 0;
  max-height: none;
  display: block;
  height: auto;
  object-fit: initial;
  filter: saturate(0.94) contrast(0.98);
  transition:
    transform 220ms ease,
    filter 220ms ease;
}

.club-image-wrap.is-fallback {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  background:
    radial-gradient(circle at 72% 24%, rgba(255, 255, 255, 0.62), transparent 24%),
    linear-gradient(135deg, rgba(0, 15, 70, 0.92), rgba(1, 48, 167, 0.7));
}

.club-widget:hover .club-image-wrap img {
  transform: scale(1.015);
  filter: saturate(1.02) contrast(1);
}

.club-image-fallback {
  width: 100%;
  min-height: 100%;
  display: grid;
  place-items: center;
  gap: 10px;
  padding: 24px;
  color: #ffffff;
  text-align: center;
}

.club-image-fallback span {
  font-size: 38px;
  line-height: 1;
}

.club-image-fallback small {
  font-size: 12px;
  opacity: 0.78;
}

.club-card-overlay {
  position: static;
  display: grid;
  gap: 6px;
  padding: 12px 14px 14px;
  border-top: 1px solid color-mix(in srgb, var(--brand) 8%, transparent);
  background: rgba(255, 255, 255, 0.95);
  color: var(--club-ink);
  backdrop-filter: none;
}

.club-card-overlay span {
  color: var(--club-graphite);
  font-size: 11px;
}

.club-card-overlay strong {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
}

.clubs-load-more {
  width: min(420px, calc(100% - 32px));
  margin: 18px auto 0;
  padding: 12px 16px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.62);
  color: color-mix(in srgb, var(--brand) 72%, transparent);
  font-size: 13px;
  text-align: center;
  box-shadow: rgba(0, 0, 0, 0.08) 0 1px 3px 0;
}

.clubs-load-more.is-complete {
  opacity: 0.72;
}

/* Two side-by-side windows: the club window + a separate event-history window.
   align-items: stretch makes them the same height. */
.club-modal-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: center;
  gap: 20px;
  width: 100%;
  max-height: 88vh;
}

/* Left window is wider than the events window so the logo column has room to
   render the mark at its natural size instead of leaving a tall white gap. */
.club-detail-modal {
  flex: 1.7 1 0;
  min-width: 0;
  max-width: 980px;
  max-height: min(88vh, 980px);
  overflow: auto;
  border-radius: 20px;
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  scrollbar-width: none;
}

/* The second window (event history): grey padded panel with separate poster cards. */
.club-events-modal {
  flex: 1 1 0;
  min-width: 0;
  max-width: 620px;
  max-height: min(88vh, 980px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 18px;
  border-radius: 20px;
  background: #dbdbdb;
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}

.club-detail-modal::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* Two columns: logo card on the left, copy on the right. The events list is a
   separate window beside this one, so the whole screen reads as three columns. */
/* Logo column is `auto` (content-width): the card shrink-wraps to the logo, so
   its width adapts to each logo's aspect ratio. Copy takes the remaining space. */
.club-detail-body {
  display: grid;
  grid-template-columns: auto minmax(300px, 1fr);
  gap: 22px;
  padding: 22px;
  background: #dbdbdb;
  align-items: start;
}

/* Fixed height + logo scaled to fill it (height: 100% below) means EVERY logo —
   wide, tall or square — touches the top and bottom edges. The card width then
   follows the logo's own proportion: wide marks get a wide card, tall marks a
   narrow one. max-width caps ultra-wide banners so the copy keeps its room. */
.club-detail-media {
  height: clamp(300px, 30vw, 420px);
  min-width: 200px;
  max-width: 460px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: 20px;
  background: var(--surface);
  box-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px 0;
}

.club-detail-media img {
  height: 100%;
  width: auto;
  max-width: 100%;
  display: block;
  object-fit: contain;
}

.club-detail-copy {
  min-width: 0;
  padding: 22px;
  border-radius: 20px;
  background: var(--surface);
  box-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px 0;
}


.club-events-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 4px 12px;
}

.club-events-head span {
  font-size: 14px;
  font-weight: 950;
  color: #111111;
}

.club-events-head strong {
  font-size: 12px;
  font-weight: 800;
  color: #647084;
}

.club-events-list {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-auto-rows: max-content;
  gap: 14px;
  overflow-y: auto;
  padding: 2px;
}

.club-event-card {
  display: block;
  overflow: hidden;
  border-radius: 16px;
  background: var(--surface);
  box-shadow: rgba(0, 0, 0, 0.16) 0 3px 6px 0;
  color: #111111;
  text-decoration: none;
}

.club-event-card:hover {
  box-shadow: rgba(0, 0, 0, 0.24) 0 6px 14px 0;
}

/* Full poster — natural aspect ratio, not cropped. */
.club-event-poster {
  display: block;
  width: 100%;
  height: auto;
  background: var(--surface-muted);
}

.club-event-body {
  display: grid;
  gap: 3px;
  padding: 12px 16px 14px;
}

.club-event-title {
  font-size: 13px;
  font-weight: 800;
  line-height: 1.3;
  color: var(--ink-strong);
}

.club-event-meta {
  font-size: 12px;
  color: #647084;
}

.club-detail-category {
  display: inline-block;
  margin-bottom: 12px;
  border-radius: 20px;
  padding: 7px 14px;
  background: var(--surface-muted);
  color: #303030;
  font-size: 12px;
}

.club-detail-copy p {
  margin: 0 0 18px;
  color: #303030;
  font-size: 14px;
  line-height: 1.65;
}

.club-detail-links {
  display: grid;
  gap: 8px;
}

.club-detail-links a {
  display: block;
  overflow: hidden;
  border-radius: 20px;
  padding: 9px 14px;
  background: var(--surface-muted);
  color: #303030;
  font-size: 12px;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.club-detail-links a:hover {
  background: #ededed;
}

.club-wall-panel {
  display: grid;
  gap: 14px;
  padding: 0 22px 22px;
  background: #dbdbdb;
}

.club-wall-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 20px;
  border-radius: 20px;
  background: var(--surface);
  box-shadow: rgba(0, 0, 0, 0.12) 0 2px 5px 0;
}

.club-wall-head span {
  color: #808080;
  font-size: 12px;
  font-weight: 800;
}

.club-wall-head h3 {
  margin: 6px 0;
  color: #303030;
  font-size: 20px;
  line-height: 1.25;
}

.club-wall-head p {
  max-width: 620px;
  margin: 0;
  color: #5d6470;
  font-size: 13px;
  line-height: 1.5;
}

.club-wall-head strong {
  flex: 0 0 auto;
  border-radius: var(--radius-ui);
  padding: 8px 10px;
  background: var(--surface-muted);
  color: #303030;
  font-size: 12px;
}

.club-wall-actions {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 10px;
}

.club-wall-compose-button {
  min-height: 34px;
  border-color: #d7d7d7;
  background: #303030;
  color: #ffffff;
}

.club-wall-compose-button:hover {
  background: #111111;
}

.club-post-form {
  display: grid;
  gap: 12px;
  padding: 16px;
  border-radius: 20px;
  background: var(--surface);
  box-shadow: rgba(0, 0, 0, 0.12) 0 2px 5px 0;
}

.club-post-grid {
  display: grid;
  grid-template-columns: minmax(160px, 0.35fr) minmax(0, 1fr);
  gap: 12px;
}

.club-post-form label {
  display: grid;
  gap: 7px;
  color: #6b7280;
  font-size: 12px;
  font-weight: 850;
}

.club-post-form input,
.club-post-form select,
.club-post-form textarea {
  width: 100%;
  border: 1px solid #cfd7e4;
  border-radius: var(--radius-ui);
  padding: 10px 12px;
  background: var(--surface-2);
  color: #111827;
  font: inherit;
}

.club-post-form textarea {
  resize: vertical;
}

.club-post-form .primary-button {
  justify-content: center;
  min-height: 40px;
  background: #303030;
}

.club-history-list {
  display: grid;
  gap: 10px;
}

.club-history-list .mini-post,
.club-history-empty {
  border: 0;
  border-radius: 16px;
  background: var(--surface);
  box-shadow: rgba(0, 0, 0, 0.10) 0 2px 5px 0;
}

.club-history-empty {
  display: grid;
  gap: 6px;
  padding: 18px;
  color: #303030;
}

.club-history-empty span {
  color: #6b7280;
  font-size: 13px;
}

.admin-tabs {
  display: flex;
  gap: 8px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--line);
}

.admin-tabs button {
  min-height: 34px;
  padding: 0 12px;
}

.admin-body {
  display: grid;
  gap: 10px;
  padding: 18px;
}

.admin-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 12px;
  align-items: center;
}

.admin-row p,
.notification-item p,
.log-row p {
  margin: 5px 0;
  color: var(--muted);
}

.admin-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 7px;
}

.admin-actions button,
.admin-row > button {
  min-height: 32px;
  padding: 0 10px;
  background: var(--surface-muted);
}

.bottom-nav {
  display: none;
}

.mobile-only {
  display: none;
}

/* Hide the left 热度帖子 rail as soon as the middle feed leaves its full width.
   The 3-col layout (260 + gap + minmax(720,860) + gap + 260) plus the app
   sidebar needs roughly ~1650px of viewport to keep the feed at its ideal 860px;
   below that the feed compresses, so drop the rail here (was 1580, which lingered
   while the feed was already squeezed) to give the feed its width back. */
@media (max-width: 1680px) {
  .feed-hot-rail {
    display: none;
  }

  .feed-top-row {
    grid-template-columns: 44px minmax(0, 860px) 260px;
    gap: 14px;
  }

  /* The left hot rail is hidden in this range, so the feed body must not keep
     its 44px placeholder column after real posts load. Loading state already
     used the rail-empty geometry; keeping the live body on the same geometry
     prevents the visible left jump from skeleton -> feed. */
  .feed-layout {
    grid-template-columns: minmax(0, 860px) 260px;
    gap: 14px;
  }

  .feed-layout .feed-column {
    grid-column: 1;
  }

  .feed-layout .feed-sidebar {
    grid-column: 2;
  }

  /* The rail-suppressed variant for this range lives in its own
     (min-width: 1281px) and (max-width: 1680px) block further up the file, not
     here — it has to be excluded from the narrower breakpoints below, which a
     rule nested in this max-width-only block could not express.
     Note the 44px first column above is the TOOL rail's, not the hot rail's
     (the hot rail is display:none at this width). In .feed-layout that column
     has no occupant at all and is pure dead space, which is what pushed the
     feed off-centre. */
}

@media (max-width: 1280px) {
  .discussion-hero {
    grid-template-columns: 1fr;
  }

  .discussion-copy {
    min-height: 280px;
  }

  .discussion-content-grid {
    grid-template-columns: 1fr;
  }

  .origin-map {
    min-height: 0;
    height: auto;
  }

  .campus-info-page {
    width: min(960px, calc(100% - 28px));
  }

  .campus-info-hero {
    min-height: 190px;
    padding: 30px 28px;
  }

  .campus-info-hero::before,
  .campus-info-hero::after {
    opacity: 0.42;
  }

  .campus-feature-row,
  .campus-pick-grid,
  .campus-event-grid {
  }

  .campus-news-strip {
    grid-template-columns: repeat(3, minmax(180px, 1fr));
  }

  .campus-question-grid {
    grid-template-columns: 1fr;
  }

  .feed-top-row {
    grid-template-columns: 44px minmax(0, 760px) auto;
    gap: 14px;
  }

  .feed-layout {
    grid-template-columns: minmax(0, 760px);
    gap: 0;
  }

  .feed-column {
    grid-column: 1;
  }

  .feed-hot-rail,
  .feed-sidebar {
    display: none;
  }

  .masonry-feed {
    max-width: 760px;
  }

  .board-tabs {
    width: min(100%, 760px);
  }

  .profile-form-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .profile-avatar-edit-controls {
    grid-template-columns: 1fr;
  }

  .profile-avatar-card {
    grid-template-columns: 1fr;
  }
}

@media (max-height: 880px) {
  .feed-hot-rail {
    display: none;
  }

  .feed-top-row {
    grid-template-columns: 44px minmax(0, 860px) 260px;
    gap: 14px;
  }

  .feed-layout {
    grid-template-columns: minmax(0, 860px) 260px;
    gap: 14px;
  }

  .feed-layout .feed-column {
    grid-column: 1;
  }

  .feed-layout .feed-sidebar {
    grid-column: 2;
  }
}

@media (max-width: 1280px) and (max-height: 880px) {
  .feed-top-row {
    grid-template-columns: 44px minmax(0, 760px) auto;
  }

  .feed-layout {
    grid-template-columns: minmax(0, 760px);
  }
}

@media (max-width: 980px) {
  /* 侧栏在这个断点下的折叠形态不再写在这里。以前这一整段是 .sidebar.is-collapsed
     的第二份实现, 两份不一致 —— 这一份漏了 .brand-block 的 justify-content/padding/
     background 归零, 于是自动折叠时 logo 是歪的、手动折叠时却是正的(用户报的正是
     这个)。现在 is-collapsed 这个类由 main.ts 的 sidebarCollapsed() 统一给出
     (手动偏好 || 窄屏), 样式只有一份, 两条路径必然一致。
     只留下这个断点真正独有的两条: 折叠钮在自动折叠时没有意义(用户不能在这个宽度下
     展开), 以及首页左栏那个热帖挂件本来就该在窄屏收起 —— 它不在侧栏里, 与折叠无关。 */
  .sidebar-toggle {
    display: none;
  }

  .hot-posts-widget {
    display: none;
  }

  .profile-columns {
    grid-template-columns: 1fr;
  }

  .profile-hero-cover {
    height: 100px;
  }

  .profile-hero-identity {
    margin-top: -40px;
  }

  .feed-top-row {
    grid-template-columns: 44px minmax(0, 600px) auto;
  }

  .feed-layout {
    grid-template-columns: minmax(0, 600px);
  }

  .feed-column {
    grid-column: 1;
  }

  .feed-sidebar {
    display: none;
  }

  .faculty-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: none;
  }

  .discussion-main {
    padding-inline: 16px;
  }

  .discussion-question-list {
    grid-template-columns: 1fr;
  }

  .faculty-detail-grid {
    grid-template-columns: 1fr;
  }

  .faculty-subject-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .faculty-level-booklet,
  .booklet-children {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .professor-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .faculty-professor-toolbar {
    grid-template-columns: 1fr;
  }

  .professor-add-form,
  .review-select-grid {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 720px) {
  /* The feed stays ONE column on a phone (.masonry-feed is column-count: initial —
     the name is historical), so the row keeps its whole label and only the cover
     comes down to phone size. */
  .post-article {
    gap: 10px;
    padding: 10px;
  }

  .post-article-thumb {
    width: 72px;
    height: 62px;
  }

  :root {
    --sidebar-width: 0px;
    --topbar-height: 60px;
    /* Phone-wide UI scale-down. This stylesheet is 100% px (zero rem), so there
       is no root font-size to turn — `zoom` on the root is the one lever that
       shrinks type AND every border/padding/radius together, exactly like
       browser zoom, instead of us hand-editing several hundred rules.
       Media queries still evaluate against the REAL viewport (verified), so
       there is no shrink → re-match → unshrink feedback loop; the layout simply
       gets ~13% more CSS px of room to work with. */
    --ui-zoom: 0.88;
    /* 1 / --ui-zoom, kept as its own literal. calc() division by a var() is not
       reliably honoured (it silently fell back to the /1 default here), so
       anything that has to undo the zoom MULTIPLIES by this instead. Keep the
       two in sync if the zoom is ever retuned. */
    --ui-zoom-inv: 1.13636;
    zoom: var(--ui-zoom);
  }


  /* The two views that must fill the screen EXACTLY. Every other vh/dvh in the
     file is a max-height or min-height, which is fine to scale with the UI, but
     a viewport-locked height gets painted at 100dvh × zoom and leaves a band of
     dead page below it — so divide it back out. */
  /* main.<class>, not the bare class: the base `height: 100dvh` rules live LATER
     in this file, so an equal-specificity override here would lose the cascade. */
  main.posters-main,
  main.game-squad-main {
    height: calc(100dvh * var(--ui-zoom-inv, 1));
  }

  body {
    font-size: 14px;
  }

  .game-squad-main {
    padding: var(--topbar-height) 0 76px;
  }

  .game-squad-page {
    /* Fill exactly the gap between the fixed topbar and the fixed bottom-nav.
       - Multiply the viewport term by --ui-zoom-inv so it matches its parent
         main.game-squad-main (which is 100dvh * --ui-zoom-inv); on the game-squad
         view --ui-zoom-inv is 1 (zoom neutralised above), elsewhere 1.13636.
       - Subtract the REAL bottom-nav height, not a guess: .bottom-nav is
         min-height:46px (border-box) PLUS padding-bottom:env(safe-area-inset-bottom)
         for the iPhone home indicator. The old hardcoded 76px over-reserved on
         desktop (~30px dead band) and ignored the safe-area on device. */
    height: calc(100vh * var(--ui-zoom-inv, 1) - var(--topbar-height) - 46px - env(safe-area-inset-bottom)); /* fallback */
    height: calc(100dvh * var(--ui-zoom-inv, 1) - var(--topbar-height) - 46px - env(safe-area-inset-bottom));
    min-height: 520px;
    border-radius: 0;
  }

  .squadpool-frame-shell {
    border-radius: 0;
  }

  .discussion-main {
    padding: calc(var(--topbar-height) + 14px) 12px 88px;
  }

  .discussion-hero,
  .discussion-question-section {
    padding: 12px;
  }

  .discussion-copy {
    min-height: 250px;
    padding: 18px;
  }

  .discussion-poll-header {
    align-items: start;
    flex-direction: column;
    gap: 8px;
  }

  .faculty-subject-grid {
    grid-template-columns: 1fr;
  }

  /* Title + count + sort chips on a phone. Tighter gap and an unbreakable title
     let all three share one row in Chinese; in English (much longer labels) the
     group drops to its own line instead of squeezing the title into two lines —
     either way .course-sort itself stays a single scrollable row. */
  .faculty-level-heading {
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;
    gap: 6px 8px;
  }

  /* Unbreakable + a size down: at 22px the title alone ate enough width to push
     the chips off the row. .course-sort keeps margin-left:auto, so it still hugs
     the right edge. */
  .faculty-level-heading h2 {
    white-space: nowrap;
    font-size: 18px;
  }

  .course-scope-tab {
    min-height: 30px;
    padding: 0 11px;
    font-size: 13px;
  }

  /* 课程/教授 那一条(.faculty-scope-strip)只在手机上调, 桌面保持原样。
     两处偏差都是它从上面那条通用 .faculty-level-heading 继承来的:

     (1) 两边比下面的内容内缩 2px —— 通用规则有 padding: 0 2px, 而它下面的
         .faculty-course-level 面板 padding 是 0。页签本来就该和它所属的面板齐边。
     (2) 右边那组排序 chip 比页签低 —— 父级 align-items: end(底对齐), 而这个断点里
         页签 min-height 是 30、chip 是 32, 底边一齐中心就差。让两者等高即可, 不必
         去改对齐方式。 */
  .faculty-scope-strip {
    padding: 0;
  }

  .faculty-scope-strip .faculty-area-chip {
    min-height: 30px;
  }

  .faculty-detail-hero {
    grid-template-columns: 1fr;
  }

  .discussion-copy h1 {
    font-size: 42px;
  }

  .origin-map {
    min-height: 0;
    height: auto;
  }

  /* main.<class> (not the bare class) so this beats the generic .main-content
     mobile rule further down the file, which otherwise adds another 12px on top
     of the topbar offset. The board wants to sit flush under the bar — its
     sticky tab strip is the first thing you should see. */
  main.campus-info-main {
    padding: calc(var(--topbar-height) + var(--safe-top)) 0 calc(88px + var(--safe-bottom));
  }

  .campus-info-page {
    width: 100%;
  }

  .campus-info-hero {
    min-height: 230px;
    padding: 58px 18px 24px;
  }

  .campus-hero-tabs {
    top: 12px;
    width: calc(100% - 24px);
    justify-content: space-between;
  }

  .campus-hero-tabs button {
    min-width: 0;
    flex: 1;
  }

  .campus-hero-copy h1 {
    font-size: 42px;
  }

  .campus-feature-row,
  .campus-pick-grid,
  .campus-event-grid,
  .campus-news-strip,
  .campus-question-grid {
    grid-template-columns: 1fr;
  }

  .campus-feature-row,
  .campus-section {
    width: calc(100% - 24px);
    margin-left: auto;
    margin-right: auto;
  }

  .auth-modal {
    width: min(100% - 20px, 440px);
    max-height: calc(100dvh - 24px);
    grid-template-columns: 1fr;
    overflow-x: hidden;
    overflow-y: auto;
  }

  .auth-modal:not(.auth-register-modal) .auth-info-panel {
    display: none;
  }

  .auth-info-panel,
  .auth-register-profile-panel {
    border-right: 0;
  }

  .auth-info-panel {
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  }

  .auth-register-profile-panel {
    border-bottom: 1px solid #d8e1ef;
  }

  .auth-info-panel,
  .auth-form-panel,
  .auth-register-profile-panel {
    padding: 22px;
  }

  .auth-register-modal {
    width: min(100% - 20px, 460px);
  }

  .auth-register-profile-panel {
    gap: 12px;
  }

  .auth-register-panel-heading strong {
    font-size: 20px;
  }

  .auth-register-edit-card,
  .auth-form {
    padding: 14px;
  }

  .auth-avatar-dropzone {
    grid-template-columns: auto minmax(0, 1fr);
    padding: 10px;
  }

  .auth-avatar-dropzone .auth-avatar-copy small {
    display: none;
  }

  .avatar-crop-stage {
    width: min(128px, 42vw);
    height: min(128px, 42vw);
  }

  .auth-register-profile-panel input:not([type="range"]):not([type="file"]),
  .auth-register-profile-panel select,
  .auth-form input,
  .auth-send-code-button {
    min-width: 0;
  }

  .auth-modal .primary-button,
  .auth-form > .auth-send-code-button {
    width: 100%;
  }

  .auth-code-row {
    grid-template-columns: minmax(0, 1fr) minmax(104px, auto);
    align-items: end;
    gap: 8px;
  }

  .auth-code-row .auth-send-code-button {
    width: auto;
    padding: 0 10px;
  }

  .profile-page {
    padding: 14px 12px 88px;
  }

  .profile-field-head {
    display: grid;
    grid-template-columns: 1fr;
  }

  .profile-field-head span {
    max-width: none;
    text-align: left;
  }

  .profile-form-grid {
    grid-template-columns: 1fr;
  }

  .mobile-only {
    display: grid;
  }

  .sidebar {
    width: min(290px, 86vw);
    transform: translateX(-104%);
    transition: transform 180ms ease;
    align-items: stretch;
  }

  .sidebar.is-open {
    transform: translateX(0);
  }

  /* Lock the page while the slide-in nav is open so iOS Safari can't scroll the
     content under the fixed sidebar (the source of the "UI 变奇怪" glitch). */
  body.has-menu-open {
    overflow: hidden;
  }

  .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 25;
    background: rgba(10, 16, 34, 0.42);
    -webkit-tap-highlight-color: transparent;
    overscroll-behavior: contain;
  }

  /* Keep the open menu's own scroll from chaining to the locked page. */
  .sidebar.is-open {
    overscroll-behavior: contain;
  }

  .sidebar-close {
    position: absolute;
    top: 12px;
    right: 12px;
    width: 34px;
    height: 34px;
    place-items: center;
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.06);
    color: #fff;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
  }

  .sidebar-toggle {
    display: none;
  }

  .brand-block div:not(.brand-mark),
  .nav-item span:last-child,
  .board-link span,
  .section-label,
  .tag-follow-section,
  .admin-link {
    display: block;
  }

  /* The mobile sidebar is a full-width slide-in drawer, not the 78px rail, so the
     icon-only treatment the ≤980px block applies has to be undone here — same as
     every label above. A bare ⊞ / ✉ in a drawer this wide is a guessing game. */
  .sidebar-links-link span:not([aria-hidden]),
  .sidebar-feedback-link span:not([aria-hidden]) {
    display: block;
  }

  .nav-item,
  .board-link,
  .sidebar-links-link,
  .sidebar-feedback-link {
    width: 100%;
    justify-content: flex-start;
  }

  .sidebar-links-link,
  .sidebar-feedback-link {
    padding: 8px 10px;
  }

  .board-link::before {
    display: none;
  }

  .topbar {
    display: flex;
    left: 0;
    gap: 8px;
    /* Extend the fixed bar up into the status-bar / notch inset so its brand
       background fills that area; padding pushes the controls below the inset.
       Off-device --safe-top is 0px → identical to the old 9px 10px bar. */
    height: auto;
    min-height: var(--topbar-height);
    padding: calc(9px + var(--safe-top)) 10px 9px;
  }

  .topbar .mobile-create-button {
    width: 36px;
    height: 36px;
    min-width: 36px;
    border: 1px solid color-mix(in srgb, var(--topbar-ink) 28%, transparent);
    color: var(--topbar-ink);
    font-size: 19px;
    font-weight: 800;
  }

  .topbar .notification-button,
  .topbar .profile-chip {
    display: none;
  }

  .search-shell {
    position: static;
    transform: none;
    grid-column: auto;
    justify-self: auto;
    flex: 1;
    width: auto;
    min-width: 0;
  }

  .header-actions {
    grid-column: auto;
    justify-self: auto;
  }

  .desktop-action,
  .profile-chip span:not(.avatar) {
    display: none;
  }

  /* No +12px: that constant put a strip of bare page background under the fixed
     topbar on EVERY view. Whatever sits first (the feed's sticky tab row, a board
     header, a card) carries its own padding, so flush is correct here. */
  .main-content {
    margin-left: 0;
    padding: calc(var(--topbar-height) + var(--safe-top)) 10px calc(64px + var(--safe-bottom));
  }

  .faculty-grid {
    grid-template-columns: 1fr;
    grid-template-rows: none;
  }

  .faculty-detail-hero {
    grid-template-columns: 1fr;
    padding: 28px 18px;
  }

  .faculty-detail-hero > span {
    width: 92px;
  }

  .faculty-search-form {
    grid-template-columns: 1fr;
  }

  .faculty-detail-actions {
    align-items: stretch;
    flex-direction: column;
  }

  .faculty-detail-actions > div:last-child {
    justify-content: flex-start;
  }

  .faculty-level-booklet,
  .booklet-children {
    grid-template-columns: 1fr;
  }

  .faculty-subject-grid,
  .professor-grid,
  .professor-add-form,
  .review-slider-grid,
  .review-select-grid {
    grid-template-columns: 1fr;
  }

  .faculty-subject-filter-block {
    position: relative;
    gap: 6px;
  }

  .faculty-subject-filter-block.is-collapsed .faculty-subject-filters {
    max-height: 158px;
    overflow: hidden;
  }

  /* bottom:0 — the expand/collapse button now lives at the PANEL's foot (below
     the level directory), so the fade no longer has to clear it inside this
     block; it just feathers the clipped last row of chips. */
  .faculty-subject-filter-block.is-collapsed::after {
    content: "";
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 38px;
    pointer-events: none;
    background: linear-gradient(180deg, rgba(255, 255, 255, 0), var(--surface) 82%);
  }

  /* The level directory (Undergraduate / Professor / Graduate coursework /
     Research) folds away with the subject tags — on a phone the two stacked
     filter groups pushed the actual course list a whole screen down. Driven by
     is-levels-collapsed, which the view withholds while one of these rows is the
     active filter, so a drilled-in student can always get back out. */
  .faculty-filter-panel.is-levels-collapsed .faculty-level-booklet {
    display: none;
  }

  /* Big faculty colour block is desktop-only: on a phone it burned ~200px above
     the fold for a name the student just tapped. The back button keeps context. */
  .faculty-detail-hero {
    display: none;
  }

  .faculty-area-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    justify-self: stretch;
    min-height: 34px;
    border: 1px solid rgba(217, 223, 235, 0.9);
    border-radius: var(--radius-ui);
    background: var(--surface);
    color: color-mix(in srgb, var(--faculty-detail-theme, var(--ink-strong)) 72%, var(--ink-strong));
    font-size: 12px;
    font-weight: 900;
  }

  .faculty-area-chip {
    min-height: 29px;
    padding: 0 9px;
    font-size: 11px;
    gap: 5px;
  }

  .booklet-row {
    min-height: 38px;
    padding: 0 10px;
    font-size: 13px;
  }

  .faculty-course-results,
  .faculty-filter-panel {
    gap: 12px;
  }

  .faculty-search-expanded-note {
    align-items: flex-start;
    flex-direction: column;
  }

  .subject-card-cover {
    grid-template-columns: 78px minmax(0, 1fr) 92px;
    gap: 12px 10px;
  }

  .subject-card-side {
    grid-column: 3;
    grid-row: 1;
    justify-items: end;
    align-self: start;
    gap: 6px;
    text-align: right;
  }

  .subject-card-side span {
    max-width: 92px;
    font-size: 10px;
    line-height: 1.18;
  }

  .faculty-subject-card.is-en-card .subject-offered-side {
    display: none;
  }

  .faculty-subject-card.is-en-card .subject-offered-inline {
    display: block;
  }

  .faculty-subject-card.is-en-card .subject-card-side {
    justify-content: start;
  }

  .faculty-subject-card.is-zh-card .subject-cn-name {
    font-size: 15px;
    line-height: 1.12;
  }

  .faculty-subject-card.is-zh-card h3:not(.subject-cn-name) {
    font-size: 16px;
    line-height: 1.14;
  }

  .faculty-subject-card h3.subject-en-name-only {
    font-size: 17px;
  }

  .subject-score-tile {
    min-height: 74px;
  }

  .subject-score-tile strong {
    font-size: 24px;
  }

  .subject-code {
    font-size: 12px;
  }

  .subject-meta span,
  .review-scoreline span {
    font-size: 10px;
  }

  .faculty-subject-card p {
    font-size: 12px;
    line-height: 1.42;
  }

  .subject-card-review-button {
    min-height: 28px;
    padding: 0 8px;
    font-size: 10px;
  }

  .subject-review-summary,
  .subject-review-item {
    grid-template-columns: 1fr;
  }

  .subject-review-summary .subject-score-tile,
  .review-item-score {
    width: 92px;
  }

  .feed-top-row {
    grid-template-columns: 44px minmax(0, 1fr);
    gap: 8px;
    align-items: start;
  }

  /* On phones the topbar "+" mobile-create-button is the publish entry, so the
     inline feed button steps aside to keep the tab row uncrowded. */
  .feed-create-rail {
    display: none;
  }

  .feed-layout {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .feed-rail-spacer {
    display: none;
  }

  .sort-menu summary {
    width: 42px;
    min-width: 42px;
    height: 42px;
    padding: 0;
  }

  .masonry-feed {
    border-radius: 0;
    border-left: 0;
    border-right: 0;
  }

  .post-card {
    grid-template-columns: 40px minmax(0, 1fr);
    padding: 12px 10px;
  }

  .post-card-body {
    padding: 0;
  }

  .thread-meta {
    padding-right: 96px;
  }

  .media-strip {
    width: 100%;
    padding: 0;
  }

  .card-actions {
    gap: 14px;
  }

  .card-actions button {
    padding: 0;
  }

  /* Website mobile feed card (default, unchanged): full-width, no avatar column. */
  .post-card {
    grid-template-columns: 1fr;
    gap: 8px;
    padding: 16px 16px 14px;
  }

  .thread-avatar {
    display: none;
  }

  /* App-only: denser card with a small author avatar to the left + smaller type. */
  .is-native-app .post-card {
    grid-template-columns: 28px minmax(0, 1fr);
    gap: 9px;
    padding: 13px 14px 12px;
  }

  .is-native-app .thread-avatar {
    display: block;
    padding-top: 1px;
  }

  .is-native-app .thread-avatar .avatar-normal {
    width: 28px;
    height: 28px;
    font-size: 11px;
  }

  .is-native-app .post-card h2 {
    margin: 3px 0 5px;
    font-size: 15px;
    line-height: 1.3;
  }

  .is-native-app .post-card p {
    margin: 0 0 9px;
    font-size: 13px;
    line-height: 1.5;
  }

  .is-native-app .thread-meta {
    gap: 4px;
    font-size: 11px;
  }

  /* App-only: the publish time is hidden on the feed card — it shows in the
     post detail view once the card is opened. */
  .is-native-app .post-card .post-time {
    display: none;
  }

  /* The clamp + toggle now live in the base stylesheet — they apply at every width. */

  .card-action-group {
    flex-wrap: nowrap;
    width: 100%;
    justify-content: space-between;
    gap: 8px;
  }

  .modal-backdrop {
    /* Centred, not bottom-anchored. A short modal (login, report, settings) was
       glued to the bottom edge with a tall band of scrim above it; anything that
       fills the height is unaffected either way. Home-indicator inset added
       underneath so a card's rounded bottom never sits under the gesture bar. */
    padding: 8px 8px calc(8px + env(safe-area-inset-bottom));
    align-items: center;
  }

  /* THE systemic bit. Every modal variant had its own vh/dvh height cap
     (.auth-modal, .dual-modal-row, the club windows, …) and viewport units do
     NOT account for the root zoom — each one silently came up ~12% short, which
     is what left a dead band above the login card and the course windows.
     Measuring against the BACKDROP instead sidesteps the whole problem: it is
     fixed inset:0, so 100% of its content box is exactly the usable area, at any
     zoom, with the padding and safe-area already subtracted.
     Prefixed with .modal-backdrop > so this beats every single-class cap
     regardless of where those rules sit in the file. */
  .modal-backdrop > .modal-card,
  .modal-backdrop > .drawer-card,
  .modal-backdrop > .dual-modal-row,
  .modal-backdrop > .club-modal-row,
  .modal-backdrop > .detail-modal,
  .modal-backdrop > .auth-modal {
    max-height: 100%;
  }

  /* ALL FOUR corners rounded (was square-bottomed): a body too long to fit now
     scrolls inside the card and ends on a rounded edge instead of reading as
     content sliced off by the screen edge. Height comes from the shared
     .modal-backdrop > … cap above. */
  .modal-card,
  .drawer-card {
    width: 100%;
    /* dvh is NOT divided by the root zoom — it resolves to the raw device height
       inside the zoomed coordinate space, so it paints ~12% short and left a
       ~100px band above the sheet. Divide it back out, same as .posters-main. */
    border-radius: 14px;
  }

  /* Review list rows: drop the 84px score GUTTER. It ran the full height of the
     card, so the body text was indented past it with a tall empty column
     alongside — on a phone that is a quarter of the width spent on nothing. The
     tile becomes a compact horizontal pill at the top of the row and the body
     gets the full width underneath.
     NOTE: this block is deliberately DOWN HERE rather than up with the other
     review rules — the base .subject-review-item grid is declared at ~8824, so
     an equal-specificity override placed earlier loses the cascade. */
  /* Block + float, not a grid column. A grid column runs the FULL card height, so
     the body text was indented past it with a tall empty gutter alongside. As a
     float, the square sits beside the two chip rows and the body reclaims the
     full width the moment it clears the square's height. */
  .subject-review-item {
    display: block;
  }

  .subject-review-item::after {
    content: "";
    display: block;
    clear: both;
  }

  /* A square holding the SCORE caption and the number. Only the trailing
     Course / StudentVIP line is dropped — it repeats what the chip row beside it
     already says. 56px is sized to two rows of metric chips (24px chip + 6px
     gap), so the body starts on the line right below them. */
  /* 靠右浮动（2026-08-29）：靠左时方块压在正文的起始边上，前三行被推进去、第四行
     突然弹回左边界，读起来像左边被啃了一口 —— 中文正文没有首行缩进的习惯，这个缺口
     比英文更显眼。挪到右边之后正文每一行都从同一条左边界起排，方块变成右上角的一枚
     角标；分数本来就是顺带看一眼的信息，不该占着视线最先落下的位置。 */
  /* 只留数字, 收成单行高的胶囊（2026-08-29）。原来是 56px 的方块, 里面叠着
     SCORE 小字和分数两行 —— 那个 SCORE 是给桌面那张大方块写的标签, 在手机上
     它和分数抢同一块 56px, 结果两个都小。评分本来就是一个数字, 一行装得下。 */
  .review-item-score {
    float: right;
    width: auto;
    min-width: 44px;
    height: auto;
    min-height: 0;
    /* 外边距跟着换边：右浮动要的是左侧留白，右侧贴齐卡片内边距。 */
    margin: 0 0 4px 10px;
    padding: 3px 10px;
    gap: 0;
  }

  .review-item-score span {
    display: none;
  }

  .review-item-score em {
    display: none;
  }

  .review-item-score span {
    font-size: 9px;
  }

  .review-item-score strong {
    font-size: 20px;
    line-height: 1;
  }

  .detail-modal,
  .user-profile-modal {
    width: 100%;
  }

  .detail-post,
  .comment-section,
  .user-profile-posts {
    padding-left: 16px;
    padding-right: 16px;
  }

  .detail-post h1 {
    font-size: 24px;
  }

  .user-profile-summary {
    padding-left: 16px;
    padding-right: 16px;
  }

  .user-profile-cover {
    margin-left: -16px;
    margin-right: -16px;
  }

  .user-profile-identity,
  .user-profile-stats {
    grid-template-columns: 1fr;
  }

  /* .create-type-tabs deliberately NOT stacked here any more: it dropped to one
     column because each tab carried a caption. The labels are single words now, so
     three columns fit a phone, and stacking would both waste a third of the screen
     and strand the close button beside the middle tab. */
  .create-two-column,
  .create-rating-grid {
    grid-template-columns: 1fr;
  }

  .create-type-tabs {
    gap: 6px;
  }

  .create-type-tabs button {
    min-height: 40px;
    padding: 6px 4px;
    font-size: 13px;
  }

  .drawer-card {
    position: static;
  }

  .admin-row {
    grid-template-columns: 1fr;
  }

  .admin-actions {
    justify-content: flex-start;
  }

  .comment-item {
    margin-left: calc(var(--depth) * 8px);
  }

  .bottom-nav {
    position: fixed;
    inset: auto 0 0;
    z-index: 25;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    min-height: 46px;
    padding-bottom: env(safe-area-inset-bottom);
    border-top: 1px solid var(--line);
    background: rgba(255, 255, 255, 0.96);
  }

  /* 下滑收起、上滑露出（方向判定在 src/ui/render.js 的 initBottomNavAutoHide）。
     只动 transform，不动布局 —— 页面底部为底栏预留的空间保持不变，收起时内容不会
     被抻一下。translateY(100%) 是 border-box 高度，已经含了 home indicator 那圈
     padding-bottom: env(safe-area-inset-bottom)，所以能整条滑出屏幕。 */
  .bottom-nav {
    transition: transform 200ms cubic-bezier(0.33, 1, 0.68, 1);
    will-change: transform;
  }

  html.is-bottom-nav-hidden .bottom-nav {
    transform: translateY(100%);
  }

  @media (prefers-reduced-motion: reduce) {
    .bottom-nav {
      transition: none;
    }
  }

  /* Website's original text-glyph nav buttons (the SVG-icon nav is app-only). */
  .bottom-nav button {
    display: grid;
    place-items: center;
    gap: 1px;
    border: 0;
    background: transparent;
    color: var(--muted);
    font-size: 15px;
    padding: 4px 0;
  }

  .bottom-nav span {
    font-size: 9px;
  }

  /* App-only: frosted material bar the feed scrolls under. -webkit- prefix is
     required in iOS WKWebView. */
  .is-native-app .bottom-nav {
    border-top: 1px solid rgba(255, 255, 255, 0.5);
    background: rgba(255, 255, 255, 0.72);
    -webkit-backdrop-filter: blur(20px) saturate(180%);
    backdrop-filter: blur(20px) saturate(180%);
  }

  @media (prefers-reduced-transparency: reduce) {
    .is-native-app .bottom-nav {
      background: var(--surface);
      -webkit-backdrop-filter: none;
      backdrop-filter: none;
    }
  }

  /* App-only press feedback: instant, subtle tactile response on tap. The
     app-only nav item carries its own transform transition (below). */
  .bottom-nav-item:active {
    transform: scale(0.93);
  }
  .is-native-app .post-action-button,
  .is-native-app .tab-pill {
    transition: transform 140ms var(--ease-out);
  }
  .is-native-app .post-action-button:active,
  .is-native-app .tab-pill:active {
    transform: scale(0.93);
  }

  .bottom-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: 46px;
    border: 0;
    background: transparent;
    color: var(--muted);
    padding: 6px 0 4px;
    -webkit-tap-highlight-color: transparent;
    transition: color 140ms var(--ease-out), transform 140ms var(--ease-out);
  }

  .bottom-nav-item.is-active {
    color: var(--brand);
  }

  .bottom-nav-ico {
    display: grid;
    place-items: center;
  }

  .bottom-nav-ico svg {
    width: 23px;
    height: 23px;
    display: block;
  }

  .bottom-nav-label {
    font-size: 10px;
    line-height: 1;
    letter-spacing: 0.2px;
  }
}

/* The club grid used to jump 6 -> 4 at 980px, so the whole 980-1180 band ran six
   columns that were far too narrow for a logo and two lines of name. One step in
   between. Must stay ABOVE the 980px block: both match at narrower widths, and the
   later rule wins. */
@media (max-width: 1600px) {
  .clubs-main .clubs-grid {
    gap: 22px;
  }
}

@media (max-width: 1180px) {
  .clubs-main .clubs-grid {
    gap: 20px;
  }
}

@media (max-width: 980px) {
  .clubs-desktop-top,
  .club-detail-body {
    grid-template-columns: 1fr;
  }

  /* Stack the two windows on narrow screens. */
  .club-modal-group {
    flex-direction: column;
    align-items: stretch;
    max-height: none;
  }

  .club-detail-modal,
  .club-events-modal {
    width: 100%;
    max-height: none;
  }

  .club-post-grid {
    grid-template-columns: 1fr;
  }

  .club-wall-head {
    display: grid;
  }

  .clubs-main .clubs-grid {
    gap: 18px;
  }

  .club-masonry-column {
    gap: 18px;
  }

  .clubs-main .club-widget.is-wide {
    width: 100%;
  }
}

@media (max-width: 720px) {
  .clubs-main {
    margin-left: 0;
    padding: var(--topbar-height) 0 76px;
  }

  .clubs-main .clubs-page {
    padding: 14px;
  }

  .clubs-slogan-panel {
    min-height: auto;
    padding: 18px 0;
    margin: 0 0 14px;
  }

  .clubs-slogan-panel h1 {
    font-size: 40px;
  }

  .clubs-main .clubs-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
  }

  .club-masonry-column {
    gap: 14px;
  }

  .clubs-main .club-widget.is-wide,
  .clubs-main .club-widget.is-tall {
    width: 100%;
  }

  .club-card-overlay {
    padding: 9px;
  }

  .club-card-overlay strong {
    font-size: 13px;
  }

  .club-category-list {
    grid-template-columns: 1fr;
  }

  .club-detail-body {
    padding: 12px;
  }

  .club-wall-panel {
    padding: 0 12px 12px;
  }

  .club-detail-media {
    min-height: 240px;
  }
}

/* ---- Poster wall --------------------------------------------------------- */

/* Full-bleed board that starts right under the topbar; the toolbar floats over
   its top row so the wall is truly the whole area. */
.posters-main {
  height: 100vh; /* fallback for browsers without dvh */
  height: 100dvh;
  overflow: hidden;
  padding: var(--topbar-height) 0 0;
  background: #eceef2;
}

.posters-page {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
}

/* Toolbar floats over the top of the wall; the empty parts of the row let
   clicks fall through to the board underneath. */
.posters-toolbar {
  position: absolute;
  top: 12px;
  left: 0;
  right: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 42px;
  pointer-events: none;
}

.posters-filter,
.poster-upload-button {
  pointer-events: auto;
}

.poster-upload-button {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
}

/* Segmented pill filter, styled after the dark rounded switcher reference. */
.posters-filter {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  max-width: 100%;
  padding: 4px;
  background: #14171d;
  border-radius: 999px;
  overflow-x: auto;
  scrollbar-width: none;
}

.posters-filter::-webkit-scrollbar {
  display: none;
}

.poster-filter-pill {
  border: 0;
  border-radius: 999px;
  padding: 7px 16px;
  background: transparent;
  color: rgba(255, 255, 255, 0.62);
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}

.poster-filter-pill:hover {
  color: #ffffff;
}

.poster-filter-pill.is-active {
  background: #2c313b;
  color: #ffffff;
}

.poster-upload-button {
  border: 0;
  border-radius: 999px;
  padding: 9px 18px;
  background: var(--button-bg);
  color: var(--button-ink);
  font-size: 14px;
  font-weight: 800;
  white-space: nowrap;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.poster-upload-button .pub-plus {
  line-height: 1;
}

.poster-upload-button:hover {
  filter: brightness(1.12);
}

/* The wall fills the entire board area (edge to edge, up to the topbar) with the
   toolbar floating over its top. Scrollbars are hidden — panning is by dragging
   empty space; zoom is the mouse wheel. */
.poster-wall {
  position: absolute;
  inset: 0;
  border: 0;
  background:
    linear-gradient(rgba(15, 23, 42, 0.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(15, 23, 42, 0.035) 1px, transparent 1px),
    #f4f5f8;
  background-size: 40px 40px, 40px 40px, auto;
  /* Scroll the grid WITH the content while panning, so a drag actually looks like
     the board is moving (a fixed grid gave no sense of motion). */
  background-attachment: local, local, local;
  overflow: auto;
  scrollbar-width: none;
  cursor: grab;
  /* iOS Safari otherwise claims every touch-drag as a native scroll/zoom gesture
     and fires pointercancel instead of pointermove, so our drag-to-pan never runs
     — leaving off-screen posters unreachable. Disable native touch gestures so
     the pointer handlers own the pan. */
  touch-action: none;
}

.poster-wall::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

.poster-wall.is-panning {
  cursor: grabbing;
}

/* Fixed-size board (matches POSTER_BOARD_W/H in main.ts) so the layout is
   identical on every device; the view is framed onto the poster cluster by JS.
   Wheel zooms; transform-origin 0 0 keeps the scroll math simple. */
.poster-canvas {
  position: relative;
  width: 1600px;
  height: 1200px;
  transform-origin: 0 0;
}

.poster-wall-empty {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  color: #8a93a3;
  font-size: 14px;
  text-align: center;
}

.poster-card {
  position: absolute;
  border-radius: 4px;
  overflow: hidden;
  background: var(--surface);
  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.18);
  cursor: pointer;
  user-select: none;
  touch-action: none;
}

.poster-card.is-own {
  cursor: grab;
}

.poster-card.is-own.is-dragging {
  cursor: grabbing;
  box-shadow: 0 14px 32px rgba(15, 23, 42, 0.3);
}

.poster-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}

.poster-tag-chip {
  position: absolute;
  left: 6px;
  top: 6px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #ffffff;
  font-size: 10px;
  font-weight: 700;
  pointer-events: none;
}

.poster-delete {
  position: absolute;
  right: 5px;
  top: 5px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #ffffff;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease;
}

.poster-card.is-own:hover .poster-delete {
  opacity: 1;
}

.poster-form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.poster-upload-form .form-error {
  margin: 0;
  color: var(--danger, #d64545);
  font-size: 13px;
}

/* Highlight the wall while dragging an image file over it. */
.poster-wall.is-drop-target {
  border-color: var(--brand);
  box-shadow: inset 0 0 0 2px var(--brand);
  background-color: #eef2fb;
}

.poster-file-ready {
  padding: 9px 12px;
  border-radius: var(--radius-ui);
  background: #eef2fb;
  border: 1px solid #cfd8ee;
  color: var(--ink-strong);
  font-size: 13px;
}

.poster-file-ready strong {
  font-weight: 800;
}

@media (max-width: 720px) {
  .posters-main {
    padding: var(--topbar-height) 0 0;
  }

  /* Left-align the filter pills and reserve the right edge for the round + button
     so the two no longer overlap. Sit closer to the topbar than on desktop — on a
     phone the desktop inset just reads as an empty strip under the bar. */
  .posters-toolbar {
    top: 6px;
    justify-content: flex-start;
    gap: 8px;
    padding: 0 58px 0 12px;
  }

  .posters-filter {
    max-width: 100%;
  }

  /* Collapse the "+ Upload" button to a compact round icon button showing only
     the plus; the label is hidden but kept for accessibility (aria-label). */
  .poster-upload-button {
    right: 10px;
    width: 40px;
    height: 40px;
    padding: 0;
    gap: 0;
    border-radius: 50%;
  }

  .poster-upload-button .pub-plus {
    font-size: 22px;
  }

  .poster-upload-button .pub-label {
    display: none;
  }

  .poster-form-row {
    grid-template-columns: 1fr;
  }
}

/* --- Reddit panel: a SECOND window parallel to the review modal (migration 0021).
   The two cards sit side by side in .dual-modal-row; each scrolls independently.
   Stacks vertically on narrow screens. --- */
.dual-modal-row {
  display: flex;
  gap: 16px;
  align-items: stretch;            /* both windows share one height */
  justify-content: center;
  max-width: min(1720px, calc(100vw - 40px));
}

.reddit-panel-modal {
  flex: 0 0 440px;
  width: 440px;
  max-width: 440px;
  height: min(1040px, 94vh);       /* tall parallel window */
  max-height: 94vh;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Left review window matches the Reddit window's height (equal parallel windows).
   overflow-y: auto means the scrollbar only appears when its content actually
   overflows — short review lists show no scroll track. */
.dual-modal-row .subject-review-modal {
  /* Fixed width (from the base rule: min(1200px, 100vw-40)) regardless of which
     scope tab is active, so switching to an empty Tutor list doesn't shrink the
     card to its content. Still scales with the viewport via the calc(). */
  flex: 0 1 auto;
  height: min(1040px, 94vh);
  max-height: 94vh;
}

.reddit-panel-modal .modal-header {
  padding: 16px 18px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
}

.reddit-panel-modal .modal-header h2 {
  font-size: 16px;
  margin: 0;
}

.reddit-panel-modal .reddit-column-note {
  padding: 12px 18px 0;
  margin: 0;
}

.reddit-panel-modal .reddit-thread-list {
  padding: 12px 18px 18px;
  overflow-y: auto;
}

/* --- 介绍信息 / Reddit toggle in the side panel (migration 0029) --- */
.side-panel-tabs {
  display: flex;
  gap: 4px;
  padding: 10px 12px 0;
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
  flex: none;
}

.side-panel-tab {
  flex: 1;
  padding: 9px 8px 11px;
  border: none;
  background: none;
  font-size: 14px;
  font-weight: 700;
  color: var(--muted, #7a8699);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.side-panel-tab.is-active {
  color: var(--text, #22303f);
  border-bottom-color: var(--brand, var(--ink-strong));
}

.side-panel-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Inside the toggled body the panel itself scrolls, so the reddit list doesn't. */
.side-panel-body .reddit-thread-list {
  overflow: visible;
}

/* --- Mobile combined course modal: ONE card, three top tabs (历史评价 / 课程详情 /
   Reddit) that replace the desktop dual-window split. Only rendered ≤975px (the
   renderer is JS-gated), so these rules never touch the desktop layout. --- */
.course-combined-modal {
  width: 100%;
  max-width: 100%;
  /* 900px 的硬顶在窄窗口里是浪费: 869x1314 实测卡片只占了不到七成高度, 下面一大片
     空白, 而卡里的评价列表正憋在一个很短的滚动区里。窄模式本来就没有别的东西要看,
     让它吃满可用高度; 24px 是上下各留 12px 的呼吸位。 */
  height: calc(100vh - 24px);
  height: calc(100dvh - 24px);
  max-height: calc(100dvh - 24px);
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.course-combined-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 8px 12px 16px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--surface);
  flex: none;
}

.course-combined-header h2 {
  margin: 0;
  font-size: 16px;
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}

.course-combined-header h2 span {
  font-size: 13px;
  font-weight: 600;
  color: var(--muted, #7a8699);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Top-level section switcher, styled as an iOS-style SEGMENTED control (filled pill
   track, raised white active chip) — deliberately different from the underline
   .subject-review-scope-tabs below it, which are a secondary in-page filter. */
.course-modal-tabs {
  display: flex;
  gap: 3px;
  margin: 10px 12px 12px;
  padding: 3px;
  background: #eceff4;
  border-radius: 10px;
  flex: none;
}

.course-modal-tab {
  flex: 1;
  padding: 8px 6px;
  border: none;
  background: none;
  border-radius: 7px;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--muted, #7a8699);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}

.course-modal-tab.is-active {
  background: var(--surface);
  color: var(--text, #22303f);
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.16);
}

/* The single scroll region below the sticky header+tabs. Bodies pulled in here
   (review inner, guide, reddit) lost their .reddit-panel-modal wrapper, so restore
   the reddit body's padding that wrapper used to provide. */
.course-modal-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.course-modal-scroll .reddit-column-note {
  padding: 12px 16px 0;
  margin: 0;
}

.course-modal-scroll .reddit-thread-list {
  padding: 12px 16px 18px;
  overflow: visible;
}

.guide-content {
  padding: 14px 18px 18px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text, #22303f);
}

.guide-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 10px;
}

.guide-head strong {
  font-size: 16px;
}

.guide-sem {
  font-size: 11px;
  font-weight: 800;
  color: #d93a00;
  background: #fff0e8;
  border-radius: 999px;
  padding: 1px 8px;
}

.guide-facts {
  display: grid;
  gap: 6px;
  margin: 0 0 14px;
  padding: 12px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-ui, 12px);
  background: #fbfcfe;
}

.guide-fact {
  display: grid;
  grid-template-columns: 84px minmax(0, 1fr);
  gap: 10px;
  align-items: start;
}

.guide-fact dt {
  font-size: 11px;
  font-weight: 800;
  color: var(--muted, #7a8699);
}

.guide-fact dd {
  margin: 0;
  font-size: 12px;
}

.guide-narrative h4 {
  margin: 16px 0 6px;
  font-size: 13px;
  color: var(--brand, var(--ink-strong));
}

.guide-narrative p {
  margin: 0 0 10px;
}

.guide-source {
  margin: 16px 0 0;
  padding-top: 12px;
  border-top: 1px solid #eef2f7;
  font-size: 11px;
  color: var(--muted, #7a8699);
}

.guide-source a {
  color: #d93a00;
  font-weight: 700;
}

/* ── Course mini-wiki (介绍信息 panel): editable body + file uploads ───────── */
/* Attribution + 编辑 button, pinned at the bottom of the panel with a top divider. */
.wiki-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid #eef2f7;
  font-size: 11px;
  color: var(--muted, #7a8699);
}

.wiki-edit-btn,
.wiki-upload-btn {
  border: 1px solid #dfe5ee;
  background: var(--surface);
  color: var(--brand, #1652f0);
  font-weight: 800;
  font-size: 12px;
  border-radius: 8px;
  padding: 3px 10px;
  cursor: pointer;
}

.wiki-edit-btn:hover,
.wiki-upload-btn:hover { background: #f2f6ff; }

.wiki-view ul { margin: 0 0 10px; padding-left: 18px; }
.wiki-view li { margin: 0 0 4px; }

.wiki-editor { display: grid; gap: 6px; margin-bottom: 12px; }

.wiki-field-label {
  font-size: 12px;
  font-weight: 800;
  color: var(--muted, #5b6577);
  margin-top: 4px;
}

.wiki-textarea,
.wiki-facts-textarea {
  width: 100%;
  resize: vertical;
  border: 1px solid #dfe5ee;
  border-radius: 10px;
  padding: 12px;
  font: inherit;
  font-size: 13px;
  line-height: 1.6;
}

.wiki-textarea { min-height: 200px; }
.wiki-facts-textarea { min-height: 150px; }

.wiki-editor-actions,
.wiki-upload-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.wiki-hint { font-size: 11px; color: var(--muted, #7a8699); }

/* 复习资料/笔记 sits at the TOP of the panel now; the separator lives below it. */
.wiki-files {
  margin-top: 4px;
}

.guide-divider {
  border: 0;
  border-top: 1px solid #eef2f7;
  margin: 14px 0;
}

.wiki-files-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
  font-size: 13px;
}

.wiki-upload {
  display: grid;
  gap: 8px;
  margin-bottom: 12px;
  padding: 12px;
  background: #f7f9fc;
  border: 1px solid #e6ebf3;
  border-radius: 10px;
}

.wiki-file-input,
.wiki-file-title {
  font: inherit;
  font-size: 13px;
  border: 1px solid #dfe5ee;
  border-radius: 8px;
  padding: 7px 9px;
  background: var(--surface);
}

.wiki-file-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

.wiki-file {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border: 1px solid #e6ebf3;
  border-radius: 10px;
  background: var(--surface);
}

.wiki-file-main {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

.wiki-file-ico {
  flex: 0 0 auto;
  width: 38px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 6px;
  background: #eef2f9;
  color: var(--brand, #1652f0);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.02em;
}

.wiki-file-text { min-width: 0; display: grid; gap: 1px; }
.wiki-file-text strong { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wiki-file-text em { font-style: normal; font-size: 11px; color: var(--muted, #7a8699); }

.wiki-file-del {
  flex: 0 0 auto;
  border: 0;
  background: transparent;
  color: var(--muted, #9aa4b2);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 6px;
}

.wiki-file-del:hover { background: #fdecea; color: #d93a00; }

.wiki-files-empty { font-size: 12px; }

/* AI-generated society comment card (with quality/difficulty tiles) */
.guide-comment {
  margin: 0 0 14px;
  padding: 12px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-ui, 12px);
  background: var(--surface);
}

.guide-scores {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

.guide-score {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 1px;
  min-width: 62px;
  padding: 6px 10px;
  border-radius: 10px;
  color: #22303f;
}

.guide-score span { font-size: 9px; font-weight: 900; text-transform: uppercase; }
.guide-score strong { font-size: 18px; line-height: 1; }
.guide-score em { font-size: 8px; font-style: normal; font-weight: 800; }
.guide-score.is-score-1 { background: #ff8f95; }
.guide-score.is-score-2 { background: #ffbf69; }
.guide-score.is-score-3 { background: #ffeb6a; }
.guide-score.is-score-4 { background: #a8e875; }
.guide-score.is-score-5 { background: #52d69d; }
.guide-score.is-score-empty { background: #e5eaf2; color: #7a8699; }

.guide-comment-text {
  margin: 0 0 8px;
  font-size: 13px;
  line-height: 1.55;
}

.guide-comment-by {
  margin: 0;
  font-size: 11px;
  color: var(--muted, #7a8699);
}

.guide-comment-by a {
  color: #d93a00;
  font-weight: 700;
}

/* Society (精算学生会) review card in the course-reviews list */
.subject-review-item.is-society-review {
  background: #f6fbf8;
  border: 1px solid #cfe6da;
  border-radius: var(--radius-ui, 12px);
  padding: 10px;
}

.society-review-badge {
  font-weight: 800;
  color: #0e7a4f;
  background: #e3f5ec;
  border-radius: 999px;
  padding: 1px 8px;
}

@media (max-width: 975px) {
  .dual-modal-row {
    flex-direction: column;
    align-items: stretch;
    max-height: calc(100vh - 32px); /* fallback */
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
  }

  .reddit-panel-modal {
    flex: none;
    width: 100%;
    max-width: 100%;
    height: auto;
    max-height: 60vh;
  }

  .dual-modal-row .subject-review-modal {
    height: auto;
    max-height: none;
  }

  /* Combined single card owns its own internal scroll (sticky header + tabs, one
     scrolling body), so the row itself must NOT add a second scrollbar/height cap. */
  .dual-modal-row.is-mobile-combined {
    width: 100%;
    max-width: 100%;
    max-height: none;
    overflow: visible;
  }

  /* 历史评价 summary, narrow layout: the 推荐度 ring pairs with the title on the top
     row, and the three meter bars take their own full-width row below (more room for
     the bars). display:contents promotes .review-summary-main's children into this
     grid so they can be placed by area. */
  .subject-review-summary {
    /* 这一列的宽度是写死来配 .review-meter-ring 的方块的, 两处必须同时改 —— 只改一处
       会让方块和标题之间多出或少掉一截空白。 */
    /* 标题在左、推荐度方块在右（2026-08-29）：中文标题是这张卡真正要先读的东西，
       让它从卡片左边界起排；分数挪到右上角当角标，和下面评价卡里的分数胶囊同一侧，
       视线不用在左右之间来回跳。 */
    grid-template-columns: minmax(0, 1fr) 108px;
    grid-template-areas:
      "title ring"
      "bars bars";
    gap: 8px 14px;
    padding: 14px 16px;
    align-items: center;
  }

  /* 84 → 108px。桌面是 138px, 窄屏砍到 84 砍过头了: 推荐度是这张卡最先被读的数字,
     而它外圈那道百分比描边(.rmr-fill, stroke-width 8)在 84px 的方块上只有约 8 度弧长
     对应一个百分点, 71% 和 79% 看上去几乎一样。放大到 108 之后弧长增加 29%, 描边
     也不再显得比方块本身还粗。仍比桌面小一圈, 留给右边的标题两行中文。 */
  .review-meter-ring {
    grid-area: ring;
    width: 108px;
    height: 108px;
  }

  .review-summary-main {
    display: contents;
  }

  .review-summary-title {
    grid-area: title;
  }

  .review-summary-main .review-meter-bars {
    grid-area: bars;
    margin: 4px 0 0;
    width: 100%;
    max-width: none;
  }

  .rmr-center strong {
    font-size: 25px;
  }

  .review-meter-summary h3 {
    font-size: 19px;
  }

  .review-meter-summary p {
    font-size: 13px;
  }

  .subject-review-scope-tabs {
    flex-wrap: wrap;
    padding: 8px 12px 0;
  }

  .subject-review-scope-tabs .primary-button {
    margin-left: auto;
  }

  /* This used to force a two-column grid back on to keep the tile beside the text.
     The ≤720px block now floats a small square instead, which already sits beside
     the chip rows — re-imposing the grid here would undo that, so it only keeps
     the square from being stretched. */
  /* 721–975 这一档以前只写了 width: 56px, 但 min-height 还是桌面的 84 —— 方块变成
     一条又窄又高的竖条, 正是 ~900px 下看到的那个。这里补齐和 ≤720 同一套紧凑处理:
     右浮动、单行胶囊、只留数字。 */
  .course-modal-scroll .review-item-score {
    float: right;
    width: auto;
    min-width: 44px;
    height: auto;
    min-height: 0;
    margin: 0 0 4px 10px;
    padding: 3px 10px;
    gap: 0;
  }

  .course-modal-scroll .review-item-score span,
  .course-modal-scroll .review-item-score em {
    display: none;
  }

  .course-modal-scroll .subject-review-item {
    display: block;
  }

  .course-modal-scroll .subject-review-item::after {
    content: "";
    display: block;
    clear: both;
  }
}

/* --- (legacy) embedded Reddit column styles, kept harmless below --- */
.subject-review-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  align-items: start;
}

.subject-review-main {
  min-width: 0;
}

.subject-reddit-column {
  min-width: 0;
  border-left: 1px solid var(--line-soft);
  padding: 16px 20px 18px 18px;
}

.reddit-column-heading {
  display: flex;
  align-items: center;
  gap: 8px;
}

.reddit-column-heading h3 {
  margin: 0;
  font-size: 15px;
}

.reddit-column-heading span {
  font-size: 11px;
  font-weight: 800;
  color: #ff4500;
  background: #fff0e8;
  border-radius: 999px;
  padding: 1px 8px;
}

.reddit-column-note {
  margin: 6px 0 12px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--muted, #7a8699);
}

.reddit-thread-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.reddit-column-hint {
  font-size: 12px;
  color: var(--muted, #7a8699);
  padding: 8px 0;
}

.reddit-thread-item {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: 10px;
  padding: 10px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-ui, 12px);
  background: #fbfcfe;
}

.reddit-thread-score {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 1px;
  height: 44px;
  border-radius: 10px;
  color: #22303f;
}

.reddit-thread-score span {
  font-size: 8px;
  font-weight: 900;
  text-transform: uppercase;
}

.reddit-thread-score strong {
  font-size: 17px;
  line-height: 1;
}

.reddit-thread-score em {
  font-size: 8px;
  font-style: normal;
  font-weight: 800;
}

.reddit-thread-score.is-score-1 { background: #ff8f95; }
.reddit-thread-score.is-score-2 { background: #ffbf69; }
.reddit-thread-score.is-score-3 { background: #ffeb6a; }
.reddit-thread-score.is-score-4 { background: #a8e875; }
.reddit-thread-score.is-score-5 { background: #52d69d; }
.reddit-thread-score.is-score-empty { background: #e5eaf2; color: #7a8699; }
/* Engagement block (upvotes + comments) — replaces the old AI score tile. */
.reddit-thread-score.is-votes { background: #fff0e8; color: #d93a00; }
.reddit-thread-score.is-votes span { font-size: 10px; line-height: 1; }
.reddit-thread-score.is-votes em { color: #8a5a44; }

.reddit-thread-body {
  min-width: 0;
}

.reddit-thread-title {
  font-size: 13px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--text, #22303f);
  text-decoration: none;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

a.reddit-thread-title:hover {
  color: #ff4500;
  text-decoration: underline;
}

.reddit-thread-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 6px;
  font-size: 11px;
  color: var(--muted, #7a8699);
}

@media (max-width: 720px) {
  .subject-review-body {
    grid-template-columns: 1fr;
  }

  .subject-reddit-column {
    border-left: none;
    border-top: 1px solid var(--line-soft);
    padding: 16px 20px 18px;
  }
}

/* Review-card action row (点赞 / 点踩 / 欢乐 / 评论) pinned to the card's
   bottom-right. Like + comment share their count with the mirrored homepage
   post; 点踩 / 欢乐 show no public count (backend-only), just an active state. */
.subject-review-item footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}
.review-byline {
  color: #6a7280;
  font-size: 12px;
  font-weight: 800;
}
.review-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}
.review-action {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid #e2e7f0;
  background: var(--surface);
  border-radius: 999px;
  padding: 4px 9px;
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
  color: #374151;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.review-action:hover {
  background: #f3f6fb;
}
.review-action:active {
  transform: scale(0.94);
}
.review-action.is-active {
  background: #eef4ff;
  border-color: #b9d0ff;
}
.review-action-count {
  font-size: 12px;
  font-weight: 700;
  color: #6a7280;
}


/* ── 网站导航 (site links board) ──────────────────────────────────────────────
   A directory of external sites, grouped by what the reader is trying to do.
   Token-driven throughout, so a tenant's palette repaints it for free.

   The rule that keeps this page from breaking: every text box inside a grid or
   flex cell carries min-width:0 and truncates its single-line text. Site names
   and hosts are third-party data of unpredictable length — without that, one
   long domain silently stretches its column and knocks the row out of alignment
   (the same failure the media-carousel wrapper hit). */
.site-links-page {
  max-width: 1180px;
  margin: 0 auto;
}

/* Hero. A short brand rule instead of an eyebrow label: it carries the school's
   colour onto the page without inventing copy that would need four translations. */
.site-links-head h1 {
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 33px;
  line-height: 1.08;
  letter-spacing: -0.015em;
}

.site-links-head h1::before {
  content: "";
  display: block;
  width: 34px;
  height: 3px;
  margin-bottom: 15px;
  border-radius: 2px;
  background: var(--brand);
}

.site-links-head p {
  margin: 10px 0 0;
  max-width: 58ch;
  color: var(--muted);
  font-size: 13.5px;
  line-height: 1.6;
}

/* Sticky under the topbar. The gradient fades the canvas out beneath the row so
   cards scrolling under it do not collide with the chips. */
.site-links-toolbar {
  position: sticky;
  top: var(--topbar-height);
  z-index: 12;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 22px -8px 0;
  padding: 14px 8px 16px;
  background: linear-gradient(var(--canvas) 78%, transparent);
}

.site-links-search {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 13px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--soft);
  transition: border-color 140ms var(--ease-out), box-shadow 140ms var(--ease-out);
}

.site-links-search:focus-within {
  border-color: color-mix(in srgb, var(--brand-bright) 40%, var(--line));
  box-shadow: var(--focus);
}

.site-links-search input {
  width: 232px;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--text);
  font-size: 13px;
}

/* One row, always. Wrapping looked fine at 1440px and turned the sticky toolbar
   into a 148px slab at 1024px — three rows of chips pinned under the topbar ate
   a quarter of the viewport. Sideways scroll keeps the bar one line at every
   width; on a wide screen every chip fits and nothing scrolls. */
.site-links-chips {
  display: flex;
  /* basis 0, not auto: with an auto basis the row wraps on the chips' full
     content width before it ever tries to shrink them, which put the chip row
     on its own line and doubled the sticky bar's height. */
  flex: 1 1 0;
  min-width: 0;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
}

.site-links-chips::-webkit-scrollbar {
  display: none;
}

.site-links-chips .tab-pill {
  flex: 0 0 auto;
}

.site-links-chips .tab-pill {
  height: 34px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
  transition: border-color 140ms var(--ease-out), color 140ms var(--ease-out),
    background 140ms var(--ease-out);
}

.site-links-chips .tab-pill:hover {
  border-color: color-mix(in srgb, var(--brand-bright) 26%, var(--line));
  color: var(--text);
}

/* Re-state the selected look at THIS specificity: the rule above matches
   `.tab-pill.is-active` (same weight, declared earlier) and would otherwise
   repaint the active chip as an inactive one. Solid brand fill, because the
   chip row is the only control on the page and should read as the anchor. */
.site-links-chips .tab-pill.is-active {
  border-color: transparent;
  background: var(--brand);
  color: #ffffff;
}

/* ── 常用直达 strip ── */
.site-links-pin-label {
  margin: 4px 0 10px 2px;
  color: var(--soft);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
}

.site-links-pinned {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(184px, 1fr));
  gap: 10px;
}

/* Fixed 52px row so the strip stays one clean band — a quick-launch bar, not a
   second set of cards. One line of text, so nothing can wrap a tile taller than
   the ones beside it. */
.site-link-pin {
  display: flex;
  align-items: center;
  gap: 11px;
  height: 52px;
  padding: 0 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
  transition: border-color 160ms var(--ease-out), box-shadow 160ms var(--ease-out),
    transform 160ms var(--ease-out);
}

.site-link-pin-text {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.25;
}

/* ── Category groups ── */
.site-links-group {
  margin-top: 34px;
}

.site-links-group-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.site-links-group-head h2 {
  margin: 0;
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.03em;
}

/* Fades the label into the row instead of leaving it stranded left of a long
   empty gap. */
.site-links-group-head::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--line), transparent 55%);
}

.site-links-grid {
  margin-top: 16px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(276px, 1fr));
  gap: 12px;
}

.site-link-card {
  position: relative;
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  gap: 13px;
  padding: 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
  transition: border-color 160ms var(--ease-out), box-shadow 160ms var(--ease-out),
    transform 160ms var(--ease-out);
}

.site-link-card:hover,
.site-link-pin:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--brand-bright) 32%, var(--line));
  box-shadow: 0 10px 26px color-mix(in srgb, var(--brand) 9%, transparent);
}

.site-link-card:active,
.site-link-pin:active {
  transform: translateY(0);
}

/* Two states, one box. With a real logo the tile is a plain white plate so the
   brand's own colours read correctly; with the glyph fallback it is the tinted
   lettered tile. Fixed 38px either way, so a failed icon never reflows the card. */
.site-link-tile {
  position: relative;
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  color: var(--brand);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 15px;
  font-weight: 800;
}

.site-link-icon {
  width: 22px;
  height: 22px;
  /* Favicons come in wildly different aspect ratios; contain keeps wordmarks
     readable instead of cropping them to a square. */
  object-fit: contain;
}

.site-link-tile .site-link-glyph {
  display: none;
}

.site-link-tile.is-glyph {
  border-color: transparent;
  background: var(--brand-weak);
}

.site-link-tile.is-glyph .site-link-glyph {
  display: block;
}

.site-link-body {
  min-width: 0;
}

.site-link-body b {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.35;
}

.site-link-blurb {
  display: block;
  margin-top: 6px;
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.55;
}

/* Host + its arrow: the domain truncates, the arrow never does, and the arrow
   slides on hover to signal that the card leaves the site. */
.site-link-host {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 10px;
  color: var(--soft);
  font-size: 11px;
}

.site-link-host > span {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.site-link-host > i {
  flex: 0 0 auto;
  font-style: normal;
  transition: transform 160ms var(--ease-out), color 160ms var(--ease-out);
}

.site-link-card:hover .site-link-host > i {
  transform: translate(1px, -1px);
  color: var(--brand-bright);
}

.site-link-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--brand-weak);
  color: var(--brand);
  font-size: 10px;
  font-weight: 700;
  vertical-align: 1px;
}

.site-links-empty {
  margin-top: 46px;
  color: var(--soft);
  font-size: 13px;
  text-align: center;
}

.site-links-note {
  margin-top: 46px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  max-width: 78ch;
  color: var(--soft);
  font-size: 11.5px;
  line-height: 1.8;
}

@media (max-width: 720px) {
  .site-links-head h1 {
    font-size: 26px;
  }
  .site-links-search {
    flex: 1 1 100%;
  }
  .site-links-search input {
    width: 100%;
  }
  /* Search takes its own row on a phone; the chips keep the scrolling row they
     already have at every width. */
  .site-links-chips {
    flex: 1 1 100%;
  }
  .site-links-grid,
  .site-links-pinned {
    grid-template-columns: 1fr;
  }
}

/* Paste-a-link row on the create-post panel. Reads as a shortcut above the normal
   fields rather than a separate mode — the board picker below it still applies. */
.import-faculty-field {
  display: grid;
  gap: 8px;
}

.import-faculty-label {
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
}

.import-faculty-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.import-faculty-chip {
  padding: 6px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-size: 12px;
  font-weight: 650;
  line-height: 1.35;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.import-faculty-chip:hover {
  border-color: var(--brand);
}

/* 选中态用主题色填充而不是只换描边: 一排学院里通常只勾一两个, 描边差别在这个
   字号下扫一眼看不出来。 */
.import-faculty-chip.is-on {
  border-color: var(--brand);
  background: var(--brand);
  color: var(--surface);
}
/* 转发语输入框。外框/字号全部吃 .stack-form label + textarea 那一套, 这里只放
   它自己需要的两件事: 别一行高, 以及允许竖着拉。 */
.import-note-field textarea {
  min-height: 62px;
  line-height: 1.6;
  resize: vertical;
}

/* 转发语底下的标签回执。空态是一句灰色说明, 有标签时是一排小胶囊 —— 认出是课程的
   那颗用主题色, 因为"会出现在那门课页面上"正是它和普通标签唯一的区别。 */
.import-note-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  min-height: 20px;
}

.import-note-tag {
  padding: 3px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-muted);
  color: var(--muted);
  font-size: 12px;
  font-weight: 650;
  line-height: 1.4;
}

.import-note-tag.is-course {
  border-color: var(--brand);
  background: var(--brand);
  color: var(--surface);
}

.import-note-tag em {
  margin-left: 6px;
  font-style: normal;
  font-weight: 600;
  opacity: 0.85;
}

.import-link-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  padding: 12px;
  border: 1px dashed var(--soft);
  border-radius: var(--radius-md);
  background: var(--surface-muted);
}

.import-link-field {
  flex: 1 1 260px;
  min-width: 0;
}

.import-link-row .secondary-button {
  flex: 0 0 auto;
  min-height: 38px;
}

.import-link-status {
  flex: 1 1 100%;
  margin: 0;
  color: #667085;
  font-size: 12px;
}


.import-link-status.is-error {
  color: var(--danger);
}

/* Imported video sits in the same media strip as stills, but a 150px-wide thumbnail
   with native controls is unusable — the control bar alone is wider than that. Give it
   a real player footprint and let object-fit letterbox rather than crop, since a
   cropped video loses whatever the poster framed. */
/* 尺寸给的是上限而不是定值，真正定形状的是内联的 aspect-ratio（renderVideoTag 从
   post_media.width/height 写出来）。写死 280×210 的时候，抖音那种 9:16 的竖屏在一个
   194px 宽的位置里只剩中间 82px 是画面，其余全是黑边。
   拿不到宽高的行（老帖、宽高没入库的来源）退回 4/3，就是原来的形状。 */
.post-media.is-video {
  width: auto;
  height: auto;
  aspect-ratio: 4 / 3;
  max-width: 280px;
  /* 竖屏由高度封顶：9:16 撞上 360 之后是 202px 宽，原生控制条还放得下
     （150px 那一档是放不下的，见上面那段注释）。 */
  max-height: 360px;
  flex: 0 0 auto;
  /* .media-track 是 flex，默认 align-items: stretch。原来高度写死所以没事，现在
     height:auto 会被拉成整条的高度，aspect-ratio 再反推宽度 —— 形状就不由自己定了。 */
  align-self: flex-start;
  object-fit: contain;
  background: #000;
  cursor: default;
}

/* 竖屏收窄、横屏放宽 —— 和 App 的 VideoTile 一个道理(它传的是内容宽的 42% / 76%)。
   一条满宽的 9:16 在信息流里能占掉一整屏, 而一条 16:9 收到 220px 又太小、原生控制
   条都快放不下。拿不到宽高的老帖两个类都不带, 落回上面那档 280。 */
.post-media.is-video.is-portrait {
  max-width: 190px;
}

.post-media.is-video.is-wide {
  max-width: 320px;
}

@media (max-width: 640px) {
  .post-media.is-video {
    max-width: 220px;
    max-height: 320px;
  }

  .post-media.is-video.is-portrait {
    max-width: 168px;
  }

  .post-media.is-video.is-wide {
    max-width: 100%;
  }
}

/* Back-to-top. Lives on <body>, outside #app, so a repaint cannot destroy it
   mid-transition; visibility is toggled by a scroll listener (see initBackToTop). */
.back-to-top {
  position: fixed;
  right: 24px;
  bottom: 28px;
  z-index: 30;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.94);
  color: var(--ink-strong, #10233a);
  font-size: 19px;
  font-weight: 800;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--ink-strong-bg) 16%, transparent);
  opacity: 0;
  /* Unclickable until it is actually shown, so it cannot intercept a click on the
     card underneath while invisible. */
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity 0.16s ease, transform 0.16s ease;
}

.back-to-top.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.back-to-top:hover {
  background: var(--surface);
  box-shadow: 0 8px 24px color-mix(in srgb, var(--ink-strong-bg) 22%, transparent);
}

@media (max-width: 720px) {
  /* Clear the fixed bottom nav (46px + safe area) instead of sitting on top of it. */
  .back-to-top {
    right: 14px;
    bottom: calc(58px + env(safe-area-inset-bottom));
    width: 40px;
    height: 40px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .back-to-top {
    transition: opacity 0.16s ease;
    transform: none;
  }
}

/* ===================================================================
   课程评价首页 (.ch-*) + 学院主页 (.fh-*) + the sidebar group rule.
   Sketches: .planning/sketches/001-course-review-home (variant C),
   002-faculty-home (variant A).
   =================================================================== */

/* The rule between the three headline destinations (首页 / 课程评价 / 学院主页)
   and everything else. Reads the sidebar ink so a light-sidebar tenant gets a
   dark rule instead of an invisible white one. */
/* currentColor + opacity, 不是 color-mix(var(--sidebar-ink) …)。两者在浅色下等价
   —— 这条线就在 .sidebar 里, 继承到的正是 --sidebar-ink —— 但暗色下差别是这条线
   存在与否: Dark Reader 重写样式表时把 var(--sidebar-ink) 改名成
   var(--darkreader-bg--sidebar-ink), 即按"背景色"那一极去推导, 得到的是深灰
   (实测 rgb(24,26,27)); 20% 的深灰压在已被钉成 #0f1011 的侧栏上, 等于没有。
   currentColor 引擎原样放行(它不是变量), 而 .sidebar 的 color 走的是文字那一极,
   已被正确转成浅色(实测 rgb(232,232,232))。上面那句"读侧栏墨色, 所以浅侧栏租户
   得到深色线"的语义完全保留 —— currentColor 就是侧栏墨色本身。 */
.nav-divider {
  height: 1px;
  margin: 10px 12px;
  background: currentColor;
  opacity: 0.2;
}

/* ---------------- 课程评价首页 ---------------- */
.ch-page {
  width: min(1540px, 100%);
  margin: 0 auto;
  color: var(--ink-strong);
}

.ch-wrap {
  display: grid;
  gap: 20px;
}

/* 课程 / 教授 are file tabs on the search bar, not two buttons parked above it:
   the active one shares the bar's surface and its border opens into the field,
   so the tab and the thing it scopes read as one object. The negative bottom
   margin eats .ch-wrap's grid gap; the -1px on the buttons pulls them down over
   the bar's top border so the active tab can cover it. */
.ch-search-mode {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 3px;
  margin: 8px 0 -20px 0;
  padding-left: 10px;
}

/* These two decide what the whole page is about — search, boards and all — so
   they are sized like a decision, not like a filter chip. */
.ch-search-mode button {
  min-height: 40px;
  margin-bottom: -1px;
  padding: 0 26px;
  border: 1px solid var(--line-strong);
  border-bottom: 0;
  border-radius: var(--radius-ui) var(--radius-ui) 0 0;
  background: #eaeff7;
  color: #55617a;
  font-size: 15.5px;
  font-weight: 950;
  transition: background 140ms ease, color 140ms ease;
}

/* An unselected tab is behind the sheet — hovering brings it to the front. */
.ch-search-mode button:hover,
.ch-search-mode button:focus-visible {
  background: #f4f7fb;
  color: var(--ink-strong);
  outline: 0;
}

/* Same white as the bar + the 1px overlap = no seam between tab and field. The
   ink rule is an inset shadow rather than a fatter border so switching tabs does
   not nudge the label by 2px. */
.ch-search-mode button.is-active {
  background: var(--surface);
  color: var(--ink-strong);
  box-shadow: inset 0 3px 0 var(--ink-strong-bg);
}

/* Typing turns the bar's border ink; the tab it is attached to follows, or the
   seam runs two colours. */
.ch-search-mode:has(+ .ch-searchbar:focus-within) button.is-active {
  border-color: var(--ink-strong-bg);
}

/* The hero IS the search bar: one row, no headline. Icon, a hairline rule, the
   field, then the low-key actions — the reference was a plain browser-style bar
   and anything more became chrome competing with the boards below it. */
.ch-searchbar {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0;
  /* Full width of the content column, so its edges line up with the boards
     below instead of floating in the middle of them. A little breathing room
     above and below separates it from the top edge and the filter row. */
  width: 100%;
  /* No top margin: the tab row above is attached to this edge. */
  margin: 0 0 26px;
  padding: 0 8px 0 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
  transition: border-color 140ms ease, box-shadow 140ms ease;
}

.ch-searchbar:focus-within {
  border-color: var(--ink-strong-bg);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-bright) 12%, transparent);
}

/* The icon column and its hairline: fixed width so the caret always starts at
   the same x whether or not the clear button is showing. */
.ch-searchbar-icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 78px;
  height: 46px;
  border-right: 1px solid var(--line);
  color: var(--muted);
}

.ch-searchbar-icon svg {
  width: 32px;
  height: 32px;
  stroke-width: 1.4;
}

.ch-searchbar .ch-search-field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

.ch-searchbar input {
  width: 100%;
  height: 74px;
  padding: 0 20px;
  border: 0;
  background: transparent;
  color: var(--text);
  font-size: 17.5px;
  font-weight: 650;
  outline: none;
}

.ch-searchbar input::placeholder {
  color: var(--soft);
  font-weight: 600;
}

/* The global input:focus-visible rule in tokens.css paints its own --focus ring,
   which drew a second box INSIDE the bar's own :focus-within ring. The bar is
   the focus indicator here, so the field carries none of its own. */
.ch-searchbar input:focus,
.ch-searchbar input:focus-visible {
  outline: none;
  box-shadow: none;
}

/* The native search input paints its own clear affordance in some engines; ours
   is the × below, which also drops the query from state. */
.ch-searchbar input::-webkit-search-cancel-button {
  appearance: none;
}

.ch-searchbar-clear {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--soft);
}

.ch-searchbar-clear:hover,
.ch-searchbar-clear:focus-visible {
  background: var(--surface-muted);
  color: var(--ink-strong);
  outline: 0;
}

.ch-searchbar-clear svg {
  width: 17px;
  height: 17px;
}

.ch-searchbar-review {
  flex: 0 0 auto;
  min-height: 36px;
  margin-left: 4px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 900;
}

.ch-searchbar-review:hover,
.ch-searchbar-review:focus-visible {
  border-color: var(--ink-strong-bg);
  background: var(--brand-weak);
  outline: 0;
}


.ch-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

.ch-chip,
.ch-select {
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: #55617a;
  font-size: 12.5px;
  font-weight: 850;
}

/* Left to itself the select sizes to its LONGEST option ("Medicine, Dentistry
   and Health Sciences"), which made a control that reads 全部学院 most of the
   time about a quarter wider than it ever needs to be. Capped, with the long
   names trimmed rather than clipped mid-glyph. */
.ch-select {
  max-width: 230px;
  text-overflow: ellipsis;
}

.ch-chip.is-active {
  border-color: var(--ink-strong-bg);
  background: var(--ink-strong-bg);
  color: #ffffff;
}

/* Pushed to the far end of the row. `margin-left: auto` rather than
   justify-content, so when the row wraps on a narrow window the group drops to
   its own line intact instead of the two halves interleaving. */
/* 「筛选 ⌄」只服务手机（见 ≤720 那块）：桌面一行放得下 仅Summer/仅Winter 两枚，
   多一层展开只是多一次点击。 */
.ch-term-toggle { display: none; }

.ch-filters-right {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-left: auto;
}

/* quick task (260828-crw): 这一行放不下、要折成两行时, 把学院筛选让出去。
   is-tight 由 syncCourseFilterRow() 打上(见 courseHome.js 里那段注释: 为什么是实测
   而不是媒体查询, 以及"测量前先摘掉这个 class"如何避免 显示↔隐藏 的来回抖动)。
   让 select 而不是让学期 chip: 两个学期 chip 加起来也没有这个 select 宽(它顶到
   max-width 230px), 让它一个就够, 而且下拉筛选在课程页本身还有一份。 */
.ch-filters.is-tight .ch-select {
  display: none;
}

.ch-boards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 18px;
}

.ch-board {
  display: grid;
  gap: 6px;
  align-content: start;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

/* Each board is identified by a 4px rule in the score palette: amber = volume,
   green = the good end, red = the bad end. Same three colours as the tiles. */
.ch-board.is-hot { border-top: 4px solid #ffeb6a; }
.ch-board.is-good { border-top: 4px solid #52d69d; }
.ch-board.is-bad { border-top: 4px solid #ff8f95; }

.ch-board-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  margin-bottom: 4px;
  padding: 0;
  border: 0;
  background: transparent;
  text-align: left;
  /* Not a control on desktop: the boards sit side by side and never fold, so
     the header must not invite a click there. */
  cursor: default;
}

.ch-board-caret {
  display: none;
}

.ch-board-head h2 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 18px;
  font-weight: 950;
}

.ch-board-head span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 800;
}

/* Swipe dots for the compact board carousel. Hidden on desktop (three boards
   sit side by side there); the ≤1180px block turns them on. Each dot wears its
   board's accent so the indicator doubles as a legend: amber = volume,
   green = good end, red = bad end. */
.ch-board-dots {
  display: none;
  justify-content: center;
  gap: 7px;
  margin-top: -6px;
}

.ch-board-dots i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  opacity: 0.35;
  transition: width 160ms ease, opacity 160ms ease;
}

.ch-board-dots i:nth-child(1) { background: #e8cf2f; }
.ch-board-dots i:nth-child(2) { background: #52d69d; }
.ch-board-dots i:nth-child(3) { background: #ff8f95; }

.ch-board-dots i.is-active {
  width: 20px;
  border-radius: 4px;
  opacity: 1;
}

.ch-rank {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  gap: 11px;
  align-items: center;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  text-align: left;
}

.ch-rank:hover,
.ch-rank:focus-visible {
  background: #f4f7fb;
  outline: 0;
}

.ch-no {
  color: var(--soft);
  font-size: 15px;
  font-style: italic;
  font-weight: 950;
  font-variant-numeric: tabular-nums;
}

.ch-body {
  min-width: 0;
  display: grid;
  gap: 2px;
}

.ch-code {
  color: var(--muted);
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 0.06em;
}

.ch-name {
  color: var(--ink-strong);
  font-size: 14.5px;
  font-weight: 900;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ch-meta {
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
}

/* Same 1-5 palette as .subject-score-tile — the is-score-N classes come from
   scoreClassForValue, so a tile here and a tile on a course card agree. */
.ch-score {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 1px;
  min-width: 48px;
  min-height: 48px;
  padding: 4px 6px;
  border-radius: var(--radius-ui);
  background: #e7edf4;
  color: #111111;
}

.ch-score b {
  font-size: 19px;
  line-height: 1;
  font-weight: 950;
}

.ch-score i {
  font-size: 9px;
  font-style: normal;
  font-weight: 800;
  opacity: 0.75;
}

.ch-score.is-score-1 { background: #ff8f95; }
.ch-score.is-score-2 { background: #ffbf69; }
.ch-score.is-score-3 { background: #ffeb6a; }
.ch-score.is-score-4 { background: #a8e875; }
.ch-score.is-score-5 { background: #52d69d; }

.feed-risk-row .ch-score.is-score-1,
.feed-risk-row .ch-score.is-score-2,
.feed-risk-row .ch-score.is-score-3,
.feed-risk-row .ch-score.is-score-4,
.feed-risk-row .ch-score.is-score-5 {
  background: #f7f9fc;
}

[data-theme="dark"] .feed-risk-row .ch-score.is-score-1,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-2,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-3,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-4,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-5 {
  background: #20242b;
}

[data-theme="black"] .feed-risk-row .ch-score.is-score-1,
[data-theme="black"] .feed-risk-row .ch-score.is-score-2,
[data-theme="black"] .feed-risk-row .ch-score.is-score-3,
[data-theme="black"] .feed-risk-row .ch-score.is-score-4,
[data-theme="black"] .feed-risk-row .ch-score.is-score-5 {
  background: #15171b;
}

.ch-lower {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 18px;
}

/* quick task (260828-lf, 第二轮): 最新评价填满整格, 且**绝不撑高这一行**。

   第一轮只做到了前半句。卡片从 4 条提到 24 条之后, 左格的内容高度反而超过了右边的
   按学院浏览, 于是 grid 行改由左格决定, 右边的学院瓷砖被 stretch 拉长, 每张卡里多出
   一大片空白 —— 正好是这个改动本来要消除的那种空白, 只是换了一边。我把因果搞反了:
   两格等高不代表对齐正确, 得看是谁在决定行高。

   真正的要求是不对称的: 左格的高度必须**只**来自右格, 它自己的内容一点都不能参与行高
   计算。CSS 里让一段内容彻底退出父元素的尺寸计算, 唯一可靠的手段是把它**绝对定位** ——
   overflow-y:auto / min-height:0 / flex-basis:0 都不行, 它们只决定能不能收缩, 不改变
   这个元素向上贡献的固有高度。

   于是三层:
     .ch-panel-latest   flex 列; 标题 flex:none
     .ch-latest-scroll  flex:1 1 0 + min-height:0 + position:relative
                        —— 里面没有任何在流内的子元素, 所以它向上贡献 0
     .ch-latest-list    position:absolute; inset:0; overflow-y:auto
                        —— 吃满外壳, 装不下就自己滚
   面板的固有高度因此只剩标题那一行, 行高完全由右格决定, 左格再 stretch 回来填满。

   只在两列布局下生效 —— ≤1180px 时 .ch-lower 塌成一列, 没有"邻格高度"可对齐, 绝对定位
   反而会把这一段抽出文档流、叠在下面的内容上。

   ⚠ 选择器写成 .ch-lower .ch-panel-latest 而不是 .ch-panel-latest, 是踩过一次的:
   媒体查询**不增加特异度**。基础规则 .ch-panel { display: grid } 和 .ch-panel-latest
   同为 (0,1,0), 而这个块写在它前面, 于是后声明的 grid 赢, display:flex 静默失效 ——
   面板仍是 grid, 壳成了高度 auto 的 grid 项, 而它唯一的子元素是绝对定位的(不占高度),
   壳高 0 → 列表 inset:0 也是 0 → 一整格的评价全都看不见, 只剩标题。
   加一层父级把它抬到 (0,2,0), 就和这个块在文件里的位置无关了。 */
@media (min-width: 1181px) {
  .ch-lower .ch-panel-latest {
    display: flex;
    flex-direction: column;
  }

  .ch-lower .ch-panel-latest .ch-panel-head {
    flex: none;
  }

  .ch-lower .ch-panel-latest .ch-latest-scroll {
    position: relative;
    flex: 1 1 0;
    min-height: 0;
  }

  .ch-lower .ch-panel-latest .ch-latest-list {
    position: absolute;
    inset: 0;
    overflow-y: auto;
  }
}

/* 反过来, 单列布局下这 24 条没有任何"要填的空格"可填 —— 它们只是把页面拉长。
   条数是为两列那一格的高度选的, 所以那个前提不成立时就把尾巴收起来。
   用 CSS 截断而不是在 renderLatest 里按宽度分支: 渲染路径保持一条, 而且窗口在断点
   两侧拖动时不需要重新渲染。 */
@media (max-width: 1180px) {
  .ch-latest-list > :nth-child(n + 7) {
    display: none;
  }
}

.ch-latest-list {
  display: grid;
  gap: 10px;
  align-content: start;
}

.ch-panel {
  display: grid;
  gap: 10px;
  align-content: start;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.ch-panel-head h2 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 17px;
  font-weight: 950;
}

.ch-panel-head p {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
}

.ch-empty {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.6;
}

.ch-latest {
  display: grid;
  gap: 6px;
  width: 100%;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  text-align: left;
}

.ch-latest:hover {
  border-color: #b9c3d4;
  background: #f9fbfd;
}

.ch-latest-top {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: baseline;
  color: var(--muted);
  font-size: 12px;
  font-weight: 800;
}

.ch-latest-top b {
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 950;
}

/* Pushed to the far end of the row so the time never reads as part of the
   course name it follows. */
.ch-latest-top em {
  margin-left: auto;
  color: var(--soft);
  font-style: normal;
  font-size: 11px;
  font-weight: 800;
}

/* The reviewer's overall verdict — same banded palette as the board tiles, so
   a reader learns "praise or warning" without opening the modal. */
.ch-latest-score {
  min-width: 34px;
  padding: 2px 8px;
  border-radius: 8px;
  color: #111111;
  font-style: normal;
  font-size: 12px;
  font-weight: 950;
  text-align: center;
  font-variant-numeric: tabular-nums;
  background: #e7edf4;
}

.ch-latest-score.is-score-1 { background: #ff8f95; }
.ch-latest-score.is-score-2 { background: #ffbf69; }
.ch-latest-score.is-score-3 { background: #ffeb6a; }
.ch-latest-score.is-score-4 { background: #a8e875; }
.ch-latest-score.is-score-5 { background: #52d69d; }

.ch-latest p {
  margin: 0;
  color: var(--text);
  font-size: 13px;
  line-height: 1.55;
}

.ch-latest-silent {
  color: var(--soft);
  font-style: italic;
}

/* The panel gives its heading its natural height and hands the rest to the grid,
   so the 3×3 stretches to whatever height the column beside it sets. */
.ch-faculty-strip {
  grid-template-rows: auto minmax(0, 1fr);
}

.ch-faculty-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: minmax(74px, 1fr);
  gap: 8px;
}

/* Tile evidence line: 门课/评价 counts, pinned to the tile's bottom edge. */
.ch-tile-stats {
  margin-top: auto;
  padding-top: 4px;
  color: var(--soft);
  font-style: normal;
  font-size: 10.5px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/* Light tile. The faculty accent cannot be the ink here — FEIT #c3fe0d,
   Education #e3f10d and FBE #f1f1f1 were chosen against a #111 card — so it
   carries identity as a soft tint (--fc, the true accent, which is exactly what
   a wash wants) plus a 4px bar in --fc-line, which is the tenant's `lightAccent`
   where one is authored and the accent itself otherwise. Mixing alone could not
   do this: 72% of near-white FBE is still near-white, so its bar vanished. */
.ch-faculty-tile {
  display: grid;
  align-content: start;
  gap: 3px;
  padding: 12px 13px;
  border: 1px solid color-mix(in srgb, var(--fc-line, #64707f) 34%, var(--line-strong));
  border-left: 4px solid var(--fc-line, #64707f);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--ink-strong);
  text-align: left;
  transition:
    background 140ms ease,
    border-color 140ms ease,
    transform 140ms ease;
}

/* The wash is a hover state now, not the resting one: nine filled tiles read as
   nine states, while nine outlined ones read as a set. */
.ch-faculty-tile:hover,
.ch-faculty-tile:focus-visible {
  background: color-mix(in srgb, var(--fc, var(--surface)) 14%, var(--surface));
  border-color: color-mix(in srgb, var(--fc-line, #64707f) 58%, var(--line-strong));
  border-left-color: var(--fc-line, #64707f);
  outline: 0;
  transform: translateY(-1px);
}

.ch-faculty-tile em {
  color: var(--muted);
  font-style: normal;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

/* The name in the reader's own language leads: it is the biggest thing on the
   tile and carries the faculty's colour. Still mixed toward --ink-strong — the
   pale accents (FEIT, Education) would sit near 3:1 on their own, and this is
   below the 18.66px where bold text may drop to a 3:1 floor. */
.ch-faculty-tile strong {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: color-mix(in srgb, var(--fc-line, var(--ink-strong)) 88%, var(--ink-strong));
  font-size: 21px;
  font-weight: 950;
  line-height: 1.24;
  /* CJK gets no heavier than the fallback face's bold (700), so font-weight
     alone cannot match the weight the Latin name renders at. A hairline stroke
     in the same colour thickens the strokes themselves. */
  -webkit-text-stroke: 0.4px currentColor;
}

/* 暗色模式下这块瓦片换读另一支学院色, 并且不描边。
   (1) 颜色: --fc-line 是 lightAccent —— 专门为白卡片挑的深色版(FBE 是 #111111),
       放在深色瓦片上要么看不清, 要么直接隐形。--fc 是 textColor, 本来就是"给黑卡片
       用的那一支"(FBE 是 #F1F1F1), 而暗色下这张瓦片正是一张黑卡片, 所以它才是这里
       该读的那个变量。
   (2) 不再往里混 --ink-strong: 上面那条 color-mix 的用意是"把浅色学院色往正文墨色
       压一点以保对比度", 而 --ink-strong 在暗色下是 #ffffff —— 同一条式子于是变成
       往学院色里掺 12% 白, 方向正好相反, 把每个学院色都洗淡了。
   (3) -webkit-text-stroke 归零: 0.4px 描边在浅底上只是加粗笔画, 在深底上会让 CJK
       字形发虚起毛边 —— 用户看到的"白色描边"。字重本身由 font-weight 保证。 */
[data-theme="dark"] .ch-faculty-tile strong {
  color: var(--fc, var(--ink-strong));
  -webkit-text-stroke: 0;
}

/* ============================================================
 * 学院配色在暗色下的"双保险"第二半 —— 必须和 darkReaderBoot.js 的
 * ignoreInlineStyle 名单一起看, 两半缺一不可, 单上任何一半都比现状更糟。
 *
 * 背景: 学院配色是通过内联自定义属性下发的(courseHome/coursesPage/
 * facultyHome/subjectReviews 各自往元素上写 style="--fc: …" 之类)。
 * Dark Reader 的内联通道会把 `--` 开头的自定义属性当真颜色属性重写, 所以
 * 第一步是把这些元素放进 ignoreInlineStyle, 让作者写的值原样留在 DOM 上。
 *
 * 但那只救了"值", 没救"读值的人"。引擎在重写样式表时, 会把每一处 var(--x)
 * 无条件改名成 var(--darkreader-{bg,text,border}--x)。实测它给
 * .ch-faculty-tile strong 发出来的是:
 *     color: var(--darkreader-text--fc, var(--darkreader-text--ink-strong))
 * 而 --darkreader-text--fc 本该由内联通道注册 —— 正是我们刚跳过的那一步。
 * 于是它是 undefined, 整条掉进兜底的白色。九个学院名全变白, 4px 左条全变灰,
 * 学院详情页的 hero 变灰底白字 —— 这就是用户报的那几张截图。
 *
 * 修法: 我们自己把这些别名定义出来, 指回作者写的那支变量。别名声明本身留在
 * [data-theme="dark"] 作用域内, 浅色模式一个字节都不受影响。
 *
 * 一个变量要写三份(bg/text/border), 因为同一支学院色在不同规则里分别当背景、
 * 文字和边框用, 引擎按用途分别改名。没有 bgimg 那一份: 这些变量装的都是纯色,
 * 引擎只对"装图片的变量"才用 --darkreader-bgimg-- 前缀。
 *
 * 变量在某个选择器上不存在是安全的: `--darkreader-text--fc: var(--fc)` 在
 * --fc 未定义时整条 computed-value-time 失效, 等同于没写, 消费端照常走它自己的
 * 兜底。所以这里一次性把六个载体和十支变量交叉列全, 不必逐个裁剪。
 *
 * MAINTENANCE: 往 ignoreInlineStyle 加新载体时, 这里要同步补它携带的变量;
 * 新增一支内联学院变量时, 这里要补它的三份别名。漏了就是白字/灰条。
 * ============================================================ */
[data-theme="dark"] .ch-faculty-tile,
[data-theme="dark"] .faculty-card,
[data-theme="dark"] .faculty-page,
[data-theme="dark"] .fh-page,
[data-theme="dark"] .faculty-subject-card,
[data-theme="dark"] .review-faculty-line {
  --darkreader-bg--fc: var(--fc);
  --darkreader-text--fc: var(--fc);
  --darkreader-border--fc: var(--fc);
  --darkreader-bg--fc-line: var(--fc-line);
  --darkreader-text--fc-line: var(--fc-line);
  --darkreader-border--fc-line: var(--fc-line);
  --darkreader-bg--faculty-theme: var(--faculty-theme);
  --darkreader-text--faculty-theme: var(--faculty-theme);
  --darkreader-border--faculty-theme: var(--faculty-theme);
  --darkreader-bg--faculty-text: var(--faculty-text);
  --darkreader-text--faculty-text: var(--faculty-text);
  --darkreader-border--faculty-text: var(--faculty-text);
  --darkreader-bg--faculty-hover-theme: var(--faculty-hover-theme);
  --darkreader-text--faculty-hover-theme: var(--faculty-hover-theme);
  --darkreader-border--faculty-hover-theme: var(--faculty-hover-theme);
  --darkreader-bg--faculty-hover-text: var(--faculty-hover-text);
  --darkreader-text--faculty-hover-text: var(--faculty-hover-text);
  --darkreader-border--faculty-hover-text: var(--faculty-hover-text);
  --darkreader-bg--faculty-detail-theme: var(--faculty-detail-theme);
  --darkreader-text--faculty-detail-theme: var(--faculty-detail-theme);
  --darkreader-border--faculty-detail-theme: var(--faculty-detail-theme);
  --darkreader-bg--faculty-detail-text: var(--faculty-detail-text);
  --darkreader-text--faculty-detail-text: var(--faculty-detail-text);
  --darkreader-border--faculty-detail-text: var(--faculty-detail-text);
  --darkreader-bg--faculty-line: var(--faculty-line);
  --darkreader-text--faculty-line: var(--faculty-line);
  --darkreader-border--faculty-line: var(--faculty-line);
  --darkreader-bg--faculty-accent: var(--faculty-accent);
  --darkreader-text--faculty-accent: var(--faculty-accent);
  --darkreader-border--faculty-accent: var(--faculty-accent);
}

/* 左侧那条 4px 学院色也改读 --fc, 理由同上。hover 的填充本来就读 --fc, 不用动。
   只覆盖 border-left-color 这一个长手属性, 并且写成一句裸 var()、不套 color-mix:
   darkReaderBoot.js 的 CONTROL_LINE_SELECTORS 注释 (2) 记过一次事故 —— 往
   [data-theme="dark"] 规则里塞 color-mix 之后整张样式表都没被引擎处理。这里根本
   不需要 color-mix 就能达到效果, 没有理由去踩。整圈细边框保持不动, 交给既有规则
   和 CONTROL_LINE_SELECTORS 的 pin。 */
[data-theme="dark"] .ch-faculty-tile {
  border-left-color: var(--fc, #64707f);
}

[data-theme="dark"] .ch-faculty-tile:hover,
[data-theme="dark"] .ch-faculty-tile:focus-visible {
  border-left-color: var(--fc, #64707f);
}

/* The English name under the Chinese one: present only when the tenant has
   authored a translation, so it never repeats the line above it. */
.ch-faculty-tile span {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--soft);
  font-size: 10.5px;
  font-weight: 750;
  line-height: 1.3;
}

.ch-results {
  display: grid;
  gap: 14px;
}

.ch-results-head {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: baseline;
}

.ch-results-head h2 {
  margin: 0;
  color: var(--ink-strong);
  font-size: 20px;
  font-weight: 950;
}

.ch-results-head span {
  color: var(--muted);
  font-size: 12px;
  font-weight: 800;
}

.ch-results-head .ghost-button {
  margin-left: auto;
}

.ch-results-more {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 750;
}

.ch-professor-hits {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.ch-professor-hit {
  display: grid;
  gap: 2px;
  padding: 10px 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
  text-align: left;
}

.ch-professor-hit b {
  color: var(--ink-strong);
  font-size: 14px;
  font-weight: 950;
}

.ch-professor-hit span {
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 750;
}

/* The professors already on record, shown under the not-enough-to-rank card.
   Same .professor-card the faculty page uses; this only lays them out. */
.ch-professor-grid {
  margin-top: 0;
}

@media (max-width: 720px) {
  .ch-professor-grid {
    grid-template-columns: 1fr;
  }
}

/* The review form floats over the modal it was opened from: the course you were
   reading stays on screen behind its own scrim, so closing the form returns you
   to it rather than to the page. */
.modal-backdrop.is-stacked {
  position: fixed;
  inset: 0;
}

.modal-overlay-layer {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 24px;
  overflow: auto;
  background: rgba(8, 12, 24, 0.46);
}

/* 教授卡片 is the course card (.faculty-subject-card) with a professor in it, so
   it needs no styles of its own — only the two places a person differs from a
   subject: no long description to fill the middle, and 详情 in the corner where
   a subject prints its offering. */
.is-professor-card {
  min-height: 0;
  /* The faculty colour runs all the way round, not just along the top: with no
     description or meta chips the card is mostly white space, and a single top
     rule left it looking unfinished. --faculty-line is the light-surface accent,
     so FBE's near-white becomes its black rather than vanishing. */
  border: 1px solid color-mix(in srgb, var(--faculty-line, var(--line-strong)) 38%, var(--line-strong));
  border-top: 4px solid var(--faculty-line, var(--line-strong));
}

.is-professor-card:hover,
.is-professor-card:focus-visible {
  border-color: color-mix(in srgb, var(--faculty-line, var(--line-strong)) 62%, var(--line-strong));
  border-top-color: var(--faculty-line, var(--line-strong));
}

.is-professor-card .subject-offered-side {
  color: var(--brand-bright);
  font-weight: 900;
  white-space: nowrap;
}

/* Its own row under the cover, full card width: three dimensions, one line,
   always. Wrapped they dropped 相处体验 onto a second row and the card grew a
   step taller than the ones beside it; kept in the title column they had barely
   100px to share. min-width: 0 is what lets a grid item narrower than its
   nowrap min-content shrink instead of spilling past the card edge. */
.is-professor-card .review-scoreline {
  flex-wrap: nowrap;
  gap: 5px;
  min-width: 0;
}

.is-professor-card .review-scoreline span {
  min-width: 0;
  padding: 5px 6px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.ch-add-professor-chip {
  border-color: var(--ink-strong-bg);
  color: var(--ink-strong);
}

.ch-add-professor-chip:hover,
.ch-add-professor-chip:focus-visible {
  background: var(--brand-weak);
  outline: 0;
}

/* 教授列表 (below the ranking threshold): the cards ARE the content, so they get
   the panel to themselves with the add control above them. */
.ch-professor-list {
  display: grid;
  gap: 12px;
  padding: 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
}

.ch-professor-list .ch-professor-grid {
  margin-top: 0;
}

/* ---------------- 教授详情 ---------------- */
/* Sized like the course modal it sits beside in the reader's head — without this
   the card falls back to fit-content and reads as a tooltip next to 历史评价.
   (a695ed4 deleted the professor CARD's css and took this window sizing with it;
   the markup in subjectReviews.js never changed, so it just lost its shape.) */
.professor-detail-modal {
  width: min(1200px, calc(100vw - 40px));
  max-width: 1200px;
  padding: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
}

/* flex: 0 1 auto, matching the course window: with 1 1 auto the card GREW to
   fill the row, so the pair overflowed the backdrop and flex's centring pushed
   the whole thing left — the "crooked" page. It may shrink, never stretch. */
.dual-modal-row .professor-detail-modal {
  flex: 0 1 auto;
  min-width: 0;
  height: min(1040px, 94vh);
  max-height: 94vh;
}

/* The 详情 window, same proportions as the course modal's Reddit/guide panel. */
.professor-side-modal {
  flex: 0 1 380px;
  width: 380px;
  max-width: 380px;
  min-width: 0;
  height: min(1040px, 94vh);
  max-height: 94vh;
  padding: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.pd-side-body {
  display: grid;
  gap: 14px;
  align-content: start;
  padding: 18px 20px;
}

.professor-detail-summary {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 18px;
  align-items: center;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line-soft);
  /* 与 .subject-review-summary 同一条渐变、同一个理由 —— 见那条规则上的注释。 */
  background: linear-gradient(135deg, var(--surface) 0%, var(--surface-2) 100%);
}

.pd-score {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 2px;
  min-width: 96px;
  min-height: 96px;
  padding: 10px;
  border-radius: var(--radius-ui);
  background: #e7edf4;
  color: #111111;
}

.pd-score.is-score-1 { background: #ff8f95; }
.pd-score.is-score-2 { background: #ffbf69; }
.pd-score.is-score-3 { background: #ffeb6a; }
.pd-score.is-score-4 { background: #a8e875; }
.pd-score.is-score-5 { background: #52d69d; }

.pd-score span {
  font-size: 10px;
  font-weight: 950;
  letter-spacing: 0.04em;
}

.pd-score strong {
  font-size: 30px;
  line-height: 1;
  font-weight: 950;
}

.pd-score em {
  font-size: 10px;
  font-style: normal;
  font-weight: 800;
  opacity: 0.8;
}

.pd-meta {
  display: grid;
  gap: 10px;
  justify-items: start;
}

.pd-area {
  margin: 0;
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 800;
}

.pd-dims {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.pd-dim {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
}

.pd-dim small {
  color: var(--muted);
  font-size: 11px;
  font-weight: 850;
}

.pd-dim b {
  color: var(--ink-strong);
  font-size: 14px;
  font-weight: 950;
}

/* The card became the way in, so it has to look like one. */
.professor-card[data-action="open-professor-detail"] {
  cursor: pointer;
}

.professor-card[data-action="open-professor-detail"]:hover,
.professor-card[data-action="open-professor-detail"]:focus-visible {
  border-color: #b9c3d4;
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ink-strong-bg) 10%, transparent);
  outline: 0;
  transform: translateY(-1px);
}

@media (max-width: 1180px) {
  /* Same fold the course dual row uses: the details window drops below rather
     than squeezing the reviews out of the way. */
  .dual-modal-row:has(.professor-detail-modal) {
    flex-direction: column;
    overflow-y: auto;
    max-height: 94vh;
    /* Stacked, the row sizes to its content and its base cap is written in vw,
       which the root's mobile zoom: 0.88 does NOT divide out — the two windows
       came up ~75 CSS px narrower than the course modal's mobile card on the same
       screen. Percentages resolve against the backdrop, which is already zoomed. */
    width: 100%;
    max-width: 100%;
  }
  .professor-side-modal,
  .dual-modal-row .professor-detail-modal {
    flex: 0 0 auto;
    width: 100%;
    max-width: 100%;
    height: auto;
  }
}

@media (max-width: 640px) {
  .professor-detail-summary {
    grid-template-columns: 1fr;
  }
}

/* ---------------- 学院主页 ---------------- */
.fh-page {
  /* Wider than the course view's 1540 cap: the poster headline and the
     multi-column board both want the room. On a common 1920 screen this is
     effectively full-bleed inside .faculty-main's 24px gutters.
     The page is viewport-sized: back button, hero and chips take what they
     need, the BOARD gets exactly the leftover (1fr) — so the panels end at
     the bottom of the window instead of running past it. The 320px floor
     lets a very tall hero push the page into normal scrolling. */
  width: min(1880px, 100%);
  height: calc(100dvh - var(--topbar-height) - 52px);
  margin: 0 auto;
  display: grid;
  gap: 16px;
  grid-template-rows: auto auto auto minmax(320px, 1fr);
  color: var(--ink-strong);
}

/* A poster, not a card: the faculty name is the page's headline, set as large
   and tight as the column allows, sitting directly on the canvas with no border
   or fill. The grid you arrive from is dark — that is its own page — but past
   the click it is all light and typographic. */
.fh-hero {
  padding: 12px 4px 14px;
  color: var(--ink-strong);
  transition: padding 240ms ease;
}

/* Collapsed-on-scroll state (facultyHome.js toggles .is-hero-collapsed on
   .fh-page when a board column scrolls down): the poster gives its height to
   the content and comes back at the top. Font-size carries the whole animation
   — the woven tiles are em-sized, so they shrink with it for free. */
.is-hero-collapsed .fh-hero {
  padding: 2px 4px 6px;
}

.is-hero-collapsed .fh-hero h1 {
  font-size: clamp(22px, 2.4vw, 38px);
  letter-spacing: -0.02em;
}

.is-hero-collapsed .fh-sub {
  max-height: 0;
  margin-top: 0;
  opacity: 0;
}

/* Neutral ink, poster-scale: the name is the loudest thing on the page. The
   faculty's own colour comes back only as the full stop after it — one vivid
   mark that says WHICH faculty without tinting the whole headline.
   A wrapping flex row, not text flow: words are rigid, the photo tiles grow,
   so every wrapped line is justified flush to the right edge (the tiles
   swallow exactly the whitespace their line's words leave). The plain name
   lives in aria-label — this box's content is words interleaved with tiles. */
.fh-hero h1 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0.22em;
  row-gap: 0.06em;
  margin: 0;
  color: var(--ink-strong);
  font-size: clamp(38px, 8vw, 104px);
  line-height: 0.92;
  letter-spacing: -0.03em;
  font-weight: 950;
  text-transform: uppercase;
  transition: font-size 240ms ease, letter-spacing 240ms ease;
}


.fh-hero h1 .fh-word {
  white-space: nowrap;
}

/* Collapse stage one (setHeroCollapsed): each woven photo hands back its flex
   share and narrows to nothing, its line's words sliding over to take the room
   — then the font-size transition above shrinks the words as one block. The
   settle weave lifts this class before adding the new tiles, so they appear
   at full width like the original reweave always did. */
.fh-hero h1 .fh-pic {
  transition: flex-grow 180ms ease;
}

.fh-hero h1.is-pics-leaving .fh-pic:not(.fh-pic--last) {
  flex-grow: 0;
}

/* One-shot entrance for tiles woven into a photo-less hero (the settle after a
   staged collapse): they grow from nothing instead of popping in at full
   width. A keyframe animation, not a held class state, on purpose — its final
   frame IS the natural state, so even a stuck is-pics-entering has no lasting
   effect (it only ever plays on insertion). */
@keyframes fh-pic-in {
  from {
    flex-grow: 0;
  }
}

.fh-hero h1.is-pics-entering .fh-pic {
  animation: fh-pic-in 200ms ease;
}

/* The full stop is the faculty's colour — one vivid mark, kept in the markup
   (not ::after) so the woven photo tiles can follow it in the inline flow. */
.fh-hero h1 .fh-dot {
  color: var(--faculty-line, var(--ink-strong));
}

/* One big black-and-white photo per gap, woven between the headline's words.
   flex-grow ≫ words (which never grow): each tile stretches to fill exactly
   the whitespace its line has left over, which is what justifies the poster.
   The basis/height vary per slot so the collage feels placed, not tiled;
   grayscale so a colour photo dropped in later still reads monochrome. */
.fh-hero h1 .fh-pic {
  /* Zero basis: the tile takes NO space of its own, so weaving it into a
     measured line can never re-wrap the words — grow (99 ≫ words' 0) then
     stretches it to swallow exactly that line's leftover whitespace. */
  position: relative;
  flex: 99 1 0px;
  min-width: 0;
  /* One shared height: mixed heights read as a mistake once the tiles sit on
     neighbouring lines of the same headline. */
  height: 0.85em;
  border-radius: 0.06em;
  overflow: hidden;
  /* isolate: the ::after screen-blend below must composite against the photo
     layer only, never against whatever sits behind the tile. */
  isolation: isolate;
  background: var(--surface);
}

/* Gradient-map tint, Photoshop style — two-ended: shadows map to the faculty
   colour, highlights to the PAGE CANVAS colour (--fh-tint-hi matches the
   .faculty-detail-main background) so the bright parts of a tile melt into
   the page. One element, four background layers blending bottom-up: photo →
   gray "saturation" layer (desaturates, keeping luminosity) → faculty colour
   SCREEN (black end) → highlight colour MULTIPLY (white end). Every faculty's
   photos get this same treatment — only --faculty-line changes. --fh-pic-src
   is set per-tile by weaveHeroTiles(). */
.fh-hero h1 .fh-pic::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(var(--fh-tint-hi, #f3f5fa), var(--fh-tint-hi, #f3f5fa)),
    linear-gradient(var(--faculty-line, #000000), var(--faculty-line, #000000)),
    linear-gradient(#808080, #808080),
    var(--fh-pic-src) center / cover no-repeat;
  background-blend-mode: multiply, screen, saturation, normal;
}

.fh-sub {
  margin: 14px 0 0;
  color: var(--muted);
  font-size: clamp(13px, 1.1vw, 16px);
  font-weight: 800;
  letter-spacing: 0.01em;
  /* Collapsible on scroll: animate to nothing instead of popping away. */
  max-height: 40px;
  overflow: hidden;
  opacity: 1;
  transition: max-height 240ms ease, opacity 200ms ease, margin-top 240ms ease;
}

/* Faculty-coloured fill (explicit request): the light-surface accent with
   white 950-weight text. Note some accents (MDHS pink) run under 4.5:1 at
   13px — accepted trade-off for the branding. */
.fh-cta-primary {
  min-height: 38px;
  padding: 0 16px;
  border: 0;
  border-radius: var(--radius-ui);
  background: var(--faculty-line, var(--ink-strong-bg));
  color: #ffffff;
  font-size: 13px;
  font-weight: 950;
}

.fh-cta-primary:hover,
.fh-cta-primary:focus-visible {
  filter: brightness(0.88);
  outline: 0;
}

.fh-cats {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}

/* Write-CTA lives at the far end of the category row now (it used to float over
   the headline). margin-left:auto pins it right and lets it drop cleanly to its
   own right-aligned line when the categories wrap on a phone. */
.fh-cats-cta {
  margin-left: auto;
}

.fh-cat {
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: var(--radius-ui);
  background: transparent;
  color: #55617a;
  font-size: 13px;
  font-weight: 900;
}

.fh-cat.is-active {
  background: var(--faculty-line, var(--ink-strong-bg));
  color: #ffffff;
}

/* ---- Riptype-style column board: every toggled chip is a panel with its own
   header bar and internal scroll. The reference is a black type-foundry site;
   here it is only the LAYOUT that crosses over — panels stay white on the
   site's light palette. --fh-cols is set inline by the view. */
.fh-board {
  display: grid;
  /* --fh-grid is a full track list built by the view: post columns wide,
     hot-courses slim (the media queries below still win when they re-set
     this property for narrow screens). */
  grid-template-columns: var(--fh-grid, repeat(3, minmax(0, 1fr)));
  gap: 12px;
  align-items: stretch;
  /* Height comes from .fh-page's 1fr row — the board is exactly the space
     left under the hero and chips, ending at the viewport bottom. */
  min-height: 0;
}

.fh-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
  background: var(--surface);
  overflow: hidden;
}

/* The reference's black header strip — ink instead of pure black, with the
   faculty's colour as the little left-hand mark standing in for its icons. */
.fh-col-head {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 34px;
  padding: 0 30px;
  background: var(--ink-strong-bg);
  color: #ffffff;
}

.fh-col-head::before {
  content: "";
  position: absolute;
  left: 11px;
  top: 50%;
  width: 10px;
  height: 10px;
  transform: translateY(-50%);
  border-radius: 2px;
  background: var(--faculty-line, var(--surface));
}

.fh-col-head h2 {
  margin: 0;
  max-width: 100%;
  font-size: 12.5px;
  font-weight: 900;
  letter-spacing: 0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Plain block flow, NOT grid: grid row sizing fought .post-card's
   content-visibility containment and crushed every card to a uniform sliver.
   Block children just stack at their natural height. */
.fh-col-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: block;
}

/* And no content-visibility inside the column's own scroller: the skipped
   cards' placeholder sizing is what made them unscrollable mush. A column
   caps at 30 posts, cheap enough to render outright. */
.fh-col-body .post-card {
  content-visibility: visible;
  contain-intrinsic-size: auto;
}

/* Post columns carry the SAME full cards as the home feed. .post-card is
   transparent with no own frame — give it the feed's hairline dividers. */
.fh-col-body .post-card {
  border-bottom: 1px solid var(--line);
}

.fh-col-body .post-card:last-child {
  border-bottom: 0;
}

/* 收藏夹 cards: parsed links with cover / title / summary / tags / source
   footer — the bookmark-manager look, one card per row inside the column. */
.fh-fav-add {
  border-bottom: 1px solid var(--line);
  color: var(--faculty-line, var(--brand-bright));
  font-weight: 900;
}

.fh-fav-card {
  display: grid;
  gap: 8px;
  padding: 14px;
  border-bottom: 1px solid var(--line);
}

.fh-fav-cover {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-ui);
  background: var(--surface-3);
  overflow: hidden;
}

.fh-fav-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.fh-fav-cover span {
  position: absolute;
  left: 8px;
  bottom: 8px;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(17, 17, 17, 0.72);
  color: #ffffff;
  font-size: 10.5px;
  font-weight: 800;
}

.fh-fav-title {
  color: var(--ink-strong);
  font-size: 14px;
  font-weight: 900;
  line-height: 1.35;
  text-decoration: none;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.fh-fav-title:hover {
  text-decoration: underline;
}

.fh-fav-summary {
  margin: 0;
  color: #55617a;
  font-size: 12px;
  font-weight: 650;
  line-height: 1.55;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.fh-fav-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  font-size: 10.5px;
  font-weight: 800;
}

.fh-fav-tags i {
  font-style: normal;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--faculty-line, var(--ink-strong-bg));
  color: #ffffff;
}

.fh-fav-tags span {
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: #55617a;
}

.fh-fav-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 750;
}

.fh-fav-actions {
  display: inline-flex;
  gap: 10px;
}

.fh-fav-actions a {
  color: var(--faculty-line, var(--brand-bright));
  font-weight: 850;
  text-decoration: none;
}

.fh-fav-actions button {
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 11px;
  font-weight: 750;
}

.fh-fav-actions button:hover {
  color: var(--danger, #c0392b);
}

/* 新收藏 modal — a single-column sheet like the reference app, narrower than
   the create modal, generous field rhythm, cover block up top. */
.bookmark-modal {
  width: min(600px, calc(100vw - 32px));
  max-height: min(86vh, 900px);
  overflow-y: auto;
  padding: 20px 22px 22px;
}

.bookmark-modal-title {
  margin: 0;
  font-size: 18px;
  font-weight: 950;
  color: var(--ink-strong);
}

.bookmark-parse-row {
  margin: 14px 0 16px;
  padding: 14px;
  border: 1px dashed #c3cddd;
  border-radius: var(--radius-ui);
  background: #f8fafd;
}

.bookmark-parse-row .import-link-field input {
  min-height: 44px;
  font-size: 13.5px;
}

.bookmark-parse-row .secondary-button {
  min-height: 44px;
  padding: 0 20px;
  border: 0;
  background: var(--ink-strong-bg);
  color: #ffffff;
  font-weight: 900;
}

.bookmark-form {
  gap: 14px;
}

.bookmark-form > label {
  display: grid;
  gap: 6px;
  color: #55617a;
  font-size: 12.5px;
  font-weight: 850;
}

.bookmark-form input,
.bookmark-form select,
.bookmark-form textarea {
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid #dbe2ee;
  border-radius: 12px;
  background: #f6f8fc;
  color: var(--ink-strong);
  font-size: 14px;
  font-weight: 700;
}

.bookmark-form textarea {
  min-height: 84px;
  padding: 10px 12px;
  line-height: 1.55;
  resize: vertical;
}

.bookmark-form input:focus,
.bookmark-form select:focus,
.bookmark-form textarea:focus {
  border-color: var(--ink-strong-bg);
  background: var(--surface);
  outline: 2px solid color-mix(in srgb, var(--brand-bright) 16%, transparent);
}

.bookmark-form .create-two-column {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.bookmark-cover {
  position: relative;
  aspect-ratio: 21 / 9;
  border-radius: 14px;
  background: var(--surface-3);
  overflow: hidden;
}

.bookmark-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Pre-parse placeholder: says where the cover will land instead of the form
   just starting cold. */
.bookmark-cover.is-empty {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
  border: 1px dashed #c3cddd;
  background: #f8fafd;
  color: #8b96ad;
  font-size: 12px;
  font-weight: 750;
}

.bookmark-cover.is-empty svg {
  width: 34px;
  height: 34px;
}

.bookmark-cover-site,
.bookmark-cover-type {
  position: absolute;
  bottom: 10px;
  padding: 2px 9px;
  border-radius: 999px;
  background: rgba(17, 17, 17, 0.72);
  color: #ffffff;
  font-size: 11px;
  font-weight: 800;
}

.bookmark-cover-site { left: 10px; }
.bookmark-cover-type { right: 10px; }

.bookmark-form-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 4px;
}

.bookmark-save {
  min-height: 44px;
  padding: 0 26px;
  font-weight: 950;
}

@media (max-width: 720px) {
  .bookmark-form .create-two-column {
    grid-template-columns: 1fr;
  }
}

/* 课程群聊: column-reverse scroller keeps the newest message pinned to the
   bottom without any scroll scripting (messages render newest-first). */
.course-chat {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 0;
  height: 100%;
}

.course-chat-note,
.course-chat-empty {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 750;
}

.course-chat-scroll {
  flex: 1;
  min-height: 240px;
  max-height: 52vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
  padding: 4px 2px;
}

.course-chat-msg {
  display: grid;
  gap: 2px;
  justify-items: start;
  max-width: 86%;
}

.course-chat-msg.is-mine {
  justify-self: end;
  justify-items: end;
}

.course-chat-name {
  color: var(--muted);
  font-size: 10.5px;
  font-weight: 800;
}

.course-chat-bubble {
  padding: 8px 12px;
  border-radius: 14px;
  background: var(--surface-3);
  color: var(--ink-strong);
  font-size: 13px;
  font-weight: 650;
  line-height: 1.5;
  word-break: break-word;
}

.course-chat-msg.is-mine .course-chat-bubble {
  background: var(--ink-strong-bg);
  color: #ffffff;
}

.course-chat-time {
  color: var(--soft);
  font-size: 10px;
  font-weight: 700;
}

.course-chat-form {
  display: flex;
  gap: 8px;
}

.course-chat-form input {
  flex: 1;
  min-height: 40px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ui);
}

.course-chat-form .primary-button {
  min-height: 40px;
  padding: 0 16px;
}

.fh-col-empty {
  margin: 0;
  padding: 18px 14px;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 750;
  line-height: 1.6;
}

/* Footer row of the courses column. */
.fh-col-more {
  width: 100%;
  padding: 12px 14px;
  border: 0;
  background: transparent;
  color: var(--faculty-line, var(--brand-bright));
  font-size: 12px;
  font-weight: 850;
  text-align: left;
}

.fh-col-more:hover {
  background: #f7f9fc;
}

.fh-course {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  text-align: left;
}

/* Inside a board column the rows sit flush — hairline dividers, no own frame
   (the framed look was for the old right-rail card). */
.fh-col-body .fh-course {
  padding: 10px 14px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}

.fh-col-body .fh-majors {
  padding: 12px 14px;
}

.fh-course:hover {
  border-color: #b9c3d4;
  background: #f7f9fc;
}

.fh-course-score {
  display: grid;
  place-items: center;
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--radius-ui);
  background: #e7edf4;
  color: #111111;
}

.fh-course-score.is-score-1 { background: #ff8f95; }
.fh-course-score.is-score-2 { background: #ffbf69; }
.fh-course-score.is-score-3 { background: #ffeb6a; }
.fh-course-score.is-score-4 { background: #a8e875; }
.fh-course-score.is-score-5 { background: #52d69d; }

.fh-course-score b {
  font-size: 17px;
  line-height: 1;
  font-weight: 950;
}

.fh-course-body {
  min-width: 0;
  display: grid;
  gap: 2px;
}

.fh-course-body b {
  color: var(--ink-strong);
  font-size: 13.5px;
  font-weight: 900;
}

.fh-course-body span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 750;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.fh-course-go {
  color: var(--faculty-line, var(--soft));
  font-size: 16px;
}

.fh-majors {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.fh-major {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: #33405c;
  font-size: 12px;
  font-weight: 850;
}

.fh-major:hover {
  border-color: #b9c3d4;
  color: var(--ink-strong);
}

.fh-major i {
  color: var(--faculty-line, var(--soft));
  font-style: normal;
  font-size: 11px;
  font-weight: 800;
}

.fh-empty {
  display: grid;
  gap: 10px;
  justify-items: start;
  padding: 26px;
  border: 1px dashed #c9d4e4;
  border-radius: var(--radius-ui);
  background: #fbfcfe;
}

.fh-empty strong {
  color: var(--ink-strong);
  font-size: 17px;
  font-weight: 950;
}

.fh-empty p {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.6;
}

.fh-empty .fh-cta-primary {
  background: var(--ink-strong-bg);
  color: #ffffff;
}

@media (max-width: 1180px) {
  /* Compact: the three boards share ONE carousel slot — swipe sideways to
     switch. Replaces the old stack-and-fold model (three full boards were a
     very long scroll, and folded boards read as empty). 88% slides leave the
     next board peeking in as the swipe hint; the dots below are the legend.
     is-collapsed is deliberately inert here — the carousel already shows one
     board at a time, so the folded-by-default 最好评/避雷榜 must still render
     their rows. */
  .ch-boards {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .ch-boards::-webkit-scrollbar { display: none; }
  .ch-board {
    flex: 0 0 88%;
    scroll-snap-align: center;
  }
  .ch-board-dots { display: flex; }
  .ch-lower { grid-template-columns: 1fr; }
  /* Stacked, 按学院浏览 outranks 最新评价: the faculty tiles are navigation,
     the latest reviews are a feed — the way somewhere beats the newest words. */
  .ch-lower .ch-faculty-strip { order: -1; }
  /* Back to normal page flow: the viewport-sized page + 1fr board row only
     make sense with the panels side by side. */
  /* .fh-page deliberately NOT reverted here any more: the base viewport lock
     (fixed height, board = leftover 1fr) is what makes the panels TOUCH THE
     BOTTOM of the window, and with the two-track placement below the panels
     are still side by side, so the lock keeps making sense. Only the ≤720px
     single-column block returns the page to normal flow. */
  .fh-board {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    height: auto;
    /* One row per right-stack widget, the last one 1fr (built by
       renderFacultyDetail) — the final widget soaks up the leftover height. */
    grid-template-rows: var(--fh-rows-template, none);
  }
  /* A third (or fourth) widget never lands UNDER 全部帖子: the first widget
     owns the whole left track (spanning one row per right-stack widget —
     --fh-side-rows comes from renderFacultyDetail), everything else stacks in
     the right track. Undone in the ≤720px single-column block below. */
  .fh-board > .fh-col:first-child {
    grid-column: 1;
    grid-row: 1 / span var(--fh-side-rows, 1);
  }
  .fh-board > .fh-col:not(:first-child) {
    grid-column: 2;
  }
  /* Middle right-stack widgets (auto rows) stay content-sized but never hog
     the track — the last widget is the one that stretches. */
  .fh-board > .fh-col:not(:first-child):not(:last-child) { max-height: 44dvh; }
}

@media (max-width: 720px) {
  /* The bar stays one row on a phone — the icon column narrows and 评价课程
     drops out, since the field is the only thing that has to survive. */
  .ch-searchbar input { height: 54px; font-size: 15px; padding: 0 14px; }
  .ch-searchbar-icon { width: 52px; height: 34px; }
  .ch-searchbar-icon svg { width: 22px; height: 22px; }
  .ch-searchbar-review { display: none; }
  .ch-search-mode { padding-left: 6px; }
  .ch-search-mode button { min-height: 36px; padding: 0 20px; font-size: 14px; }
  /* 学院 + 仅 Summer + 仅 Winter 一行装下。允许换行的话 flex 会先把 仅 Winter 甩到
     第二行而不是让 select 变窄(换行优先于收缩), 所以这一组在手机上不换行: 两个 chip
     保持原尺寸, 只有 select 伸缩。
     flex-basis: 0 是关键 — select 不再按最长的院系名贡献宽度, 否则 nowrap 会把整页
     撑出横向滚动条(320 机型上实测 337px)。它只吃剩下的空间, 上限 140px, 装 全部学院
     四个字绰绰有余, 选中长院系名时省略号收尾。 */
  .ch-filters-right { flex-wrap: nowrap; }
  .ch-filters-right .ch-chip { flex: 0 0 auto; white-space: nowrap; }
  /* 手机上「仅 Summer / 仅 Winter」收进「筛选」后面。这两枚合起来占掉整整一行，
     而它们是少数人才会用的限定条件；上面那排维度 chip 才是主操作。收起后这一行
     只剩一个「筛选 ⌄」，展开时两枚照旧出现在它右边。
     .ch-term-toggle 只在这一档存在 —— 桌面宽度足够，两枚一直摆在外面更快。 */
  .ch-term-toggle { display: inline-flex; align-items: center; gap: 4px; }
  .ch-filters-right:not(:has(.ch-term-toggle.is-open)) .ch-term-chip { display: none; }
  /* The faculty select drops out on a phone — the carousel boards are already
     one-board-at-a-time, and 按学院浏览 below covers the per-faculty question. */
  .ch-select { display: none; }
  /* The bar's 26px breathing room is desktop air; on a phone it read as a
     dead band between the search field and the chips. .ch-wrap's grid gap
     still separates them. */
  .ch-searchbar { margin-bottom: 0; }
  /* 顶栏四枚控件统一成同一套模板（2026-08-29）。实测 430 视口下它们是三种尺寸、
     其中一枚还没有描边：☰ 33x33 无边、＋ 32x32 有边、主题 28x28 有边、语言 28 高有边。
     左上角那两枚是后加的手机专用按钮，没跟上右上角那套配方。统一到 28px 并共用
     同一条 color-mix 描边声明 —— 同一条声明经过同一个转换，明暗两个主题都只会得到
     同一个结果，不需要为深色再钉一次。 */
  .topbar .icon-button,
  .topbar .mobile-create-button {
    /* flex 容器会压缩它们（实测 ☰ 被压到 25px、＋ 被压成 32x25 的长方形），所以三组
       尺寸都要写：flex-basis 关掉伸缩，min-* 挡住压缩，width/height 定死。 */
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    min-width: 32px;
    min-height: 32px;
    padding: 0;
    border: 1px solid color-mix(in srgb, var(--topbar-ink) 32%, transparent);
    border-radius: var(--radius-ui);
    font-size: 15px;
  }

  /* 信息流正文向 App 靠拢：网页手机版原来是 19/16px，App 是 16.5/15（PostCard.tsx
     的 title/body）。同一条帖子在两端读起来该是同一个重量，网页这边偏大一档，
     一屏少放小半条帖子。 */
  .post-card h2 { font-size: 17px; }
  .post-card p { font-size: 15px; }
  /* 手机上课程弹窗铺满整屏: 上下各 12px 的留白在桌面是「这是一张浮起来的卡」的
     信号, 在手机上只是两条黑边 —— 弹窗本来就占满宽度, 圆角和阴影已经没有对照物
     可比。占满之后正文多出 24px, 正好多一条评价。 */
  .course-combined-modal {
    height: 100dvh;
    max-height: 100dvh;
    border-radius: 0;
  }
  .dual-modal-row.is-mobile-combined { padding: 0; }
  /* 手机上排六个。原来是五个 —— 八个确实太长(每划一屏都在滚)，但五个又太少：
     榜单的意义是"看个大概排名"，卡到第五名正好切在很多榜单最有信息量的那一段。
     把行内元素同步收一档，六行占的高度反而比原来五行还略矮：
       行内边距 6→4、槽距 11→9、分数方块 48→40、数字 19→16、课名 14.5→13.5。
     这些覆盖只活在这一档，桌面端一个都读不到。 */
  .ch-board-rows .ch-rank:nth-child(n + 9) { display: none; }
  /* 方块里那行「综合评分」在手机上是纯噪声: 榜单头顶已经写着按什么排, 每一行再
     重复一遍只是把方块撑高。去掉之后方块能收到 34px, 八行的总高比原来六行还矮。 */
  .ch-score i { display: none; }
  .ch-rank { padding: 4px 6px; gap: 9px; grid-template-columns: 18px minmax(0, 1fr) auto; }
  .ch-no { font-size: 13px; }
  .ch-name { font-size: 13.5px; }
  .ch-code, .ch-meta { font-size: 10.5px; }
  .ch-score { min-width: 38px; min-height: 34px; padding: 2px 6px; }
  .ch-score b { font-size: 16px; }
  /* Poster headline gets tighter tracking room on a narrow column. */
  .fh-hero { padding-left: 2px; padding-right: 2px; }
  .fh-hero h1 { letter-spacing: -0.02em; }
  .fh-board { grid-template-columns: 1fr; grid-template-rows: none; }
  /* Single column again — undo the viewport lock and the two-track placement:
     on a phone the widgets simply stack in order at a comfortable capped
     height and the page scrolls normally. */
  .fh-page { height: auto; min-height: 0; grid-template-rows: none; }
  .fh-board > .fh-col:first-child,
  .fh-board > .fh-col:not(:first-child) {
    grid-column: auto;
    grid-row: auto;
    max-height: 460px;
  }
}

/* ── 小红书 note cards (course modal side panel) ─────────────────────────────
   XHS-style: the whole card is the link, bold title, 3-line prose excerpt,
   footer = author/date left + red-heart likes right. Mirrors the real app's
   list density rather than the Reddit vote-block layout. */
.xhs-note-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.xhs-note-card {
  display: block;
  background: var(--surface, var(--surface));
  border: 1px solid #f0e3e6;
  border-radius: 12px;
  padding: 12px 14px;
  text-decoration: none;
  color: inherit;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.xhs-note-card:hover {
  border-color: #ffb8c5;
  box-shadow: 0 2px 10px rgba(255, 36, 66, 0.08);
}

.xhs-note-title {
  margin: 0 0 4px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--text, #1f2733);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.xhs-note-excerpt {
  margin: 0 0 8px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--muted, #7a8699);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.xhs-note-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 11px;
  color: var(--muted, #98a2b3);
}

.xhs-note-author {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.xhs-note-stats {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}

.xhs-note-stats em {
  font-style: normal;
  font-weight: 700;
  color: var(--muted, #98a2b3);
}

.xhs-note-stats .xhs-like {
  color: #ff2442;
}

/* 「墙上」那一栏的卡片。内件(标题/摘要/页脚)整套借小红书那一栏的 .xhs-note-*, 只有
   卡片自己这层重写: 这是站内内容, 不该穿别人家的粉色描边。是 <button> 不是 <a>,
   因为它开的是站内的帖子弹窗, 所以还要抹掉按钮自带的那身默认样式。 */
.wall-post-card {
  display: block;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.wall-post-card:hover {
  border-color: var(--brand);
  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
}


/* ============================================================
 * Dark Reader 引擎盲区补丁（quick 260826-50n）
 *
 * 不是把 3tj（quick 260826-3tj）删掉的手写深色层加回来。3tj 的 RESEARCH Q8
 * 反对的是在引擎已经正确处理的元素上再叠一层手写深色，那会悄悄改掉调好的
 * 对比度。这里是相反的情况：本次会话直接读取活的 CSSOM 与 Dark Reader 注入
 * 的 style.darkreader 标签确认，下面六条 base 规则（.comment-line
 * app.css:5110、.profile-verify-card app.css:5616、.discussion-main
 * app.css:7408、.discussion-map-card app.css:7480、.discussion-poll-card
 * app.css:7668、.faculty-detail-main app.css:7926）被引擎完全跳过、零重绘。
 * 补的是引擎有盲区的洞，不是在同一个东西上跑两层。
 *
 * 盲区的形状：引擎对纯色（字面量或 var() 都算）处理得很好，并且会重写
 * :root 上自定义属性的值，于是每一处 var() 引用都通过正常级联自动继承——
 * 这是整个 3tj 迁移真正的承重机制。但一个字面量的多段渐变函数当背景时，
 * 引擎既不改写它的颜色停靠点，也不整体替换。
 *
 * 覆盖值只准写 token、不准写深色十六进制：写 var(--…) 就能骑在上面那套
 * "引擎重写自定义属性" 的机制上；写死一个深色值等于开第二套要人肉维护的
 * 调色板，正是 3tj 想摆脱的东西。取值来源见 tokens.css:63-78。
 * ============================================================ */

[data-theme="dark"] .discussion-main {
  background: var(--canvas);
}

[data-theme="dark"] .faculty-detail-main {
  background: var(--canvas);
}

[data-theme="dark"] .discussion-map-card {
  background: var(--surface);
}

[data-theme="dark"] .discussion-poll-card {
  background: var(--surface);
}

[data-theme="dark"] .profile-verify-card {
  background: var(--surface);
}

/*
 * quick task (260827-x9d)：同一个 pattern 的第十条，也是"课程弹窗里颜色太多"最后
 * 剩下的那一种。.subject-review-item.is-society-review 的浅绿底 #f6fbf8 是纯色
 * 字面量，引擎确实重绘了，但忠实保留了色相 —— 实测 rgb(18, 41, 31)，一张绿卡片
 * 混在一列 rgb(19, 21, 22) 的中性卡片里。同一条规则的浅绿描边已经由
 * darkReaderBoot.js 的 CONTROL_LINE_SELECTORS 收编成统一细线，底色是最后一处。
 * 收成 var(--surface) 与兄弟卡片一致；"这是学生会评价"的身份由卡内的
 * .society-review-badge 徽章承担，不需要整张卡染色来说。
 */
[data-theme="dark"] .subject-review-item.is-society-review {
  background: var(--surface);
}

/*
 * quick task (260827-x9d)：表单输入框在暗色下要比它所在的卡片高一档, 否则不成其为
 * 输入框。.auth-form input 本来就写了 background: var(--surface-2), 但 .stack-form
 * input 的 var(--surface) 与它特异度相同 (0,1,1) 且写在后面 (app.css ~5100 vs
 * ~4410), 按顺序赢掉了 —— 实测登录/注册弹窗里每个输入框都是 rgb(19,21,22), 和卡片
 * 一模一样, 只剩一根 hairline 在示意"这里能打字"。浅色下白底白框还能靠边框看出来,
 * 深色下这点差别基本消失。
 *
 * 只在暗色作用域内抬升, 不动浅色的既有观感(那是另一回事, 要改该单独判断)。范围覆盖
 * 所有 .stack-form 表单 —— 登录/注册、发帖、评论输入框、个人资料编辑器走的是同一套
 * 类, 一起抬。
 */
[data-theme="dark"] .stack-form input,
[data-theme="dark"] .stack-form select,
[data-theme="dark"] .stack-form textarea,
[data-theme="dark"] .profile-editor input,
[data-theme="dark"] .profile-editor select,
[data-theme="dark"] .profile-editor textarea,
[data-theme="dark"] .auth-form input,
/* 评论输入框走的是 .comment-form 而不是 .stack-form, 所以要单列一条 —— 它正是
   用户截图里帖子详情底下那个"回复这条帖子"的框。 */
[data-theme="dark"] .comment-form textarea,
[data-theme="dark"] .comment-form input {
  background: var(--surface-2);
}

[data-theme="dark"] .comment-line {
  background: var(--line-strong);
}

/*
 * 社团页整页底（.clubs-main .clubs-page）是 50n 同一个 bug pattern 的第七条，不是新形状：
 * 实测该背景声明的第三层（135deg 底填充）引擎完全不重绘，压在已被重绘成深色的
 * backgroundColor 之上导致整页发浅。当初没进 50n 的枚举清单，是因为选择器是双类复合
 * （.clubs-main .clubs-page）且底色停靠点被 color-mix() 包着，早先的扫描模式没匹配到。
 * 取 canvas 而非 surface：本 section 在 .clubs-main 下撑满整个内容区，扮演页面画布角色；
 * 其内部的社团卡片背景是另一种引擎能处理的纯色形状，会被引擎单独重绘成面板色，
 * 页面与卡片两层因此仍能拉开对比。
 */
[data-theme="dark"] .clubs-main .clubs-page {
  background: var(--canvas);
}

/*
 * quick task (260827b)：同一个 bug pattern 的第八、九条。.campus-info-main
 * 的字面量背景是两层渐变（180deg 线性 + color-mix() 包着停靠点的径向），引擎
 * 直接跳过不重绘，整块还是亮色渐变原样透出——用户反馈"校园信息页面底色是白色"，
 * 实测 backgroundImage 里还是原始的 rgba(245, 247, 251, ...) 字面量，没有被重绘。
 * .game-squad-main / .game-squad-page 是另一种形状：字面量是单层纯色
 * background: #f7f8f5（没有 var(--canvas) 迁移过的遗留页面，SquadPool 原生化时
 * 新增，没赶上早前的深色迁移），引擎确实重绘了，但把这个偏暖白色转成了偏黄绿的
 * 深色（实测 rgb(25, 27, 19)），和页面其余部分的中性深灰不搭——用户反馈"游戏
 * 组队是黄色"。两者的根因不同（一个是引擎不认渐变，一个是引擎认字面量但转色结果
 * 偏色），但修法一致：换成 var(--canvas)，让引擎重新分析这个更简单的声明。
 */
[data-theme="dark"] .campus-info-main {
  background: var(--canvas);
}

[data-theme="dark"] .game-squad-main,
[data-theme="dark"] .game-squad-page {
  background: var(--canvas);
}

[data-theme="dark"] .feed-risk-row .ch-score,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-1,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-2,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-3,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-4,
[data-theme="dark"] .feed-risk-row .ch-score.is-score-5 {
  border-color: #667085 !important;
  background: #303846 !important;
  color: #f5f7fb !important;
}

[data-theme="black"] .feed-risk-row .ch-score,
[data-theme="black"] .feed-risk-row .ch-score.is-score-1,
[data-theme="black"] .feed-risk-row .ch-score.is-score-2,
[data-theme="black"] .feed-risk-row .ch-score.is-score-3,
[data-theme="black"] .feed-risk-row .ch-score.is-score-4,
[data-theme="black"] .feed-risk-row .ch-score.is-score-5 {
  border-color: #596273 !important;
  background: #252b35 !important;
  color: #f5f7fb !important;
}

[data-theme="dark"] .feed-risk-spotlight,
[data-theme="dark"] .feed-risk-board,
[data-theme="dark"] .feed-risk-row,
[data-theme="black"] .feed-risk-spotlight,
[data-theme="black"] .feed-risk-board,
[data-theme="black"] .feed-risk-row {
  border-color: color-mix(in srgb, var(--text) 20%, transparent) !important;
}


/* 顶栏居中: 宽屏改三列网格 —— 空白左栏 | 搜索框 | 右侧控件。
   为什么需要: 基线那套靠 margin-inline:auto 在**剩余空间**里居中, 于是右侧那块
   一变宽搜索框就跟着横移。会话水合正好会变它: 水合前是一颗占位按钮,
   水合后已登录的人换成铃铛 + 头像。线上实测(1700px, 已登录):
     右侧 223px -> 266px, 搜索框 x 468 -> 446 —— 就是那一下"闪一下然后跳过去"。
   网格把搜索框钉在中间那一列, 两侧各自一份 1fr, 右边多宽都不影响它。

   为什么只在 1360px 以上: minmax(0,1fr) 的侧列在空间不够时会被压到 0,
   而右列里那堆控件不会跟着缩 —— 它带着 justify-self:end 从一条 7px 的轨道里
   向**左**溢出, 直接盖在搜索框上。本地逐档实测(直接把网格当基线时):
     760 / 900 / 1024 / 1100px 均重叠(最多 -111px), 1280px 以上才不重叠。
   取 1360 而不是 1280, 是因为需要修的两段位移恰好都在 1360 以上(1360–1599
   游客态登录按钮回到顶栏, ≥1600 两种状态都在顶栏), 窄的那几档本来就不会抱
   位移(账号簇在侧栏), 没必要去碰那条溢出线。 */
@media (min-width: 1360px) {
  .topbar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 720px) minmax(0, 1fr);
    justify-content: stretch;
  }

  .topbar .search-shell {
    grid-column: 2;
    width: 100%;
    margin-inline: 0;
    justify-self: center;
  }

  .topbar .header-actions {
    grid-column: 3;
    justify-self: end;
  }
}

/* ── 十大最差课程评选（活动页 + 首页入口卡） ───────────────────────────────
   配色和字号刻意贴着避雷榜那套走：两处是同一份榜、同一批数据，长得像是对的。
   容器一律用 --surface / --text 这类 token，不写死底色，暗色下自动跟随。 */
/* 首页信息流里的活动横幅。就是活动页 hero 的缩小版 —— 同一套标识、同一句宋体标题,
   点进去看到的东西和横幅上写的对得上。 */
.wt-entry {
  display: grid;
  gap: 10px;
  width: 100%;
  margin-bottom: 12px;
  padding: 16px 18px;
  border: 1px solid var(--soft, #d9e0ec);
  border-radius: var(--radius-md);
  /* 一层极淡的本校底色, 把它和信息流里那些白卡分开; 八所各是各的颜色。 */
  background: linear-gradient(135deg, color-mix(in srgb, var(--brand-bright, var(--brand)) 7%, var(--surface)) 0%, var(--surface) 62%);
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.wt-entry:hover,
.wt-entry:focus-visible {
  border-color: var(--brand-bright, var(--brand));
  outline: 0;
}

/* 墙的标识与「正在进行」只给手机。宽屏上信息流旁边就立着侧栏的同一颗标识,
   横幅里再摆一遍是重复; 手机没侧栏, 那一行才有用。 */
.wt-entry-brand {
  display: none;
  align-items: center;
  gap: 9px;
}

@media (max-width: 720px) {
  .wt-entry-brand {
    display: flex;
  }
}

.wt-entry-brand .brand-mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 9px;
  background: var(--brand, #29328f);
  color: #ffffff;
  font-size: 16px;
  font-style: normal;
  font-weight: 900;
  line-height: 1;
}

.wt-entry-brandtext {
  display: grid;
  gap: 1px;
  margin-right: auto;
}

.wt-entry-brandtext strong {
  font-size: 14px;
  font-weight: 950;
  line-height: 1.1;
}

.wt-entry-brandtext em {
  color: var(--muted);
  font-size: 10px;
  font-style: normal;
  font-weight: 800;
  line-height: 1.1;
}

.wt-entry-tag {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 999px;
  background: #ff4d5a;
  color: #ffffff;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.04em;
}

/* 标题用和活动页同一支宋体栈 —— 中文标题用宋体系比黑体更有"榜单"的分量。 */
.wt-entry-title {
  font-family: "Songti SC", "Source Han Serif SC", "Noto Serif SC", "SimSun", Georgia, serif;
  font-size: 30px;
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.wt-entry-title em {
  font-style: normal;
  /* 和 hero 同理: --brand 在多数学校是深色调, 压在标题里跳不出来。 */
  color: var(--brand-bright, var(--brand));
}

/* 标题和「查看榜单」同一行: 那句副文案删了之后, 再把入口单独撑一行
   只是把横幅拉高。baseline 对齐 —— 两边字号差很多, 对中会看起来没对齐。 */
.wt-entry-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.wt-entry-peek {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--brand-bright, var(--brand));
  font-size: 12px;
  font-weight: 900;
}

.wt-entry-peek i {
  font-style: normal;
}

@media (max-width: 720px) {
  .wt-entry {
    padding: 14px 15px;
  }

  .wt-entry-title {
    font-size: 25px;
  }
}

/* 页面本体 */
.wt-page {
  display: grid;
  gap: 14px;
  max-width: 860px;
  margin: 0 auto;
}

.wt-hero {
  /* display:grid 必须写在这里。它原本是从 .ch-panel 拿的, 而 hero 开了那个类;
     把 hero 收进合并卡、去掉 .ch-panel 之后它退回了 block —— 两栏就没了,
     两颗按钮掉到标题下面各占一整行。 */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  align-content: center;
  gap: 20px;
  /* hero 现在是合并卡的第一段, 不再自己包一张卡 —— 标题与下方色块之间
     那道缝就是两张卡之间的 gap。只保留内距和一条分隔线。 */
  padding: 18px;
  border-bottom: 1px solid var(--line);
}

/* 墙的标识。形状跟左上角侧栏那颗一致(圆角方块, 不是圆), 尺寸大一号;
   整块可点, 回本校首页。 */
/* 不再包描边 —— 它旁边就是一整张带描边的 hero 卡, 再套一圈只是多一道框。 */
.wt-hero-tag {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  text-align: left;
}

.wt-hero-brand {
  display: grid;
  gap: 1px;
}

.wt-hero-brand strong {
  font-size: 17px;
  font-weight: 950;
  line-height: 1.1;
}

.wt-hero-brand em {
  color: var(--muted);
  font-size: 11px;
  font-style: normal;
  font-weight: 800;
  line-height: 1.1;
}

.wt-hero-tag:hover .wt-hero-brand strong {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.wt-hero-tag .brand-mark {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: var(--brand, #29328f);
  color: #ffffff;
  font-size: 18px;
  font-style: normal;
  font-weight: 900;
  line-height: 1;
}

/* 拉新页的标题就是封面。字体栈把衡线体排在前面 —— 中文标题用宋体系
   (Songti / 思源宋体)比黑体更有"榜单"的分量, 英文回落到 Georgia。
   字号从 26 提到 40, 并把字间距收紧一点。 */
/* 「最拉」用本校的代表色, 不再写死一支红 —— 八所各是各的颜色。
   暗色下改读 --brand-bright: --brand 在部分学校是深色(墨大就是海军蓝), 压在深底上读不出来。 */
.wt-hero h1 em {
  font-style: normal;
  /* --brand-bright 而不是 --brand: 后者在多数学校是深色调(墨大 rgb(0,11,52)),
     压在白底标题里与正文色只差 1.23 —— 根本看不出是重点。
     --brand-bright 实测 rgb(10,77,179): 对白底 7.71, 与标题色差 2.02, 一眼就跳出来。 */
  color: var(--brand-bright, var(--brand));
}

[data-theme="dark"] .wt-hero h1 em,
[data-theme="black"] .wt-hero h1 em {
  color: var(--brand-bright, var(--brand));
}

.wt-hero h1 {
  margin: 0;
  font-family: "Songti SC", "Source Han Serif SC", "Noto Serif SC", "SimSun", Georgia, serif;
  font-size: 40px;
  line-height: 1.1;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.wt-hero-lede {
  margin: 0;
  max-width: 62ch;
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.6;
}

.wt-hero-copy {
  display: grid;
  justify-items: start;
  gap: 8px;
}

/* 两颗按钮同宽同高: 它们是并列的两个去处, 宽度不一致只会让人以为它们不同级。 */
.wt-hero-actions {
  display: grid;
  justify-items: stretch;
  gap: 6px;
  text-align: center;
}

.wt-cta,
.wt-ghost {
  min-height: 38px;
  padding: 0 16px;
  border-radius: var(--radius-ui);
  font-family: inherit;
  font-size: 13px;
  font-weight: 900;
  cursor: pointer;
}

/* 跟站内其他主按钮同一对令牌。原来写死的 #29328f 是墨大的海军蓝 ——
   到了另七所就是一颗不属于这个站的蓝按钮。--button-ink 不能用白字代替:
   新南的主色是黄的, 白字压上去读不出来。 */
.wt-cta {
  border: 1px solid var(--button-bg, var(--brand));
  background: var(--button-bg, var(--brand));
  color: var(--button-ink, #ffffff);
}

.wt-ghost {
  border: 1px solid var(--soft, #ccd5e6);
  background: transparent;
  color: var(--text);
}

/* 学院色块。这是活动页上唯一的筛选控件, 所以给到一整行而不是塞进侧栏;
   颜色用各学院自己的主调, 与课号胶囊、学院主页同一套。 */
/* 色块现在是合并块的顶栏。手机上它会被压成一条横向滚动的带,
   刚好填进收起条那 335px 的空白里(见窄屏那块)。 */
.wt-faculties {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 12px 12px 10px;
  border-bottom: 1px solid var(--line);
}

.wt-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 13px;
  border: 1px solid color-mix(in srgb, var(--wt-fc, #29328f) 55%, transparent);
  border-radius: 999px;
  /* 里面填一层本学院色的淡底: 只有一圈细环时这排胶囊看着是空的, 不成块。 */
  background: color-mix(in srgb, var(--wt-fc, #29328f) 12%, transparent);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  font-weight: 900;
  cursor: pointer;
}

/* 学院的颜色用一颗实心圆点来说, 而不是拿它去染背景。
   背景染色要么淡到看不出颜色(8% 实测就是这样), 要么浓到把文字压在
   不同底色上 —— 而十个学院十种底色, 对比度无法逐个保证。圆点把"颜色"和
   "可读性"分开: 颜色全部交给圆点和描边, 文字始终是 --text。 */
.wt-chip::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--wt-fc, #29328f);
}

.wt-chip.is-active::before {
  background: #ffffff;
}

.wt-chip:hover {
  border-color: var(--wt-fc, #29328f);
}

.wt-chip.is-active {
  border-color: var(--wt-fc, #29328f);
  background: var(--wt-fc, #29328f);
  color: #ffffff;
}

/* 冠军框。里面那块是 renderSubjectScoreSummary() —— 和课程弹窗里一模一样,
   包括三条可直接点击打分的格子。拉新的人落在这一页, 不用点开任何东西
   就能投出第一票。 */
/* 分数框 + 榜单合成一张卡。分开时它们是两个相邻的框, 读起来像两件不相干的
   东西; 合在一起才看得出"上面那一位就是下面第一行"。 */
.wt-block {
  position: relative;
  z-index: 2;
  gap: 0;
  padding: 0;
}

.wt-block .review-meter-summary {
  border-bottom: 1px solid var(--line);
}

/* 推荐度方块和课名现在就是「看全部评价」的入口, 得看得出来能点。 */
.review-meter-ring.is-openable,
.review-title-names.is-openable {
  cursor: pointer;
  border-radius: 12px;
}

.review-title-names.is-openable {
  display: block;
}

.review-meter-ring.is-openable:hover,
.review-title-names.is-openable:hover h3 {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

.review-meter-ring.is-openable:hover {
  text-decoration: none;
  opacity: 0.88;
}

/* 最近的学习资料贡献 */
.wt-files {
  gap: 10px;
}

.wt-files-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
}

.wt-files-head strong {
  font-size: 14px;
  font-weight: 950;
}

.wt-files-head span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 800;
}

.wt-file-list {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.wt-file {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 6px;
  border: 0;
  border-radius: var(--radius-ui);
  background: transparent;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.wt-file:hover {
  background: color-mix(in srgb, var(--text) 5%, transparent);
}

.wt-file-code {
  padding: 2px 8px;
  border: 1px solid var(--soft, #d9e0ec);
  border-radius: 999px;
  color: var(--muted);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.05em;
}

.wt-file-text {
  min-width: 0;
  display: grid;
  gap: 1px;
}

.wt-file-text strong {
  overflow: hidden;
  font-size: 13px;
  font-weight: 850;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.wt-file-text em {
  overflow: hidden;
  color: var(--muted);
  font-size: 11px;
  font-style: normal;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.wt-board {
  display: grid;
  gap: 8px;
  padding: 14px;
  border: 1px solid var(--soft, #d9e0ec);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.wt-board-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.wt-board-head strong {
  font-size: 14px;
  font-weight: 950;
}

.wt-board-head span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 800;
}

/* 榜单行里的课程代码: 默认黑白, 展开后自上而下依次上色, 收起再一起褪回去。
   
   用 filter 去饱和而不是改颜色值 —— 那颗胶囊的学院色(review-subject-code 的
   --code-accent / --code-accent-dark)一个字节都没动, 所以收起时能原样退回本色,
   暗色下那套 --faculty-text 的修复也不受影响。
   
   作用域死钉在 .wt-list 里: review-subject-code 这个类在课程弹窗和榜首那张大卡上
   也在用, 那两处必须保持学院本色, 不能被这条规则扫到。 */
.wt-list .review-subject-code {
  filter: grayscale(1);
  transition: filter 0.4s ease;
  /* 收起时不排队, 十行一起褪掉 —— 倒着再演一遍 0.7 秒只会显得拖沓。 */
  transition-delay: 0ms;
}

/* --wt-i 是行号, 由 renderRow 内联写在 .wt-row 上, 这里靠继承拿到。
   用它算延迟而不是写十条 nth-child: 榜单长度以后要是变了, 这条规则不用跟着改。 */
.wt-list.is-open .review-subject-code {
  filter: grayscale(0);
  transition-delay: calc(var(--wt-i, 0) * 70ms);
}

@media (prefers-reduced-motion: reduce) {
  .wt-list .review-subject-code,
  .wt-list.is-open .review-subject-code {
    transition: none;
    transition-delay: 0ms;
  }
}

.wt-list {
  display: grid;
}

/* 和避雷榜同构的四列：名次 | 课程 | 证据 | 分数。证据列 nowrap 且不设 max-width，
   「几条评价撑着」永远不会被课名挤掉 —— 那是这张榜唯一的可信度依据。 */
.wt-row {
  display: grid;
  gap: 6px;
  width: 100%;
  padding: 12px 10px;
  border: 0;
  /* --line 两头都有值(浅色跟调色板, 暗色 #3d3d3d); 原来用的 --soft 在暗色下是
     rgba(255,255,255,.3), 压在面板底上又白又脉, 行与行之间像拉了一排亮线。 */
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.wt-row:last-child {
  border-bottom: 0;
}

.wt-row:hover,
.wt-row:focus-visible {
  background: color-mix(in srgb, var(--wt-fc, #29328f) 6%, transparent);
  outline: 0;
}

/* 除了评语以外的部分是一个整体: 名次 | 课程 | 评价数 | 分数。
   评语现在排在这个模块下面、占整行 —— 不再被夹在中间那一列里受宽度拘束。 */
.wt-rowtop {
  display: grid;
  /* 四列变三列: 评价数不再单占一列。第三列装的是分数 + 它下面那句评语,
     所以封个顶: fit-content 让短评语只占自己那点宽, 长的也不会越过半行。 */
  grid-template-columns: 30px minmax(0, 1fr) fit-content(46%);
  align-items: center;
  gap: 10px;
}

.wt-rank {
  color: var(--muted);
  font-size: 17px;
  font-style: italic;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* 三行堆叠: 课号胶囊 / 课名 / 几条评价·学院。
   课名单占一行是关键 —— 中文课名动辄十几字, 和课号挤一行只能省略号。 */
/* justify-items 不能用 start: 那会让课名/meta 按自身内容宽度收缩, 实测在 375px
   下比它们实际需要的宽度还短十几像素, 于是十行课名全部被省略号截掉 ——
   可那一列明明还剩一百多像素。改成擑满整列, 只把胶囊单独收缩。 */
.wt-main {
  min-width: 0;
  display: grid;
  gap: 3px;
}

/* 中英文课名的容器。桌面上上下两行; 手机上改成同一行(见窄屏那块)。
   之前只靠 flex-wrap 实现不了: 放不下时弹性换行会把英文整个挤到下一行,
   而不是把它缩成省略号 —— 得有个容器才能 nowrap。 */
.wt-titleline {
  min-width: 0;
  display: grid;
  gap: 2px;
}

.wt-toprow {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* 前三名标红 —— 榜单的通例, 一眼分得出前三和其余。 */
.wt-rank.is-top3 {
  color: #d92b3a;
}

[data-theme="dark"] .wt-rank.is-top3,
[data-theme="black"] .wt-rank.is-top3 {
  color: #ff7a85;
}

/* 评分块。不再是带底色的方块: 大号彩色数字 + 下面一行灰字写多少人打分。
   颜色取本校主题色。 */
.wt-score {
  display: grid;
  justify-items: end;
  gap: 1px;
  text-align: right;
  white-space: nowrap;
}

.wt-score b {
  /* 分值颜色由 worstTen.js 逐行内联写死(scoreTextColor)。没有分数的行("—")不带内联
     color, 回落到本校主题色。

     色相取自评分界面那五档底色(.is-score-1…5), 同一门课在两处是同一种颜色; 明度
     另算, 因为那五个值是给大色块当底的粉彩, 当字放白底上一个都读不出来(#ffeb6a
     只有 1.3:1)。明暗跨度是刻意拉大的(8.5:1 → 3.4:1): 细笔画上的颜色比大色块难
     判断, 只靠色相分不开, 明暗才是能读出来的那条通道。 */
  color: var(--brand-bright, var(--brand));
  font-size: 26px;
  font-weight: 950;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* 评语跟着数字右对齐, 宽度由所在列封顶(见 .wt-rowtop 的 fit-content)
   —— 否则一句长评价会把课名那一列挤成一条线。 */
.wt-score .wt-snippet {
  justify-self: end;
  max-width: 100%;
  text-align: right;
}




/* 榜单里的课号胶囊直接用分数框那颗(.review-subject-code), 不再自己画一份 ——
   同一个东西在一页上两种长相是没道理的。只把它为大标题调的那几项排版收一收:
   它带着 margin-bottom:8px 和 -6px 的左负边距(为了和下方标题对齐), 在行里不适用。 */
.wt-toprow .review-subject-code {
  margin: 0;
  padding: 1px 6px;
  border-radius: 5px;
  font-size: 9px;
  letter-spacing: 0.03em;
  /* 比基础那一条鲜艳: 基础是 45% 学院色混深蓝(为大号标题调的, 稳但发灋),
     榜单里这颗只有 9px, 需要更接近学院主色才认得出。 */
  background: color-mix(in srgb, var(--code-accent, var(--brand)) 22%, #ffffff);
  color: color-mix(in srgb, var(--code-accent, var(--brand)) 88%, #1c2740);
}

.wt-name {
  max-width: 100%;
  overflow: hidden;
  font-size: 18px;
  font-weight: 950;
  line-height: 1.25;
  letter-spacing: -0.01em;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.wt-meta {
  max-width: 100%;
  overflow: hidden;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 24 小时涨跌。没有新评价就画一条短横线, 不写 0.0% —— 那两件事不一样。 */
.wt-delta {
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.wt-delta i {
  font-size: 9px;
  font-style: normal;
}

.wt-delta.is-up {
  color: #12a150;
}

.wt-delta.is-down {
  color: #e0524a;
}



.wt-empty,
.wt-foot {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  line-height: 1.6;
}

.wt-foot {
  padding: 0 4px 8px;
  font-size: 11px;
}

@media (max-width: 720px) {
  .wt-hero {
    padding: 16px;
  }

  .wt-hero h1 {
    font-size: 28px;
  }

  .wt-rules {
    padding: 14px 16px 14px 32px;
  }

  .wt-cta,
  .wt-ghost {
    flex: 1 1 auto;
  }

  /* 手机上也保持“左文案 / 右两颗按钮”。之前堆成一列是因为按钮文案太长
     (「写评价 · 投一票」「查看所有课程」)把右栏挤到 111px; 文案缩成
     「写评价」「所有课程」之后两栏放得下。 */
  .wt-hero {
    gap: 12px;
    padding: 14px;
  }

  .wt-hero-actions {
    gap: 6px;
  }

  /* 手机: 学院色块不再折三行(实测 375px 下占 77px), 压成一条横向滚动的带,
     与右上角那颗折叠按钮共用同一条 —— 那条本来只有按钮, 剩下 335px 全是空的。 */
  /* 换回换行: 横向滚动会把后面几个学院藏到条外, 不滑一下就不知道它们存在。 */
  .wt-faculties {
    flex-wrap: wrap;
    gap: 5px;
    padding: 10px;
  }

  .wt-chip {
    min-height: 26px;
    gap: 5px;
    padding: 0 9px;
    font-size: 11px;
  }

  .wt-chip::before {
    width: 6px;
    height: 6px;
  }

  .wt-faculties {
    gap: 5px;
  }

  .wt-cta,
  .wt-ghost {
    min-height: 34px;
    padding: 0 12px;
    font-size: 12px;
    white-space: nowrap;
  }

  /* 窄屏不再给涨跌列固定宽度, 交给内容自己擑 */
  .wt-rowtop {
    /* 第三列装的是分数 + 它下面那句评语, 所以封个顶: fit-content 让短评语只占
       自己那点宽, 长的也不会越过半行。 */
    grid-template-columns: 24px minmax(0, 1fr) fit-content(50%);
    gap: 8px;
  }


  .wt-name {
    font-size: 16px;
  }
}


/* ── 第一名那块的手机端展开/收起 ─────────────────────────────────────────
   桌面端完全不参与: 下面所有规则都关在 max-width:720px 里, .wt-champion-wrap 在宽屏
   只是一个透明的包裹层。 */
.wt-champion-wrap {
  position: relative;
}

/* 折叠开关坐在 hero 标题下面左下角, 是一行小字而不是一颗按钮 ——
   它不是这一屏的主行动(写评价/所有课程才是), 长成按钮会和那两颗抢。
   它同时收学院色块和第一名那块 —— 所以叫「筛选」而不是「展开」:
   展开它的理由就是要换学院看。 */
.wt-filter {
  display: none;
}

@media (max-width: 720px) {
  .wt-filter {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin: -2px 0 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--muted);
    font-family: inherit;
    font-size: 12px;
    font-weight: 800;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-color: var(--soft, #ccd5e6);
    cursor: pointer;
  }

  .wt-filter i {
    font-style: normal;
    font-size: 9px;
    transition: transform 200ms ease;
  }

  .wt-filter[aria-expanded="true"] i {
    transform: rotate(180deg);
  }

  /* 学院色块跟第一名那块一起收 —— 同一套 0fr/1fr, 同一段时长。
     内距和底线同样要收: overflow:hidden 裁的是 padding box。 */
  .wt-faculties-wrap {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows 260ms ease;
  }

  .wt-faculties-wrap:not(.is-open) {
    grid-template-rows: 0fr;
  }

  .wt-faculties {
    min-height: 0;
    overflow: hidden;
    transition: padding 260ms ease, border-width 260ms ease;
  }

  .wt-faculties-wrap:not(.is-open) .wt-faculties {
    padding-top: 0;
    padding-bottom: 0;
    border-bottom-width: 0;
  }

  @media (prefers-reduced-motion: reduce) {
    .wt-faculties-wrap,
    .wt-faculties {
      transition: none;
    }
  }

  /* 收起就是整块收起: 推荐度方块、课名、打分格子全部一起收。
     只留一条带按钮的窄条 —— 否则收起之后没有任何可点的东西, 展开不回来。
     用 display:none 而不是 max-height 折叠: 高度是内容决定的, 写死一个数迟早会在某个
     课名长度上截半行。 */
  /* 展开/收起的动画。display:none 是动不了的, 改用 grid-template-rows 0fr -> 1fr:
     不需要知道内容多高(写死的 max-height 总会在某个课名长度上截半行),
     浏览器自己插值。子元素必须 min-height:0 + overflow:hidden, 否则缩不下去。 */
  .wt-champion-wrap {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows 260ms ease;
  }

  .wt-champion-wrap:not(.is-open) {
    grid-template-rows: 0fr;
  }

  /* 内距和那条底线也得一起收。
     只把行高掉到 0fr 是不够的: overflow:hidden 裁的是 padding box, 所以
     18+18+1px 的内距与底线依然占高, 而内容由 align-items:center 向两边溢出,
     正好落在那一层内距里 —— 屏上就是收起后还漏出一条带课号的窄条。 */
  .wt-champion-wrap .review-meter-summary {
    min-height: 0;
    overflow: hidden;
    transition: padding 260ms ease, border-width 260ms ease;
  }

  .wt-champion-wrap:not(.is-open) .review-meter-summary {
    padding-top: 0;
    padding-bottom: 0;
    border-bottom-width: 0;
  }

  @media (prefers-reduced-motion: reduce) {
    .wt-champion-wrap,
    .wt-champion-wrap .review-meter-summary {
      transition: none;
    }
  }


  /* 课号 / 中文 / 英文 三行, 和桌面一致(上一版曾把中英文强制同行, 改回来)。 */
  .wt-snippet {
    margin: 0;
  }


}

/* 第一名的课名在这一页是主视觉, 不该跟着课程弹窗那套走。
   ≤975px 那条覆盖(为弹窗调的)把它压到 19px, 而桌面是 28px —— 手机上就是用户说的
   "字太小"。这里用同一个断点(而不是 720)盖回去, 否则 721–975 那一段(iPad 竖屏 820)
   会夹在中间继续用 19px。 */
@media (max-width: 975px) {
  .wt-block .review-meter-summary h3 {
    font-size: 23px;
    line-height: 1.18;
  }

  .wt-block .review-meter-summary p {
    font-size: 14px;
  }
}


/* 榜单每行那句评语。像虎扑评分榜那样，把「这门课到底怎么样」用一句人话摆在名字下面
   —— 分数说程度，这句说是什么感受。选谁的规则见 worstTen.js 的 pickSnippet()。 */
/* 评语自成一件: 它在 .wt-rowtop 之外单独一层, 不与课号/中文/英文那三行争宽度,
   左边直接顶到行首。底中性, 字取本校主题色 —— 学院色在这一行里试过,
   十行十种淡底会把这一列变花; 学院身份由课号胶囊一处说就够了。 */
.wt-snippet {
  /* 宽度跟着字走, 不拉满整行 —— 字已经到「…」了, 后面再拖一条空底只是
     在说"这里本该还有字"。justify-self 是必要的: .wt-row 是 grid, 默认 stretch
     会把 inline-block 也拉到满列宽。 */
  display: inline-block;
  justify-self: start;
  max-width: 100%;
  overflow: hidden;
  margin: 0;
  padding: 6px 10px;
  border-radius: 6px;
  background: color-mix(in srgb, var(--text) 5%, transparent);
  color: var(--text);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.5;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 管理员指定的那一句略重一点 —— 颜色不再分化(整列都是主题色), 拿字重区分。 */
.wt-snippet.is-featured {
  font-weight: 800;
}

[data-theme="dark"] .wt-snippet,
[data-theme="black"] .wt-snippet {
  background: color-mix(in srgb, var(--text) 10%, transparent);
}


/* 触屏设备上的可输入控件一律 16px。
   iOS Safari 在聚焦一个字号小于 16px 的输入框时会自己把整页放大(而且失焦后不缩回去)。
   这个行为关不掉: 只能靠 viewport 的 maximum-scale=1 压住, 而那会连用户自己的
   双指缩放一起禁掉 —— 对看不清小字的人是实打实的伤害。把字号顶到 16px 是唯一
   没有副作用的做法。
   用 (hover:none) and (pointer:coarse) 而不是宽度断点: 桌面浏览器缩到很窄也不会被注入,
   而平板(768以上)同样会放大, 宽度断点盖不到。
   必须 !important: 全站几十处组件各自写了 13–14px 的字号, 类选择器都比这条高。 */
@media (hover: none) and (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]),
  select,
  textarea {
    font-size: 16px !important;
  }

  /* 邮箱域名那颗下拉是固定文本(@student.unimelb.edu.au, 23 个字符), 字号一提到
     16px 就被 60% 的上限截掉尾巴。放宽上限并收紧内距 —— 前面那格只填邮箱前缀,
     87px 完全够。实测 375 宽: 60% 截断, 70% 不截。 */
  .email-domain-select,
  .email-domain-fixed {
    max-width: 70%;
    padding: 0 8px;
  }
}


/* 右下角的转发任务面板(importDock.js)。一条转发一张卡, 没任务时整块不渲染。 */
/* 面板的固定宿主节点。它一直在（有没有任务都在），好让轮询只换这一小块的
   innerHTML，而不用整页 renderApp —— 整页重画会把正在输入的框连同已输入的字
   一起销毁（手机上就是"打字时每 7 秒输入框被清空"）。display: contents 让空
   宿主完全不产生盒子，.import-dock 自己是 fixed，布局和以前逐像素一致。 */
.import-dock-host {
  display: contents;
}

.import-dock {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 60;
  display: grid;
  gap: 8px;
  width: min(300px, calc(100vw - 36px));
  pointer-events: none;
}

.import-dock-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--soft, #d9e0ec);
  /* 左边那条是状态色 —— 不用读字也能一眼看出是在跑、成了还是砍了。 */
  border-left: 3px solid var(--muted);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 6px 20px rgba(16, 24, 40, 0.12);
  pointer-events: auto;
}

.import-dock-card.is-pending { border-left-color: #8a94a6; }
.import-dock-card.is-running { border-left-color: var(--brand-bright, var(--brand)); }
.import-dock-card.is-done    { border-left-color: #0f9b6c; }
.import-dock-card.is-failed  { border-left-color: #d92b3a; }

.import-dock-body {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.import-dock-source {
  overflow: hidden;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.import-dock-status {
  overflow: hidden;
  color: var(--text);
  font-size: 13px;
  font-weight: 800;
  line-height: 1.35;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 跑着的那两态加一下呼吸 —— 静止的"排队中"和卡死的"排队中"长得一模一样。 */
.import-dock-card.is-pending .import-dock-status,
.import-dock-card.is-running .import-dock-status {
  animation: import-dock-pulse 1.6s ease-in-out infinite;
}

@keyframes import-dock-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  .import-dock-card.is-pending .import-dock-status,
  .import-dock-card.is-running .import-dock-status {
    animation: none;
  }
}

.import-dock-open {
  padding: 4px 10px;
  border: 0;
  border-radius: var(--radius-ui);
  background: var(--button-bg, var(--brand));
  color: var(--button-ink, #ffffff);
  font-family: inherit;
  font-size: 12px;
  font-weight: 900;
  cursor: pointer;
}

.import-dock-close {
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-family: inherit;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

/* 手机上底部导航占着一条, 面板得往上让 —— 否则卡片就压在导航后面了。 */
@media (max-width: 720px) {
  .import-dock {
    right: 12px;
    left: 12px;
    bottom: calc(64px + env(safe-area-inset-bottom, 0px));
    width: auto;
  }
}

/* ===== skill wall (技能墙) ===== */
/* 版式/类结构值来自 .planning/sketches/003-skill-wall/index.html，但颜色一律换绑
   站内已有 token（--brand / --surface / --line / --text / ... 见 tokens.css），
   所以 light / dark / black 三套主题自动跟随，不需要在这里各写一份。
   页头是下方新的分裂式布局，旧的双 pill 标签行已经不用了。`.primary-button` /
   `.ghost-button` 仍复用 app.css 已有的定义（约 1256-1600 行区）——这里只写
   .skill-wall-* 前缀的后代覆盖，不重定义它们。 */

.skill-wall-page {
  /* 唯一的强调色: 沿用社团页 --club-yellow 的同一个值（局部作用域 token，不是
     全局的）。只用在两处 —— 筛选栏的「只看开放组队的」chip 和卡片右上角的
     「开放组队」角标。 */
  --skill-marker: #f9f5a2;
  --skill-marker-ink: #2a2600;
  /* 站内没有 --radius-card 这个 token；卡片圆角固定 14px（设计稿的
     --radius-card），下面直接写字面量。 */
}
[data-theme="dark"] .skill-wall-page {
  --skill-marker: #cdbf2a;
  --skill-marker-ink: #1a1600;
}
[data-theme="black"] .skill-wall-page {
  --skill-marker: #b3a622;
  --skill-marker-ink: #141200;
}

/* 分裂式页头：两个面板始终在 DOM 里，只靠 .is-active 切换 —— skillWall.js 的
   paint() 不许重设 shell 的 innerHTML（会丢焦点、丢正在填的表单），这同时也是
   宽度过渡能跑起来的前提：innerHTML 替换的话新节点一上来就是目标宽度，过渡
   等于没有。圆角 14px 是字面量，站内没有 --radius-card 这个 token，与块内
   其余卡片同值。 */
.skill-wall-hero {
  display: flex;
  align-items: stretch;
  /* Inline-size container: lets .skill-wall-pane-body lock itself to the
     ACTIVE width (100cqw − strip) so text never re-wraps while a pane is
     mid-transition — the pane just clips it. That, not the width change
     itself, was the visible jank. */
  container-type: inline-size;
  margin-bottom: 16px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  overflow: hidden;
}
.skill-wall-pane {
  position: relative;
  flex: 0 1 148px;
  min-width: 148px;
  /* No min-height: the ACTIVE pane's content (eyebrow / giant word / foot) is
     what sets the hero's height, so the card hugs the type instead of floating
     it in empty space. */
  min-height: 0;
  background: var(--brand-weak);
  overflow: hidden;
  contain: paint;
  transition: flex-grow .42s cubic-bezier(.22, .9, .3, 1), flex-basis .42s cubic-bezier(.22, .9, .3, 1), background .3s ease;
}
.skill-wall-pane + .skill-wall-pane {
  border-left: 1px solid var(--line);
}
.skill-wall-pane.is-active {
  flex-grow: 1;
  background: var(--surface);
}
.skill-wall-pane:not(.is-active):hover {
  flex-basis: 168px;
}

.skill-wall-pane-hit {
  position: absolute;
  inset: 0;
  /* Above the collapsed body: that body is absolute too and later in the DOM,
     so without this the giant letter paints over the label pill. */
  z-index: 2;
  width: 100%;
  padding: 0;
  border: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-top: 0;
  background: transparent;
  color: var(--muted);
  font-family: inherit;
  cursor: pointer;
}
/* Slider feel, no fades: the label pill is parked 43px from the pane's OUTER
   edge (the edge that does not move), so while the pane slides shut the pill
   stays put and the giant word simply slides under it. 43 + 62 + 43 = 148,
   i.e. centred once the strip is at rest. */
.skill-wall-pane[data-pane="people"] .skill-wall-pane-hit {
  justify-content: flex-start;
  padding-left: 43px;
}
.skill-wall-pane[data-pane="projects"] .skill-wall-pane-hit {
  justify-content: flex-end;
  padding-right: 43px;
}
.skill-wall-pane.is-active .skill-wall-pane-hit {
  display: none;
}
/* No separate "peek" element any more: the collapsed strip shows the body's
   own giant word, clipped by the pane — the word just slides with its pane
   like a slider, and the start of RECRUIT / SKILL WALL is what stays visible. */
.skill-wall-pane-strip {
  writing-mode: vertical-rl;
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 30px;
  font-weight: 800;
  letter-spacing: .16em;
  color: var(--ink-strong);
  /* Sits on top of the peeking letter, so it needs its own ground. */
  position: relative;
  padding: 18px 12px;
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--line);
}
.skill-wall-pane-strip .skill-wall-n {
  font-size: 16px;
  padding: 2px 9px;
}

/* 正文是三行栅格：眉题 / 巨字 / 按钮 —— 各自有位，巨字裁切只发生在自己那一行。
   visibility: hidden 不能只写 opacity: 0，否则收起态里那颗看不见的主按钮
   还能被 Tab 键聚焦。 */
.skill-wall-pane-body {
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 6px;
  height: 100%;
  width: calc(100cqw - 149px);
  padding: 12px 16px 14px;
}
/* The collapsed pane's body stays in the DOM for the fade, but it must not
   take part in layout: a 112px-wide column makes the tagline wrap into a
   dozen lines and that (invisible) column was what stretched the whole hero
   to twice the height of the giant word. Absolute = zero height contribution. */
.skill-wall-pane:not(.is-active) .skill-wall-pane-body {
  position: absolute;
  inset: 0 auto 0 0;
}
/* Collapsed: the body stays laid out at the active width and simply gets
   clipped (that is the peek), but the small furniture — eyebrow, tagline,
   button — is hidden so the strip reads as one big letterform + the pill.
   visibility (not opacity) so the hidden button cannot take focus. */
.skill-wall-pane:not(.is-active) .skill-wall-eyebrow,
.skill-wall-pane:not(.is-active) .skill-wall-pane-foot {
  visibility: hidden;
}
.skill-wall-eyebrow {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
}
.skill-wall-beta {
  margin-left: 8px;
  vertical-align: 3px;
  font-size: 10px;
  font-weight: 800;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--brand-weak);
  color: var(--brand);
}
.skill-wall-giant {
  align-self: center;
  margin: 0;
  font-family: "Arial Black", "Inter", Helvetica, Arial, sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.04em;
  line-height: .9;
  font-size: clamp(56px, 11vw, 210px);
  color: var(--ink-strong);
  white-space: nowrap;
  overflow: hidden;
}
/* Bottom row of the active pane: the one-line intro on the left (the sentence
   the old page head used to carry), the pane's primary button on the right. */
.skill-wall-pane-foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
}
.skill-wall-tagline {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
  max-width: 60ch;
}
.skill-wall-pane-act {
  flex: 0 0 auto;
  justify-self: end;
}

.skill-wall-n {
  font-size: 11px;
  font-weight: 800;
  color: var(--muted);
  background: var(--surface-muted);
  padding: 0 6px;
  border-radius: 999px;
}

.skill-wall-filters {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
.skill-wall-tagcloud {
  display: contents; /* chips flow inline with the rest of .skill-wall-filters */
}
.skill-wall-chip {
  min-height: 26px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.skill-wall-chip:hover {
  border-color: var(--line-strong);
  color: var(--text);
}
.skill-wall-chip.is-active {
  background: var(--ink-strong);
  border-color: var(--ink-strong);
  color: var(--surface);
}
.skill-wall-chip.is-marker {
  background: var(--skill-marker);
  border-color: var(--skill-marker);
  color: var(--skill-marker-ink);
}
.skill-wall-chip.is-marker.is-off {
  background: var(--surface);
  border-color: var(--line);
  color: var(--muted);
}
.skill-wall-spacer {
  flex: 1;
}
.skill-wall-search {
  min-height: 30px;
  width: 220px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
}

/* No width cap on the hero or the board: the filter row above them is full
   width, and a capped card under a full-width search box left the box floating
   alone at the far right of a wide monitor. */
.skill-wall-board {
  min-width: 0;
}

.skill-wall-empty {
  grid-column: 1 / -1;
  color: var(--muted);
  padding: 36px;
  text-align: center;
  border: 1px dashed var(--line-strong);
  border-radius: 14px;
  background: var(--surface);
}
.skill-wall-empty-error p {
  margin: 0 0 8px;
}
.skill-wall-empty-detail {
  font-size: 12.5px;
  color: var(--soft);
}

/* ---- 找人: 卡片网格 ---- */
.skill-wall-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
}
@media (max-width: 900px) {
  .skill-wall-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .skill-wall-grid { grid-template-columns: 1fr; }
}
.skill-wall-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.skill-wall-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 30px color-mix(in srgb, var(--ink-strong-bg) 16%, transparent);
}
.skill-wall-who {
  display: flex;
  gap: 10px;
  align-items: center;
  padding-right: 76px;
}
.skill-wall-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-weight: 800;
  font-size: 16px;
  color: var(--brand);
  background: var(--brand-weak);
  flex: none;
  overflow: hidden;
}
.skill-wall-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.skill-wall-name {
  font-weight: 800;
  font-size: 16px;
  color: var(--ink-strong);
}
.skill-wall-prog {
  color: var(--muted);
  font-size: 13px;
}
.skill-wall-pitch {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
}
.skill-wall-tags {
  display: flex;
  gap: 5px;
  flex-wrap: wrap;
}
.skill-wall-tag {
  font-size: 12px;
  font-weight: 700;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--surface-muted);
  color: var(--text);
}
.skill-wall-mark {
  position: absolute;
  top: 14px;
  right: 14px;
  padding: 2px 8px;
  border-radius: 3px;
  background: var(--skill-marker);
  color: var(--skill-marker-ink);
  font-size: 11px;
  font-weight: 800;
}
.skill-wall-thumb {
  display: block;
  width: 100%;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  aspect-ratio: 16 / 8;
  position: relative;
  text-align: left;
  overflow: hidden;
  cursor: pointer;
  font-family: inherit;
}
.skill-wall-thumb-site {
  position: absolute;
  left: 8px;
  bottom: 6px;
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10.5px;
  color: var(--muted);
  background: var(--surface);
  padding: 1px 6px;
  border-radius: 3px;
}
.skill-wall-links {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 11.5px;
}
.skill-wall-links a {
  color: var(--soft);
}
.skill-wall-foot {
  display: flex;
  gap: 6px;
  margin-top: auto;
}
.skill-wall-foot .ghost-button,
.skill-wall-foot a.ghost-button {
  flex: 1;
  text-align: center;
  text-decoration: none;
}

/* Mini portfolio-template previews, drawn with divs. Kept single-theme (a
   webpage thumbnail is white paper whatever the host theme) — matches the
   sketch's .mini/.mini.cv/.mini.gal/.mini.dev exactly. */
.skill-wall-mini {
  width: 100%;
  height: 100%;
  background: #fff;
  color: #1a1a1a;
  border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  overflow: hidden;
  font-size: 8px;
  line-height: 1.3;
}
.skill-wall-mini-cv {
  padding: 14px 16px;
}
.skill-wall-mini-nm {
  height: 13px;
  width: 50%;
  background: #2a2a2a;
  border-radius: 2px;
  margin-bottom: 4px;
}
.skill-wall-mini-ln {
  height: 5px;
  background: #e6e6e6;
  border-radius: 2px;
  margin: 4px 0;
}
.skill-wall-mini-sec {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 6.5px;
  letter-spacing: 0.1em;
  color: #888;
  margin-top: 8px;
}
.skill-wall-mini-gal {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  padding: 6px;
  background: #111;
}
.skill-wall-mini-gal div {
  border-radius: 2px;
  aspect-ratio: 1;
}
.skill-wall-mini-dev {
  display: grid;
  grid-template-columns: 62px 1fr;
}
.skill-wall-mini-side {
  background: #0d1117;
  padding: 10px 8px;
}
.skill-wall-mini-av {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #58a6ff;
}
.skill-wall-mini-main {
  padding: 10px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
  align-content: start;
}
.skill-wall-mini-main div {
  border: 1px solid #aeb7c4;
  background: #eef1f5;
  border-radius: 3px;
  height: 26px;
}

/* ---- 组队: 招募列表 ---- */
.skill-wall-projects {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}
.skill-wall-proj {
  display: grid;
  /* text | cover | roles — the cover column is what used to be dead white
     space between the description and the roles list. */
  grid-template-columns: minmax(0, 1fr) 200px 200px;
  gap: 18px;
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink-strong-bg) 6%, transparent);
}
.skill-wall-proj-top {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.skill-wall-proj h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
  color: var(--ink-strong);
}
.skill-wall-stage {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--ink-strong);
  color: var(--surface);
  font-size: 11px;
  font-weight: 800;
}
.skill-wall-stage.is-recruiting {
  background: #1c7f5f;
  color: #fff;
}
.skill-wall-stage.is-idea {
  background: var(--brand-weak);
  color: var(--brand);
}
.skill-wall-stage.is-active {
  background: var(--brand);
  color: var(--surface);
}
.skill-wall-stage.is-closed {
  background: var(--surface-muted);
  color: var(--muted);
}
.skill-wall-meta {
  margin-top: 6px;
  color: var(--muted);
  font-size: 13px;
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.skill-wall-meta b {
  color: var(--brand);
  font-weight: 700;
}
.skill-wall-desc {
  margin: 8px 0 10px;
  font-size: 14px;
  line-height: 1.55;
}
/* Two cards per row only when each still gets ~700px; at that width the cover
   and roles columns slim down so the text column keeps 300px+. Below that a
   three-column card is too tight, so cards go one per row with a wider cover. */
@media (min-width: 1600px) {
  .skill-wall-projects { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .skill-wall-proj { grid-template-columns: minmax(0, 1fr) 150px 180px; }
}
.skill-wall-proj-cover {
  align-self: stretch;
  min-height: 118px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-muted);
}
.skill-wall-proj-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.skill-wall-proj-cover.is-empty {
  display: grid;
  place-items: center;
  border: 1px dashed var(--line-strong);
  color: var(--soft);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
}
.skill-wall-roles {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-left: 1px solid var(--line);
  padding-left: 16px;
}
.skill-wall-roles-h {
  font-size: 11px;
  font-weight: 800;
  color: var(--soft);
  letter-spacing: 0.04em;
}
.skill-wall-role {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 14px;
}
.skill-wall-role.is-full {
  color: var(--soft);
  text-decoration: line-through;
}
.skill-wall-role-cnt {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.skill-wall-roles .primary-button {
  margin-top: auto;
}

/* ---- 表单浮层 (创建主页 / 发招募 / 申请加入 / 作品集详情) ---- */
.skill-wall-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--ink-strong-bg) 45%, transparent);
}
.skill-wall-modal {
  width: min(760px, 100%);
  max-height: 90vh;
  overflow: auto;
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--ink-strong-bg) 25%, transparent);
}
.skill-wall-modal-head {
  position: sticky;
  top: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 20px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.skill-wall-modal-head h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 800;
  color: var(--ink-strong);
}
.skill-wall-modal-close {
  border: 0;
  background: none;
  font-size: 20px;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
}
.skill-wall-modal-body {
  padding: 20px;
}
.skill-wall-modal-loading {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: 12.5px;
}

.skill-wall-form {
  display: grid;
  gap: 14px;
}
.skill-wall-form label {
  display: grid;
  gap: 5px;
  font-size: 12.5px;
  color: var(--muted);
}
.skill-wall-form input,
.skill-wall-form textarea,
.skill-wall-form select {
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface-2);
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
}
.skill-wall-form-two {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.skill-wall-tplhead {
  margin-bottom: 6px;
  font-size: 12.5px;
  color: var(--muted);
}
.skill-wall-tplpick {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.skill-wall-tplpick button {
  display: grid;
  gap: 6px;
  padding: 8px;
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font-size: 12.5px;
  font-weight: 700;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}
.skill-wall-tplpick button.is-on {
  border-color: var(--brand);
}
.skill-wall-tplprev {
  display: block;
  aspect-ratio: 16 / 10;
  padding: 8px;
  background: var(--surface-muted);
  border-radius: 4px;
}
.skill-wall-checks {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  font-size: 13px;
}
.skill-wall-checks label {
  flex-direction: row;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
}
.skill-wall-form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.skill-wall-form-error {
  margin: 0;
  padding: 8px 10px;
  border-radius: var(--radius-ui);
  background: color-mix(in srgb, #c0392b 12%, var(--surface));
  color: #c0392b;
  font-size: 12.5px;
}
.skill-wall-form-error.is-warning {
  background: color-mix(in srgb, var(--skill-marker) 35%, var(--surface));
  color: var(--text);
}
.skill-wall-role-rows {
  display: grid;
  gap: 6px;
}
.skill-wall-role-row {
  display: grid;
  grid-template-columns: 1fr 70px auto;
  gap: 8px;
  align-items: center;
}
.skill-wall-role-remove {
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  padding: 4px 8px;
  cursor: pointer;
}

@media (max-width: 640px) {
  .skill-wall-proj-cover.is-empty {
    display: none; /* no room for a placeholder tile on a phone */
  }
  .skill-wall-proj {
    grid-template-columns: 1fr;
  }
  .skill-wall-roles {
    border-left: 0;
    padding-left: 0;
    border-top: 1px solid var(--line);
    padding-top: 12px;
  }
  .skill-wall-form-two {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 720px) {
  /* 两个面板上下堆叠，非活动的收成一根横条，标签横排。巨字的 clamp 不在这里
     覆盖 —— 面板此时是整屏宽，9vw 会撞到 48px 下限，正好。 */
  .skill-wall-hero {
    flex-direction: column;
  }
  .skill-wall-pane {
    flex-basis: 44px;
    min-height: 44px;
  }
  .skill-wall-pane.is-active {
    min-height: 160px;
  }
  .skill-wall-pane + .skill-wall-pane {
    border-left: 0;
    border-top: 1px solid var(--line);
  }
  .skill-wall-pane:not(.is-active):hover {
    /* 触屏上 hover 会粘住，别让它在这个断点下变宽 */
    flex-basis: 44px;
  }
  .skill-wall-pane-body {
    width: auto;
  }
  .skill-wall-pane:not(.is-active) .skill-wall-pane-body {
    display: none;
  }
  .skill-wall-pane-hit {
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
  }
  .skill-wall-pane-hit {
    align-items: center;
    padding-top: 0;
  }
  /* Phone: back to the original 44px bar — small horizontal label, no pill.
     The 30px vertical pill is a desktop-strip thing and has no room here. */
  .skill-wall-pane-strip {
    writing-mode: horizontal-tb;
    font-size: 13px;
    font-weight: 700;
    letter-spacing: .08em;
    gap: 8px;
    padding: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    color: var(--muted);
  }
  .skill-wall-pane-strip .skill-wall-n {
    font-size: 11px;
    padding: 0 6px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skill-wall-card {
    transition: none;
  }
  .skill-wall-pane,
  .skill-wall-pane-body {
    transition: none;
  }
}

/* ---- Task 2: 图片上传器 / 作品编辑器 / 作品集渲染 ---------------------------- */
.skill-wall-upload {
  display: grid;
  gap: 8px;
}
.skill-wall-upload-prev {
  width: 120px;
  height: 80px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  border: 1px dashed var(--line);
  overflow: hidden;
}
.skill-wall-upload-prev img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.skill-wall-upload-hint {
  margin: 0;
  font-size: 12px;
  color: var(--muted);
}
.skill-wall-works {
  display: grid;
  gap: 10px;
}
.skill-wall-work-row {
  display: grid;
  gap: 8px;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.skill-wall-work-remove {
  border: 1px solid var(--line);
  border-radius: var(--radius-ui);
  background: var(--surface);
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  padding: 4px 8px;
  cursor: pointer;
  justify-self: start;
}
.skill-wall-about {
  margin: 0;
  font-size: 14px;
  line-height: 1.7;
  color: var(--text);
}
.skill-wall-works-view {
  display: grid;
  gap: 12px;
  margin-top: 12px;
}
.skill-wall-works-view.is-cv {
  gap: 0;
}
.skill-wall-works-view.is-cv .skill-wall-work {
  padding: 12px 0;
  border-top: 1px solid var(--line);
}
.skill-wall-works-view.is-cv .skill-wall-work:first-child {
  border-top: 0;
  padding-top: 0;
}
.skill-wall-works-view.is-gal {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.skill-wall-works-view.is-gal .skill-wall-work,
.skill-wall-works-view.is-gal a.skill-wall-work {
  display: block;
  color: inherit;
  text-decoration: none;
}
.skill-wall-works-view.is-dev {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.skill-wall-works-view.is-dev .skill-wall-work {
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.skill-wall-work-title {
  font-weight: 700;
  color: var(--text);
  font-size: 13.5px;
}
.skill-wall-works-view.is-dev .skill-wall-work-title {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.skill-wall-work-desc {
  margin: 4px 0 0;
  font-size: 12.5px;
  color: var(--muted);
}
.skill-wall-work-link {
  display: inline-block;
  margin-top: 6px;
  font-size: 12px;
  color: var(--soft);
}
.skill-wall-work-img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-sm);
}
.skill-wall-work-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  aspect-ratio: 4 / 3;
  padding: 8px;
  border-radius: var(--radius-sm);
  background: var(--surface-muted);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 700;
}
.skill-wall-thumb-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (max-width: 640px) {
  .skill-wall-work-row {
    grid-template-columns: 1fr;
  }
  .skill-wall-works-view.is-gal,
  .skill-wall-works-view.is-dev {
    grid-template-columns: 1fr;
  }
}

/* ---- Task 3: 招募帖身份态 —— 发起人管理浮层 / 申请人状态与撤回 ------------- */
.skill-wall-proj-act {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: auto;
}
.skill-wall-apps {
  display: grid;
  gap: 10px;
}
.skill-wall-app {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "who acts"
    "msg msg"
    "status status";
  gap: 6px 10px;
  align-items: center;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.skill-wall-app-who {
  grid-area: who;
  display: flex;
  align-items: center;
  gap: 8px;
}
.skill-wall-app-who .skill-wall-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  overflow: hidden;
}
.skill-wall-app-who .skill-wall-avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.skill-wall-app-msg {
  grid-area: msg;
  margin: 0;
  font-size: 13px;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.skill-wall-app-acts {
  grid-area: acts;
  display: flex;
  gap: 8px;
  justify-self: end;
}
.skill-wall-status {
  grid-area: status;
  justify-self: start;
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 700;
  background: var(--surface-muted);
  color: var(--muted);
}
.skill-wall-status.is-pending {
  background: color-mix(in srgb, var(--skill-marker) 35%, var(--surface));
  color: var(--text);
}
.skill-wall-status.is-accepted {
  background: color-mix(in srgb, var(--brand) 18%, var(--surface));
  color: var(--brand);
}
.skill-wall-status.is-rejected {
  background: var(--surface-muted);
  color: var(--muted);
}
.skill-wall-pending {
  margin-left: 6px;
  font-size: 11px;
  font-weight: 700;
  color: var(--brand);
}
.skill-wall-manage-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
.skill-wall-manage-error,
.skill-wall-board-error {
  margin: 0 0 10px;
  padding: 8px 10px;
  border-radius: var(--radius-ui);
  background: color-mix(in srgb, #c0392b 12%, var(--surface));
  color: #c0392b;
  font-size: 12.5px;
}
.skill-wall-danger {
  border-color: color-mix(in srgb, #c0392b 40%, var(--line));
  color: #c0392b;
}
.skill-wall-confirm {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--text);
}

@media (max-width: 640px) {
  .skill-wall-app {
    grid-template-columns: 1fr;
    grid-template-areas:
      "who"
      "msg"
      "status"
      "acts";
  }
  .skill-wall-app-acts {
    justify-self: start;
  }
}

/* ===== skill wall end ===== */

/* ---- 发布前的社区规范墙（renderRulesGateHtml） + 注册页的同意勾选 ---------- */
/* 卡片本身沿用 .modal-card；这里只管内部的层级和滚动。高度用 dvh 而不是 vh：
   移动端浏览器的地址栏收放会让 vh 算多，底部那颗按钮被顶出屏幕。 */
.rules-gate-card {
  max-width: 520px;
  display: flex;
  flex-direction: column;
  max-height: min(86dvh, 720px);
}

/* 左右 18px 和 .modal-header 对齐。.modal-card 本身没有 padding —— 弹窗的内边距一向
   由内部那层（.stack-form / .modal-header）自己给。这堵墙改成自定义结构之后漏了这件
   事，正文和分隔线几乎贴着卡片边，看着像一条横线直接顶到边框上。 */
.rules-gate-body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0 18px 4px;
}

.rules-gate-list {
  margin: 0 0 14px;
  padding-left: 18px;
  font-size: 14px;
  line-height: 1.7;
  color: var(--text);
}
/* 每条 lead + detail 两截：粗体那行扫一眼就够，
   细节留给愿意读的人。列表符号去掉 —— 有粗体标题就不需要点了，留着反而挤。 */
.rules-gate-list { list-style: none; padding-left: 0; }
.rules-gate-list li { margin: 10px 0; }
.rules-gate-list li strong { display: block; font-weight: 700; }
.rules-gate-list li span { color: var(--muted); font-size: 12.5px; line-height: 1.5; }

/* 「违规会被移除 / 你可以举报」——讲系统怎么运作的，不是规范，所以压得更轻。 */
.rules-gate-note {
  margin: 18px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--muted);
}

/* 在底栏里，不跟正文滚。 */
.rules-gate-links {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
}
.rules-gate-links a { color: inherit; }
.rules-gate-links a:hover { color: var(--text); }

/* 底部这块不跟着正文滚 —— 勾选框和按钮必须一直看得见，否则长内容下用户要先滚到底
   才知道有个必须勾的东西。 */
.rules-gate-foot {
  flex: 0 0 auto;
  border-top: 1px solid var(--line);
  /* 和正文同样的左右 18px；上下自己留够，底部那颗按钮不该贴着卡片边。 */
  padding: 12px 18px 16px;
  margin-top: 12px;
  display: grid;
  gap: 10px;
}

/* 自己的按钮行，不再借发帖器的 .create-form-actions（那条是给「发布 / 匿名发布」
   两颗等宽按钮排的，用在这里会把「取消」也撑成半屏宽）。 */
.rules-gate-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.rules-gate-actions .ghost-button,
.rules-gate-actions .primary-button {
  min-width: 96px;
}

.rules-gate-consent,
.auth-legal-consent {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 13.5px;
  line-height: 1.5;
  cursor: pointer;
}
.rules-gate-consent input,
.auth-legal-consent input {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  margin-top: 2px;
  accent-color: var(--brand-bright, var(--brand));
  cursor: pointer;
}

.auth-legal-consent { color: var(--muted); }
.auth-oauth-legal {
  margin: 10px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  text-align: center;
}

/* 禁用态要看得出来是禁用，而不是"我点了没反应"。 */
.rules-gate-foot .primary-button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* 侧栏底部的法律文件入口。一行小字，不跟「网站导航 / 意见反馈」两个按钮抢注意力 ——
   它需要的是"随时能找到"，不是"显眼"。 */
.sidebar-legal {
  display: flex;
  /* 三个要在一行里放得下 —— 原来 11.5px + 10px 间距会折成两行。缩字号、收间距，
     并且不许换行；每个链接自身也不许被拆断。 */
  flex-wrap: nowrap;
  justify-content: space-between;
  gap: 6px;
  padding: 10px 12px 2px;
  font-size: 10.5px;
  line-height: 1.5;
}
.sidebar-legal a { white-space: nowrap; }
.sidebar-legal a {
  color: color-mix(in srgb, var(--sidebar-ink, #fff) 58%, transparent);
  text-decoration: none;
}
.sidebar-legal a:hover { color: var(--sidebar-ink, #fff); text-decoration: underline; }
/* 侧栏收窄成图标条时整块藏起来 —— 一行文字链在 64px 宽里没法读。 */
.sidebar.is-collapsed .sidebar-legal { display: none; }

/* 左下角的下载浮窗会压在侧栏底部这一块上，这是接受的 —— 浮窗本身可以关掉，
   关掉之后这几项就露出来了，不为此改动侧栏布局。 */

/* 左下角「下载 App」浮窗（src/ui/appDownloadDock.js）。挂在 <body> 上，不在
   #app 里 —— 理由同 .back-to-top（第 15901 行注释）：一次整页重绘会把 #app 里
   的东西全部销毁。右下角已经被 .import-dock（z-index 60）和 .back-to-top
   （z-index 30）占了，左下角是空的。 */
.app-dock {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 60;
  display: flex;
  align-items: center;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: 0 6px 20px rgba(16, 24, 40, 0.12);
  max-width: calc(100vw - 36px);
}

.app-dock-link {
  display: flex;
  /* 竖排：文案在上、二维码在下。横排时文案挤在码右边，一列只放得下两三个字，
     读起来别扭；竖排让两行文案有整幅宽度。 */
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: inherit;
  min-width: 0;
}

/* 白框补静区：uqr 只给了 border:1（一个模块宽），规范要求四个，剩下三个靠这圈
   白色内边距补 —— 这是功能性的，不是装饰，少了它二维码可能扫不出来。形状照抄
   .auth-qr-frame（第 4800 行），尺寸缩到角落浮窗的量级（不是登录弹窗的 252px）。 */
.app-dock-qr-frame {
  width: 128px;
  height: 128px;
  flex: none;
  display: grid;
  place-items: center;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #ffffff;
}
.app-dock-qr-frame svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* 竖排之后文案整幅居中；手机上二维码被藏掉，这块就是浮窗的全部内容。 */
.app-dock.is-phone .app-dock-link { gap: 0; }

.app-dock-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 2px;
  min-width: 0;
}
.app-dock-title {
  font-size: 13.5px;
  font-weight: 800;
  color: var(--text);
}

/* 绝对定位而不是并排：它当 flex 子项时会把卡片右边顶出一条只放得下一个 × 的空白，
   竖排之后那条空白尤其显眼。压在卡片右上角，不参与布局。 */
.app-dock-close {
  position: absolute;
  top: 4px;
  right: 4px;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.app-dock-close:hover { background: var(--surface-muted); color: var(--text); }

/* 两个不同的判据，各管各的一件事：

   1) 二维码显不显示，看的是**设备**（.is-phone，由 detectAppPlatform() 打上）。
      手机扫不了自己屏幕上的码，所以在手机上收起来；而桌面浏览器窗口拉多窄，人
      手里照样有另一台手机可以扫，二维码仍然有用。用宽度判会把后一种情况误伤。
      文案同理：手机上是「点击下载」，桌面才是「扫码下载」。

   2) 位置和内边距，看的是**窗口宽度**（媒体查询）。这本来就是布局问题。

   两种情况下整块都仍然是那个 <a>，点了照样走 /app 分流，JS 不用为此另外分支。 */
.app-dock.is-phone .app-dock-qr-frame {
  display: none;
}

.app-dock.is-phone .app-dock-link {
  padding-right: 22px;
}

@media (max-width: 640px) {
  .app-dock {
    left: 12px;
    /* 让开固定底栏（46px + 安全区），不要压在它上面 —— 和 .back-to-top 用的是
       同一个值（见第 15957 行）。实测未让开时浮窗会盖住底栏 30px。 */
    bottom: calc(58px + env(safe-area-inset-bottom));
    padding: 8px 12px;
  }
  .app-dock-text { max-width: 150px; }
}
