@import url('tokens.css');

/* ---- 字体自托管：Geist 变量字重 100–900 ------------------------------ */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/geist-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/geist-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+1E00-1E9F, U+2020, U+20A0-20AB, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* 中文：思源黑，按站内实际用字裁的子集（tools/subset_cn.py）。
   自托管而不是靠系统字，否则 Windows 会回退到微软雅黑、细字重直接消失。
   裁成两个静态实例并按 Geist 光学对齐——同一个 font-weight 下思源黑本来更重，
   直接用变量轴混排就会中文发黑、英文偏细。 */
@font-face {
  font-family: 'Noto Sans SC Subset';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/notosanssc-300.woff2') format('woff2');
}
@font-face {
  font-family: 'Noto Sans SC Subset';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/notosanssc-400.woff2') format('woff2');
}

/* ---- 跨文档换页：交给浏览器的 View Transition ------------------------
   之前是自己拦下跳转、手动淡出再 location.href——那必然会闪：旧文档已经卸掉、
   新文档从零布局，导航是重新渲染出来的，第一帧位置对不上就是那一下位移。
   声明 navigation: auto 之后，浏览器会在两个文档之间做快照过渡。
   注意：导航【不】参与快照。给它 view-transition-name 试过两轮，两个毛病：
   —— 快照过渡超预算会被整段跳过，胶囊就硬切，而 JS 驱动的文字照样动；
   —— .nav 带 backdrop-filter，快照是矩形贴图，模糊按矩形重算，胶囊外面
      就浮出几块半透明方块（正是"方形的半透明框"）。
   所以导航退回普通元素，伸缩与滚字由 CSS 动画自己演，见文件末尾。 */
@view-transition { navigation: auto; }

/* 正文用默认的 root 快照（只有一个视口大小，所以每次都跑得起来）。
   千万不要给 main 加 view-transition-name——项目页的 main 有一万六千像素高，
   整元素快照又大又慢，超预算时浏览器会直接跳过整个过渡。 */
::view-transition-old(root) {
  animation: page-out 260ms cubic-bezier(0.7, 0, 0.84, 0) both;
}
::view-transition-new(root) {
  animation: page-in 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* 只淡出淡入的话，两页的底色和版式又差不多，换页那一下几乎看不出来
   （项目页之间尤其明显）。加一点纵向位移与极轻的缩放，动作才读得出来：
   旧页向上退、新页从下方浮上来。位移用 vh，跟视口一起走。 */
@keyframes page-out {
  to { opacity: 0; filter: blur(6px); transform: translateY(-2.4vh) scale(0.992); }
}
@keyframes page-in {
  from { opacity: 0; filter: blur(6px); transform: translateY(2.8vh) scale(1.006); }
}

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

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

html, body {
  margin: 0;
  overflow-x: clip;          /* 不用 hidden，避免 sticky 失效 */
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto;     /* 平滑滚动交给 Lenis */
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-text);
  line-height: var(--leading-body);
  letter-spacing: var(--track-tight);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-synthesis-weight: none;   /* 缺字重时宁可不显示，也不要合成假粗 */
}

img, svg, video { display: block; max-width: 100%; height: auto; }
h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;        /* 焦点环不做动画，必须立刻出现 */
}

/* ---- 排版角色：字重只有 300（display）和 400（其余）两档 -------------- */
/* 只给"压在图上的一句陈述"用，页面标题不要用它 */
.t-statement {
  font-size: var(--text-statement);
  font-weight: var(--weight-display);
  letter-spacing: var(--track-statement);
  line-height: var(--leading-statement);
}
.t-h1 {
  font-size: var(--text-h1);
  font-weight: var(--weight-text);
  letter-spacing: var(--track-h1);
  line-height: var(--leading-tight);
}
.t-h2 {
  font-size: var(--text-h2);
  font-weight: var(--weight-text);
  letter-spacing: var(--track-h2);
  line-height: var(--leading-tight);
}
.t-lead { font-size: var(--text-lg); font-weight: var(--weight-text); line-height: var(--leading-cn); }
.t-body { font-size: var(--text-base); line-height: var(--leading-cn); max-width: var(--measure); }
.t-body + .t-body { margin-top: 1.1em; }
.t-cap { font-size: var(--text-sm); color: var(--ink-soft); line-height: 1.35; }
.t-label {
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-soft);
}
.t-num {                      /* 0.1 / 0.2 这种序号，用等宽数字对齐 */
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.t-accent { color: var(--accent); }

/* ---- 页面骨架 -------------------------------------------------------- */
.page { max-width: var(--page-max); margin-inline: auto; padding-inline: var(--margin); }

.grid5 {
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  column-gap: var(--gutter);
}

/* ---- 色带：整屏换底，是这套设计的分章手段 ---------------------------- */
.band { position: relative; }
.band--paper { background: var(--paper); color: var(--ink); }
.band--ink { background: var(--ink); color: var(--on-ink); }
.band--accent { background: var(--accent); color: var(--accent-ink); }
.band--ink .t-cap, .band--ink .t-label, .band--ink .t-num { color: var(--on-ink-soft); }
.band--accent .t-cap, .band--accent .t-label, .band--accent .t-num {
  color: rgba(255, 255, 255, 0.72);
}

/* ---- 发丝分隔行：augen 的信息行就是这个结构 --------------------------
   分隔线不用 border 而是伪元素：border 没法按比例伸长，伪元素可以按
   --rule-scale 从左边延伸出来（和作品索引行同一套做法）。
   默认值是 1，所以没有 JS 或降级时线照常在。 */
.row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 3fr);
  column-gap: var(--gutter);
  align-items: start;
  padding-block: clamp(1.25rem, 2.2vw, 2rem);
}
.row::before, .row:last-child::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: var(--rule);
  background: var(--hair);
  transform: scaleX(var(--rule-scale, 1));
  transform-origin: 0 50%;
}
.row::before { top: 0; }
.row:last-child::after { bottom: 0; }
.band--ink .row::before,
.band--ink .row:last-child::after { background: var(--hair-on-ink); }

/* ---- 胶囊标签／按钮 -------------------------------------------------- */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.4em 0.95em;
  border: var(--rule) solid var(--accent);
  border-radius: var(--radius-pill);
  color: var(--accent);
  font-size: var(--text-sm);
  letter-spacing: 0;
  white-space: nowrap;
  transition: background-color var(--dur-2) var(--ease-out),
    color var(--dur-2) var(--ease-out);
}
.pill:hover { background: var(--accent); color: var(--accent-ink); }
/* 悬停时文字整条往上滚一格，露出下面那一份完全一样的字（augen 首页的做法）。
   两份字叠在一个裁切框里，滚的是 transform，不动布局。 */
.pill--roll .pill__clip { display: block; overflow: hidden; height: 1.25em; }
.pill--roll .pill__roll {
  display: flex;
  flex-direction: column;
}
.pill--roll .pill__text { display: block; line-height: 1.25; }
/* 只向上滚一次，不回滚：动画跑完由 js 摘掉 class，transform 归零，
   而上下两份字完全一样，所以归零的那一帧看不出任何变化。
   （用 :hover + transition 的话，鼠标移开必然倒着滚回去。）
   缓动不用 --ease-out：那条曲线 40% 就走完 98%，时长拉长也只是"先窜一下再等"。
   换成两头都缓的曲线，位移摊在整段时间里，读起来才真的是慢。 */
.pill--roll.is-rolling .pill__roll {
  animation: pill-roll 620ms cubic-bezier(0.4, 0, 0.25, 1) both;
}
@keyframes pill-roll {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}
.band--ink .pill { border-color: var(--accent); color: #6ab0ff; }
.band--accent .pill { border-color: rgba(255, 255, 255, 0.6); color: #fff; }
.band--accent .pill:hover { background: #fff; color: var(--accent); }

/* 标签：比胶囊小一档，只描边不填色 */
.tag {
  display: inline-flex;
  align-items: center;
  padding: 0.3em 0.75em;
  border-radius: var(--radius-pill);
  border: var(--rule) solid var(--hair);
  color: var(--ink-soft);
  font-size: var(--text-xs);
  letter-spacing: 0;
  white-space: nowrap;
}
.work__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--space-xs);
}
.band--ink .tag { border-color: var(--hair-on-ink); color: var(--on-ink-soft); }

/* ---- 悬浮胶囊导航（augen 用的就是这种 N5） --------------------------- */
.nav {
  position: fixed;
  top: var(--space-lg);
  left: 50%;
  z-index: 90;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.35rem;
  border-radius: var(--radius-pill-lg);
  transform: translate(-50%, 0);
  transition: transform var(--dur-3) var(--ease-out),
    opacity var(--dur-2) var(--ease-out);
  will-change: transform;
}
/* 玻璃底单独一层：胶囊的伸缩是这一层在 scaleX，布局一点不动 */
.nav__glass {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: var(--veil);
  -webkit-backdrop-filter: var(--blur-veil) saturate(150%);
  backdrop-filter: var(--blur-veil) saturate(150%);
  border: var(--rule) solid rgba(14, 15, 17, 0.06);
  transition: background-color var(--dur-3) var(--ease-out),
    border-color var(--dur-3) var(--ease-out);
  will-change: transform;
}
.nav__home {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  overflow: hidden;          /* 悬停位移不许跑出圆形按钮 */
  background: var(--ink);
  color: var(--paper);
  font-size: var(--text-sm);
  transition: background-color var(--dur-2) var(--ease-out);
}
.nav__home:hover { background: var(--accent); }

/* 图标滚动换位：两个字形上下叠在一个裁切框里，整条往上滚一格就换成另一个。
   augen 进内页时最左侧那个图标就是这么换的。项目页给 .nav__home 加
   .is-back，它会滚到返回箭头；悬停时再微微上顶一点，交代"可点"。 */
.nav__glyphs {
  display: block;
  width: 20px;
  height: 20px;
  overflow: clip;
}
.nav__roll {
  display: flex;
  flex-direction: column;
  transition: transform var(--dur-3) var(--ease-out);
}
.nav__roll > * { flex: 0 0 20px; height: 20px; }
.nav__home.is-back .nav__roll { transform: translateY(-20px); }

/* 自制标记：两片错开的圆角方，悬停分开。
   注意别再用 .nav__roll 的 translateY 做悬停反馈——那一层是 20px 的裁切框，
   往上顶一下图标底部就会被切掉一条。位移只放在 rect 上，画框里有余量。 */
.mark rect {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  transition: transform var(--dur-3) var(--ease-out);
}
.nav__home:hover .mark__a { transform: translate(-0.7px, -0.7px); }
.nav__home:hover .mark__b { transform: translate(0.7px, 0.7px); }
.mark--back path { transition: transform var(--dur-3) var(--ease-out); }
.nav__home.is-back:hover .mark--back path { transform: translateX(-1.5px); }
.nav__list { display: flex; align-items: center; flex: 0 0 auto; }
.nav__link {
  flex: 0 0 auto;
  display: inline-flex;      /* 必须自己成块：里面有 display:block 的文字裁切层 */
  align-items: center;
  padding: 0.55rem 0.9rem;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: var(--weight-display);   /* 顶栏比正文细一档 */
  letter-spacing: 0;
  white-space: nowrap;
  color: var(--ink);
  transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.nav__link:hover { background: rgba(14, 15, 17, 0.06); }

/* 文字滚动：外层裁切，内层上下位移。换页时旧的往上滚出、新的从下滚入。 */
.nav__label { display: block; overflow: hidden; }
.nav__text { display: block; will-change: transform; }
.nav__link[aria-current='true'] { color: var(--accent); }

/* 导航压到深色／强调色带上时整体反色（augen 的做法） */
.nav.is-inverted .nav__glass { background: var(--veil-ink); border-color: rgba(242, 242, 244, 0.1); }
.nav.is-inverted .nav__link { color: var(--on-ink); }
.nav.is-inverted .nav__link:hover { background: rgba(242, 242, 244, 0.12); }
.nav.is-inverted .nav__home { background: var(--paper); color: var(--ink); }
.nav.is-inverted .nav__home:hover { background: var(--accent); color: var(--accent-ink); }

/* ---- 无限漂移带：两条不同速度叠出层次（mandandan 用 46s / 58s） ------ */
.drift { overflow: clip; width: 100%; }
.drift__row {
  display: flex;
  width: max-content;
  gap: var(--space-xl);
  animation: drift var(--drift-dur, 46s) linear infinite;
  will-change: transform;
}
.drift--slow .drift__row { animation-duration: var(--drift-dur, 58s); animation-direction: reverse; }
@keyframes drift {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

/* ---- 入场：JS 未接管时也要可读，所以默认可见，加 .js 后才隐藏 -------- */
.js [data-reveal] { opacity: 0; }
.js [data-reveal='up'] { transform: translate3d(0, 1.5rem, 0); }
.js [data-reveal='line'] { opacity: 1; }
/* fog 只走 opacity + blur，所以这里不给 transform，免得多一次位移 */
.js [data-reveal='fog'] { filter: blur(8px); }

/* SplitText 拆出来的行，靠 clip 容器做上推 */
.line-clip { overflow: clip; }
.js .line-inner { transform: translate3d(0, 105%, 0); }

/* 首屏那三行字和六个胶囊的起始态。原来只写在 heroIntro() 的 fromTo 里，慢网络上
   CSS 先到（页面开始画）而底部那 155KB 阻塞脚本还要等一两个往返，于是它们先满
   不透明地画在屏上、再被 GSAP 按回去——实测冷缓存 + 600ms 延迟下这一帧持续 652ms。
   本地看不到，因为本地是 JS 先跑完（567ms）才第一次上屏（728ms）。
   数值必须和 heroIntro() 的 from 一字不差，否则 GSAP 接手那一帧会跳。 */
.js [data-hero='text'] { opacity: 0; transform: translateY(14px); filter: blur(10px); }
.js [data-hero='move'] { opacity: 0; transform: translateY(18px); }

/* 上面这些"加了 .js 就先藏起来"的状态，前提是 GSAP 一定会把它们放出来。
   万一入场那一批建不起来（ScrollTrigger 递归崩、脚本 404、旧浏览器），
   内容就永远留在隐藏态了。这个 class 由 js/index.js 的 catch 挂上：动画
   可以不播，字和图不能不在。!important 是用来压过 GSAP 写进元素的内联态。 */
html.motion-fallback [data-reveal],
html.motion-fallback .line-inner,
html.motion-fallback [data-hero='text'],
html.motion-fallback [data-hero='move'],
html.motion-fallback [data-about-head],
html.motion-fallback .hero__key img,
html.motion-fallback .interlude__line {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
}
html.motion-fallback .work__row,
html.motion-fallback .about__row { --rule-scale: 1 !important; }
html.motion-fallback .work__row .tag { opacity: 1 !important; transform: none !important; }

/* ---- 换页过渡：轻微模糊 + 渐隐渐显（augen 的做法） -------------------
   只作用在 main 上，导航和过渡层本身不参与，避免整屏"抖一下"。 */
/* ---- 换页时的导航动作 ------------------------------------------------
   全部是 CSS：transform 动画跑在合成线程上，新页面头几百毫秒主线程再忙
   （解析脚本、建 ScrollTrigger）也不掉帧。JS 只负责量出上一页的胶囊宽度，
   写进 --pill-from（旧宽/新宽的比例）并挂上 .is-morph。 */
.js .nav__text {
  transform: translateY(110%);
  transition: transform 520ms var(--ease-out);
}
.js .nav.is-ready .nav__text { transform: translateY(0); }
/* 文字逐条滚上来：第一条等胶囊先伸出去一点再动 */
.nav__list li:nth-child(1) .nav__text { transition-delay: 150ms; }
.nav__list li:nth-child(2) .nav__text { transition-delay: 205ms; }
.nav__list li:nth-child(3) .nav__text { transition-delay: 260ms; }
.nav__list li:nth-child(4) .nav__text { transition-delay: 315ms; }
.nav__list li:nth-child(5) .nav__text { transition-delay: 370ms; }

/* 胶囊：从上一页的宽度补到本页的宽度。
   玻璃层按【左边缘】scaleX——左边缘就是图标所在的那一端，图标于是一直贴在
   胶囊头上，不会浮在胶囊外面；同时整条导航反向平移半个差值（--pill-shift），
   所以看上去胶囊始终居中，是两端各自伸出去／收回来。
   两个动画都只动 transform，交给合成线程，主线程忙也不掉帧。 */
.nav.is-morph {
  animation: pill-slide 640ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.nav.is-morph .nav__glass {
  transform-origin: left center;
  animation: pill-stretch 640ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes pill-slide {
  from { transform: translate(calc(-50% + var(--pill-shift, 0px)), 0); }
  to { transform: translate(-50%, 0); }
}
@keyframes pill-stretch {
  from { transform: scaleX(var(--pill-from, 1)); }
  to { transform: scaleX(1); }
}

/* 顶栏入场：整条胶囊从视口上方滑下来。
   只在没有胶囊伸缩的时候演——首次进站、刷新、以及前后两页胶囊同宽的换页。
   有伸缩的时候那条动作本身就是连续的，再叠一层下滑就是两个方向在打架。
   只动 transform，绝对不要动 opacity：opacity < 1 会给 .nav 建一个 backdrop
   root，里面的 .nav__glass 就采样不到页面内容，磨砂整段失效、只剩一层半透明
   底色，等动画结束才"啪"地糊上来。位移本身已经够读出入场了。
   起手时间由 --nav-drop-delay 给：首页要等主视觉那段模糊转清晰走完（见
   index.css），内页没有那一段，短一点就够。
   移动端胶囊在底部（.nav 改成 bottom），所以那边从下面升上来。 */
.js .nav.is-drop {
  animation: nav-drop 1s cubic-bezier(0.16, 1, 0.3, 1) var(--nav-drop-delay, 0.2s) both;
}
@keyframes nav-drop {
  from { transform: translate(-50%, calc(-100% - var(--space-lg))); }
  to { transform: translate(-50%, 0); }
}
@media (max-width: 767px) {
  @keyframes nav-drop {
    from { transform: translate(-50%, calc(100% + var(--space-lg))); }
    to { transform: translate(-50%, 0); }
  }
}


/* ---- 无障碍 ---------------------------------------------------------- */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1 !important; transform: none !important; }
  .js .line-inner { transform: none !important; }
  /* heroIntro() 在 reduced 下直接 return，所以起始态得在这里撤掉 */
  .js .hero__key img { opacity: 1 !important; filter: none !important; transform: none !important; }
  /* 箭头的入场是 CSS 动画，上面那条 animation-duration: 1ms 只压时长、不动
     animation-delay，所以 1.75s 的延迟还在，配 fill: both 就是"前 1.75s 完全看不见"。
     减弱动效下两条动画都不该跑，直接关掉并落到终态。 */
  .hero__cue .cue__clip { animation: none !important; transform: none !important; opacity: 0.6 !important; }
  .hero__cue .cue__roll { transform: translateY(calc(-1 * var(--cue-track))) !important; animation: none !important; }
  /* heroIntro() 在 reduced 下不跑，它那几个初始态都得在这里撤掉 */
  .js [data-hero='text'],
  .js [data-hero='move'] { opacity: 1 !important; transform: none !important; filter: none !important; }
  .nav { transition-duration: 150ms; }
  * { animation-duration: 1ms !important; animation-iteration-count: 1 !important; }
}

/* ---- 移动端 ---------------------------------------------------------- */
@media (max-width: 767px) {
  :root { --cols: 2; --nav-h: 4rem; }
  .row { grid-template-columns: minmax(0, 1fr); row-gap: var(--space-sm); }
  .nav { top: auto; bottom: var(--space-md); }
  /* 五个章节的胶囊在 390 宽的屏上比视口还宽，居中之后右边的 Outcome 直接被切掉。
     把胶囊夹进视口内，章节条自己横向滚（滚动条藏掉），字距与内边距各收一档。 */
  .nav { max-width: calc(100vw - 2 * var(--space-md)); }
  .nav__list {
    /* 桌面是 flex:0 0 auto（不许被压），移动端必须允许收缩，
       否则它保持内容宽度直接溢出胶囊，右边的 Outcome 被视口切掉。 */
    flex: 1 1 auto;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    overscroll-behavior-x: contain;
  }
  .nav__list::-webkit-scrollbar { display: none; }
  /* 右端淡出：滚动被裁的那一下要读成「还有」，不是读成没画完 */
  .nav__list {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.4rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 1.4rem), transparent);
  }
  .nav__link { padding: 0.5rem 0.62rem; letter-spacing: 0; }
}
@media (max-width: 479px) {
  .t-statement { overflow-wrap: anywhere; min-width: 0; }
}
