/* ============================================================
   Liu Ke — Personal Digital Space
   Cinematic / Minimal / Editorial / Digital / Personal
   Dark theme design tokens & shared styles
   ============================================================ */

/* 氛围层三段光雾注册为 <color> 自定义属性。
   必须注册（@property）才能被 transition 插值：未注册的自定义属性是「离散跳变」的，
   主题切换时 --amb-* 瞬间换值，而 background-color 走 320ms 过渡，两者不同步
   会在深→浅切换的头 ~300ms 内出现「暗底 + 已是浅色主题的白色光雾」白雾闪烁。
   实测：仅加 background-image 过渡无效（渐变仍瞬跳，档位数=1）；
   注册后再把 transition 挂在 --amb-* 本身，即可逐帧插值（见下方 theme-anim 段）。 */
@property --amb-1 { syntax: "<color>"; inherits: true; initial-value: rgba(143, 184, 216, 0.20); }
@property --amb-2 { syntax: "<color>"; inherits: true; initial-value: rgba(122, 142, 196, 0.15); }
@property --amb-3 { syntax: "<color>"; inherits: true; initial-value: rgba(108, 138, 178, 0.10); }

/* ---------- Tokens ---------- */
:root {
  color-scheme: dark;        /* UA 画布/滚动条与暗色一致；与浅色的 color-scheme: light 对称 */
  /* color
     暗色基底不取近纯黑：#0b0d10 相对亮度仅 0.40%，大面积平铺会掉进「死黑」，
     读感压抑且丢失明度层次。改为带冷调（蓝偏）的夜空梯度——同样是深色，
     但因色相偏移与逐级抬升的明度，视觉上呈现「空气」而非「板」；
     实测死黑像素占比由 95%+ 降至可辨层次区间。 */
  --bg: #0e1117;              /* L 0.56% */
  --bg-soft: #131720;
  --bg-card: #191f2a;         /* 与 bg 分离度 ΔL 0.80%，卡片可辨但不割裂 */
  --bg-elevated: #202734;
  --text: #e9ebee;
  --text-dim: #9aa3af;
  --text-faint: #78838f;      /* 原 #626c78 在新背景上仅 3.5:1，未达 AA；提至 4.9:1 */
  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);
  --accent: #8fb8d8;          /* mist blue */
  --accent-soft: rgba(143, 184, 216, 0.14);
  --accent-line: rgba(143, 184, 216, 0.35);
  --green-soft: #9dc4a7;

  /* 氛围层：全站背景的三段光雾（冷调，不引入新色相家族）
     挂在 body::before（可动层，见下方 Ambient atmosphere 段）。
     强度经实测标定：0.10 时死黑区仍占 40%，0.20 降至 ~18%、
     中位亮度 1.02%→1.41%。本次按反馈再提亮一档至 0.24/0.18/0.12，
     实测死黑区进一步降至个位数百分比，仍保持暗色主题基调不泛灰。 */
  --amb-1: rgba(143, 184, 216, 0.24);   /* 顶部天光：面积最大，抬升整体明度 */
  --amb-2: rgba(122, 142, 196, 0.18);   /* 右下冷紫反衬：破单色沉闷 */
  --amb-3: rgba(108, 138, 178, 0.12);   /* 中部呼吸区：视线落点微微隆起 */
  --amb-grain: 0.022;                   /* 噪点不透明度：消除大面积渐变的塑料感 */

  /* typography */
  --font-sans: "Inter", "Helvetica Neue", Helvetica, Arial,
    "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
  --text-xs: 0.75rem;
  --text-sm: 0.8125rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.375rem;
  --text-2xl: 1.75rem;
  --text-3xl: clamp(1.75rem, 3.5vw, 2.5rem);
  --text-hero: clamp(3rem, 8.5vw, 6.5rem);
  --text-display: clamp(2.25rem, 5.5vw, 4rem);

  /* spacing (8pt) */
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-28: 112px;

  /* layout */
  --container: 1240px;
  --gutter: clamp(20px, 5vw, 64px);
  --radius-md: 10px;
  --radius-lg: 16px;

  /* motion
     --ease-in-out 此前被 4 处 animation 简写引用（hero-drift / scroll-dot /
     chev-down / 氛围层）但从未定义。CSS 中未定义的 var() 会令整条声明失效，
     导致这些 animation 静默不生效（computed animation-name 回退为 none），
     且不报错、难以察觉。现补齐定义，三处既有动画随之恢复。 */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-fast: 160ms;
  --dur-base: 280ms;
  --dur-slow: 700ms;
}

/* ---------- Reset & base ---------- */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  /* html 自身持底色：VT root 快照自带背景，不依赖 body 向画布传播（跨文档过渡防闪烁关键）
     底色与氛围层定义见下方 "Ambient atmosphere" 段 */
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* 滚动条槽位常驻：经典滚动条下 overflow 为 auto/scroll/hidden 均保留 gutter（MDN 规范行为），
     页面切换、刷新与开场锁（is-locked → overflow:hidden）全程内容零横移；
     overlay 滚动条（macOS/移动端）本就不占空间，此声明无副作用 */
  scrollbar-gutter: stable;
}

/* ---------- Ambient atmosphere (site-wide) ----------
   内页原本是一整片 --bg 平铺：实测 95%~99% 像素落在 L<1% 的死黑区，
   量化色数低至 17~41 色，视觉等同死黑板。此层为全站补三段低透明度光雾 +
   极淡噪点，制造明度梯度与空气感；均为低透明度叠加，不改变文字对比度。

   实现要点（曾踩坑，勿改回）：
   1) 氛围层不能只挂 html 的 background-image —— 需要逐帧动画时无法只动
      background-position：那会每帧重绘整幅文档背景（掉帧，且与
      background-attachment:fixed 叠加开销更大）。故改为独立 fixed 图层，
      用 transform / opacity 驱动（GPU 合成，不触发 layout / paint 重排）。
   2) 但也不能简单用 body::before + z-index:-1 挂回 html —— body 未建立
      层叠上下文（无 isolation/transform），其负层级 ::before 并入「根层叠
      上下文」第 2 步绘制，而 body 自身不透明背景在第 3 步，会把图层完整
      遮挡。红色探针实测：正常态红像素 0 个；body 背景改透明后 1,249,853 个。
      故 body 背景保持 transparent，底色由 html 承担（VT root 快照要求）。
   3) 氛围层放大到 120%（inset:-10%），位移循环期间不会露出边缘。 */
html {
  background-color: var(--bg);
}

/* 氛围层（可动）：三段光雾。放大 120% 后用 translate 循环位移，
   上下端点一致 → 无缝循环；inset:-10% 保证位移期间不露边。 */
body::before {
  content: "";
  position: fixed;
  inset: -10%;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(120% 82% at 16% -8%, var(--amb-1) 0%, transparent 58%),
    radial-gradient(105% 72% at 92% 106%, var(--amb-2) 0%, transparent 62%),
    radial-gradient(78% 55% at 50% 42%, var(--amb-3) 0%, transparent 70%);
  background-repeat: no-repeat;
  will-change: transform, opacity;
  animation:
    amb-drift 46s var(--ease-in-out) infinite alternate,
    amb-breathe 31s ease-in-out infinite alternate;
}

/* 位移循环：极缓慢的大幅漂移（±2%），营造「空气在流动」而非「背景在动」。
   周期 46s 远长于眨眼与呼吸节律，不构成视觉干扰。 */
@keyframes amb-drift {
  from { transform: translate3d(-1.9%, 1.4%, 0) scale(1.015); }
  to   { transform: translate3d(1.9%, -1.4%, 0) scale(1.05); }
}

/* 亮度呼吸：透明度 0.86 ↔ 1 缓慢起伏，模拟环境光变化。
   周期 31s 与位移的 46s 互质，避免长时间观察时出现可察觉的同步节拍。 */
@keyframes amb-breathe {
  from { opacity: 0.86; }
  to   { opacity: 1; }
}

/* 噪点：必须独立成层以承载 opacity。feTurbulence 输出本身是不透明灰阶，
   若直接混入 html 的 background-image 列表将失去透明度控制、整片压灰页面
   （曾实测：平均亮度飙至 16%、暗色主题被冲成浅灰，文字对比度破坏）。
   改用 ::after 让氛围层独占 ::before；二者 z-index 同为 -1，
   按 DOM 顺序噪点绘于氛围层之上。 */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--amb-grain);
  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='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* 减弱动效：停掉漂移与呼吸，保留静态氛围层（不剥夺明度层次，只停运动）。 */
@media (prefers-reduced-motion: reduce) {
  body::before {
    animation: none;
    opacity: 0.95;
  }
}

body {
  margin: 0;
  background: transparent;   /* 底色与氛围层统一由 html 承担，见上方说明 */
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.65;
  letter-spacing: 0.01em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out);
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

p { margin: 0; }

::selection { background: var(--accent-soft); color: var(--text); }

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

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  transition: background var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    backdrop-filter var(--dur-base) var(--ease-out);
  border-bottom: 1px solid transparent;
}

.site-header.is-scrolled {
  background: rgba(11, 13, 16, 0.72);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom-color: var(--border);
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  height: 76px;
}

.logo {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-base);
  letter-spacing: 0.04em;
}

.logo-mark {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  font-weight: 700;
  font-size: 1.05rem;
  background: rgba(255, 255, 255, 0.04);
}

.logo-divider {
  width: 1px;
  height: 18px;
  background: var(--border-strong);
}

.logo-name {
  color: var(--text);
  font-weight: 500;
}

.header-right {
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.nav-links a {
  position: relative;
  font-size: var(--text-sm);
  color: var(--text-dim);
  letter-spacing: 0.06em;
  padding: 6px 0;
}

.nav-links a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1.5px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}

.nav-links a:hover,
.nav-links a.is-active { color: var(--text); }

.nav-links a.is-active::after,
.nav-links a:hover::after { transform: scaleX(1); }

.lang-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 14px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  font-family: inherit;
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}

.lang-btn:hover {
  color: var(--text);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

/* burger (mobile) */
.burger {
  display: none;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  position: relative;
}

.burger span,
.burger::before,
.burger::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 16px;
  height: 1.5px;
  background: var(--text);
  transform: translateX(-50%);
  transition: transform var(--dur-base) var(--ease-out),
    opacity var(--dur-base) var(--ease-out);
}

.burger::before { top: 13px; }
.burger span { top: 19px; }
.burger::after { top: 25px; }

.burger.is-open::before { transform: translateX(-50%) translateY(6px) rotate(45deg); }
.burger.is-open span { opacity: 0; }
.burger.is-open::after { transform: translateX(-50%) translateY(-6px) rotate(-45deg); }

/* ---------- Hero (home) ---------- */
.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  isolation: isolate;
}

.hero-bg {
  position: absolute;
  inset: 0;
  z-index: -2;
}

.hero-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 38%;
  animation: hero-drift 24s var(--ease-in-out) infinite alternate;
  will-change: transform;
}

@keyframes hero-drift {
  from { transform: scale(1.02); }
  to { transform: scale(1.09); }
}

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(90deg, rgba(8, 10, 13, 0.66) 0%, rgba(8, 10, 13, 0.28) 46%, rgba(8, 10, 13, 0.12) 100%),
    linear-gradient(180deg, rgba(8, 10, 13, 0.5) 0%, rgba(8, 10, 13, 0) 30%, rgba(11, 13, 16, 0.55) 78%, var(--bg) 100%);
}

.hero-content {
  padding-top: 76px;
  max-width: 760px;
}

.hero-eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  color: var(--text-dim);
  font-size: var(--text-base);
  letter-spacing: 0.08em;
  margin-bottom: var(--space-4);
}

.hero-eyebrow .rule {
  width: 56px;
  height: 1px;
  background: var(--accent-line);
}

.hero-eyebrow .dot { color: var(--text-faint); margin: 0 2px; }

.hero-title {
  font-size: var(--text-hero);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin-bottom: var(--space-6);
}

/* "Kezn" -> kaomoji on hover：双层同格叠放交叉模糊淡变（参考 kezn.top initNick）。
   原字上移+模糊淡出、新字自下+去模糊淡入，两层同时过渡，无换字空档；
   容器宽度由 JS 按目标文字实测值设定并平滑过渡，布局零跳动。
   模糊/位移用 em：随 --text-hero（3rem~6.5rem）自动缩放，与参考站比例一致。 */
.hero-kaomoji {
  display: inline-grid;
  grid-template-columns: max-content;
  justify-items: start;      /* 叠放层各自按自身内容宽度靠左，实测宽度才有意义 */
  white-space: nowrap;       /* 颜文字整体不折行 */
  font-weight: 600;
  font-variant-ligatures: none;
  overflow: visible;
  transition: width 320ms var(--ease-out);
}

.hero-face {
  grid-area: 1 / 1;
  white-space: nowrap;
  line-height: inherit;      /* 不撑高 h1 行盒 */
  transition: opacity 320ms var(--ease-out),
    transform 320ms var(--ease-out),
    filter 320ms var(--ease-out);
}

/* 潜伏态（新字入场起点 / 旧字离场终点） */
.hero-face.hero-alt {
  opacity: 0;
  transform: translateY(0.075em);
  filter: blur(0.1em);
  pointer-events: none;
}

/* 交替态：被选中的颜文字清晰入场 */
.hero-kaomoji.is-alt .hero-face.is-on {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

/* 交替态：原字 Kezn 模糊离场 */
.hero-kaomoji.is-alt .hero-face:not(.hero-alt) {
  opacity: 0;
  transform: translateY(-0.075em);
  filter: blur(0.1em);
}

@media (prefers-reduced-motion: reduce) {
  .hero-kaomoji,
  .hero-face { transition: none; }
}

/* 桌面端标题保持单行；字号 8.5vw 随视口缩放，整体宽度不超视口，仅超出 760px 内容盒右侧（空白区，无裁切） */
@media (min-width: 860px) {
  .hero-title { white-space: nowrap; }
}

/* ---------- TechText interactive wordmark（React Bits TechText 原生移植版） ----------
   Canvas 词标取代原 hero-kaomoji 悬浮换字：指针附近字形转虚线轮廓、
   字母可拖拽弹簧回位、选框 + 尺寸标签 + specks 粒子、空闲自动扫描。
   颜色由 JS 从 --text / --accent 令牌解析，随主题切换重建。 */
.tech-text {
  position: relative;
  width: 100%;
  height: 100%;
  touch-action: pan-y;      /* 允许纵向滚动，横向拖拽字母仍生效 */
  user-select: none;
  -webkit-user-select: none;
}

.tech-text-canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* hero 集成：容器高 1.35em（相对 hero 字号），上下各留 ~0.31em，
   为选框（字形外扩 6px）、标签（框上方 ~17px）与 specks 粒子（框外 ~21px）预留画布空间；
   字号 auto 跟随 h1 的 clamp 值，宽高约束由组件内部自适应缩放。 */
.hero-title .tech-text {
  height: 1.35em;
  margin: 0;
}

@media (prefers-reduced-motion: reduce) {
  /* 空闲扫描已在 JS 内禁用；此处确保无过渡残留 */
  .tech-text { touch-action: auto; }
}

/* ---------- Screen-reader only ---------- */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.hero-desc {
  color: var(--text-dim);
  font-size: var(--text-lg);
  line-height: 1.8;
  max-width: 34em;
  margin-bottom: var(--space-10);
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 48px;
  padding: 0 26px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: rgba(255, 255, 255, 0.06);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--text);
  font-family: inherit;
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.btn .arrow { transition: transform var(--dur-base) var(--ease-out); }

.btn:hover {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.btn:hover .arrow { transform: translateX(4px); }
.btn:active { transform: scale(0.98); }

.hero-foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  padding-bottom: var(--space-6);
  pointer-events: none;
}

.hero-foot-inner {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
}

.scroll-hint {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  padding-bottom: 20px;
}

.scroll-hint .mouse {
  width: 18px;
  height: 30px;
  border: 1.5px solid var(--text-faint);
  border-radius: 999px;
  position: relative;
}

.scroll-hint .mouse::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 4px;
  width: 4px;
  height: 8px;
  border-radius: 999px;
  background: var(--text);
  transform: translateX(-50%);
  animation: scroll-dot 1.8s var(--ease-in-out) infinite;
}

@keyframes scroll-dot {
  0% { transform: translate(-50%, 0); opacity: 0; }
  25% { transform: translate(-50%, 3px); opacity: 1; }
  70% { transform: translate(-50%, 13px); opacity: 1; }
  100% { transform: translate(-50%, 15px); opacity: 0; }
}

.scroll-hint .chev {
  position: absolute;
  left: 4.5px;
  bottom: 0;
  width: 9px;
  height: 9px;
  border-right: 2px solid var(--text);
  border-bottom: 2px solid var(--text);
  transform: rotate(45deg);
  animation: chev-down 1.8s var(--ease-in-out) infinite;
}

@keyframes chev-down {
  0%, 100% { transform: rotate(45deg) translate(0, 0); opacity: 0.15; }
  45% { transform: rotate(45deg) translate(3px, 3px); opacity: 1; }
  75% { transform: rotate(45deg) translate(4px, 4px); opacity: 0.4; }
}

.hero-tags {
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.16em;
}

/* ---------- Sections (shared) ---------- */
.section {
  padding: var(--space-28) 0;
}

.section-label {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.28em;
  margin-bottom: var(--space-4);
}

.section-label::after {
  content: "";
  width: 48px;
  height: 1px;
  background: var(--accent-line);
}

.section-title {
  font-size: var(--text-3xl);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.section-intro {
  color: var(--text-dim);
  margin-top: var(--space-4);
  max-width: 40em;
}

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-8);
  margin-bottom: var(--space-16);
}

.link-more {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text-dim);
  font-size: var(--text-sm);
  letter-spacing: 0.06em;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--border-strong);
  transition: color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.link-more:hover { color: var(--text); border-color: var(--accent-line); }
.link-more .arrow { transition: transform var(--dur-base) var(--ease-out); }
.link-more:hover .arrow { transform: translateX(4px); }

/* ---------- Selected project cards (home) ---------- */
.project-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}

/* 卡片用 grid 堆叠，而非「封面固定高 + 文字绝对定位覆盖」。
   原实现下 .cover 由 aspect-ratio 定高、.overlay 绝对定位挂底；当文字高度超过封面
   （卡片变窄或切到英文）时，overlay 会溢出卡片并压到下一张卡上。
   实测溢出量：中/EN 在 360px 分别 +184/+204px；**EN 即使 1920px 仍 +26px**
   （卡片宽被容器上限 1240px 锁在 538px），故媒体查询断点无法解决，必须改结构。
   现将 cover 与 overlay 叠放进同一 grid 区域：行高取两者较大值，文字再长
   也会把卡片「撑高」而非越界；封面随之拉伸（align-self:stretch），
   视觉上仍是「文字压在图上」的原设计。 */
.project-card {
  display: grid;
  grid-template-columns: 1fr;
  position: relative;
  border-radius: var(--radius-lg);
  /* BorderGlow 外光束层需溢出卡片边界，故放开裁切；
     封面圆角裁切职责转交 .cover 自身（见下方 border-radius 补偿） */
  overflow: visible;
  border: 1px solid var(--border);
  background: var(--bg-card);
  isolation: isolate;
}

.project-card .cover {
  grid-column: 1;
  grid-row: 1;
  align-self: stretch;          /* 随行高拉伸，撑满被文字顶高的卡片 */
  /* 关键：必须显式 width:100%。父级为 grid，本轨道由 aspect-ratio 反推宽度时
     cover 会溢出到 404px（> 卡片 338px），进而 document.scrollWidth 被撑到 507px，
     使 position:fixed 的 header / 手机端菜单层按被撑宽的视口布局 → 菜单偏移。
     min-width:0 与 img max-width 均无效（实测），只有 width:100% 能约束。
     浅色主题无需覆写。 */
  width: 100%;
  position: relative;
  aspect-ratio: 21 / 9;
  overflow: hidden;
}

.project-card .cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.01);
  transition: transform 900ms var(--ease-out);
}

.project-card:hover .cover img { transform: scale(1.05); }

.project-card .cover::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(11, 13, 16, 0.05) 30%, rgba(11, 13, 16, 0.78) 100%);
  transition: opacity var(--dur-slow) var(--ease-out);
}

.project-card:hover .cover::after { opacity: 0.55; }

.project-card .overlay {
  grid-column: 1;
  grid-row: 1;
  align-self: end;               /* 仍贴卡片底部，保持原有阅读位置 */
  padding: var(--space-10) var(--space-12);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  z-index: 1;
}

.project-card .idx {
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.2em;
  margin-bottom: var(--space-2);
}

.project-card .name {
  font-size: var(--text-2xl);
  font-weight: 600;
}

.project-card .meta {
  margin-top: var(--space-2);
  color: var(--text-dim);
  font-size: var(--text-sm);
  letter-spacing: 0.05em;
}

.project-card .tags {
  margin-top: var(--space-3);
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* 基础 tag：用于页面表面（案例页 hero 等），中性描边 chip，双主题均成立。
   深色玻璃底 + 毛玻璃模糊仅属于图片浮层场景，见 .project-card .overlay .tag */
.tag {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--text-dim);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.project-card .cta {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}

.project-card:hover .cta { opacity: 1; transform: translateY(0); }

.project-card .cta .arrow { transition: transform var(--dur-base) var(--ease-out); }
.project-card .cta:hover .arrow { transform: translateX(4px); }

/* ============================================================
   Project card — BorderGlow（React Bits 正版组件 · React bundle）
   assets/js/border-glow-react.js 以「领养模式」挂载：
   [data-border-glow] 槽内的既有卡片 DOM 被原封搬入壳内
   （.adopted-host 为 display:contents，布局透明），
   reveal 入场 / 双语切换 / 项目过滤均不受影响。
   壳参数（雾蓝光束 + strands 三色 mesh 渐变）由入口默认值
   传入，与全站色彩体系同源；backgroundColor 随主题切换。
   ============================================================ */
/* 原版组件 CSS（React Bits BorderGlow.css，补 -webkit mask 前缀） */
.border-glow-card {
  --edge-proximity: 0;
  --cursor-angle: 45deg;
  --edge-sensitivity: 30;
  --color-sensitivity: calc(var(--edge-sensitivity) + 20);
  --border-radius: 28px;
  --glow-padding: 40px;
  --cone-spread: 25;

  position: relative;
  border-radius: var(--border-radius);
  isolation: isolate;
  transform: translate3d(0, 0, 0.01px);
  display: grid;
  border: 1px solid rgb(255 255 255 / 15%);
  background: var(--card-bg, #120F17);
  overflow: visible;
  box-shadow:
    rgba(0, 0, 0, 0.1) 0px 1px 2px,
    rgba(0, 0, 0, 0.1) 0px 2px 4px,
    rgba(0, 0, 0, 0.1) 0px 4px 8px,
    rgba(0, 0, 0, 0.1) 0px 8px 16px,
    rgba(0, 0, 0, 0.1) 0px 16px 32px,
    rgba(0, 0, 0, 0.1) 0px 32px 64px;
}

.border-glow-card--light {
  border-color: rgb(24 24 27 / 12%);
  box-shadow:
    rgb(24 24 27 / 4%) 0 1px 2px,
    rgb(24 24 27 / 5%) 0 8px 24px;
}

.border-glow-card--light::after,
.border-glow-card--light > .edge-light {
  mix-blend-mode: normal;
}

.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;
}

.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;
  /* 【性能】非交互态彻底退出光效渲染。
     仅 opacity:0 不等于「不重绘」：只要 --cursor-angle / --edge-proximity
     还在被 JS 写入，浏览器仍会重算 conic-gradient + mask 遮罩。
     visibility:hidden 让伪元素完全跳过绘制，且可被 transition 恢复；
     相比 content-visibility，伪元素上唯一真正生效的「跳过渲染」开关。 */
  visibility: hidden;
}

/* 悬停 / 扫光激活时恢复可见并交还 pointer 事件 */
.border-glow-card:hover::before,
.border-glow-card:hover::after,
.border-glow-card:hover > .edge-light,
.border-glow-card.sweep-active::before,
.border-glow-card.sweep-active::after,
.border-glow-card.sweep-active > .edge-light {
  visibility: visible;
}

/* 滚动期间由 <html class="lk-scrolling"> 统一关闭光标追踪：
   滚动是移动端掉帧的主场景，此时光标不在卡片上，
   发光层的逐帧遮罩重算是纯浪费。滚动停止 160ms 后由 JS 摘除该类。 */
html.lk-scrolling .border-glow-card::before,
html.lk-scrolling .border-glow-card::after,
html.lk-scrolling .border-glow-card > .edge-light {
  transition: none !important;
}

/* colored mesh-gradient border */
.border-glow-card::before {
  border: 1px solid transparent;
  background:
    linear-gradient(var(--card-bg, #120F17) 0 100%) padding-box,
    linear-gradient(rgb(255 255 255 / 0%) 0% 100%) border-box,
    var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) border-box,
    var(--gradient-base, linear-gradient(#c299ff 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,
      black calc(var(--cone-spread) * 1%),
      transparent calc((var(--cone-spread) + 15) * 1%),
      transparent calc((100 - var(--cone-spread) - 15) * 1%),
      black calc((100 - var(--cone-spread)) * 1%)
    );
  mask-image:
    conic-gradient(
      from var(--cursor-angle) at center,
      black calc(var(--cone-spread) * 1%),
      transparent calc((var(--cone-spread) + 15) * 1%),
      transparent calc((100 - var(--cone-spread) - 15) * 1%),
      black calc((100 - var(--cone-spread)) * 1%)
    );
}

/* colored mesh-gradient background fill near edges */
.border-glow-card::after {
  border: 1px solid transparent;
  background:
    var(--gradient-one, radial-gradient(at 80% 55%, hsla(268, 100%, 76%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-two, radial-gradient(at 69% 34%, hsla(349, 100%, 74%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-three, radial-gradient(at 8% 6%, hsla(136, 100%, 78%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-four, radial-gradient(at 41% 38%, hsla(192, 100%, 64%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-five, radial-gradient(at 86% 85%, hsla(186, 100%, 74%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-six, radial-gradient(at 82% 18%, hsla(52, 100%, 65%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-seven, radial-gradient(at 51% 4%, hsla(12, 100%, 72%, 1) 0px, transparent 50%)) padding-box,
    var(--gradient-base, linear-gradient(#c299ff 0 100%)) padding-box;

  -webkit-mask-image:
    linear-gradient(to bottom, black, black),
    radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),
    radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),
    conic-gradient(from var(--cursor-angle) at center, transparent 5%, black 15%, black 85%, transparent 95%);
  mask-image:
    linear-gradient(to bottom, black, black),
    radial-gradient(ellipse at 50% 50%, black 40%, transparent 65%),
    radial-gradient(ellipse at 66% 66%, black 5%, transparent 40%),
    radial-gradient(ellipse at 66% 33%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 66%, black 5%, transparent 40%),
    radial-gradient(ellipse at 33% 33%, black 5%, transparent 40%),
    conic-gradient(from var(--cursor-angle) at center, transparent 5%, black 15%, black 85%, transparent 95%);
  -webkit-mask-composite: source-over, 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;
}

/* outer glow layer */
.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, black 2.5%, transparent 10%, transparent 90%, black 97.5%
    );
  mask-image:
    conic-gradient(
      from var(--cursor-angle) at center, black 2.5%, transparent 10%, transparent 90%, black 97.5%
    );

  opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));
  mix-blend-mode: plus-lighter;
}

.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%));
}

.border-glow-inner {
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: auto;
  z-index: 1;
}

/* ============================================================
   站点调和（项目卡片场景）
   ============================================================ */
/* 槽替代卡片成为列表/网格项；hover 抬升层级，外光束不被相邻槽截断 */
.bg-slot {
  position: relative;
}

.bg-slot:has(:hover) {
  z-index: 2;
}

/* 卡片视觉让位给发光壳：边框/底色/圆角/阴影由壳承担 */
.border-glow-card .project-card {
  border: 0;
  background: transparent;
  border-radius: 0;
}

/* adopted 宿主布局透明：卡片直接参与 .border-glow-inner 排布 */
.adopted-host {
  display: contents;
}

/* 封面圆角从壳内缘推导（壳 1px 边框 + 圆角），四角全圆与壳永远吻合
   （卡片流内高度即封面高度，封面底缘 = 卡片底缘） */
.border-glow-card .project-card .cover {
  border-radius: calc(var(--border-radius) - 1px);
}

/* 卡片高度自适应，壳内无需滚动 */
.border-glow-card .border-glow-inner {
  overflow: visible;
}

/* ---------- Typographic covers (projects w/o image) ---------- */
.cover-type {
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
}

.cover-type .glyph {
  font-size: clamp(3rem, 9vw, 7rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: rgba(143, 184, 216, 0.16);
  user-select: none;
  transition: transform 900ms var(--ease-out), color var(--dur-slow) var(--ease-out);
}

.project-card:hover .cover-type .glyph {
  transform: scale(1.06);
  color: rgba(143, 184, 216, 0.28);
}

.cover-type.t-mountain { background: radial-gradient(120% 140% at 20% 15%, #17222e 0%, #0d1219 55%, #0a0d12 100%); }
.cover-type.t-drone { background: radial-gradient(120% 140% at 80% 20%, #1a2430 0%, #0e131a 55%, #0a0d12 100%); }
.cover-type.t-box { background: radial-gradient(120% 140% at 30% 80%, #1d1a26 0%, #0f0d16 55%, #0a0d12 100%); }

.cover-type .sub {
  position: absolute;
  bottom: 18px;
  right: 22px;
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.2em;
}

/* ---------- Notes list ---------- */
.note-list {
  border-top: 1px solid var(--border);
}

.note-row {
  display: grid;
  grid-template-columns: 130px 150px 1fr auto;
  gap: var(--space-6);
  align-items: baseline;
  padding: var(--space-6) var(--space-2);
  border-bottom: 1px solid var(--border);
  position: relative;
  transition: background var(--dur-base) var(--ease-out);
}

.note-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: 12px;
  bottom: 12px;
  width: 2px;
  border-radius: 1px;
  background: var(--accent);
  opacity: 0;
  transform: scaleY(0);
  transform-origin: top;
  transition: transform var(--dur-base) var(--ease-out),
    opacity var(--dur-base) var(--ease-out);
}

.note-row:hover {
  background: rgba(255, 255, 255, 0.025);
}

.note-row:hover::before {
  opacity: 1;
  transform: scaleY(1);
}

.note-row .date {
  color: var(--text-faint);
  font-size: var(--text-sm);
  letter-spacing: 0.1em;
}

.note-row .cat {
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.2em;
}

.note-row .body h3 {
  font-size: var(--text-lg);
  font-weight: 500;
  margin-bottom: 4px;
}

.note-row .body p {
  color: var(--text-faint);
  font-size: var(--text-sm);
}

.note-row .arrow {
  color: var(--text-faint);
  transition: color var(--dur-base) var(--ease-out);
  align-self: center;
}

.note-row:hover .arrow {
  color: var(--accent);
}

/* ---------- About (home preview + about page) ---------- */
.about-grid {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: var(--space-16);
  align-items: start;
}

.about-lead {
  font-size: var(--text-display);
  font-weight: 600;
  line-height: 1.18;
  letter-spacing: -0.015em;
}

.about-lead .accent { color: var(--accent); }

.about-text {
  color: var(--text-dim);
  line-height: 1.9;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-top: var(--space-2);
}

/* capability keywords */
.cap-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-4);
}

.cap-card {
  padding: var(--space-8) var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  transition: border-color var(--dur-base) var(--ease-out),
    background var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}

.cap-card:hover {
  border-color: var(--accent-line);
  background: var(--bg-elevated);
  transform: translateY(-4px);
}

.cap-card .num {
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.2em;
}

.cap-card h3 {
  margin: var(--space-3) 0 var(--space-4);
  font-size: var(--text-xl);
  letter-spacing: 0.14em;
}

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

.cap-card li {
  color: var(--text-dim);
  font-size: var(--text-sm);
}

/* timeline */
.timeline {
  position: relative;
  margin-top: var(--space-16);
  padding-left: var(--space-8);
  border-left: 1px solid var(--border-strong);
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
  max-width: 640px;
}

.tl-item { position: relative; }

.tl-item::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-8) - 5px);
  top: 8px;
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: var(--bg);
  border: 2px solid var(--accent);
}

.tl-item .year {
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.18em;
}

.tl-item h3 {
  margin: 6px 0 4px;
  font-size: var(--text-lg);
  font-weight: 500;
}

.tl-item p {
  color: var(--text-faint);
  font-size: var(--text-sm);
}

/* ---------- Contact / footer ---------- */
.contact {
  padding: var(--space-28) 0 var(--space-12);
  text-align: left;
}

.contact .kicker {
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.28em;
  margin-bottom: var(--space-6);
}

.contact-title {
  font-size: var(--text-display);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.12;
}

.contact-desc {
  color: var(--text-dim);
  margin-top: var(--space-6);
  max-width: 30em;
  line-height: 1.9;
}

.contact-mail {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  margin-top: var(--space-8);
  font-size: var(--text-xl);
  font-weight: 500;
  border-bottom: 1px solid var(--border-strong);
  padding-bottom: 6px;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}

.contact-mail:hover { color: var(--accent); border-color: var(--accent-line); }

.site-footer {
  border-top: 1px solid var(--border);
  margin-top: var(--space-20);
  padding: var(--space-6) 0;
}

.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
}

.footer-inner .dot { margin: 0 6px; }

.footer-left {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.footer-motto {
  margin: 0;
  color: var(--text-dim);
}

/* ---------- Page hero (inner pages) ---------- */
.page-hero {
  padding: calc(76px + var(--space-20)) 0 var(--space-16);
  position: relative;
  overflow: hidden;
}

.page-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(90% 120% at 85% 0%, rgba(143, 184, 216, 0.09) 0%, rgba(11, 13, 16, 0) 55%);
}

.page-hero .section-label { margin-bottom: var(--space-4); }

.page-hero .page-title {
  font-size: var(--text-display);
  font-weight: 700;
  letter-spacing: -0.015em;
}

.page-hero .page-intro {
  color: var(--text-dim);
  margin-top: var(--space-4);
  font-size: var(--text-lg);
}

/* filters */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-12);
}

.filter-btn {
  height: 34px;
  padding: 0 18px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-size: var(--text-xs);
  letter-spacing: 0.14em;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
}

.filter-btn:hover { color: var(--text); border-color: var(--border-strong); }

.filter-btn.is-active {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}

/* projects grid (inner page) */
.project-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-8);
}

.project-grid .project-card .cover { aspect-ratio: 16 / 10; }
.project-grid .project-card .overlay { padding: var(--space-8); }

[data-cat].is-hidden { display: none !important; }

/* ---------- Project detail page ---------- */
.case-hero {
  padding: calc(76px + var(--space-16)) 0 var(--space-12);
}

.case-hero .case-title {
  font-size: var(--text-display);
  font-weight: 700;
  letter-spacing: 0.02em;
}

.case-hero .case-sub {
  color: var(--text-dim);
  margin-top: var(--space-3);
  font-size: var(--text-lg);
}

.case-hero .tags { margin-top: var(--space-6); }

.case-visual {
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--border);
  margin: var(--space-8) 0 var(--space-16);
}

.case-visual img { width: 100%; }

.case-section {
  padding: var(--space-16) 0;
  border-top: 1px solid var(--border);
}

.case-inner {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--space-12);
  align-items: start;
}

.case-section .sec-no {
  color: var(--accent);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.24em;
  position: sticky;
  top: 100px;
}

.case-section .sec-no .no {
  display: block;
  font-size: var(--text-2xl);
  letter-spacing: 0.04em;
  margin-bottom: 6px;
}

.case-section h2 {
  font-size: var(--text-2xl);
  margin-bottom: var(--space-6);
}

.case-section .prose {
  color: var(--text-dim);
  line-height: 1.9;
  max-width: 42em;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* structure diagram */
.structure-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-4);
  margin-top: var(--space-6);
}

.struct-card {
  padding: var(--space-8) var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-card);
  text-align: center;
  transition: border-color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

.struct-card:hover { border-color: var(--accent-line); transform: translateY(-4px); }

.struct-card .ico {
  width: 44px;
  height: 44px;
  margin: 0 auto var(--space-4);
  display: grid;
  place-items: center;
  border-radius: 12px;
  background: var(--accent-soft);
  color: var(--accent);
}

.struct-card h3 { font-size: var(--text-lg); margin-bottom: 6px; }
.struct-card p { color: var(--text-faint); font-size: var(--text-sm); }

/* vibe coding flow */
.flow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.flow-step {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 46px;
  padding: 0 20px;
  border: 1px solid var(--accent-line);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--text);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
}

.flow-step .n { color: var(--accent); font-weight: 600; font-size: var(--text-xs); }

.flow-arrow {
  color: var(--text-faint);
  font-size: var(--text-lg);
}

.build-note {
  margin-top: var(--space-6);
  padding: var(--space-6);
  border-left: 2px solid var(--accent);
  background: rgba(143, 184, 216, 0.06);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  color: var(--text-dim);
  line-height: 1.9;
  max-width: 42em;
}

/* next project */
.case-next {
  border-top: 1px solid var(--border);
  padding: var(--space-16) 0 var(--space-20);
}

.case-next a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-6);
  transition: color var(--dur-fast) var(--ease-out);
}

.case-next .label {
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.24em;
}

.case-next .name {
  font-size: var(--text-3xl);
  font-weight: 600;
}

.case-next a:hover .name { color: var(--accent); }

/* ---------- Reveal animation ---------- */
/* ---------- CSS 纯样式降级兜底（GSAP 缺失/异常时的第二套实现） ----------
   规格：初始 opacity:0 + translateY(24px) + scale(0.97)，
   还原过渡 650ms cubic-bezier(0.16,1,0.3,1)，stagger 经 --reveal-delay

   【性能】本兜底不使用 filter: blur()。blur 每帧都要重做一次高斯卷积并
   强制离屏栅格化；滚动列表里十几项同时入场时，这是「滑动时 CPU 飙升」
   的头号元凶。去掉 blur 后仅靠 translate + opacity 上浮，观感差异极小，
   但完全走合成器线程（transform/opacity 不触发 layout/paint）。 */
.reveal {
  opacity: 0;
  transform: translateY(24px) scale(0.97);
  transition: opacity 650ms var(--ease-out), transform 650ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

/* 仅在动画进行中提示合成层：transition 结束后由下方规则撤销。
   不用 will-change 常驻——每个元素独占一个合成层的显存代价远超收益。 */
.reveal:not(.is-visible) {
  will-change: transform, opacity;
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero-bg img { animation: none; }
  /* 注意：scroll-hint（滚动提示）为微型可供性提示，特意豁免 reduced-motion，
     保证任何系统设置下都能看到「向下滚动」暗示；大面积动效仍全部禁用。 */
  .reveal {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }
  * { transition-duration: 1ms !important; }
}

/* ---------- Responsive ---------- */
@media (max-width: 1080px) {
  .cap-grid { grid-template-columns: repeat(2, 1fr); }
  .structure-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 860px) {
  .nav-links {
    position: fixed;
    inset: 0;
    z-index: -1;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: var(--space-8);
    background: rgba(11, 13, 16, 0.96);
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease-out);
  }

  .nav-links.is-open {
    opacity: 1;
    pointer-events: auto;
  }

  .nav-links a { font-size: var(--text-2xl); }

  .burger { display: block; position: relative; z-index: 1; }

  .about-grid { grid-template-columns: 1fr; gap: var(--space-10); }

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

  .note-row {
    grid-template-columns: 1fr auto;
    row-gap: 6px;
  }
  .note-row .date { order: 1; }
  .note-row .cat { order: 2; justify-self: end; }
  .note-row .body { order: 3; grid-column: 1 / -1; }
  .note-row .arrow { display: none; }

  .case-inner { grid-template-columns: 1fr; gap: var(--space-6); }
  .case-section .sec-no { position: static; }

  .project-card .overlay { padding: var(--space-6); }
  .project-card .cta { display: none; }

  .hero-foot .hero-tags { display: none; }
}

@media (max-width: 560px) {
  .cap-grid { grid-template-columns: 1fr; }
  .structure-grid { grid-template-columns: 1fr; }
  .section { padding: var(--space-20) 0; }
  .footer-inner { flex-direction: column; align-items: flex-start; gap: var(--space-2); }
}

/* ============================================================
   Theme toggle button
   ============================================================ */
.theme-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.theme-btn:hover {
  color: var(--accent);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

.theme-btn:active { transform: scale(0.94); }

.theme-btn .ico-moon { display: none; }

html[data-theme="light"] .theme-btn .ico-sun { display: none; }
html[data-theme="light"] .theme-btn .ico-moon { display: block; }

/* ---------- Text over image overlays: fixed light-on-dark ---------- */
.project-card .overlay .idx { color: #9cc3e4; }
.project-card .overlay .name { color: #ffffff; }
.project-card .overlay .meta { color: rgba(255, 255, 255, 0.78); }
.project-card .overlay .cta { color: #ffffff; }
/* 图片浮层上的 tag：深色玻璃 + 毛玻璃（backdrop-filter 从基础样式下沉至此） */
.project-card .overlay .tag {
  background: rgba(8, 10, 13, 0.55);
  border-color: rgba(255, 255, 255, 0.22);
  color: rgba(255, 255, 255, 0.88);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* ============================================================
   Day / Light theme
   ============================================================ */
html[data-theme="light"] {
  color-scheme: light;
  --bg: #f4f6f9;
  --bg-soft: #edf0f5;
  --bg-card: #ffffff;
  --bg-elevated: #f8fafc;
  --text: #1c2128;
  --text-dim: #4d5763;
  --text-faint: #6f7986;      /* 原 #7d8692 在 #f4f6f9 上仅 3.40:1，未达 AA */
  --border: rgba(15, 23, 32, 0.10);
  --border-strong: rgba(15, 23, 32, 0.20);
  --accent: #38678f;
  --accent-soft: rgba(56, 103, 143, 0.10);
  --accent-line: rgba(56, 103, 143, 0.38);
  /* 浅色「纸面天光」：同一结构、相反明度方向，暖白为主、微冷为辅，
     避免大面积纯白平铺造成的刺眼与廉价感 */
  --amb-1: rgba(255, 255, 255, 0.85);
  --amb-2: rgba(56, 103, 143, 0.075);
  --amb-3: rgba(120, 140, 170, 0.05);
  --amb-grain: 0.018;
}

/* surfaces with hardcoded dark glass */
html[data-theme="light"] .site-header.is-scrolled {
  background: rgba(244, 246, 249, 0.82);
}

html[data-theme="light"] .logo-mark,
html[data-theme="light"] .lang-btn,
html[data-theme="light"] .theme-btn,
html[data-theme="light"] .burger {
  background: rgba(15, 23, 32, 0.04);
}

html[data-theme="light"] .btn {
  background: rgba(15, 23, 32, 0.05);
}

html[data-theme="light"] .btn:hover {
  background: var(--accent-soft);
}

html[data-theme="light"] .note-row:hover {
  background: rgba(15, 23, 32, 0.035);
}

html[data-theme="light"] .build-note {
  background: rgba(56, 103, 143, 0.07);
}

/* hero: frosted light wash over the photo */
html[data-theme="light"] .hero::after {
  background:
    linear-gradient(90deg, rgba(244, 246, 249, 0.88) 0%, rgba(244, 246, 249, 0.45) 46%, rgba(244, 246, 249, 0.10) 100%),
    linear-gradient(180deg, rgba(244, 246, 249, 0.35) 0%, rgba(244, 246, 249, 0) 30%, rgba(244, 246, 249, 0.55) 78%, var(--bg) 100%);
}

/* mobile nav overlay */
@media (max-width: 860px) {
  html[data-theme="light"] .nav-links {
    background: rgba(244, 246, 249, 0.97);
  }
}

/* typographic covers */
html[data-theme="light"] .cover-type.t-mountain { background: radial-gradient(120% 140% at 20% 15%, #e2e9f1 0%, #d2dce7 55%, #c6d2df 100%); }
html[data-theme="light"] .cover-type.t-drone { background: radial-gradient(120% 140% at 80% 20%, #dfe8f0 0%, #d0dbe6 55%, #c4d0dd 100%); }
html[data-theme="light"] .cover-type.t-box { background: radial-gradient(120% 140% at 30% 80%, #e6e2ee 0%, #d8d4e4 55%, #ccc8da 100%); }

html[data-theme="light"] .cover-type .glyph {
  color: rgba(35, 60, 85, 0.14);
}

html[data-theme="light"] .cover-type .sub {
  color: var(--text-faint);
}

html[data-theme="light"] .page-hero::before {
  background: radial-gradient(90% 120% at 85% 0%, rgba(56, 103, 143, 0.10) 0%, rgba(244, 246, 249, 0) 55%);
}

/* ---------- Smooth crossfade on toggle ---------- */
/* 过渡挂在 --amb-* 自定义属性本身（配合文件头 @property 注册），
   而非 background-image——后者对 CSS 变量驱动的渐变实测为瞬时跳变。 */
/* 【性能】原本写成 `html.theme-anim *`（含 *::before / *::after），
   等于给页面每一个元素与伪元素都挂 5 条 transition，代价有三：
     ① 切换瞬间全文档样式失效重算（style recalc）；
     ② 每个元素新建 transition 对象，主题切换肉眼可见地掉帧；
     ③ 伪元素一并卷入，卡片发光层等本该瞬切的图层被迫逐帧重绘。
   → 作用域收窄到承载视觉的结构元素（见下），并显式豁免发光壳。 */
html.theme-anim body,
html.theme-anim .site-header,
html.theme-anim main,
html.theme-anim footer,
html.theme-anim .container,
html.theme-anim section,
html.theme-anim .page-hero,
html.theme-anim .hero-content,
html.theme-anim .section-head,
html.theme-anim .project-card,
html.theme-anim .note-row,
html.theme-anim .cap-card,
html.theme-anim .stat-card,
html.theme-anim .struct-card,
html.theme-anim .tl-item,
html.theme-anim .tools-row,
html.theme-anim .btn,
html.theme-anim .theme-btn,
html.theme-anim .lang-btn,
html.theme-anim .filter-btn,
html.theme-anim .link-more,
html.theme-anim .tag,
html.theme-anim p,
html.theme-anim span,
html.theme-anim h1,
html.theme-anim h2,
html.theme-anim h3,
html.theme-anim h4,
html.theme-anim li,
html.theme-anim svg,
html.theme-anim .contact-wave {
  transition: background-color 320ms var(--ease-out),
    color 320ms var(--ease-out),
    border-color 320ms var(--ease-out),
    fill 320ms var(--ease-out),
    stroke 320ms var(--ease-out) !important;
}

/* 发光壳 / 光柱 / 词标层：主题切换必须瞬时。
   conic-gradient 遮罩每帧重绘成本极高，渐变过渡等于逐帧重算整张遮罩；
   且这些图层的颜色由 data-theme 变更后由 JS/组件重算，CSS 过渡无意义。 */
html.theme-anim .border-glow-card,
html.theme-anim .border-glow-card *,
html.theme-anim .hero-pillar,
html.theme-anim .strands,
html.theme-anim .tech-text {
  transition: none !important;
}

/* html 额外承担氛围层变量的过渡；不加 opacity——噪点两主题仅 0.022→0.018，
   差值极小瞬切不可察觉，而把 opacity 塞进上面的全局 !important 列表会覆盖
   reveal / hover 元素自身的透明度动画。 */
html.theme-anim {
  transition: background-color 320ms var(--ease-out),
    --amb-1 320ms var(--ease-out),
    --amb-2 320ms var(--ease-out),
    --amb-3 320ms var(--ease-out) !important;
}

@media (prefers-reduced-motion: reduce) {
  html.theme-anim,
  html.theme-anim *,
  html.theme-anim *::before,
  html.theme-anim *::after {
    transition: none !important;
  }
}

/* ============================================================
   Resume-driven components
   ============================================================ */

/* project card description (projects page) */
.project-card .overlay .desc {
  margin-top: var(--space-3);
  max-width: 42em;
  color: rgba(255, 255, 255, 0.72);
  font-size: var(--text-sm);
  line-height: 1.7;
}

/* stat highlights (about page) */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-4);
  margin-top: var(--space-12);
}

.stat-card {
  padding: var(--space-8) var(--space-6);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
}

.stat-card .num {
  font-size: var(--text-3xl);
  font-weight: 700;
  color: var(--accent);
  letter-spacing: -0.01em;
}

.stat-card .lbl {
  margin-top: 6px;
  color: var(--text-dim);
  font-size: var(--text-sm);
}

/* toolbox rows (about page) */
.tools-list {
  border-top: 1px solid var(--border);
  margin-top: var(--space-6);
}

.tools-row {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: var(--space-6);
  align-items: baseline;
  padding: var(--space-6) var(--space-2);
  border-bottom: 1px solid var(--border);
  transition: background var(--dur-base) var(--ease-out);
}

.tools-row:hover { background: rgba(143, 184, 216, 0.04); }

.tools-row .cat {
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.2em;
}

.tools-row .items {
  color: var(--text-dim);
  font-size: var(--text-base);
  line-height: 1.9;
}

/* multiple contact links (about page) */
.contact-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  margin-top: var(--space-8);
}

.contact-links .contact-mail { margin-top: 0; }

.contact-mail.is-tel {
  font-size: var(--text-lg);
  color: var(--text-dim);
}

.contact-mail.is-tel:hover {
  color: var(--accent);
  border-color: var(--accent-line);
}

.contact-loc {
  margin-top: var(--space-6);
  color: var(--text-faint);
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
}

/* slogan quote (Klog case) */
.klog-quote {
  margin: var(--space-6) 0 0;
  padding: var(--space-6);
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  color: var(--text);
  font-size: var(--text-lg);
  line-height: 1.9;
  max-width: 42em;
}

@media (max-width: 860px) {
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .tools-row { grid-template-columns: 1fr; row-gap: 6px; }
}

/* ---------- ICP filing (footer) ---------- */
.footer-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
  text-align: right;
}

.footer-right .icp {
  color: var(--text-faint);
  transition: color var(--dur-fast) var(--ease-out);
}

.footer-right .icp:hover { color: var(--text-dim); }

@media (max-width: 560px) {
  .footer-right {
    align-items: flex-start;
    text-align: left;
  }
}

/* ============================================================
   Note article (Markdown template)
   ============================================================ */
.note-wrap {
  max-width: 900px;
  margin: 0 auto;
}

.note-hero {
  padding: calc(76px + var(--space-16)) 0 var(--space-10);
}

.note-hero .meta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.note-hero .meta-row .cat {
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.22em;
}

.note-hero .meta-row .date {
  color: var(--text-faint);
  font-size: var(--text-sm);
  letter-spacing: 0.12em;
}

.note-hero .meta-row .sep {
  width: 1px;
  height: 12px;
  background: var(--border-strong);
}

.note-title {
  font-size: var(--text-display);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.22;
}

/* article body */
.article-content {
  padding-bottom: var(--space-16);
}

.article-content > :first-child { margin-top: 0; }

.article-content h2 {
  font-size: var(--text-2xl);
  margin: var(--space-10) 0 var(--space-4);
}

.article-content h3 {
  font-size: var(--text-xl);
  margin: var(--space-8) 0 var(--space-3);
}

.article-content h4 {
  font-size: var(--text-lg);
  margin: var(--space-6) 0 var(--space-3);
}

.article-content p {
  color: var(--text-dim);
  line-height: 1.9;
  margin: 0 0 var(--space-5);
}

.article-content strong { color: var(--text); font-weight: 600; }

.article-content a {
  color: var(--accent);
  border-bottom: 1px solid var(--accent-line);
  transition: border-color var(--dur-fast) var(--ease-out);
}

.article-content a:hover { border-bottom-color: var(--accent); }

.article-content ul,
.article-content ol {
  margin: 0 0 var(--space-5);
  padding-left: 1.5em;
  color: var(--text-dim);
  line-height: 1.9;
}

.article-content li { margin-bottom: 6px; }
.article-content li::marker { color: var(--accent); }

.article-content blockquote {
  margin: 0 0 var(--space-5);
  padding: var(--space-5) var(--space-6);
  border-left: 2px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  color: var(--text);
}

.article-content blockquote p:last-child { margin-bottom: 0; }

.article-content code {
  font-family: ui-monospace, "SF Mono", Consolas, "Courier New", monospace;
  font-size: 0.875em;
}

.article-content :not(pre) > code {
  background: var(--bg-card);
  border: 1px solid var(--border);
  padding: 2px 8px;
  border-radius: 6px;
  color: var(--accent);
}

.article-content pre {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  overflow-x: auto;
  margin: 0 0 var(--space-5);
  line-height: 1.7;
}

.article-content pre code {
  background: none;
  border: none;
  padding: 0;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.article-content img {
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  margin: 0 0 var(--space-5);
}

.article-content hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--space-8) 0;
}

.article-content table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 var(--space-5);
  font-size: var(--text-sm);
}

.article-content th,
.article-content td {
  border: 1px solid var(--border);
  padding: 10px 14px;
  text-align: left;
}

.article-content th {
  background: var(--bg-card);
  color: var(--text);
  font-weight: 600;
}

.article-content td { color: var(--text-dim); }

/* back navigation */
.back-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-6);
  border-top: 1px solid var(--border);
  padding-top: var(--space-8);
  margin-bottom: var(--space-16);
}

.pending-note {
  padding: var(--space-10);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  color: var(--text-faint);
  text-align: center;
  line-height: 1.9;
}

@media (max-width: 560px) {
  .note-hero { padding-top: calc(76px + var(--space-10)); }
  .back-row { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
}

/* ============================================================
   Contact wave motion
   ============================================================ */
.contact {
  position: relative;
  overflow: hidden;
}

.contact > .container,
.contact > .site-footer {
  position: relative;
  z-index: 1;
}

.contact-wave {
  position: absolute;
  inset: auto 0 0 0;
  height: 150px;
  pointer-events: none;
  z-index: 0;
}

.contact-wave svg {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 200%;
  height: 150px;
  display: block;
}

.contact-wave path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
}

.contact-wave .w1 { opacity: 0.20; animation: wave-drift 18s linear infinite; }
.contact-wave .w2 { opacity: 0.10; animation: wave-drift 27s linear infinite reverse; }

/* ---------- Strands 发光波带（React Bits Strands 原生 WebGL2 移植版） ----------
   替代上方 SVG 波浪：多条流动辉光曲线，颜色随主题令牌（暗：白+雾蓝 / 浅：深蓝+雾蓝）。
   SVG 保留为降级 fallback —— JS 初始化成功后由容器加 .is-strands 隐藏。 */
.contact-wave .strands {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.contact-wave .strands canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.contact-wave.is-strands svg { display: none; }

@keyframes wave-drift {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

@media (max-width: 560px) {
  .contact-wave { height: 100px; }
  .contact-wave svg { height: 100px; }
}

@media (prefers-reduced-motion: reduce) {
  .contact-wave svg { animation: none; }
}

/* ---------- Page transitions (cross-document View Transitions) ---------- */
/* 同源多页导航启用过渡：旧页快速上移淡出、新页上滑淡入；
   纯 CSS 渐进增强，不支持的浏览器降级为入场渐显（见下方 @supports）。 */
@view-transition {
  navigation: auto;
}

/* 过渡层垫同色底：新旧快照交叠的半透明区间不露出 UA 画布（浅色模式下表现为白闪） */
::view-transition {
  background: var(--bg);
}

/* 页头跨页完全一致：独立快照名，过渡期间保持稳定，只有内容区动 */
.site-header {
  view-transition-name: site-header;
}

/* 旧页翻到上层溶解（z-index 覆盖默认层序：默认新页在上）：
   新页自 t=0 起完全不透明垫底、仅上滑；旧页整体淡出。
   两快照交叠区不再合成出「透明凹陷」——首页(深色 hero) → 浅色页的
   强亮度差过渡不再闪屏，任何页面组合都不可能露出底层。 */
::view-transition-old(root) {
  animation: vt-out 280ms var(--ease-out) both;
  z-index: 2;
}

::view-transition-new(root) {
  animation: vt-in 380ms var(--ease-out) both;
  z-index: 1;
}

@keyframes vt-out {
  to {
    opacity: 0;
    transform: translateY(-10px);
  }
}

/* 新页仅位移、不做透明度淡入：全程不透明是防闪屏的关键 */
@keyframes vt-in {
  from {
    transform: translateY(16px);
  }
}

/* 兜底：不支持 view-transition-name 的浏览器，首屏入场渐显。
   只做入场不做离场——离场需拦截点击人为延迟导航，得不偿失。 */
@supports not (view-transition-name: none) {
  main {
    animation: vt-in 380ms var(--ease-out) both;
  }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root),
  main {
    animation: none;
  }
}

/* ============================================================
   LightPillar（React Bits 原版组件 · React + three 本地 bundle）
   assets/js/light-pillar-react.js 自动挂载 [data-light-pillar-react]
   ============================================================ */
/* 原版组件 CSS（React Bits LightPillar.css） */
.light-pillar-fallback {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.1);
  color: #888;
  font-size: 14px;
}

.light-pillar-container {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

/* hero 挂载层：暗色主题以 LightPillar 为 hero 背景（山景图退场），
   位于暗色 wash（.hero::after, z:-1）之下，标题区由 wash 压暗保护；
   浅色主题回退山景图（浅纱压黑底会发灰） */
.hero-pillar {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
}

html[data-theme="light"] .hero-pillar {
  display: none;
}

html[data-theme="light"] .hero-bg {
  display: block;
}

/* 暗色主题（默认）：显示光柱、隐藏山景图 */
.hero-bg {
  display: none;
}

/* ============================================================
   Motion System — GSAP + ScrollTrigger 升级层
   html.motion-ready 由 assets/js/motion.js 在确认环境后挂载；
   未挂载（GSAP 缺失 / reduced-motion / 初始化异常）时，
   以下规则全部失效，全站回退到上方 CSS reveal 兜底。
   ============================================================ */

/* GSAP 接管 .reveal：解除 CSS 初始隐藏（初始态改由 gsap.set 注入） */
html.motion-ready .reveal {
  opacity: 1;
  transform: none;
  transition: none;
  transition-delay: 0ms;
}

/* 开场期间的滚动锁 */
html.is-locked,
html.is-locked body { overflow: hidden; }

/* ---------- 开场遮罩（仅首页有标记） ---------- */
.intro-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 300;
  pointer-events: none;
}

html.motion-ready .intro-overlay { display: block; }

.intro-panel {
  position: absolute;
  inset: 0;
  background: var(--bg);
  /* 显式初始裁剪：GSAP 揭开动画的确定性插值起点 */
  clip-path: inset(0% 0% 0% 0%);
}

.intro-core {
  position: absolute;
  left: 50%;
  top: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
}

.intro-mark {
  width: 58px;
  height: 58px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.04);
  font-weight: 700;
  font-size: 1.55rem;
  color: var(--text);
}

.intro-name {
  color: var(--text-dim);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.46em;
  text-indent: 0.46em;   /* 平衡尾字距，保持视觉居中 */
}

.intro-line {
  width: 132px;
  height: 1px;
  background: var(--border);
  overflow: hidden;
}

.intro-line-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
}

/* 开场计数器：与细线填充同进度 0→100，等宽数字防跳动 */
.intro-pct {
  font-size: 0.68rem;
  font-weight: 500;
  color: var(--text-dim);
  letter-spacing: 0.18em;
  text-indent: 0.18em;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

/* ---------- 标题遮罩拆分（词 / 字级联进场） ---------- */
/* msk 上下 padding 补偿（负 margin 抵消布局影响）：
   扩大溢出可视区，避免降部/升部字符在静止态被裁切 */
html.motion-ready .msk {
  display: inline-block;
  overflow: hidden;
  padding: 0.1em 0 0.14em;
  margin: -0.1em 0 -0.14em;
}

/* 潜伏态：内容整体藏于遮罩下沿之下。
   【性能】此处**不写** will-change。拆分后的每个 .msk-i 都是独立合成层，
   一个 20 字标题 = 20 层；整站标题全部拆完可达数百层，显存与合成开销
   远超收益。will-change 改由 motion.js 在 tween 的 onStart 挂载、
   onComplete 摘除（见 motion.js willChangeOn / willChangeOff），
   只有真正正在动的那些词才持有合成层。 */
html.motion-ready .msk-i {
  display: inline-block;
  transform: translateY(115%);
}

/* 揭示完成：交还 CSS 静态呈现，撤销合成层提示 */
html.motion-ready .title-in .msk-i {
  transform: none;
  will-change: auto;
}

/* section-label 刻度线：GSAP 经 --lk-line 由 0 绘制到 1 */
html.motion-ready .section-label::after {
  transform: scaleX(var(--lk-line, 1));
  transform-origin: left center;
}

/* ============================================================
   Perf Tiers — 条件降级策略（由 assets/js/perf.js 判定档位）
   <html data-perf="high|mid|low">，在首帧之前写入，避免闪降。

   总原则：**不删减视觉风格，只降低动效密度**。
   任何档位下页面都保持完整的设计语言（配色 / 留白 / 层次 / 字体），
   降级只作用于「每帧成本」：滤镜、合成层数量、WebGL、遮罩节点数。
   ============================================================ */

/* ---------- mid 档：交互发光关闭，保留静态外观 ---------- */
/* BorderGlow 的光标追踪靠 conic-gradient + mask-image 逐帧重算遮罩，
   是鼠标移动时掉帧的直接来源。mid 档把发光壳降为静态边框：
   卡片外观（边框 / 圆角 / 底色 / 内阴影）完全保留，
   仅失去「光标附近的彩色边光」这一层交互反馈。 */
html[data-perf="mid"] .border-glow-card::before,
html[data-perf="mid"] .border-glow-card > .edge-light {
  /* 关闭遮罩动画：保留 mesh 渐变边框，但不随光标角度重绘 */
  -webkit-mask-image: none;
  mask-image: none;
  opacity: calc((var(--fill-opacity, 0.5) * 0.55));
}

html[data-perf="mid"] .border-glow-card::after {
  display: none;   /* 内部柔光填充：mix-blend-mode: soft-light，最贵的一层 */
}

html[data-perf="mid"] .border-glow-card > .edge-light {
  display: none;   /* 外光束：13 层 inset box-shadow 叠加 */
}

/* ---------- low 档：全部「每帧重绘」图层关闭，骨架动效保留 ---------- */

/* 1) 卡片发光 → 静态渐变边框。
      保留描边渐变的色彩身份（蓝紫色 mesh），但去掉遮罩与混合模式。 */
html[data-perf="low"] .border-glow-card::before {
  -webkit-mask-image: none;
  mask-image: none;
  opacity: 0.62;
  transition: none;
}

html[data-perf="low"] .border-glow-card::after,
html[data-perf="low"] .border-glow-card > .edge-light {
  display: none;
}

/* 发光壳不再响应指针（避免无意义的 style 失效） */
html[data-perf="low"] .border-glow-card {
  pointer-events: auto;
}

/* 2) 标题词级遮罩整体复位：low 档 motion.js 不再拆分标题，
      此处兜底处理「已拆分但随后降档」的情况，确保文字始终可见。 */
html[data-perf="low"] .msk-i {
  transform: none !important;
  display: inline;
}

html[data-perf="low"] .msk {
  display: inline;
  overflow: visible;
  padding: 0;
  margin: 0;
}

/* 3) WebGL 容器：LightPillar / Strands 在 low 档根本不注入脚本，
      但若脚本已加载（如运行时降档），此处确保画布不参与合成。 */
html[data-perf="low"] .hero-pillar canvas,
html[data-perf="low"] .strands canvas {
  /* 保留最后一帧的视觉，不隐藏元素（避免布局塌陷），
     仅停止其后续重绘——真正的暂停由 JS 侧的 loseContext 完成 */
  pointer-events: none;
}

html[data-perf="low"] .hero-bg img {
  animation: none;   /* 24s 无限 drift 动画：常驻合成层，low 档停掉 */
}

/* 4) 词标 canvas：low 档保留静态渲染（它是 hero 的视觉主体），
      仅关闭拖拽交互带来的持续重绘。 */
html[data-perf="low"] .tech-text {
  cursor: default;
}

/* 5) 氛围噪点层（body::before，feTurbulence 位图 + fixed 合成层）：
      low 档关闭——它是一层永久 full-viewport 合成，低端 GPU 上白付代价。 */
html[data-perf="low"] body::before {
  display: none;
}

/* ---------- 尊重 prefers-reduced-motion：最严格档 ---------- */
/* 系统级「减少动态效果」开启时，motion.js 直接不加载，
   以下规则保证所有 CSS 动画与过渡彻底静止。 */
@media (prefers-reduced-motion: reduce) {
  /* 只杀「动画」，不碰 transition —— 主题切换的 320ms 颜色过渡
     属于状态反馈而非装饰动画，保留它反而更符合无障碍预期 */
  *,
  *::before,
  *::after {
    animation: none !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }

  /* 视差与无限循环动画（hero drift / 波浪漂移）全部静止 */
  .hero-bg img,
  .contact-wave svg {
    animation: none !important;
  }

  /* 发光卡片：静态边框，不做任何遮罩计算 */
  .border-glow-card::before {
    -webkit-mask-image: none !important;
    mask-image: none !important;
  }
  .border-glow-card::after,
  .border-glow-card > .edge-light {
    display: none !important;
  }

  /* 跨页过渡：直接切换 */
  ::view-transition-old(root),
  ::view-transition-new(root),
  main {
    animation: none !important;
  }

  /* 标题遮罩：文字直接可见，不做逐词上移 */
  .msk-i {
    transform: none !important;
    will-change: auto !important;
  }
}
