/* ============================================================
   设计系统
   ------------------------------------------------------------
   深色电影感 + 磨砂玻璃 + 宋体克制排版。
   一般不需要改这个文件，改内容请编辑 assets/js/data.js
   ============================================================ */

/* ---------- 1. 变量 ---------- */

:root {
  --bg:        #08090a;
  --bg-soft:   #100e0c;
  --surface:   rgba(232, 200, 164, 0.045);
  --surface-2: rgba(232, 200, 164, 0.075);

  --line:      rgba(226, 190, 150, 0.11);
  --line-2:    rgba(226, 190, 150, 0.19);

  --text:      #f2e9dc;
  --text-2:    #b09b85;
  --text-3:    #8a7864;

  --accent:    #c99066;
  --accent-2:  #e4ae7b;
  --accent-3:  #8b5e40;

  /* 只用本机字体，不联网、不下载，双击打开也能正常显示 */
  --font-serif: "Songti SC", "STSong", "Source Han Serif SC", "Noto Serif SC",
                "Noto Serif CJK SC", "SimSun", "Times New Roman", Georgia, serif;
  --font-sans:  "PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei",
                system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;

  --shell:   1280px;
  --gutter:  clamp(20px, 4.5vw, 56px);
  --nav-h:   72px;
  --section-y: clamp(64px, 9vw, 132px);

  --radius:    999px;   /* 胶囊 */
  --radius-sm: 3px;     /* 图片、卡片 */

  /* 磨砂玻璃。注意：只有背后有东西可以模糊（视频、图片）的地方才用，
     压在纯色背景上会完全看不见效果。 */
  --glass-bg:     rgba(14, 12, 10, 0.42);
  --glass-bg-2:   rgba(255, 244, 230, 0.09);
  --glass-line:   rgba(232, 200, 164, 0.22);
  --glass-blur:   blur(22px) saturate(150%);
  /* 顶边那道高光是玻璃的关键特征，少了就只是一块半透明灰 */
  --glass-inner:  inset 0 1px 0 rgba(255, 240, 220, 0.14);
  --glass-shadow: 0 10px 44px rgba(0, 0, 0, 0.45), var(--glass-inner);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------- 2. 基础重置 ---------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.75;
  letter-spacing: 0.01em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

body.menu-open { overflow: hidden; }

img, video, iframe { display: block; max-width: 100%; }
img { height: auto; }

/* <picture> 只是选图的包装元素，本身不该参与布局。
   如果让它生成盒子，里面的 img 用 height:100% 就会失效
   （百分比高度在 height:auto 的父元素上无法解析）。 */
picture { display: contents; }

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

h1, h2, h3, h4, p, dl, dd, figure, blockquote { margin: 0; }

button, input, textarea, select { font: inherit; color: inherit; }

::selection { background: var(--accent); color: #16130e; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* 背景颗粒，增加质感。指针对穿透，不影响点击 */
.grain {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* 顶部滚动进度条 */
.progress {
  position: fixed;
  top: 0; left: 0;
  height: 1px;
  width: 0;
  z-index: 90;
  background: var(--accent);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.progress.is-on { opacity: 0.85; }

.skip {
  position: fixed;
  top: -100px; left: 16px;
  z-index: 200;
  padding: 10px 18px;
  background: var(--accent);
  color: #16130e;
  border-radius: var(--radius);
  font-size: 14px;
  transition: top 0.2s var(--ease);
}
.skip:focus { top: 16px; }

/* ---------- 3. 排版 ---------- */

.title-xl {
  font-family: var(--font-serif);
  font-size: clamp(3.2rem, 12vw, 9.5rem);
  font-weight: 400;
  line-height: 0.98;
  letter-spacing: 0.06em;
}

.title-l {
  font-family: var(--font-serif);
  font-size: clamp(1.9rem, 4.8vw, 3.4rem);
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: 0.02em;
}

.title-m {
  font-family: var(--font-serif);
  font-size: clamp(1.25rem, 2.5vw, 1.85rem);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0.02em;
}

/* 主标题下面那行英文副标题。文案在 data.js 的 SITE.en 里，
   由 app.js 的 initSubtitles() 按中文标题匹配之后插进来。
   跟首屏那句 PORTFOLIO · 作品集 是同一套处理：小号、大写、拉字距、金色 ——
   整站于是有了一句统一的英文语汇。
   这里是左对齐的，字距加在最后一个字母右边那点空白不用补偿。 */
.title__en {
  display: block;
  margin-top: 0.32em;
  font-family: var(--font-sans);
  font-size: clamp(9px, 0.72vw, 11px);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--accent);
}

.eyebrow {
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--accent);
}

.lead {
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  color: var(--text-2);
  max-width: 46ch;
}

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

.prose {
  max-width: 62ch;
  color: var(--text-2);
  font-size: clamp(0.98rem, 1.3vw, 1.08rem);
  line-height: 1.95;
}
.prose p + p { margin-top: 1.4em; }

/* 设计说明的配图侧栏。
   右栏固定 400px，是为了让「两栏加起来」的高度可控：
   两张 4/3 的图（400x300）加图注大约 680px，能稳稳待在
   sticky 的那点可视高度里；再宽就会出现「图被钉住、下半张永远看不到」。
   position: sticky 让它跟着正文走，读到第五段图还在旁边；
   万一屏幕比图还矮，它会自然退回普通排版，不会卡住。 */
.brief { display: block; }
.brief--split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 400px);
  gap: clamp(28px, 4vw, 64px);
  align-items: start;
}
/* 两栏之后正文这一栏由栅格定宽，再去卡 62ch 就会和右栏之间空出一条缝 */
.brief--split .prose { max-width: none; }

.brief__aside {
  display: grid;
  gap: clamp(18px, 2.2vw, 30px);
  position: sticky;
  top: calc(var(--nav-h) + 32px);
}
.brief__fig { margin: 0; }
/* 图片放大落回的那点位移需要裁剪，但图注不能跟着被裁，
   所以裁剪只加在包住图片的 span 上，不加在 figure 上 */
.brief__frame {
  display: block;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--bg-soft);
}
.brief__frame img { width: 100%; height: auto; }
.brief__fig figcaption {
  margin-top: 10px;
  color: var(--text-3);
  font-size: 12px;
  line-height: 1.55;
}

/* 窄屏改成上下排：正文读完接参考图，比挤成两栏好读 */
@media (max-width: 980px) {
  .brief--split { grid-template-columns: 1fr; gap: 30px; }
  .brief__aside { position: static; gap: 20px; }
  .brief__fig figcaption { font-size: 11px; }
}

/* ---------- 4. 布局 ---------- */

.container {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: var(--section-y); }
.section--tight { padding-block: clamp(48px, 7vw, 96px); }

.section__bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  padding-bottom: clamp(24px, 4vw, 44px);
  margin-bottom: clamp(28px, 4.5vw, 52px);
  border-bottom: 1px solid var(--line);
}

.section__more {
  font-size: 13px;
  letter-spacing: 0.1em;
  color: var(--text-2);
  transition: color 0.3s var(--ease);
}
.section__more:hover { color: var(--accent); }

.sec__no {
  font-family: var(--font-sans);
  font-size: 0.62em;
  color: var(--accent);
  margin-right: 0.9em;
  letter-spacing: 0.1em;
  vertical-align: 0.18em;
}

.sec__note {
  font-size: 13px;
  color: var(--text-3);
  letter-spacing: 0.04em;
}

/* 「动画漫游」还没填视频时的占位。
   做成虚线框，一眼看出是「待填」而不是设计成这样的留白。 */
.sec__empty {
  padding: clamp(28px, 5vw, 56px) 24px;
  border: 1px dashed var(--line-2);
  border-radius: var(--radius-sm);
  color: var(--text-3);
  font-size: 13.5px;
  line-height: 1.9;
  text-align: center;
}
.sec__empty code {
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--text-2);
  background: var(--surface-2);
  padding: 2px 7px;
  border-radius: 3px;
}

.stack-sm > * + * { margin-top: 12px; }
.stack-md > * + * { margin-top: 24px; }
.stack-lg > * + * { margin-top: clamp(28px, 4vw, 48px); }
.stack-xs > * + * { margin-top: 6px; }

/* ---------- 5. 导航 ---------- */

.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 80;
  padding-top: env(safe-area-inset-top, 0);
  transition: background 0.45s var(--ease), backdrop-filter 0.45s var(--ease), border-color 0.45s var(--ease);
  border-bottom: 1px solid transparent;
}

.nav.is-stuck {
  background: rgba(8, 9, 10, 0.6);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom-color: var(--glass-line);
}

.nav__inner {
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 0 0 auto;
}

.brand__mark {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  font-family: var(--font-serif);
  font-size: 17px;
  color: var(--accent);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.brand__text {
  display: flex;
  flex-direction: column;
  line-height: 1.25;
  font-size: 14px;
  letter-spacing: 0.06em;
}
.brand__text em {
  font-style: normal;
  font-size: 11px;
  color: var(--text-3);
  letter-spacing: 0.08em;
}

/* 桌面端：玻璃胶囊导航 */
.nav__links {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  background: var(--glass-bg-2);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);
}

.nav__links a {
  padding: 8px 18px;
  border-radius: var(--radius);
  font-size: 13.5px;
  letter-spacing: 0.08em;
  color: var(--text-2);
  transition: color 0.3s var(--ease), background 0.3s var(--ease);
  white-space: nowrap;
}
.nav__links a:hover { color: var(--text); }
.nav__links a[aria-current="page"] {
  color: var(--text);
  background: var(--surface-2);
}

.nav__toggle {
  display: none;
  width: 40px; height: 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  position: relative;
}
.nav__toggle span,
.nav__toggle span::before,
.nav__toggle span::after {
  position: absolute;
  left: 50%;
  width: 16px; height: 1px;
  margin-left: -8px;
  background: var(--text);
  transition: transform 0.35s var(--ease), opacity 0.2s var(--ease);
  content: "";
}
.nav__toggle span { top: 50%; }
.nav__toggle span::before { top: -5px; }
.nav__toggle span::after  { top: 5px; }
body.menu-open .nav__toggle span { background: transparent; }
body.menu-open .nav__toggle span::before { transform: translateY(5px) rotate(45deg); }
body.menu-open .nav__toggle span::after  { transform: translateY(-5px) rotate(-45deg); }

/* ---------- 6. 按钮 ---------- */

.btn {
  position: relative;
  overflow: hidden;              /* 给下面那道扫光用，别让它溢出胶囊 */
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 0 26px;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  background: var(--glass-bg-2);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  font-size: 14px;
  letter-spacing: 0.08em;
  color: var(--text);
  cursor: pointer;
  /* translate 是给「鼠标磁吸」用的（React Bits 的 Magnet，见文件后面的说明）。
     和 transform 分开写：transform 负责 hover 抬起 2px，translate 负责跟着光标挪，
     两者互不覆盖。位移量由 JS 写在 --mag-x / --mag-y 上，最多 7px。 */
  translate: var(--mag-x, 0px) var(--mag-y, 0px);
  transition: background 0.4s var(--ease), border-color 0.4s var(--ease),
              color 0.4s var(--ease), transform 0.4s var(--ease),
              box-shadow 0.4s var(--ease), translate 0.22s var(--ease);
  white-space: nowrap;
}

/* 扫光：平时整条停在左边外面，鼠标一上去就从左滑到右，
   像一层光从胶囊上抹过去。纯装饰，不挡点击。 */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(105deg,
              transparent 32%, rgba(255, 242, 224, 0.18) 50%, transparent 68%);
  transform: translateX(-110%);
  transition: transform 0.75s var(--ease);
  pointer-events: none;
}

/* 悬浮：抬起来 2px、描边转金、字色偏金、底下托一层暖影 */
.btn:hover {
  background: rgba(232, 200, 164, 0.15);
  border-color: var(--accent);
  color: var(--accent-2);
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.42), 0 0 0 1px rgba(201, 144, 102, 0.16);
}
.btn:hover::after { transform: translateX(110%); }

/* 按下去马上回原位，不然会觉得黏手 */
.btn:active {
  transform: translateY(0);
  box-shadow: none;
  transition-duration: 0.1s;
}

/* 键盘走查时也要看得见焦点 */
.btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* 实心按钮不做玻璃，它是页面里唯一的强对比行动点 */
.btn--solid {
  background: var(--text);
  border-color: var(--text);
  color: #0b0c0d;
  font-weight: 500;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.btn--solid:hover {
  background: var(--accent-2);
  border-color: var(--accent-2);
  color: #17110a;
  box-shadow: 0 12px 34px rgba(201, 144, 102, 0.3);
}

.btn__arrow { transition: transform 0.4s var(--ease); }
.btn:hover .btn__arrow { transform: translateX(5px); }

/* 触屏上「悬浮」是假的：手指点完样式会卡在悬浮态。
   所以只在真的支持悬浮的设备上给上浮 + 扫光。 */
@media (hover: none) {
  .btn:hover { transform: none; box-shadow: none; }
  .btn::after { display: none; }
}

/* ---------- 7. 首屏 ---------- */

/* 首屏：外层比屏幕高，里面吸顶。
   多出来的那截滚动距离就是「推进」的行程；app.js 会把视频的时间轴
   映射到这段行程上，所以往下滚 = 镜头往空间里走。
   没有视频时 app.js 会把 --hero-run 设成 0，wrap 自动收回一屏。 */
.hero-wrap {
  --hero-run: 0px;
  height: calc(100svh + var(--hero-run));
}

.hero {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  isolation: isolate;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
}
.hero__media img,
.hero__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.04);
}
/* 配了 profile.heroVideo 时，由 app.js 插进来盖在图片上。
   一开始是透明的 —— 底下那张图就是海报帧，视频没出画面之前不该挡着它，
   否则会闪一下黑。等真的出画面了（app.js 加 is-ready）再淡进来。 */
.hero__video {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 600ms var(--ease);
}
.hero__video.is-ready { opacity: 1; }

/* 跟着光标走的微尘 */
.hero__fx {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* 上下压暗，保证文字可读。
   手机屏幕窄，径向渐变会把整屏都吃掉，所以手机上只用一条纵向渐变 */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to bottom,
    rgba(8, 9, 10, 0.74) 0%,
    rgba(8, 9, 10, 0.26) 24%,
    rgba(8, 9, 10, 0.40) 58%,
    var(--bg) 100%);
}

@media (min-width: 861px) {
  /* 首屏换成了动态视频，压暗层要放得比以前轻，否则画面就看不见了。
     视频本身整体偏暗（平均亮度只有 52/255），所以只需要
     压住最上面的导航带、最下面的按钮带，以及四周边缘。
     中间留给画面；大字可读性靠「轻压暗 + 一层很淡的投影」一起兜。
     实测大字背后最差 4.4:1、中位 8.6:1，大字的门槛是 3:1，余量很足。 */
  .hero__scrim {
    background:
      radial-gradient(120% 80% at 50% 45%, rgba(8, 9, 10, 0.08) 0%, rgba(8, 9, 10, 0.60) 60%, var(--bg) 100%),
      linear-gradient(to bottom, rgba(8, 9, 10, 0.72) 0%, rgba(8, 9, 10, 0.10) 30%, rgba(8, 9, 10, 0.74) 100%);
  }
  /* 大字压在任何画面上都得读得清，加一层很淡的投影兜底
     （几乎看不出来，但边缘一下子干净很多） */
  .hero__title { text-shadow: 0 2px 44px rgba(0, 0, 0, 0.9), 0 1px 6px rgba(0, 0, 0, 0.55); }
}

/* 手机上图片铺满整屏，文字正好压在画面中间最亮的地方。
   加一层柔和的椭圆遮罩单独压暗文字区，四周和上下仍然看得见画面。 */
@media (max-width: 860px) {
  .hero__scrim {
    background:
      radial-gradient(92% 34% at 50% 47%, rgba(8, 9, 10, 0.82) 0%, rgba(8, 9, 10, 0.5) 52%, rgba(8, 9, 10, 0) 100%),
      linear-gradient(to bottom,
        rgba(8, 9, 10, 0.7) 0%,
        rgba(8, 9, 10, 0.14) 18%,
        rgba(8, 9, 10, 0.2) 68%,
        var(--bg) 100%);
  }
  .hero__title { text-shadow: 0 2px 40px rgba(0, 0, 0, 0.85), 0 0 12px rgba(0, 0, 0, 0.5); }

  /* 手机屏幕窄，贴底那行改成上下两排，否则身份行会被按钮挤断 */
  .hero__bar {
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-end;
    gap: 14px;
    bottom: max(22px, env(safe-area-inset-bottom, 22px));
  }
  .hero__meta { justify-content: center; padding-bottom: 0; }
  .hero__actions { justify-content: center; }
}

.hero__content {
  position: relative;
  z-index: 2;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-block: calc(var(--nav-h) + 40px) clamp(120px, 18vh, 170px);
}

/* 光标视差加在这一层，滚动变换加在外层，两者互不打架 */
.hero__inner {
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0);
  transition: transform 0.9s var(--ease);
  will-change: transform;
}

.hero__title {
  color: var(--text);
  text-shadow: 0 2px 60px rgba(0, 0, 0, 0.5);
}

/* 名字和下面那行之间的一条金线，两端渐隐，不做成生硬的实线 */
.hero__rule {
  display: block;
  width: clamp(52px, 8vw, 112px);
  height: 1px;
  /* 外面那层 .hero__inner 是普通块（不是 flex），
     所以固定宽度 + 左右 auto 外边距才是真的居中，写 0 会贴到左边去 */
  margin: clamp(18px, 3.2vh, 36px) auto clamp(14px, 2.4vh, 26px);
  background: linear-gradient(to right,
              rgba(201, 144, 102, 0), var(--accent), rgba(201, 144, 102, 0));
}

/* 金线下面那一行：金色、拉宽字距的「PORTFOLIO · 作品集」。
   字距是加在右边一格的，所以补一个等量的缩进才真的看着居中。 */
.hero__tag {
  font-size: clamp(10px, 0.92vw, 12.5px);
  letter-spacing: 0.44em;
  text-indent: 0.44em;
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: 0 1px 22px rgba(0, 0, 0, 0.9);
}

/* 贴底通栏一行：左边身份，右边按钮。
   不用玻璃盒子 —— 一块小方块飘在正中间会跟大字打架。 */
.hero__bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: max(28px, env(safe-area-inset-bottom, 28px));
  z-index: 3;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;   /* 身份行靠右。原来左边身份、右边两个按钮，按钮删了 */
  gap: 24px;
}

/* 推进进度条 */
.hero__track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  height: 1px;
  background: rgba(255, 255, 255, 0.12);
}
.hero__track i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: opacity 0.4s var(--ease);
}

.hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0 14px;
  padding-bottom: 12px;
  font-size: 12.5px;
  letter-spacing: 0.18em;
  color: var(--text-2);
  text-shadow: 0 1px 20px rgba(0, 0, 0, 0.7);
}
.hero__meta span { position: relative; }
.hero__meta span + span::before {
  content: "·";
  margin-right: 14px;
  color: var(--text-3);
}


.hero__actions {
  display: none;   /* 首屏那两个按钮已删除，这里留个空壳防止旧缓存里还引用着 */
}


/* 贴着底边的两行小字，参考站那种编排感 */
/* ---------- 8. 作品网格与卡片 ----------
   卡片的做法：整张图就是卡片本身，标题和一行信息压在图片左下角。
   高矮由 --shape 控制（在 app.js 的 CARD_SHAPES 里循环），
   所以网格是错落的，不是一排一样高的方块。 */

.grid-works {
  column-count: 2;
  column-gap: clamp(14px, 1.6vw, 24px);
}

.card {
  position: relative;
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin: 0 0 clamp(14px, 1.6vw, 24px);
  border-radius: clamp(14px, 1.4vw, 22px);
  overflow: hidden;
  background: var(--bg-soft);
  aspect-ratio: var(--shape, 4 / 3);
  isolation: isolate;
}

.card__link { display: block; position: absolute; inset: 0; }

.card__media { position: absolute; inset: 0; }
.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 1.1s var(--ease), filter 0.7s var(--ease);
  filter: saturate(0.92) brightness(0.88);
}
.card__link:hover .card__media img,
.card__link:focus-visible .card__media img {
  transform: scale(1.045);
  filter: saturate(1) brightness(0.98);
}

/* 图片下缘压暗。玻璃面板自己有模糊，这里只需要轻轻一层，
   太重会把图片压死、玻璃后面就什么都看不见了 */
.card__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top,
    rgba(6, 7, 8, 0.5) 0%,
    rgba(6, 7, 8, 0.26) 32%,
    rgba(6, 7, 8, 0) 66%);
  transition: opacity 0.5s var(--ease);
}
.card__link:hover .card__scrim { opacity: 0.8; }

/* 卡片底部那块信息做成磨砂玻璃：玻璃背后就是卡片自己的图片，
   虚化之后白字读得清，图片也还看得见 */
.card__body {
  position: absolute;
  left: clamp(10px, 1.1vw, 16px);
  right: clamp(10px, 1.1vw, 16px);
  bottom: clamp(10px, 1.1vw, 16px);
  z-index: 2;
  padding: clamp(13px, 1.5vw, 20px) clamp(15px, 1.7vw, 22px);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  border-radius: 14px;
  border: 1px solid var(--glass-line);
  /* 上轻下重：上面透出背后被虚化的图（玻璃感），下面压暗保证白字读得清。
     用一层平铺半透明的话，遇到白底图纸会变成中灰、字就看不清了。 */
  background: linear-gradient(to top,
    rgba(6, 7, 8, 0.78) 0%,
    rgba(6, 7, 8, 0.52) 58%,
    rgba(6, 7, 8, 0.34) 100%);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  transition: border-color 0.45s var(--ease);
}
.card__link:hover .card__body,
.card__link:focus-visible .card__body {
  border-color: rgba(255, 255, 255, 0.34);
}

.card__text {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.card__title {
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1rem, 1.6vw, 1.42rem);
  line-height: 1.32;
  letter-spacing: 0.02em;
  color: #fff;
}

.card__no {
  flex: none;
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--accent);
}

.card__meta {
  font-size: 12px;
  letter-spacing: 0.07em;
  color: rgba(255, 255, 255, 0.78);
}

.card__go {
  flex: none;
  font-size: 15px;
  color: rgba(255, 255, 255, 0.5);
  transition: transform 0.4s var(--ease), color 0.4s var(--ease);
}
.card__link:hover .card__go,
.card__link:focus-visible .card__go {
  color: var(--accent);
  transform: translate(3px, -3px);
}

/* 首页精选：排成「左边上下两张 + 右边一张通高」的 L 形，
   参考站就是这种排法，三张卡正好铺满、不留空洞。
   （作品列表页仍然用上面的错落瀑布流） */
@media (min-width: 861px) {
  .grid-works--featured {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(14px, 1.6vw, 24px);
    column-count: auto;
  }
  .grid-works--featured .card { margin: 0; aspect-ratio: auto; }
  .grid-works--featured .card:nth-child(1) { grid-area: 1 / 1; aspect-ratio: 4 / 3; }
  .grid-works--featured .card:nth-child(2) { grid-area: 1 / 2 / 3 / 3; }
  .grid-works--featured .card:nth-child(3) { grid-area: 2 / 1; aspect-ratio: 4 / 3; }

  /* 不到三张时并排，多于三张时通栏往下排，都不会留洞 */
  .grid-works--featured:has(.card:nth-child(2):last-child) .card:nth-child(1),
  .grid-works--featured:has(.card:nth-child(2):last-child) .card:nth-child(2) {
    grid-area: auto;
    aspect-ratio: 4 / 3;
  }
  .grid-works--featured .card:nth-child(n + 4) { grid-column: 1 / -1; aspect-ratio: 16 / 7; }
}

.empty {
  padding: 72px 0;
  text-align: center;
  color: var(--text-3);
  font-size: 14px;
}

/* 「完整方案」那张宽卡片。
   素材全部复用 .card（图片层、遮罩、玻璃信息条），只改两处：
   · 比例换成 21:9 的横幅——窄屏退回 4:3，否则在手机上会被压成一条几乎看不见的缝；
   · 信息条不再拉满整行，限宽之后读起来才不像一条通栏通知。 */
.card--deck {
  aspect-ratio: 21 / 9;
  margin: 0;
}
.card--deck .card__body {
  right: auto;
  width: min(680px, calc(100% - 2 * clamp(10px, 1.1vw, 16px)));
}
.card--deck .card__title { font-size: clamp(17px, 1.9vw, 25px); }

@media (max-width: 820px) {
  .card--deck { aspect-ratio: 4 / 3; }
  .card--deck .card__body { right: clamp(10px, 1.1vw, 16px); width: auto; }
}

/* ---------- 9. 分类筛选（纯文字 tab + 下划线） ---------- */

/* 做成吸顶玻璃条：滚动时它会压在作品图片上面，
   背后一直有图可以模糊，玻璃感才成立 */
.filters {
  position: sticky;
  top: var(--nav-h);
  z-index: 55;
  display: flex;
  align-items: baseline;
  gap: clamp(20px, 3vw, 40px);
  flex-wrap: wrap;
  padding-top: clamp(10px, 1.4vw, 16px);
  margin-bottom: clamp(34px, 5vw, 60px);
  background: rgba(8, 9, 10, 0.6);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-line);
}

.filter {
  position: relative;
  padding: 0 0 16px;
  border: 0;
  background: none;
  font-size: 15px;
  letter-spacing: 0.06em;
  color: var(--text-3);
  cursor: pointer;
  transition: color 0.3s var(--ease);
  white-space: nowrap;
}
.filter:hover { color: var(--text-2); }

.filter[aria-pressed="true"] { color: var(--text); }

.filter::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease);
}
.filter[aria-pressed="true"]::after { transform: scaleX(1); }

.filter__n {
  font-size: 10px;
  color: var(--accent);
  margin-left: 5px;
  vertical-align: super;
  letter-spacing: 0;
}

/* ---------- 10. 作品详情 ---------- */

.detail__hero {
  padding-top: calc(var(--nav-h) + clamp(48px, 9vw, 110px));
  padding-bottom: clamp(40px, 6vw, 72px);
}

.detail__title { margin-top: 18px; }

.detail__hero .lead { margin-top: 20px; }

.detail__cover {
  margin-top: clamp(34px, 5vw, 60px);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-soft);
}
/* 封面图限一个高度：4:3 的图在 1300px 宽时会有 975px 高，比一屏还高，
   整张图压在标题下面会显得很沉。压到 76vh 并用 object-fit: cover 居中裁切，
   竖构图的天空/地面留白正好被裁掉，画面反而更聚焦。 */
.detail__cover img {
  width: 100%;
  max-height: 76vh;
  object-fit: cover;
  object-position: center;
}

/* 项目信息表 */
.specs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  margin-top: clamp(28px, 4vw, 48px);
  background: var(--line);
  border-block: 1px solid var(--line);
}
.specs__row {
  background: var(--bg);
  padding: 18px 4px;
}
.specs__k {
  font-size: 11px;
  letter-spacing: 0.16em;
  color: var(--text-3);
  text-transform: uppercase;
}
.specs__v {
  margin: 6px 0 0;
  font-size: 14.5px;
  color: var(--text);
}

/* 吸顶内容导航 */
.subnav {
  position: sticky;
  top: var(--nav-h);
  z-index: 60;
  background: rgba(8, 9, 10, 0.68);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-block: 1px solid var(--glass-line);
}

.subnav__inner {
  display: flex;
  align-items: center;
  gap: clamp(18px, 2.6vw, 34px);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.subnav__inner::-webkit-scrollbar { display: none; }

.subnav__item {
  position: relative;
  padding: 18px 0;
  font-size: 13.5px;
  letter-spacing: 0.08em;
  color: var(--text-3);
  white-space: nowrap;
  transition: color 0.3s var(--ease);
}
.subnav__item:hover { color: var(--text-2); }

.subnav__item.is-on { color: var(--text); }

.subnav__item::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease);
}
.subnav__item.is-on::after { transform: scaleX(1); }

/* 图库 */
.gallery {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(18px, 3vw, 40px);
}
.gallery--1 { grid-template-columns: 1fr; }
.gallery--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.figure { margin: 0; }

.figure button {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-soft);
}
.figure img {
  width: 100%;
  transition: opacity 0.5s var(--ease), transform 1.1s var(--ease);
  opacity: 0.94;
}
.figure button:hover img { opacity: 1; transform: scale(1.02); }

/* 图纸类：白底图纸用浅色衬底，别被深色背景吃掉 */
.figure--paper button { background: #e9e7e2; padding: 14px; }

.figure figcaption {
  margin-top: 12px;
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: var(--text-3);
  line-height: 1.7;
}

/* 视频 / 全景 */
.tour {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-soft);
  aspect-ratio: 16 / 9;
}
.tour video,
.tour iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* 上一个 / 下一个 */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
}

.pager__next {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  text-align: right;
}
.pager__label {
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--text-3);
  text-transform: uppercase;
}
.pager__next .title-m { transition: color 0.35s var(--ease); }
.pager__next:hover .title-m { color: var(--accent); }

/* ---------- 11. 关于页 ---------- */

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(36px, 6vw, 96px);
  align-items: start;
}

.portrait {
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-soft);
  aspect-ratio: 4 / 5;
}
.portrait img { width: 100%; height: 100%; object-fit: cover; }

/* 还没填 profile.photo 时的空位。做得「一眼看得出是待填」，
   而不是拿一张作品图顶上去假装有照片。 */
.portrait__empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 20px;
  text-align: center;
  border: 1px dashed var(--line-2);
  border-radius: var(--radius-sm);
}
.portrait__empty span {
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
.portrait__empty code {
  font-family: var(--font-sans);
  font-size: 11.5px;
  color: var(--text-2);
  background: var(--surface-2);
  padding: 4px 9px;
  border-radius: 3px;
}

.facts { margin-top: 28px; }
.facts > div {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding: 13px 0;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}
.facts dt { color: var(--text-3); letter-spacing: 0.06em; }
.facts dd { margin: 0; color: var(--text); text-align: right; }

/* 通栏横排版：放在关于页两栏下面 */
.facts--row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1px;
  margin-top: clamp(40px, 6vw, 72px);
  background: var(--line);
  border-block: 1px solid var(--line);
}
.facts--row > div {
  flex-direction: column;
  justify-content: flex-start;
  gap: 8px;
  padding: 20px 4px;
  border-bottom: 0;
  background: var(--bg);
}
.facts--row dt {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.facts--row dd { text-align: left; font-size: 15px; }

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}
.chip {
  padding: 7px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 12.5px;
  letter-spacing: 0.05em;
  color: var(--text-2);
  background: var(--surface);
}

/* 按熟练度分组的软件列表。
   左边一列窄标签（精通 / 熟练 / 熟悉…），右边是一排 chip。
   比不分级的一长排更能说明问题。 */
.skill-groups { margin-top: 14px; }
.skill-group {
  display: grid;
  grid-template-columns: 76px 1fr;
  gap: 12px 16px;
  align-items: start;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.skill-group:last-child { border-bottom: 0; }
.skill-group__label {
  padding-top: 8px;
  font-size: 11.5px;
  letter-spacing: 0.14em;
  color: var(--text-3);
}
.skill-group .chips { margin-top: 0; }

/* 主修课程：排成一行普通文字，不抢软件的注意力 */
.courses {
  margin: 14px 0 0;
  font-size: 14px;
  line-height: 1.9;
  color: var(--text-2);
}

/* 竞赛与荣誉：左边年份、右边内容，年份对齐成一列好扫 */
.honors { margin-top: clamp(40px, 6vw, 72px); }
.honor-list { list-style: none; margin: 14px 0 0; padding: 0; }
.honor-list li {
  display: grid;
  grid-template-columns: 92px 1fr;
  gap: 20px;
  align-items: baseline;
  padding: 13px 0;
  border-bottom: 1px solid var(--line);
  font-size: 14.5px;
}
.honor-list__date {
  font-size: 12.5px;
  letter-spacing: 0.08em;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.honor-list__text { color: var(--text); }

@media (max-width: 560px) {
  .skill-group { grid-template-columns: 1fr; gap: 4px; }
  .skill-group__label { padding-top: 0; }
  .honor-list li { grid-template-columns: 76px 1fr; gap: 12px; }
}

/* ---------- 12. 联系 ---------- */

.contact-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1px;
  max-width: 900px;
  background: var(--line);
  border-block: 1px solid var(--line);
}

.contact-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 26px 24px;
  background: var(--bg);
  transition: background 0.4s var(--ease);
}
.contact-card:hover { background: var(--bg-soft); }

.contact-card__body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.contact-card__k {
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--text-3);
  text-transform: uppercase;
}
.contact-card__v {
  font-size: clamp(0.98rem, 1.5vw, 1.15rem);
  font-family: var(--font-serif);
  color: var(--text);
  overflow-wrap: anywhere;
}

.copy-btn {
  flex: 0 0 auto;
  padding: 7px 15px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--text-2);
  cursor: pointer;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.copy-btn:hover,
.copy-btn.is-done {
  color: var(--accent);
  border-color: var(--accent);
  background: transparent;
}

/* ---------- 12.5 首页：个人经历 / 整屏联系 ---------- */

.bio {
  display: grid;
  grid-template-columns: minmax(0, 0.58fr) minmax(0, 1.42fr);
  gap: clamp(32px, 5vw, 76px);
  align-items: stretch;
}

/* 三块内容在右栏均匀铺开，避免底部空一大块或者中间空一条 */
.bio__body {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(26px, 3.5vw, 40px);
}

.bio__text {
  font-size: clamp(15px, 1.15vw, 17px);
  line-height: 2;
  color: var(--text-2);
}

.bio__contact {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 32px;
  font-size: 14px;
}
.bio__contact a {
  color: var(--text);
  border-bottom: 1px solid var(--line);
  padding-bottom: 3px;
  transition: border-color 0.4s var(--ease), color 0.4s var(--ease);
}
.bio__contact a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* 项目数据：三个数字，用发丝线隔开 */
.bio__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border-block: 1px solid var(--line);
}
.bio__stat {
  /* 原来是 var(--bg) 的纯黑底 —— 那会在首屏下方那层渐变中间挖出三个黑洞。
     改成 72% 的深色：渐变的暖意还能透上来一点，不会显得是块死黑，
     同时把 11px 小字背后的亮度压住，对比度才够（实测 4.6:1 以上）。
     不能改成全透明 —— 那样这行小字只剩 3.8:1，不达标。 */
  background: rgba(8, 9, 10, 0.72);
  padding: 20px 2px;
  text-align: center;   /* 每格里的数字和说明都居中 */
}
.bio__stat b {
  display: block;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.4vw, 2.7rem);
  line-height: 1;
  letter-spacing: -0.01em;
}
.bio__stat span {
  display: block;
  margin-top: 10px;
  font-size: 11px;
  letter-spacing: 0.16em;
  /* 字距是加在最后一个字右边的，补一个等量的缩进才是真的居中 */
  text-indent: 0.16em;
  color: var(--text-3);
}

/* ---------- 首页下方：鼠标光斑（React Bits 的 SpotlightCard） ----------

   和首屏区别开：首屏是内容跟着鼠标动，这里什么都不动，只是光标附近亮一点，
   像在暗墙上打了一束手电。

   isolation: isolate + z-index: -1 是这个写法能成立的关键：
   区块自己开一个层叠上下文，-1 层就落在「页面背景之上、区块内容之下」，
   所以既能透出背后的渐变，又不会盖住任何一行字。 */
.has-spotlight {
  position: relative;
  isolation: isolate;
}
.spotlight {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;      /* 绝不能挡住下面的链接和按钮 */
  opacity: 0;
  transition: opacity 0.7s var(--ease);
  background: radial-gradient(
    440px circle at var(--sx, 50%) var(--sy, 30%),
    rgba(201, 144, 102, 0.16),
    rgba(201, 144, 102, 0.05) 45%,
    transparent 72%
  );
}
.has-spotlight.is-lit .spotlight {
  opacity: 1;
}

/* ---------- 首页下方：按钮磁吸（React Bits 的 Magnet） ----------

   位移量由 JS 写在 --mag-x / --mag-y 上，最多 7px。
   注意这里**没有**再写一条 .btn 规则 ——
   文件前面已经有一条 .btn，新增一条会盖掉它原有的
   background / transform / box-shadow 过渡（hover 抬起的手感就没了）。
   所以磁吸用的 translate 是直接加在前面那条 .btn 里的，
   用的是独立的 translate 属性而不是 transform：
   transform 上已经有 hover 的 translateY(-2px)，两者写在一起会互相覆盖。 */

/* 关于页的小标签共用同一套磁吸变量，幅度由 JS 压到 4px。
   translate 只对非 inline 元素生效，.chip 本身就是 inline-flex 的 pill。 */
.chip {
  translate: var(--mag-x, 0px) var(--mag-y, 0px);
  transition: translate 0.22s var(--ease), color 0.3s var(--ease), border-color 0.3s var(--ease);
}

/* 作品卡片跟着光标轻微转（React Bits 的 TiltedCard）。

   perspective 加在网格上、不加在卡片上：加在卡片上等于每张卡各有一个
   消失点，整排一起转的时候会各转各的，看着散。

   只给 .grid-works 里的卡片做，没给详情页那张横贯整行的 .card--deck 做 ——
   它的父级不是这个网格，没有 perspective，rotate 会变成横向压扁，很难看。 */
.grid-works {
  perspective: 1100px;
}
.grid-works .card {
  rotate: y var(--tilt, 0deg);

  /* ⚠ 这里必须把 .reveal 原本那条 transition 一字不差地抄过来，再追加 rotate。

     原因是选择器特异性：`.grid-works .card`（0,2,0）比 `.reveal`（0,1,0）高，
     所以只要我在这条规则里写 transition，无论写在文件哪个位置都会赢 ——
     原本只写 `transition: rotate 0.5s` 就把揭示用的
     opacity / transform 过渡连同 --d 错峰延迟一起干掉了，
     卡片变成瞬间跳出来（实测 96ms 就从不透明变可见，错峰全废）。

     --d 那一项尤其不能丢：卡片一张接一张的节奏全靠它。 */
  transition:
    opacity var(--dur-2) var(--ease-expo) var(--d, 0ms),
    transform var(--dur-2) var(--ease-expo) var(--d, 0ms),
    rotate 0.5s var(--ease);
}

/* 整屏联系收尾 */
.end {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-block: clamp(90px, 14vh, 160px);
  text-align: center;
  border-top: 1px solid var(--line);
}
.end__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(20px, 3vw, 34px);
}

.end__mail {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(1.5rem, 5.4vw, 3.9rem);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--text);
  word-break: break-all;
  max-width: 100%;
  transition: color 0.45s var(--ease);
}
.end__mail::after {
  content: "";
  display: block;
  height: 1px;
  margin-top: 14px;
  background: var(--line);
  transition: background 0.45s var(--ease);
}
.end__mail:hover { color: var(--accent); }
.end__mail:hover::after { background: var(--accent); }

/* 邮件地址上缓慢掠过的一道暖光（React Bits 的 ShinyText）。

   做法是把一条渐变裁进字的形状里（background-clip: text）。
   整套包在 @supports 里：万一浏览器不认，就退回上面那两条纯色规则，
   字绝不会变成透明消失 —— 这是这种写法最容易踩的坑。

   注意 --mail-ink：一旦用了 background-clip: text，
   渐变就接管了字色，原来的 color: var(--accent) 悬停就没作用了。
   所以让渐变的底色跟着一个变量走，悬停时改这个变量。 */
.end__mail { --mail-ink: var(--text); }
.end__mail:hover { --mail-ink: var(--accent); }

@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .end__mail {
    background-image: linear-gradient(100deg,
      var(--mail-ink) 0%, var(--mail-ink) 40%,
      var(--accent-2) 50%,
      var(--mail-ink) 60%, var(--mail-ink) 100%);
    background-size: 250% 100%;
    background-repeat: no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    animation: mailSheen 9s linear infinite;
  }
}

/* 光带从右边扫到左边；周期给到 9 秒，是刻意慢的 ——
   快了就从「金属反光」变成「跑马灯」，那就躁了。 */
@keyframes mailSheen {
  from { background-position: 135% 0; }
  to   { background-position: -35% 0; }
}

/* 用户开了「减少动态效果」就停在中间那一下，不再扫。
   这里单独写一条，不去改文件里已有的那两段 reduce-motion —— 只加不改。 */
@media (prefers-reduced-motion: reduce) {
  .end__mail {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: currentColor;
  }
}

.end__meta {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px 32px;
  font-size: 13.5px;
  letter-spacing: 0.04em;
  color: var(--text-3);
}
.end__meta a {
  color: var(--text-2);
  border-bottom: 1px solid transparent;
  transition: color 0.4s var(--ease), border-color 0.4s var(--ease);
}
.end__meta a:hover { color: var(--accent); border-bottom-color: var(--accent); }

.end__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: clamp(4px, 1.5vw, 14px);
}

/* ---------- 13. 页脚 ---------- */

.footer {
  border-top: 1px solid var(--line);
  padding-block: clamp(44px, 6vw, 72px);
  margin-top: clamp(40px, 6vw, 90px);
}

.footer__inner {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 28px;
  flex-wrap: wrap;
  font-size: 13px;
  color: var(--text-3);
}

.footer__links { display: flex; gap: 20px; }
.footer__links a {
  color: var(--text-2);
  transition: color 0.3s var(--ease);
}
.footer__links a:hover { color: var(--accent); }

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

/* ---------- 14. 图片灯箱 ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 4vw, 56px);
  background: rgba(5, 6, 7, 0.94);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s var(--ease), visibility 0.35s;
}
.lightbox.is-open { opacity: 1; visibility: visible; }

.lightbox__inner {
  max-width: 100%;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}
.lightbox img {
  max-width: 100%;
  max-height: 78svh;
  object-fit: contain;
  border-radius: var(--radius-sm);
}
.lightbox__cap {
  font-size: 13px;
  color: var(--text-2);
  text-align: center;
  max-width: 60ch;
}

.lightbox__close,
.lightbox__nav {
  position: absolute;
  width: 46px; height: 46px;
  display: grid;
  place-items: center;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  background: var(--glass-bg-2);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  color: var(--text);
  font-size: 20px;
  cursor: pointer;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.lightbox__close:hover,
.lightbox__nav:hover { background: rgba(255, 255, 255, 0.16); border-color: var(--accent); }

.lightbox__close { top: clamp(14px, 3vw, 30px); right: clamp(14px, 3vw, 30px); }
.lightbox__nav--prev { left: clamp(14px, 3vw, 30px); top: 50%; transform: translateY(-50%); }
.lightbox__nav--next { right: clamp(14px, 3vw, 30px); top: 50%; transform: translateY(-50%); }

/* ---------- 15. 提示条 ---------- */

.toast {
  position: fixed;
  left: 50%;
  bottom: max(28px, env(safe-area-inset-bottom, 28px));
  transform: translate(-50%, 16px);
  z-index: 180;
  padding: 12px 24px;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius);
  background: rgba(20, 21, 24, 0.72);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  font-size: 13.5px;
  color: var(--text);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }

/* ==========================================================================
   16. 滚动进场动画（动效体系）
   --------------------------------------------------------------------------
   三条规矩，别破：

   1. 只动 transform / opacity / clip-path。
      这三个能上合成层，不触发重排重绘。绝不animate width / height / top /
      letter-spacing / filter 这类要重新计算布局的属性。

   2. 缓动统一用 expo / quart 这种「起步就很快、收尾拖得很长」的曲线。
      高级感基本就是这个收尾给的。全站禁止任何带回弹（back / elastic）的曲线 ——
      不要廉价弹跳。

   3. 时长比一般站点长一档（0.7s ~ 2.4s），节奏慢下来才显得从容。
      要调快调慢，改下面 --dur-* 这一组就够，不用翻到各处去改。

   用法：
     .reveal                —— 默认：从下方 30px 浮上来（说明文字、小元素）
     .reveal--rise          —— 大标题：从遮罩里顶上来 + 纵向拉伸后压缩归位
     .reveal--img           —— 图片：整块从下往上揭开，同时画面从 1.1 倍慢慢落回
     --d: 120ms             —— 行内写在元素上，控制 stagger 的先后
   ========================================================================== */

:root {
  /* 缓动：expo 收尾最慢最丝滑，quart 次之，io 两头都慢（给遮罩揭幕用） */
  --ease-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-quart: cubic-bezier(0.25, 1, 0.5, 1);
  --ease-io: cubic-bezier(0.76, 0, 0.24, 1);
  /* 时长阶梯 */
  --dur-1: 0.7s;
  --dur-2: 1.05s;
  --dur-3: 1.45s;
  --dur-4: 1.9s;
  --dur-5: 2.4s;
}

/* ---- 默认：小幅浮上来 ---- */
.reveal {
  opacity: 0;
  transform: translate3d(0, 30px, 0);
  transition:
    opacity var(--dur-2) var(--ease-expo) var(--d, 0ms),
    transform var(--dur-2) var(--ease-expo) var(--d, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }

/* ---- 大标题：遮罩揭幕 + 位移 + 压缩归位 ----
   ⚠ 静止态里绝对不能写 clip-path。
   Chrome 的 IntersectionObserver 会把元素自身的 clip-path 也算进「可见面积」：
   一开始 inset(100%) 面积就是 0，观察器永远等不到它滚进视口，
   于是标题再也不会出现 —— 死锁。
   所以遮罩只在「is-in 触发的那一次动画」里存在，元素本身的静止态永远是完整的。

   缓动这里用 expo 而不是 ease-in-out：ease-in-out 起步太慢，
   标题还在慢慢起范儿，底下的内容已经落地了，顺序看着就反了。
   expo 是"一上来就大幅度动、最后慢慢收"，标题才压得住场。 */
.reveal--rise {
  opacity: 0;
  transform-origin: 50% 100%;
  transition: opacity var(--dur-3) var(--ease-expo) var(--d, 0ms);
}
.reveal--rise.is-in {
  opacity: 1;
  animation: riseIn var(--dur-3) var(--ease-expo) var(--d, 0ms) both;
}
@keyframes riseIn {
  from { clip-path: inset(100% 0 0 0); transform: translate3d(0, 56px, 0) scaleY(1.16); }
  to   { clip-path: inset(0 0 0 0);   transform: none; }
}

/* ---- 图片：整块从下往上揭开，画面同时从 1.08 倍慢慢落回原位 ----
   同样的道理，静止态用 clip-path 会死锁，所以改用**透明度**藏起来：
   没进场时 opacity:0，进场后立刻给 1，真正的"揭开"交给动画里的 clip-path
   （动画带 both 填充，在延迟那段时间就已经把 from 的遮罩挂上了，不会先露一下）。
   下面这行 opacity 同时堵住一个闪烁：没有它的话，图片在 is-in 之前完全可见，
   等 is-in 一到动画又从"整个遮住"开始 —— 先闪一下、再重新揭开。

   选择器用后代 img 而不是 > img：site.css 第 84 行把 picture 设成了
   display: contents，<picture> 不生成盒子，用直接子选择器选不中。 */
.reveal--img { opacity: 0; }
.reveal--img.is-in {
  opacity: 1;
  animation: imgIn var(--dur-3) var(--ease-io) var(--d, 0ms) both;
}
@keyframes imgIn {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}

.reveal--img.is-in img {
  animation: imgScale var(--dur-4) var(--ease-expo) var(--d, 0ms);
}
@keyframes imgScale {
  from { transform: scale(1.08); }
  to   { transform: none; }
}
/* 带视差的图不动 scale —— 它的 transform 每帧都要被 --py 重写，
   再叠一个动画会在动画结束时「啪」地跳回 1.08 倍 */
.reveal--img.px.is-in img { animation: none; }

/* ---- 轻微视差：图片比画框大一圈，滚动时上下挪几个百分点 ----
   --py 由 app.js 每帧写进来，只写这一个自定义属性，不碰布局。
   这里绝对不能加 transform 过渡 —— 那是每帧都在变的量，加过渡会拖成橡皮筋。 */
.px img {
  transform: scale(1.08) translate3d(0, var(--py, 0px), 0);
  will-change: transform;
}

/* ==========================================================================
   首屏开幕：一条完整的时间线（媒体 → 标题 → 金线 → 小字 → 底栏）
   用 CSS animation 排时间，比一堆 JS 定时器稳，而且全程在合成层上跑。
   没加 is-enter 时元素就是静止的最终状态 —— 所以测试脚本里
   直接 animation: none 就能拿到确定的终态。
   ========================================================================== */

/* 标题的遮罩容器：上下补一圈内边距再把外边距收回去，布局完全不变，
   但那圈内边距让 clip 不会切到字（.title-xl 的行高只有 0.98，字比盒子高） */
.hero__mask {
  display: block;
  overflow: hidden;
  padding-block: clamp(10px, 1.4vw, 22px);
  margin-block: calc(-1 * clamp(10px, 1.4vw, 22px));
}

@keyframes heroMediaIn {
  0%   { transform: scale(1.18); clip-path: inset(0 0 100% 0); }
  55%  { clip-path: inset(0 0 0 0); }
  100% { transform: scale(1); clip-path: inset(0 0 0 0); }
}
@keyframes heroScrimIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes heroTitleIn {
  0%   { transform: translate3d(0, 160%, 0) scaleY(1.34); }
  100% { transform: translate3d(0, 0, 0) scaleY(1); }
}
@keyframes heroRuleIn {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
@keyframes heroFadeUp {
  from { opacity: 0; transform: translate3d(0, 26px, 0); }
  to   { opacity: 1; transform: none; }
}

.hero__title { transform-origin: 50% 100%; will-change: transform; }

body.is-enter .hero__media { animation: heroMediaIn var(--dur-5) var(--ease-expo) both; }
body.is-enter .hero__scrim { animation: heroScrimIn 1.2s var(--ease-expo) 0.42s both; }
body.is-enter .hero__title { animation: heroTitleIn 1.5s var(--ease-expo) 0.14s both; }
body.is-enter .hero__rule  { animation: heroRuleIn 1.35s var(--ease-expo) 0.56s both; }
body.is-enter .hero__tag   { animation: heroFadeUp 1.3s var(--ease-expo) 0.78s both; }
body.is-enter .hero__meta  { animation: heroFadeUp 1.2s var(--ease-expo) 0.96s both; }
body.is-enter .nav__inner  { animation: heroFadeUp 1.1s var(--ease-expo) 0.28s both; }

/* 系统里勾了「减弱动态效果」：全部退回静止状态，不做任何过渡 */
@media (prefers-reduced-motion: reduce) {
  .hero__media, .hero__scrim, .hero__title, .hero__rule,
  .hero__tag, .hero__meta, .nav__inner {
    animation: none !important;
  }
  .reveal, .reveal--rise, .reveal--img,
  .reveal--img img, .px img {
    transition: none !important;
  }
  /* 系统里勾了「减弱动态效果」时，所有遮罩动画都不放，直接就是完整状态 */
  .reveal--rise.is-in, .reveal--img.is-in, .reveal--img.is-in img {
    animation: none !important;
  }
  .px img { transform: none !important; }
}

/* ---------- 17. 响应式 ---------- */

@media (max-width: 1024px) {
  .about-grid { grid-template-columns: 1fr; }
  .portrait { max-width: 340px; }

  .bio { grid-template-columns: 1fr; gap: clamp(26px, 5vw, 40px); }
  .bio__portrait { max-width: 300px; }
}

@media (max-width: 860px) {
  :root { --nav-h: 62px; }

  /* 手机端：全屏玻璃菜单 */
  .nav__toggle { display: grid; place-items: center; }

  .nav__links {
    position: fixed;
    inset: 0;
    z-index: -1;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: 4px;
    padding: 0 var(--gutter);
    border: 0;
    border-radius: 0;
    background: rgba(8, 9, 10, 0.96);
    backdrop-filter: blur(26px) saturate(140%);
    -webkit-backdrop-filter: blur(26px) saturate(140%);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.4s var(--ease), visibility 0.4s;
  }
  body.menu-open .nav__links { opacity: 1; visibility: visible; }

  .nav__links a {
    padding: 18px 0;
    font-family: var(--font-serif);
    font-size: 1.6rem;
    text-align: center;
    border-bottom: 1px solid var(--line);
    background: none;
  }
  .nav__links a[aria-current="page"] { color: var(--accent); background: none; }

  .grid-works { column-count: 1; }
  .gallery { grid-template-columns: 1fr; }


  .specs { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .subnav { top: calc(var(--nav-h) - 1px); }

  .pager { flex-direction: column; align-items: stretch; }
  .pager__next { align-items: flex-start; text-align: left; }
}

@media (max-width: 560px) {
  .hero__meta { font-size: 12px; letter-spacing: 0.12em; }
  .hero__actions .btn { flex: 1 1 100%; justify-content: center; }
  .filters { gap: 18px; }
  .filter { font-size: 14px; }
  .footer__inner { flex-direction: column; }
}

/* 手机上分类一共 6 个，换行会把分割线挤到两行中间很难看。
   改成单行横向滑动，多出来的部分手指划一下就出来。 */
@media (max-width: 860px) {
  .filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 22px;
    padding-bottom: 1px;          /* 给选中下划线留 1px，避免被 overflow 裁掉 */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .filters::-webkit-scrollbar { display: none; }
}

/* ---------- 18. 降低动效 / 打印 ---------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .hero__media img { transform: none !important; }
}

/* ============================================================
   首页「精选项目」：扇形卡牌
   ------------------------------------------------------------
   几张作品卡像手里摊开的牌一样叠成扇面，中间那张正面朝前。
   点侧边的牌会把它翻到中间，翻到中间之后再点才进作品页。

   扇面张开的大小（每张转多少度、往两边挪多少）是 app.js 里
   一个叫 FAN 的对象控制的，卡片尺寸和舞台高度是 initFan() 按
   容器宽度算出来的，所以窄屏会自动收小。
   ============================================================ */
.fan { position: relative; }

.fan__stage {
  position: relative;
  overflow: hidden;          /* 扇面比容器宽，防止把页面撑出横向滚动条 */
  touch-action: pan-y;       /* 横向拖动归我们处理，纵向留给页面滚动 */
}

.fan__card {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--fan-w);
  height: var(--fan-h);
  margin-left: calc(var(--fan-w) / -2);
  margin-top: calc(var(--fan-h) / -2);
  /* 转轴放在卡片下方，转起来才是扇子（上面张开得多，下面收着） */
  transform-origin: 50% 150%;
  transform: translateX(var(--dx, 0px)) translateY(var(--dy, 0px))
             rotate(var(--rot, 0deg)) scale(var(--sc, 1));
  border-radius: clamp(12px, 1.4vw, 20px);
  overflow: hidden;
  background: var(--bg-soft);
  cursor: pointer;
  filter: brightness(0.7) saturate(0.85);
  box-shadow: 0 24px 56px rgba(0, 0, 0, 0.55), var(--glass-inner);
  transition: transform 0.75s var(--ease), filter 0.5s var(--ease),
              opacity 0.5s var(--ease), box-shadow 0.5s var(--ease);
  will-change: transform;
}
.fan__card.is-active {
  cursor: default;
  filter: none;
  box-shadow: 0 32px 76px rgba(0, 0, 0, 0.72), var(--glass-inner);
}
.fan__card:not(.is-active):hover { filter: brightness(0.98) saturate(1); }

.fan__link { display: block; position: absolute; inset: 0; }

.fan__media { position: absolute; inset: 0; }
.fan__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 1.1s var(--ease);
}
.fan__card.is-active .fan__link:hover .fan__media img { transform: scale(1.04); }

/* 只有正面那张压一层暗角 + 写字，侧边的牌就纯粹是牌面，干净 */
.fan__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top,
    rgba(6, 7, 8, 0.84) 0%,
    rgba(6, 7, 8, 0.34) 42%,
    rgba(6, 7, 8, 0) 74%);
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}
.fan__card.is-active .fan__scrim { opacity: 1; }

.fan__body {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: clamp(18px, 2vw, 26px);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.5s var(--ease) 0.1s, transform 0.55s var(--ease) 0.1s;
}
.fan__card.is-active .fan__body { opacity: 1; transform: none; }

.fan__no {
  font-size: 12px;
  letter-spacing: 0.14em;
  color: var(--accent);
}
.fan__title {
  margin: 7px 0 0;
  font-size: clamp(17px, 1.5vw, 21px);
  font-weight: 500;
  line-height: 1.35;
  color: var(--text);
}
.fan__meta {
  display: block;
  margin-top: 8px;
  font-size: 12.5px;
  letter-spacing: 0.03em;
  color: var(--text-2);
}

.fan__dots {
  display: flex;
  justify-content: center;
  gap: 11px;
  margin-top: clamp(18px, 2.4vw, 28px);
}
.fan__dot {
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  cursor: pointer;
  transition: background 0.35s var(--ease), transform 0.35s var(--ease);
}
.fan__dot:hover { background: rgba(255, 255, 255, 0.42); }
.fan__dot.is-on { background: var(--accent); transform: scale(1.45); }
.fan__dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------- 其他可点元素的悬浮反馈 ----------
   统一成同一套语言：平时是灰的/暗的，鼠标一上去就转金、稍微放大一点。
   放在文件后面，直接盖掉上面那些旧的 hover。 */

/* 作品页顶部的分类按钮：转金 + 底下那条线先伸出来一段 */
.filter:hover { color: var(--accent); }
.filter:hover::after { transform: scaleX(0.6); }

/* 扇形卡牌下面的小圆点 */
.fan__dot:hover { background: var(--accent); transform: scale(1.25); }

/* 顶部导航：转成亮金 */
.nav__links a:hover { color: var(--accent-2); }

/* 「全部作品 →」「更多 →」这类文字链 */
.section__more:hover { color: var(--accent-2); }

/* 作品详情页的吸顶导航 */
.subnav__item:hover { color: var(--accent-2); }

/* 「复制」小按钮 */
.copy-btn:hover { border-color: var(--accent); color: var(--accent-2); }


/* ============================================================
   重点文字：加粗 + 跳金调，把信息层级拉开
   ------------------------------------------------------------
   数据文件里写 **这样** 的地方会渲染成 <strong>。
   正文本来是 --text-2 的暖灰，加粗后跳到最亮的金，一眼能看出重点。
   三级层次：标题/加粗 = 最亮，正文 = 中灰，说明小字 = 最暗。
   ============================================================ */
strong, b {
  font-weight: 600;
  color: var(--accent-2);
  font-style: normal;
  letter-spacing: 0.01em;
}

/* 标题已经是最亮的了，里面再嵌加粗不用换色，只要更重 */
h1 strong, h2 strong, h3 strong, h4 strong,
.title-xl strong, .title-l strong, .title-m strong {
  color: inherit;
  font-weight: 700;
}

/* 深色底上的数字、编号用金，弱化的说明文字保持最暗的一档 */
.num, .stat__v { color: var(--accent-2); }

/* ==========================================================================
   首屏（头图）下方那一整片流动渐变背景
   --------------------------------------------------------------------------
   .post-hero 把「个人经历 / 精选项目 / 联系」三屏包在一起，
   里面垫一层 WebGL 画布（assets/js/grainient.js），再压一层薄纱。
   层次从下到上是：画布(0) → 薄纱(0，靠后 → 盖住画布) → 内容(1)。
   所以内容永远压在渐变上面，怎么动都不会糊到字。

   薄纱那层用 rgba(8,9,10,…)（就是 --bg 的色值），
   所以画布没加载出来时它跟纯色底一模一样，看不出差别 —— 失败也是干净的。
   ========================================================================== */
.post-hero {
  position: relative;
  /* 单独开一个层叠上下文，免得里面的 z-index 跟外面打架 */
  isolation: isolate;
}

.post-hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1200ms var(--ease);
  /* 上下两端淡出，跟首屏、页脚之间不会出现一条硬边 */
  -webkit-mask-image: linear-gradient(to bottom,
      rgba(0, 0, 0, 0) 0%, #000 10%, #000 90%, rgba(0, 0, 0, 0) 100%);
  mask-image: linear-gradient(to bottom,
      rgba(0, 0, 0, 0) 0%, #000 10%, #000 90%, rgba(0, 0, 0, 0) 100%);
}

/* 浓度由 JS 写进 --grainient-opacity（data.js 里的 opacity），
   想让它更淡/更明显只改那个数就行 */
.post-hero__bg.is-on { opacity: var(--grainient-opacity, 0.6); }

.post-hero__bg canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* 薄纱：把渐变压暗一点，保证小字（--text-2 / --text-3）还能看清。
   数值是拿真实渲染出来的像素量过对比度之后定的，别随手调小。 */
.post-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(to bottom,
      rgba(8, 9, 10, 0.86) 0%,
      rgba(8, 9, 10, 0.62) 22%,
      rgba(8, 9, 10, 0.64) 55%,
      rgba(8, 9, 10, 0.76) 82%,
      rgba(8, 9, 10, 0.92) 100%);
}

.post-hero > .section,
.post-hero > .end {
  position: relative;
  z-index: 1;
}

/* ==========================================================================
   BorderGlow —— 指针靠近卡片边缘时，边上亮起一道跟着指针走的流光 + 网格渐变描边
   --------------------------------------------------------------------------
   原组件是 React Bits 的 <BorderGlow />。算法和下面这些数值都跟它逐条对齐
   （七个径向渐变的位置、色号分配、七档辉光透明度、锥形遮罩的开口角度、
   悬停渐显 / 移开渐隐的时长、入场扫光的四段补间），只按「照片卡」改了两处：

   1. 不在这里写 position / transform。
      原组件有一句 transform: translate3d(0, 0, 0.01px)（只是为了强制开合成层），
      但扇形卡片是靠自己的 transform 摆位的，写上会把扇形的位移旋转全部冲掉。
      卡片本身已经有 will-change: transform 了，不需要那句。

   2. 描边和色晕放到画面之上（z-index: 1）。
      原组件是用不透明底色把中间盖住、只留最外一圈边；照片卡没有不透明底色，
      所以这里改用 mask 把中间挖空（三层 mask：整块 → 挖掉 padding-box → 剩下 1px 环），
      再和锥形遮罩求交集，效果和原组件一致。
   ========================================================================== */
.border-glow-card {
  --edge-proximity: 0;
  --cursor-angle: 45deg;
  --edge-sensitivity: 30;
  --color-sensitivity: calc(var(--edge-sensitivity) + 20);
  --glow-padding: 40px;
  --cone-spread: 25;
  --fill-opacity: 0.5;
  isolation: isolate;
  overflow: visible;
}

.border-glow-card::before,
.border-glow-card::after,
.border-glow-card > .edge-light {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  transition: opacity 0.25s ease-out;
  z-index: 1;
  pointer-events: none;
}

/* 不悬停、也不在放入场扫光的时候，三层全部淡出 */
.border-glow-card:not(:hover):not(.sweep-active)::before,
.border-glow-card:not(:hover):not(.sweep-active)::after,
.border-glow-card:not(:hover):not(.sweep-active) > .edge-light {
  opacity: 0;
  transition: opacity 0.75s ease-in-out;
}

/* 彩色网格渐变描边（只在最外一圈 + 指针那一侧） */
.border-glow-card::before {
  border: 1px solid transparent;
  background:
    var(--gradient-one, radial-gradient(at 80% 55%, #c99066 0px, transparent 50%)) border-box,
    var(--gradient-two, radial-gradient(at 69% 34%, #e4ae7b 0px, transparent 50%)) border-box,
    var(--gradient-three, radial-gradient(at 8% 6%, #8b5e40 0px, transparent 50%)) border-box,
    var(--gradient-four, radial-gradient(at 41% 38%, #c99066 0px, transparent 50%)) border-box,
    var(--gradient-five, radial-gradient(at 86% 85%, #e4ae7b 0px, transparent 50%)) border-box,
    var(--gradient-six, radial-gradient(at 82% 18%, #8b5e40 0px, transparent 50%)) border-box,
    var(--gradient-seven, radial-gradient(at 51% 4%, #e4ae7b 0px, transparent 50%)) border-box,
    var(--gradient-base, linear-gradient(#c99066 0 100%)) border-box;

  opacity: calc((var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));

  -webkit-mask-image:
    conic-gradient(from var(--cursor-angle) at center,
      #000 calc(var(--cone-spread) * 1%),
      transparent calc((var(--cone-spread) + 15) * 1%),
      transparent calc((100 - var(--cone-spread) - 15) * 1%),
      #000 calc((100 - var(--cone-spread)) * 1%)),
    linear-gradient(#000 0 0),
    linear-gradient(#000 0 0);
  -webkit-mask-clip: border-box, padding-box, border-box;
  -webkit-mask-composite: source-in, xor, source-over;
  mask-image:
    conic-gradient(from var(--cursor-angle) at center,
      #000 calc(var(--cone-spread) * 1%),
      transparent calc((var(--cone-spread) + 15) * 1%),
      transparent calc((100 - var(--cone-spread) - 15) * 1%),
      #000 calc((100 - var(--cone-spread)) * 1%)),
    linear-gradient(#000 0 0),
    linear-gradient(#000 0 0);
  mask-clip: border-box, padding-box, border-box;
  mask-composite: intersect, exclude, add;
}

/* 靠近边缘时铺上来的一层彩色色晕（跟原组件一样用 soft-light，照片上很自然） */
.border-glow-card::after {
  background:
    var(--gradient-one, radial-gradient(at 80% 55%, #c99066 0px, transparent 50%)) padding-box,
    var(--gradient-two, radial-gradient(at 69% 34%, #e4ae7b 0px, transparent 50%)) padding-box,
    var(--gradient-three, radial-gradient(at 8% 6%, #8b5e40 0px, transparent 50%)) padding-box,
    var(--gradient-four, radial-gradient(at 41% 38%, #c99066 0px, transparent 50%)) padding-box,
    var(--gradient-five, radial-gradient(at 86% 85%, #e4ae7b 0px, transparent 50%)) padding-box,
    var(--gradient-six, radial-gradient(at 82% 18%, #8b5e40 0px, transparent 50%)) padding-box,
    var(--gradient-seven, radial-gradient(at 51% 4%, #e4ae7b 0px, transparent 50%)) padding-box,
    var(--gradient-base, linear-gradient(#c99066 0 100%)) padding-box;

  -webkit-mask-image:
    linear-gradient(to bottom, #000, #000),
    radial-gradient(ellipse at 50% 50%, #000 40%, transparent 65%),
    radial-gradient(ellipse at 66% 66%, #000 5%, transparent 40%),
    radial-gradient(ellipse at 33% 33%, #000 5%, transparent 40%),
    radial-gradient(ellipse at 66% 33%, #000 5%, transparent 40%),
    radial-gradient(ellipse at 33% 66%, #000 5%, transparent 40%),
    conic-gradient(from var(--cursor-angle) at center, transparent 5%, #000 15%, #000 85%, transparent 95%);
  mask-image:
    linear-gradient(to bottom, #000, #000),
    radial-gradient(ellipse at 50% 50%, #000 40%, transparent 65%),
    radial-gradient(ellipse at 66% 66%, #000 5%, transparent 40%),
    radial-gradient(ellipse at 33% 33%, #000 5%, transparent 40%),
    radial-gradient(ellipse at 66% 33%, #000 5%, transparent 40%),
    radial-gradient(ellipse at 33% 66%, #000 5%, transparent 40%),
    conic-gradient(from var(--cursor-angle) at center, transparent 5%, #000 15%, #000 85%, transparent 95%);
  -webkit-mask-composite: source-out, source-over, source-over, source-over, source-over, source-over;
  mask-composite: subtract, add, add, add, add, add;

  opacity: calc(var(--fill-opacity, 0.5) * (var(--edge-proximity) - var(--color-sensitivity)) / (100 - var(--color-sensitivity)));
  mix-blend-mode: soft-light;
}

/* 卡片外面那圈会溢出去的辉光 */
.border-glow-card > .edge-light {
  inset: calc(var(--glow-padding) * -1);
  pointer-events: none;
  z-index: 1;
  -webkit-mask-image:
    conic-gradient(from var(--cursor-angle) at center,
      #000 2.5%, transparent 10%, transparent 90%, #000 97.5%);
  mask-image:
    conic-gradient(from var(--cursor-angle) at center,
      #000 2.5%, transparent 10%, transparent 90%, #000 97.5%);
  opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));
  mix-blend-mode: plus-lighter;
}

/* 七档内阴影叠出来的辉光，从 1px 的芯一直糊到 50px 外 */
.border-glow-card > .edge-light::before {
  content: "";
  position: absolute;
  inset: var(--glow-padding);
  border-radius: inherit;
  box-shadow:
    inset 0 0 0 1px var(--glow-color, hsl(40deg 80% 80% / 100%)),
    inset 0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
    inset 0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
    inset 0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
    inset 0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
    inset 0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
    inset 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)),
    0 0 1px 0 var(--glow-color-60, hsl(40deg 80% 80% / 60%)),
    0 0 3px 0 var(--glow-color-50, hsl(40deg 80% 80% / 50%)),
    0 0 6px 0 var(--glow-color-40, hsl(40deg 80% 80% / 40%)),
    0 0 15px 0 var(--glow-color-30, hsl(40deg 80% 80% / 30%)),
    0 0 25px 2px var(--glow-color-20, hsl(40deg 80% 80% / 20%)),
    0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));
}

/* 触屏没有 hover，指针追踪没意义；放入场扫光时还是能亮一下 */
@media (hover: none) {
  .border-glow-card:not(.sweep-active)::before,
  .border-glow-card:not(.sweep-active)::after,
  .border-glow-card:not(.sweep-active) > .edge-light { opacity: 0 !important; }
}

/* --------------------------------------------------------------------------
   扇形卡片要为这层辉光让路：
   卡片原来是 overflow: hidden（把照片裁成圆角），那样辉光整圈都会被剪掉。
   所以改成卡片自身不裁，把「裁圆角」这件事交给里面贴满整张的 .fan__link。
   卡片的尺寸、位置、旋转完全没动，看起来跟以前一模一样。
   -------------------------------------------------------------------------- */
.fan__card { overflow: visible; }

.fan__link {
  overflow: hidden;
  border-radius: inherit;
}

/* 关了动效、或者浏览器不给 WebGL 时，画布本来就是空的，
   薄纱跟纯色底同色，所以这里什么都不用特别处理 */
@media print {
  .post-hero__bg { display: none !important; }
  .nav, .progress, .grain, .lightbox, .toast, .fan { display: none !important; }
  body { background: #fff; color: #000; }
  .reveal { opacity: 1; transform: none; }
}
