/* ==========================================================================
   EIGHTEEN TANG. — style.css
   --------------------------------------------------------------------------
   风格定位：American Comic × Editorial × Street × Digital Playground
   关键词：BOLD / PLAYFUL / EDITORIAL / STREET / COMIC / PERSONAL / PREMIUM

   目录
     01  字体
     02  设计变量
     03  基础重置
     04  通用件
     05  顶栏 + 进度线
     06  左侧数字导航
     07  开场动画
     08  区块骨架
     09  ABOUT
     10  Journal 卡牌 + 浮层
     11  SKILLS 能力星球
     12  FIND ME 地图 + 胶片条
     13  CONTACT
     14  Lightbox
     15  响应式
     16  prefers-reduced-motion
   ========================================================================== */

/* ==========================================================================
   01  字体（自托管，避免第三方请求）
   ========================================================================== */

@font-face {
  font-family: 'Archivo Black';
  src: url('../assets/fonts/archivo-black-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Permanent Marker';
  src: url('../assets/fonts/permanent-marker-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Space Mono';
  src: url('../assets/fonts/space-mono-latin-400-normal.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Space Mono';
  src: url('../assets/fonts/space-mono-latin-700-normal.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   02  设计变量
   ========================================================================== */

:root {
  /* 基础色：黑 / 米白 / 深灰 */
  --ink: #0b0b0c;
  --ink-2: #141416;
  --ink-3: #1d1d20;
  --paper: #f2ebdd;
  --paper-2: #ded3bf;
  --grey: #8c8880;
  --dim: #5f5c57;

  /* 漫画强调色：红为主，黄/蓝/橙克制使用 */
  --red: #ff3b1f;
  --yellow: #ffc800;
  --blue: #1f4bff;
  --orange: #f5891f;

  /* 个人主题色 */
  --accent: var(--red);

  /* 页脚那条铁轨的木色：枕木和路牌的柱子共用。
     放在这里而不是挂在 .train 上 —— 文案和铁轨现在是两个兄弟节点，
     挂在其中一个上，另一个就取不到。 */
  --wood: #c8974f;

  --line: rgba(242, 235, 221, 0.13);
  --line-strong: rgba(242, 235, 221, 0.34);

  /* 字体 */
  --font-display: 'Archivo Black', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  --font-mono: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --font-hand: 'Permanent Marker', 'PingFang SC', cursive;
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
    'Microsoft YaHei', 'Noto Sans SC', sans-serif;

  /* 缓动 */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-back: cubic-bezier(0.34, 1.5, 0.64, 1);

  /* 布局 */
  --nav-w: 92px;
  --topbar-h: 58px;
  --maxw: 1140px;
  --pad: clamp(1.1rem, 3.6vw, 2.75rem);
}

/* ==========================================================================
   03  基础重置
   ========================================================================== */

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

[hidden] {
  display: none !important;
}

html {
  scroll-behavior: smooth;
  scroll-snap-type: y proximity;
  -webkit-text-size-adjust: 100%;
}

html.is-locked,
html.is-locked body {
  overflow: hidden;
}

/* 锁滚动时补上滚动条宽度，避免整页横向跳一下 */
html.is-locked body {
  padding-right: var(--sbw, 0px);
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* 全局 halftone 网点底纹 —— 印刷感的地基，克制到几乎看不见 */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(rgba(242, 235, 221, 0.07) 1px, transparent 1.1px);
  background-size: 5px 5px;
  opacity: 0.75;
}

/* 暗角，让画面边缘沉下去 */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 0%, transparent 40%, rgba(0, 0, 0, 0.5) 100%);
}

img,
svg {
  max-width: 100%;
}

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

ul,
ol,
figure,
h1,
h2,
h3,
p {
  margin: 0;
  padding: 0;
}

ul,
ol {
  list-style: none;
}

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

:focus-visible {
  outline: 3px solid var(--yellow);
  outline-offset: 3px;
}

/* 浮层容器是程序化聚焦的，不需要画焦点框（内部按钮仍然有） */
.cardView__card:focus,
.cardView__card:focus-visible,
.lightbox:focus,
.lightbox:focus-visible {
  outline: none;
}

.sr {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   04  通用件
   ========================================================================== */

.wrap {
  position: relative;
  /* 正文永远在边栏之上：边栏是背景，不是装饰层 */
  z-index: 1;
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
}

/* ---------- 左右边栏：让留白也参与设计 ----------
   问题：正文锁在 1140px 里，1920 的屏上左右各有 300+ px 纯黑，
   读起来像「一个窄容器浮在黑底上」，而不是「一个充满屏幕的世界」。
   解法不是把容器拉宽（正文会散），而是让那两条留白带上杂志的边角：
   出血的大页码、竖排小注释、网点、速度线、贴纸碎片。

   宽度 = 「页面宽 − 正文宽」的一半，正好是留白本身，
   所以它永远不会压到正文。窄屏算出来是 0，配下面的媒体查询直接收起。 */
.rail {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 0;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1rem, 2.6vh, 2.2rem);
  width: max(0px, calc((100% - var(--maxw)) / 2 - 12px));
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  /* 边栏里的东西按「边栏自己有多宽」缩放，不按视口——
     同样是 1440 的屏，1920 上边栏是 337px，1440 上只有 89px。 */
  container-type: inline-size;
}

.rail--l {
  left: 0;
  /* 导航展开标签时最远到 x≈117。让出 30px 之后内容从 x≈122 开始，
     居中的大字离标签右缘还有 10px。
     两侧让出同样的量是必须的：边栏的字号由 cqw 驱动，而 cqw 量的是
     内容盒——左边让 30px、右边不让，两条栏的字会差出 1.4 倍。 */
  padding-left: 30px;
}

.rail--r {
  right: 0;
  padding-right: 30px;
}

/* 1440 是量出来的：这时竖排注释离导航还有 25px，离正文 30px，
   两边都不贴。再窄下去就挤了，干脆整条收起。 */
@media (min-width: 1440px) {
  .rail {
    display: flex;
  }
}

/* 边栏里的大字。字号跟着边栏宽度走（cqw）：
   1440 上占满那条窄栏，1920 上就是个真正的大字。
   前面那行 clamp 是不支持 cqw 时的兜底。

   64cqw 是量出来的：Archivo Black 的数字每个约 0.607em，
   「18」两个字符 = 1.214em，64cqw × 1.214 ≈ 内容盒的 78%，
   留得住余量——82cqw 时占 99.5%，贴着右边界，换个字体就会被裁。

   描边从 0.16 提到 0.3——以前淡到几乎看不见，
   两条留白读起来就是「两块黑」，而不是「设计过的负空间」。 */
.rail__num {
  font-family: var(--font-display);
  /* .rail 是 <i>，里面的字默认继承斜体。大字必须正立：
     描边字一斜就像被风吹歪的招牌，而且跟正文的排印语言对不上。 */
  font-style: normal;
  text-transform: uppercase;
  font-size: clamp(2.8rem, 4.8vw, 6rem);
  font-size: clamp(2.8rem, 64cqw, 12rem);
  line-height: 0.78;
  letter-spacing: -0.06em;
  white-space: nowrap;
  color: transparent;
  -webkit-text-stroke: 1.6px rgba(242, 235, 221, 0.3);
  /* 边栏的大字极慢地漂：30s 走 ±7px。
     为什么是纵向而不是横向：1440（边栏的临界宽度）下量过，内容盒只有 62px、
     「18」本身占 54.4px，左右余量只剩 3.4 / 4.2px。横向漂 6px 会把描边切掉一块。
     纵向没有这个问题——.rail 是 top:0 / bottom:0，上下余量有几百像素。
     读起来也更对：像书脊上的字在慢慢挪，而不是被风吹。
     用独立的 translate 属性，不碰 transform（.rail__mark 的 rotate 在那边）。 */
  animation: railDrift 30s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

/* 左右两条栏反向走：一个往上一个往下，两条留白才像同一个空间的两侧，
   而不是两块各自抖动的图。
   网点和速度线不参与 —— 网点一动就变成噪点。 */
.rail--r .rail__num {
  animation-direction: reverse;
}

@keyframes railDrift {
  0%,
  100% {
    translate: 0 0;
  }
  50% {
    translate: 0 7px;
  }
}

/* 竖排的大字：边栏最窄处只有 90px 出头，「1995」这种四个字符横排
   根本放不下。竖起来就有地方了，而且读起来像书脊——正是边栏该有的样子。
   text-orientation: upright 让拉丁字母正立，不用歪着头读。
   竖排元素的「宽度」是 line-height 那一维（0.92 × 字号），
   所以系数可以比横排激进：72cqw 下大约占内容盒的 66%。
   必须写在 .rail__num 之后：同特异性，靠顺序覆盖字号。 */
.rail__num--tall {
  writing-mode: vertical-rl;
  text-orientation: upright;
  font-size: clamp(1.5rem, 3.4vw, 3.4rem);
  font-size: clamp(1.5rem, 72cqw, 6.5rem);
  line-height: 0.92;
  letter-spacing: 0.02em;
}

/* 竖排小注释：一行字顺着边栏往下走，像书脊 */
.rail__spine {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 0.6rem;
  font-size: clamp(0.6rem, 5cqw, 0.78rem);
  font-weight: 700;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--grey);
  white-space: nowrap;
}

/* 网点：漫画的调子。窄栏里竖着铺一条，两头淡出。 */
.rail--halftone::before {
  content: '';
  width: 100%;
  max-width: 240px;
  height: 52%;
  background-image: radial-gradient(circle at center, rgba(242, 235, 221, 0.46) 1.5px, transparent 1.8px);
  background-size: 10px 10px;
  -webkit-mask-image: radial-gradient(ellipse at center, #000 34%, transparent 84%);
  mask-image: radial-gradient(ellipse at center, #000 34%, transparent 84%);
}

/* 速度线：几道红线往同一个方向斜切，两头淡出 */
.rail--lines::before {
  content: '';
  width: 62%;
  height: 44%;
  background: repeating-linear-gradient(-38deg, var(--accent) 0 2px, transparent 2px 13px);
  opacity: 0.66;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 32%, #000 68%, transparent);
}

/* 贴纸碎片：山。雪板那条线上的固定角色，之前只在牌堆和地图缩略图里出现过 */
.rail--mountain::before {
  content: '';
  width: min(86%, 180px);
  aspect-ratio: 1;
  background: url(../assets/stickers/mountain.svg) center / contain no-repeat;
  opacity: 0.7;
  transform: rotate(-8deg);
  filter: drop-shadow(4px 5px 0 rgba(0, 0, 0, 0.55));
}

/* 一颗星：不是每个 section 都要有 */
.rail__mark {
  font-family: var(--font-hand);
  font-style: normal;
  font-size: clamp(1.6rem, 3vw, 2.6rem);
  font-size: clamp(1.6rem, 34cqw, 4.4rem);
  line-height: 1;
  color: var(--yellow);
  transform: rotate(-8deg);
}

/* 红箭头，指着正文那一侧 */
.rail__mark--arrow {
  font-size: clamp(1.8rem, 3vw, 2.8rem);
  color: var(--accent);
  transform: rotate(180deg);
}

/* ---------- Comic Universe：九个贴纸是固定角色，不是装饰 ----------
   规则：每个 section 只放 2–3 个，必须有主次，绝不满屏铺。
   统一用 .fig + 一个修饰类定位；尺寸/角度由 --s / --r 控制。 */
.fig {
  position: absolute;
  z-index: 3;
  width: var(--s, 88px);
  height: var(--s, 88px);
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  transform: rotate(var(--r, 0deg));
  filter: drop-shadow(5px 6px 0 rgba(0, 0, 0, 0.5));
}

/* ABOUT：黑猫趴在牌堆左下角，咖啡放在右上角。
   z-index 高过牌面，读起来是「桌上的东西压在卡片上」。
   230 是跟着牌堆走的：.deck__card:hover 会跳到 220，
   贴纸必须比它还高，否则鼠标一碰牌，猫就被牌盖住了。 */
.fig--cat {
  left: -30px;
  bottom: -16px;
  z-index: 230;
  --s: 116px;
  --r: -10deg;
}

.fig--coffee {
  right: -30px;
  top: 26px;
  z-index: 230;
  --s: 68px;
  --r: 10deg;
}

/* SKILLS：电脑和爆炸星是能力宇宙里的物件，层级压在能力标签之下，
   让标签从它们上面飘过去，像真的在一个空间里 */
.universe .fig {
  z-index: 1;
  filter: drop-shadow(4px 5px 0 rgba(0, 0, 0, 0.55));
}

.fig--laptop {
  left: 3.5%;
  bottom: 7%;
  --s: 96px;
  --r: -6deg;
}

.fig--burst {
  right: 4%;
  top: 9%;
  --s: 74px;
  --r: 8deg;
}

/* FIND ME：飞机和相机贴在图上（都放在图内，避免任何横向溢出） */
.fig--plane {
  right: 5%;
  top: 7%;
  --s: 78px;
  --r: 14deg;
  /* 飞机在图上慢慢巡航：26s 一个来回、横向 42px。
     水平位移走独立的 translate 属性，旋转和上下浮动走 transform，
     两条动画各管一个属性，互不覆盖。
     全程都往左移（0 → -42px），因为它是 right 定位的，往右会顶出图外。
     缓动取对称的 ease-in-out：--ease 是 easeOut 型的，用在往返循环里
     会在折返点猛地一顿，像撞了墙。 */
  animation: planeBob 6.4s cubic-bezier(0.45, 0, 0.55, 1) infinite,
    planeGlide 26s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

/* 上下浮动：只有 6px，和旋转一起做，读起来是「浮着」而不是「在动」 */
@keyframes planeBob {
  0%,
  100% {
    transform: rotate(14deg) translateY(0);
  }
  50% {
    transform: rotate(12deg) translateY(-6px);
  }
}

@keyframes planeGlide {
  0%,
  100% {
    translate: 0 0;
  }
  50% {
    translate: -42px 0;
  }
}

.fig--camera {
  left: 5%;
  bottom: 9%;
  --s: 66px;
  --r: -8deg;
  /* 相机基本上不动。14 秒里只有最后 0.5 秒会「咔嚓」一下——
     一次很小的弹跳加一下快门闪光，然后继续安静。
     频率刻意压到很低：它是这条路上一台旧相机，不是一颗 LED。 */
  animation: cameraSnap 14s linear infinite;
}

@keyframes cameraSnap {
  0%,
  96%,
  100% {
    transform: rotate(-8deg) translateY(0) scale(1);
    filter: drop-shadow(5px 6px 0 rgba(0, 0, 0, 0.5)) brightness(1);
  }
  97% {
    transform: rotate(-7deg) translateY(-3px) scale(1.035);
    filter: drop-shadow(5px 6px 0 rgba(0, 0, 0, 0.5)) brightness(1.85);
  }
  98% {
    transform: rotate(-9deg) translateY(1px) scale(0.985);
    filter: drop-shadow(5px 6px 0 rgba(0, 0, 0, 0.5)) brightness(1.12);
  }
  99.5% {
    transform: rotate(-8deg) translateY(0) scale(1);
    filter: drop-shadow(5px 6px 0 rgba(0, 0, 0, 0.5)) brightness(1);
  }
}

/* CONTACT：最后一页出来道别的两只 */
.fig--cat2 {
  right: 15%;
  top: 25%;
  --s: 78px;
  --r: 9deg;
}

.fig--bear {
  right: 3%;
  top: 11%;
  --s: 108px;
  --r: -7deg;
}

/* 小标签：等宽大写 + 描边，杂志边栏的味道 */
.tag {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  line-height: 1;
  padding: 0.36rem 0.5rem;
  border: 2px solid var(--line-strong);
  color: var(--paper);
}

.tag--accent {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}

/* 漫画面板：粗描边 + 硬阴影，无模糊 */
.panel {
  background: var(--paper);
  color: var(--ink);
  border: 3px solid var(--ink);
  box-shadow: 8px 8px 0 var(--accent);
}

/* 贴纸 */
.sticker {
  display: block;
  width: var(--sz, 44px);
  height: var(--sz, 44px);
  object-fit: contain;
  pointer-events: none;
  user-select: none;
}

/* 手写体的音效字，克制使用 */
.sound {
  font-family: var(--font-hand);
  color: var(--yellow);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  transform: rotate(-6deg);
  pointer-events: none;
  user-select: none;
}

/* 入场动效（仅在 JS 可用时隐藏初态，避免无 JS 白屏） */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
  transition-delay: var(--d, 0ms);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* ==========================================================================
   05  顶栏 + 进度线
   ========================================================================== */

.progress {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 90;
  display: block;
  width: 100%;
  height: 3px;
  background: var(--accent);
  transform: scaleX(var(--p, 0));
  transform-origin: 0 50%;
  will-change: transform;
}

.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 80;
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0 var(--pad) 0 calc(var(--nav-w) + var(--pad));
  pointer-events: none;
}

.topbar > * {
  pointer-events: auto;
}

.topbar__mark {
  display: inline-flex;
  align-items: baseline;
  gap: 0.32em;
  font-family: var(--font-display);
  font-size: 0.86rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  line-height: 1;
  transition: color 0.3s var(--ease);
}

.topbar__mark:hover {
  color: var(--accent);
}

.topbar__mark2 i {
  color: var(--accent);
  font-style: normal;
}

.topbar__meta {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  color: var(--grey);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  line-height: 1;
}

.topbar__sep {
  color: var(--dim);
}

/* 常驻状态：`● CURRENTLY EXPLORING`。
   圆点是唯一在动的东西——3.2s 一次很轻的呼吸；文字永远静止，
   不闪、不轮播。它要传达的是「这个人还在更新」，不是「这里有个动画」。
   放在顶栏正中：每一屏都看得见，比塞进页脚更符合「持续更新的个人空间」。 */
.topbar__now {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  line-height: 1;
  color: var(--grey);
  white-space: nowrap;
}

.now__dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: nowPulse 3.2s var(--ease) infinite;
}

@keyframes nowPulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.42;
    transform: scale(0.82);
  }
}

/* ==========================================================================
   06  左侧数字导航
   ========================================================================== */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 85;
  width: var(--nav-w);
  height: 100vh; /* 老 Safari 不认 svh，先给个 vh 兜底 */
  height: 100svh;
  display: flex;
  align-items: center;
  padding-left: clamp(1rem, 2.4vw, 1.9rem);
}

.nav__list {
  display: grid;
  gap: 1.35rem;
}

.nav__item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  line-height: 1;
  color: var(--dim);
  transition: color 0.32s var(--ease);
}

/* 当前所在区块：左边一条短红线。默认状态就只有 01 02 03 04。 */
.nav__item::before {
  content: '';
  position: absolute;
  left: -0.85rem;
  top: 50%;
  width: 3px;
  height: 0;
  margin-top: -0.5rem;
  background: var(--accent);
  transition: height 0.34s var(--ease);
}

.nav__item.is-active::before {
  height: 1rem;
}

.nav__num {
  font-style: normal;
  font-weight: 700;
  transition: color 0.32s var(--ease);
}

/* 标签本身就是「短横线 + 文字」的一行 flex。
   短横线以前是 absolute + left:1.35rem，而标签从 1.6rem 开始——
   那条红线正好压在第一个字母上。放进流里就不可能重叠。

   出场慢、收场快：收场只有 0.13s。鼠标从 01 划到 02 的途中，
   01 必须已经淡干净了，否则两个标签会同时半透明地叠在那儿。 */
.nav__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-body);
  font-size: 0.76rem;
  letter-spacing: 0.01em;
  color: var(--paper);
  opacity: 0;
  transform: translateX(-8px);
  transition: opacity 0.13s var(--ease), transform 0.13s var(--ease);
  white-space: nowrap;
}

.nav__label::before {
  content: '';
  flex: none;
  width: 0.55rem;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.14s var(--ease);
}

/* 一次只展开一个。
   .is-open 由 js/nav.js 挂：鼠标进到哪一项，就把别的项先摘干净，
   所以任何时刻最多只有一个标签在动，不会出现两个同时半开的残影。
   :hover / :focus-visible 保留着当无 JS 时的兜底。 */
.nav__item:hover .nav__label,
.nav__item.is-open .nav__label,
.nav__item:focus-visible .nav__label {
  opacity: 1;
  transform: none;
  transition-duration: 0.26s;
}

.nav__item:hover .nav__label::before,
.nav__item.is-open .nav__label::before,
.nav__item:focus-visible .nav__label::before {
  transform: scaleX(1);
  transition-duration: 0.3s;
}

.nav__item:hover,
.nav__item.is-active {
  color: var(--accent);
}

.nav__item.is-active .nav__num,
.nav__item.is-active .nav__label {
  color: var(--accent);
}

/* ==========================================================================
   07  开场动画（漫画标题卡）
   --------------------------------------------------------------------------
   六段节奏，总计 ≈3.4s（JS 在 2.8s 时掀开，掀开本身 0.62s）：

     P1  0.00–0.42s  黑场极简：一根细线拉出，角落一个 18
     P2  0.38–1.50s  速度线 + halftone 网点从中心扩散
     P3  0.98–1.62s  EIGHTEEN 擦出（带一点过冲）
     P4  1.56–2.20s  TANG. 擦出 → 1.95s 撞击：红色硬影砸出来 + 整卡震一下
     P5  2.08–2.94s  漫画角色闪现一下就收
     P6  2.80s       整张封面向上掀起

   所有延迟都写在这里，JS 只负责「什么时候掀」和「什么时候算完」。
   ========================================================================== */

.intro {
  position: fixed;
  inset: 0;
  z-index: 200;
  /* 默认不出现：没有 JS（内联脚本没跑）或已播过（.intro-skip）时，
     第一帧直接就是正文，不会白屏、也不会闪一下才消失。
     真正要播时由下面的 .js .intro 打开——那一刻 CSS 动画就自己跑起来了。 */
  display: none;
  place-items: center;
  background: var(--ink);
  overflow: hidden;
  cursor: pointer;
}

.js .intro {
  display: grid;
}

/* hidden 属性必须压得住上面的 display:grid——类选择器特异性更高，
   所以这里要写成 .js .intro[hidden] 而不是指望 UA 样式 */
.js .intro[hidden],
.js.intro-skip .intro {
  display: none;
}

.intro.is-done {
  animation: introOut 0.62s var(--ease) forwards;
  pointer-events: none;
}

/* P6：封面被掀开 */
@keyframes introOut {
  0% {
    clip-path: inset(0 0 0 0);
    transform: none;
    opacity: 1;
  }
  100% {
    clip-path: inset(0 0 100% 0);
    transform: translateY(-2%) scale(1.02);
    opacity: 0.85;
  }
}

/* ---------- P1 黑场极简 ---------- */

.intro__rule {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 1;
  width: min(52vw, 520px);
  height: 2px;
  background: var(--paper);
  opacity: 0.8;
  transform: translateX(-50%) scaleX(0);
  animation: ruleIn 0.42s var(--ease) forwards, ruleOut 0.34s var(--ease) 0.5s forwards;
}

@keyframes ruleIn {
  to {
    transform: translateX(-50%) scaleX(1);
  }
}

@keyframes ruleOut {
  to {
    transform: translateX(-50%) scaleX(1.7);
    opacity: 0;
  }
}

.intro__index {
  position: absolute;
  left: clamp(1rem, 3vw, 2.2rem);
  top: clamp(1rem, 3vw, 2.2rem);
  z-index: 1;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.3em;
  color: var(--dim);
  opacity: 0;
  animation: fadeUp 0.5s var(--ease) 0.12s forwards;
}

/* ---------- P2 速度线 + 网点 ---------- */

.intro__burst {
  position: absolute;
  inset: -25%;
  z-index: 0;
  background: repeating-conic-gradient(
    from 0deg at 50% 50%,
    var(--paper) 0deg 0.42deg,
    transparent 0.42deg 5deg
  );
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent 0 9%, #000 32%, transparent 76%);
  mask-image: radial-gradient(circle at 50% 50%, transparent 0 9%, #000 32%, transparent 76%);
  opacity: 0;
  transform: scale(0.55) rotate(-10deg);
  animation: burstIn 1.5s var(--ease) 0.38s forwards;
}

@keyframes burstIn {
  30% {
    opacity: 0.55;
  }
  100% {
    opacity: 0.16;
    transform: scale(1.06) rotate(0deg);
  }
}

.intro__dots {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: radial-gradient(rgba(242, 235, 221, 0.16) 1.4px, transparent 1.5px);
  background-size: 9px 9px;
  opacity: 0;
  -webkit-mask-image: radial-gradient(circle at 50% 50%, transparent 0 22%, #000 62%, transparent 88%);
  mask-image: radial-gradient(circle at 50% 50%, transparent 0 22%, #000 62%, transparent 88%);
  animation: dotsIn 1.2s var(--ease) 0.46s forwards;
}

@keyframes dotsIn {
  to {
    opacity: 0.5;
  }
}

/* ---------- P3 / P4 标题 ---------- */

.intro__stage {
  position: relative;
  z-index: 3;
  text-align: center;
  padding: 0 1rem;
}

.intro__card {
  font-family: var(--font-display);
  font-size: clamp(2.3rem, 10.5vw, 7.2rem);
  line-height: 0.88;
  letter-spacing: -0.025em;
  text-transform: uppercase;
  /* P4 撞击：整张标题卡震一下，不是弹跳，是「被砸中」 */
  animation: introImpact 0.44s var(--ease) 1.95s both;
}

@keyframes introImpact {
  0% {
    transform: translate(0, 0);
  }
  14% {
    transform: translate(-5px, 2px);
  }
  30% {
    transform: translate(4px, -3px);
  }
  46% {
    transform: translate(-3px, -1px);
  }
  62% {
    transform: translate(2px, 2px);
  }
  100% {
    transform: translate(0, 0);
  }
}

.intro__line {
  display: block;
  clip-path: inset(0 100% 0 0);
  animation: wipeIn 0.64s var(--ease) 0.98s forwards;
}

.intro__line--b {
  padding-left: 0.06em;
  text-shadow: 0 0 0 var(--accent);
  animation: wipeIn 0.56s var(--ease) 1.56s forwards, tangHit 0.3s var(--ease) 1.95s both;
}

.intro__dot {
  font-style: normal;
  color: var(--accent);
  text-shadow: none;
}

@keyframes wipeIn {
  0% {
    clip-path: inset(0 100% 0 0);
    transform: translateX(-0.06em);
  }
  72% {
    clip-path: inset(0 0 0 0);
    transform: translateX(0.012em);
  }
  100% {
    clip-path: inset(0 0 0 0);
    transform: none;
  }
}

/* 红色硬影是「砸」出来的，不是一开始就在那儿 */
@keyframes tangHit {
  from {
    text-shadow: 0 0 0 var(--accent);
  }
  to {
    text-shadow: 0.05em 0.05em 0 var(--accent);
  }
}

.intro__sub {
  margin-top: 0.9rem;
  font-family: var(--font-mono);
  font-size: clamp(0.6rem, 1.5vw, 0.74rem);
  font-weight: 700;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--grey);
  opacity: 0;
  animation: fadeUp 0.6s var(--ease) 2.28s forwards;
}

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ---------- P5 漫画角色闪现 ---------- */

.intro__flash {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.intro__st {
  position: absolute;
  left: var(--a, 50%);
  top: var(--b, 50%);
  width: var(--s, 80px);
  height: var(--s, 80px);
  object-fit: contain;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.4) rotate(var(--r, 0deg));
  filter: drop-shadow(5px 6px 0 rgba(0, 0, 0, 0.5));
  animation: stFlash 0.62s var(--ease-back) both;
}

.intro__st:nth-child(1) {
  animation-delay: 2.08s;
}
.intro__st:nth-child(2) {
  animation-delay: 2.16s;
}
.intro__st:nth-child(3) {
  animation-delay: 2.24s;
}
.intro__st:nth-child(4) {
  animation-delay: 2.32s;
}

@keyframes stFlash {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.4) rotate(var(--r, 0deg));
  }
  52% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.09) rotate(var(--r, 0deg));
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1) rotate(var(--r, 0deg));
  }
}

.intro__skip {
  position: absolute;
  right: clamp(1rem, 3vw, 2.2rem);
  bottom: clamp(1rem, 3vw, 2.2rem);
  z-index: 4;
  font-family: var(--font-mono);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--grey);
  padding: 0.5rem 0.8rem;
  border: 2px solid var(--line);
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}

.intro__skip:hover {
  color: var(--paper);
  border-color: var(--paper);
}

/* ==========================================================================
   08  区块骨架
   ========================================================================== */

.page {
  position: relative;
  z-index: 1;
  padding-left: var(--nav-w);
  /* 出血层的安全网。
     页脚那条铁轨靠负 margin 把自己撑到整个视口宽，两条边相加之后
     浏览器会取整到 1/64px —— 右边很容易算出 1440.0156 这种值，
     文档的 scrollWidth 一进位就是 1441，整页凭空多出一条 1px 的
     横向滚动条。所有视口的 docOver 都是 1，就是这么来的。
     .page 的 padding box 正好横跨整个视口（0 → 视口宽），
     所以在这里裁一刀最省事，也不用把铁轨缩窄 1px（那会在最右边
     留下一道 1px 的缝）。
     clip 而不是 hidden：clip 不建立滚动容器，不会连带把 overflow-y
     变成 auto、把整页变成一个可滚动盒子。
     这里没有 position: sticky 的后代（固定定位的浮层和相册都在
     <main> 外面），所以裁一刀不会伤到任何东西。 */
  overflow-x: clip;
  overflow-y: visible;
}

.section {
  position: relative;
  /* 92 而不是 100：四个区块是同一本漫画的不同跨页，不是一个一个 landing page。
     留白靠内容本身撑，不靠空 viewport。 */
  min-height: 92vh; /* 兜底 */
  min-height: 92svh;
  display: grid;
  /* 必须显式声明，否则横向胶片条的 max-content 会把隐式列顶开 */
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  padding: calc(var(--topbar-h) + clamp(0.7rem, 2.6vh, 1.7rem)) var(--pad)
    clamp(1.5rem, 4.2vh, 2.6rem);
  scroll-snap-align: start;
}

/* 右侧超大页码水印已经搬走了。
   原来它贴在正文右边缘（x ≈ 1396 @1440），位置尴尬：
   既没用到真正的留白，又和边栏里的出血页码重复。
   现在页码交给 .rail__num，站在页面最外沿。 */

/* 区块头：左 eyebrow / 右 注释 */
.secHead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.55rem;
  margin-bottom: clamp(0.65rem, 1.9vh, 1.25rem);
  border-bottom: 2px solid var(--line);
}

.eyebrow {
  display: inline-flex;
  align-items: baseline;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper);
}

.eyebrow__num {
  font-style: normal;
  color: var(--accent);
  letter-spacing: 0.08em;
}

/* 分隔号：`01 — About` 中间那一道。
   有了它，编号和名字才是一行，而不是「一个数字 + 一个词」两块东西。 */
.eyebrow__sep {
  font-style: normal;
  color: var(--dim);
  letter-spacing: 0;
}

/* 副标题：从刊头降级下来的那行大字。
   全站统一——刊头永远只有 `NN — NAME` 一行，下面这行才是 editorial 的那句话，
   而且明显矮一档，不会再出现「同一屏两个平级标题」。

   ABOUT 没有副标题：hero 本身就是身份，再补一行只是多话。 */
.secSub {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 3.6vw, 2.5rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  text-transform: uppercase;
  color: var(--paper-2);
  margin: 0 0 clamp(0.7rem, 1.8vh, 1.2rem);
}

.secSub span {
  display: block;
}

/* 最后一行落主题色硬影：和 hero 的 TANG. 同一条排印语言 */
.secSub__b {
  padding-left: 0.03em;
  color: var(--paper);
  text-shadow: 0.05em 0.05em 0 var(--accent);
  transform: rotate(-1deg);
  transform-origin: left center;
}

.secSub__b i {
  font-style: normal;
  color: var(--accent);
  text-shadow: none;
}

/* ==========================================================================
   09  ABOUT
   ========================================================================== */

.aboutGrid {
  display: grid;
  grid-template-columns: minmax(0, 1.06fr) minmax(0, 0.94fr);
  gap: clamp(1.1rem, 3vw, 2.5rem);
  align-items: center;
}

/* EIGHTEEN 空心巨大 / TANG. 巨大实心带红硬影。
   字号刻意顶到左栏宽度之外一点点——排版顶出边界是杂志的常规操作。 */
.hero {
  font-family: var(--font-display);
  font-size: clamp(2.9rem, 8.4vw, 7rem);
  line-height: 0.82;
  letter-spacing: -0.038em;
  text-transform: uppercase;
  margin-bottom: clamp(0.7rem, 2vh, 1.25rem);
}

.hero__line {
  display: block;
}

/* 第一行空心描边，第二行实心 + 主题色硬投影：漫画排印的基本对比 */
@supports (-webkit-text-stroke: 1px red) {
  .hero__line:first-child {
    color: transparent;
    -webkit-text-stroke: clamp(1.4px, 0.017em, 2.6px) var(--paper);
  }
}

.hero__line--b {
  padding-left: 0.04em;
  text-shadow: 0.05em 0.05em 0 var(--accent);
  /* 用独立的 rotate 属性，不用 transform：
     glitch 那几帧要动 transform，写成 transform 会互相覆盖。
     独立变换（translate / rotate / scale）先算，transform 后算，两者叠加。 */
  rotate: -1.1deg;
  transform-origin: left center;
}

.hero__dot {
  font-style: normal;
  color: var(--accent);
  text-shadow: none;
}

/* ---------- CRT glitch（非常克制，但这一次要看得见） ----------
   原则：平时是一行干净的字。
   每个 7.2s 周期里只有约 1.8% 的时间在抖 —— 换算过来一次 130ms 左右，
   落在 50–180ms 的约定里。两层幽灵（红 / 蓝）平时 opacity:0，
   只在那个窗口里出现，所以随便截一帧，读到的都是一句完整的话。

   周期从 9.5s 收到 7.2s：这不是「把 duration 改短」那种偷懒做法，
   而是因为干扰窗从 1.5% 放宽到 1.8%（单次时长 130ms，仍在约定内），
   再配 9.5s 周期就太稀疏了 —— 一次要等快 10 秒，
   读者根本不知道这里有动效。7.2s 是「每隔几秒能撞见一次」的下限。
   真正的强度来自位移幅度、skew、以及幽灵层从描边改填色。

   两行故意错开 0.37s：同时抖就变成「整块在闪」，那像页面坏了，
   不像信号被干扰。 */
.hero__line {
  display: block;
  position: relative;
  /* steps(1, end) 是这一版 glitch 的关键，比加大幅度更重要。
     linear 会把位移插值成一条平滑曲线，读起来只是「字轻轻晃了一下」；
     真实的数字故障是**跳**的 —— 值保持不变，然后某一刻突然跳到下一个位置。
     steps(1, end) 让每个关键帧区间内保持不动、到区间末尾才跳，
     于是 130ms 里出现 5 次瞬移，才读成「信号断了」而不是「字在摇」。 */
  animation: heroJitter var(--glitch-t) steps(1, end) infinite;
}

.hero__line::before,
.hero__line::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  /* 第二行有 0.04em 的左内距，幽灵要跟着一起让，否则平时就错着位 */
  padding-left: inherit;
  pointer-events: none;
  opacity: 0;
  /* 幽灵层填实色，不描边 —— 这是「不够明显」的头号原因。
     上一版只给描边，结果是「1.6px 红描边叠在 2.6px 白描边上」：
     两个颜色都细，叠在一起几乎看不出差别，尤其 EIGHTEEN 本身就是镂空字，
     红描边直接落在白描边上，等于没加。
     现在填实色。横带只有 15% 高，所以看到的是「一条彩色横带切过字」，
     那正是 CRT 撕裂的样子，而不是「整块字变色」。
     硬影仍然去掉：两层各带一条影子会糊成一团。 */
  text-shadow: none;
  -webkit-text-stroke-width: 0;
}

.hero__line::before {
  color: var(--red);
  animation: heroGhostRed var(--glitch-t) steps(1, end) infinite;
}

.hero__line::after {
  color: var(--blue);
  animation: heroGhostBlue var(--glitch-t) steps(1, end) infinite;
}

.hero__line--b::before,
.hero__line--b::after,
.hero__line--b {
  animation-delay: 0.37s;
}

/* 主字：一次干扰里跳 5 次，横向最多 3.5px、纵向 0.5px。
   为什么敢给到 3.5px（上一版只有 1.5px）：配合 steps 的瞬移，
   3.5px 是「跳」出来的，眼睛抓得到；1.5px 的平滑插值几乎量不出来。
   skewX 是这一版新加的 —— 只平移的话字还是正的，加 1.4deg 斜切之后
   笔画会轻微错开，才像行同步出问题，而不只是字在左右晃。 */
@keyframes heroJitter {
  0%,
  1.8%,
  100% {
    transform: translate3d(0, 0, 0) skewX(0deg);
  }
  0.3% {
    transform: translate3d(-3.5px, 0.5px, 0) skewX(-1.4deg);
  }
  0.7% {
    transform: translate3d(2.5px, -0.5px, 0) skewX(1deg);
  }
  1.1% {
    transform: translate3d(-2px, 0.5px, 0) skewX(-0.7deg);
  }
  1.5% {
    transform: translate3d(1.5px, 0, 0) skewX(0.3deg);
  }
}

/* 红色幽灵：每次切出 15% 高的一条横带，往左偏。
   15% 是量出来的 —— 再宽就变成「半个字被染红」，再窄看不出是字。
   位移给到 6px 也不显吵，正因为只有那一条在动：
   整块字平移 6px 是排版塌了，一条横带平移 6px 才叫撕裂。
   四条横带的位置刻意不重叠，读起来是扫描出问题，不是整块重影。
   幅度走 --gx 变量（基础 6px / hover 8px），这样不用维护两套关键帧。 */
@keyframes heroGhostRed {
  0%,
  1.8%,
  100% {
    opacity: 0;
    transform: translate3d(0, 0, 0);
    clip-path: inset(0 0 0 0);
  }
  0.3% {
    opacity: 0.66;
    transform: translate3d(calc(var(--gx) * -1), 1px, 0);
    clip-path: inset(18% 0 67% 0);
  }
  0.7% {
    opacity: 0.54;
    transform: translate3d(var(--gx), -1px, 0);
    clip-path: inset(56% 0 29% 0);
  }
  1.1% {
    opacity: 0.44;
    transform: translate3d(calc(var(--gx) * -0.7), 1px, 0);
    clip-path: inset(36% 0 49% 0);
  }
  1.5% {
    opacity: 0.3;
    transform: translate3d(calc(var(--gx) * 0.5), 0, 0);
    clip-path: inset(72% 0 13% 0);
  }
}

/* 蓝色幽灵：反方向、另一组横带，两层错开才读成色散而不是重影。
   四条带子跟红色那组两两错开，叠起来是四段不同颜色的切片。 */
@keyframes heroGhostBlue {
  0%,
  1.8%,
  100% {
    opacity: 0;
    transform: translate3d(0, 0, 0);
    clip-path: inset(0 0 0 0);
  }
  0.3% {
    opacity: 0.58;
    transform: translate3d(var(--gx), -1px, 0);
    clip-path: inset(52% 0 33% 0);
  }
  0.7% {
    opacity: 0.48;
    transform: translate3d(calc(var(--gx) * -1), 1px, 0);
    clip-path: inset(24% 0 61% 0);
  }
  1.1% {
    opacity: 0.4;
    transform: translate3d(calc(var(--gx) * 0.7), -1px, 0);
    clip-path: inset(64% 0 21% 0);
  }
  1.5% {
    opacity: 0.28;
    transform: translate3d(calc(var(--gx) * -0.5), 0, 0);
    clip-path: inset(8% 0 77% 0);
  }
}

/* 扫描线：一层极细的横向条纹，只在干扰的那一下亮起来 */
.hero::after {
  content: '';
  position: absolute;
  inset: -0.06em -0.04em;
  pointer-events: none;
  opacity: 0;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(242, 235, 221, 0.3) 0 1px,
    transparent 1px 3px
  );
  animation: heroScan var(--glitch-t) steps(1, end) infinite;
}

@keyframes heroScan {
  0%,
  1.8%,
  100% {
    opacity: 0;
  }
  0.4% {
    opacity: 0.34;
  }
  0.9% {
    opacity: 0.2;
  }
  1.3% {
    opacity: 0.26;
  }
}

/* 整行的 CRT 明暗闪：幅度刻意压到几乎量不出来，
   它的作用只是让上面那几层看起来像「同一台机器出的事」 */
@keyframes heroFlicker {
  0%,
  1.8%,
  100% {
    filter: none;
  }
  0.5% {
    filter: brightness(1.12);
  }
  1% {
    filter: brightness(0.94);
  }
}

/* 极慢的横向漂移：26s 走 22px。
   用独立的 translate 属性，这样和 [data-reveal] 的 transform 入场
   互不覆盖（独立变换先算，transform 后算）。

   --glitch-t 是干扰周期，--gx 是幽灵层横带的最大位移。
   两个都定义在这里（而不是写在每条动画上），好处是：
   1. 想整体调强度只改这两个数；
   2. hover 时只要覆盖这两个变量，四层动画（主字 / 红幽灵 / 蓝幽灵 / 扫描线）
      一起提速提幅度 —— 自定义属性会继承，不需要把每条规则再列一遍。 */
.hero {
  position: relative;
  --glitch-t: 7.2s;
  --gx: 6px;
  animation: heroDrift 26s var(--ease) infinite,
    heroFlicker var(--glitch-t) steps(1, end) infinite;
}

@keyframes heroDrift {
  0%,
  100% {
    translate: -11px 0;
  }
  50% {
    translate: 11px 0;
  }
}

/* hover：频率提到 3.3 倍（7.2s → 2.2s），幅度提一档（6px → 8px）。
   幅度只提一档是有意的 —— 再大就从「信号被干扰」变成「排版坏了」。
   漂移（heroDrift 26s）不参与，否则鼠标一碰整行会开始平移。 */
.hero:hover {
  --glitch-t: 2.2s;
  --gx: 8px;
}

.idcard {
  display: flex;
  align-items: flex-start;
  gap: clamp(0.9rem, 2vw, 1.4rem);
}

.avatar {
  position: relative;
  flex: none;
  width: clamp(84px, 9.5vw, 126px);
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  background: var(--paper);
  border: 3px solid var(--paper);
  box-shadow: 7px 7px 0 var(--accent);
  transform: rotate(-3deg);
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.avatar__initial {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 2.4vw, 1.7rem);
  color: var(--ink);
  opacity: 0;
}

.avatar.is-empty img {
  display: none;
}

.avatar.is-empty .avatar__initial {
  opacity: 1;
}

.idcard__meta {
  display: grid;
  gap: 0.42rem;
  justify-items: start;
  min-width: 0;
}

/* 小注（Small Copy）：跟在身份后面的一句话。不解释、不展开。
   等宽 + 全大写 + 灰色，待在名片里、头像旁边——
   读起来像杂志页脚那行小字，而不是一句自我介绍。 */
.bio {
  max-width: 24ch;
  font-family: var(--font-mono);
  font-size: clamp(0.6rem, 0.82vw, 0.7rem);
  font-weight: 700;
  line-height: 1.7;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--grey);
}

/* 角色声明（Main Copy）：四个词条，一人一行。
   字号比正文大一档、用 display 字体、全大写——它不是一段自我介绍，
   是这一页的 punch line，读起来该像杂志人物页开头的身份行，
   而不是 resume 里的技能列表。
   最后一行换成主题色：四条一模一样的灰字读下来是平的，需要一个落点。 */
.statement {
  display: grid;
  gap: 0.04em;
  margin-bottom: clamp(0.85rem, 2.4vh, 1.35rem);
  font-family: var(--font-display);
  font-size: clamp(1rem, 2.05vw, 1.72rem);
  line-height: 1.06;
  letter-spacing: -0.018em;
  text-transform: uppercase;
  color: var(--paper-2);
}

.statement span {
  display: block;
}

.statement span:last-child {
  color: var(--accent);
}

/* ---------- Journal 牌堆 ---------- */

/* 牌堆尺寸只由一个 --cw 驱动，其余按比例推导：
     牌面高 = --cw × 0.74、每张错开 --cw × 0.107 / 0.122
   小屏只改 --cw，日期角、正文、错位量会一起等比缩放，不会错位。

   下面这套值是**无 JS 时的兜底**：真正生效的 --cw / --sx / --sy，以及
   「这一叠占多宽多高」的 --dw / --dh，由 js/journal.js 的 layout()
   按「露几张、压几张、栏多宽、屏多高」算出来。
   以后加条目只改 JSON，不用动这里。 */
.deck {
  --cw: 226px;
  --ch: calc(var(--cw) * 0.74);
  --sx: calc(var(--cw) * 0.107);
  --sy: calc(var(--cw) * 0.122);
  --n: 13;
  --dw: calc(var(--cw) + var(--sx) * 3 + var(--cw) * 0.256);
  --dh: calc(var(--ch) + var(--sy) * 3 + var(--cw) * 0.256);
  position: relative;
  justify-self: end;
  width: var(--dw);
}

.deck__label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--grey);
  margin-bottom: 0.6rem;
}

.deck__count {
  font-style: normal;
  color: var(--accent);
}

.deck__stack {
  position: relative;
  width: var(--dw);
  height: var(--dh);
}

/* --i 决定层级（0 = 最新 = 最高）。--ox / --oy 是这张牌在牌堆里的落点（px），
   由 js/journal.js 的 layout() 逐张写入——可见的那几张一层层往右下压，
   压着的那几张往左上叠成一厚摞。
   --k 只是「它是第几张可见牌」，用来给每张一个极小的角度差，
   让这一叠看起来是手放上去的，不是机器排的。
   --zoom / --tilt 只有 hover 时会变，用来做「被碰到」的反馈。 */
.deck__card {
  --k: 0;
  --ox: calc(var(--sx, 0px) * var(--k));
  --oy: calc(var(--sy, 0px) * var(--k));
  --lift: 0px;
  --zoom: 1;
  --tilt: 0deg;
  position: absolute;
  top: 0;
  left: 0;
  z-index: calc(200 - var(--i, 0));
  width: var(--cw);
  height: var(--ch);
  padding: 0;
  text-align: left;
  background: var(--paper);
  color: var(--ink);
  border: 3px solid var(--ink);
  box-shadow: 7px 7px 0 var(--accent);
  transform: translate3d(var(--ox), calc(var(--oy) - var(--lift)), 0)
    rotate(calc((var(--k) - 2) * 0.32deg + var(--tilt))) scale(var(--zoom));
  transition: transform 0.44s var(--ease-back), box-shadow 0.3s var(--ease),
    opacity 0.3s var(--ease);
  will-change: transform;
  overflow: hidden;
  cursor: pointer;
}

/* 压在下面的那几张：只露一条纸边。
   日期角标是黑底的，在黑背景上露出来等于没露——收起它，露出来的才是
   纸面本身，一叠牌的厚度才看得出来。
   硬投影也去掉：它们本来就躲在上面那张下面，露出来的那点影子只会变成
   一排红色小方块，看着像噪点。
   越深越暗（--dim 由 JS 逐张写）：暗下去才像「沉到下面」，一片亮白
   只会读成一堆线。
   它们照样能点（露出的那条边就是热区），点开的就是那一条。 */
.deck__card.is-buried {
  box-shadow: none;
  filter: brightness(var(--dim, 1));
}

.deck__card.is-buried .deck__date {
  display: none;
}

/* hover：把这张牌「抽出来」。
   这里有三件事同时发生，缺一件都会穿帮：

   1. **z-index 跳到整叠之上**。这是关键。光放大、加阴影是没用的 ——
      下面那张的 z-index 本来就比它高（200 vs 199），放大只会让它
      更大面积地被压住。实测：不动 z-index 时，hover 第二张牌有 78% 的
      面积被第一张盖住，读起来就是「我碰的那张被别的牌穿了」。
   2. **transform 负责稳定态**：抬起 7px、放大 1.05、倾斜 -0.9deg。
      倾斜只写在这一条规则里，所以只有被碰的那张会歪。
   3. **translate / rotate 独立属性负责那一下抽动**（cardYank）。
      它们是相加关系，抽动是在稳定态之上再叠一层位移，
      所以不会和上面那条 transform 互相覆盖。

   全程没有 perspective，也没有 preserve-3d：
   整叠是一个平面，靠 z-index + 硬投影做前后关系。
   加 perspective 反而会让后方的牌在 3D 空间里「立起来」穿到前面。 */
.deck__card:hover {
  --lift: 7px;
  --zoom: 1.05;
  --tilt: -0.9deg;
  z-index: 220;
  box-shadow: 11px 11px 0 var(--accent), 0 0 0 3px var(--ink);
  animation: cardYank 0.34s var(--ease) 1;
}

/* 「被鼠标拽了一下」：先快速抽到左上，回弹过冲一点，再落回稳定态。
   0 → 26% 只有 88ms，快到像被扯了一把；58% 那下回弹给它重量感，
   不然就是一条直线弹回去，像没有质量。 */
@keyframes cardYank {
  0% {
    translate: 0 0;
    rotate: 0deg;
  }
  26% {
    translate: -6px -5px;
    rotate: -1.8deg;
  }
  58% {
    translate: 1.6px 1.2px;
    rotate: 0.4deg;
  }
  100% {
    translate: 0 0;
    rotate: 0deg;
  }
}

.deck__card:active {
  --lift: 2px;
  --zoom: 1.015;
}

/* 顶牌：像一张真的摊在桌上的卡片，被空气推着极轻地动一下。
   4.2s 一个周期、角度 ±0.5deg、上下 1.8px —— 小到要盯两秒才敢确认它在动。
   用独立的 rotate / translate 属性承载：上面那条 transform 负责落点、
   抬升、hover 缩放，独立变换先算、transform 后算，两边是相加的，不会打架。
   缓动用对称的 ease-in-out 而不是全站的 --ease —— 后者是 easeOut 型的，
   一次性入场很好看，放在无限往返的循环里会在折返点急停。 */
.deck__card.is-top {
  animation: cardIdle 4.2s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

/* 顶牌 hover 时要两条一起挂。
   为什么必须单写这一条：.deck__card.is-top 和 .deck__card:hover 特异性相同，
   而 is-top 那条在文件里更靠后 —— 它会把 hover 的 cardYank 整个盖掉，
   顶牌就成了唯一一张 hover 不抽动的牌。
   两条动画动的是同一组属性（rotate / translate），所以 cardYank 写在后面，
   抽动期间由它接管；它 fill-mode 是 none，播完自动把属性交还给 cardIdle。 */
.deck__card.is-top:hover {
  animation: cardIdle 4.2s cubic-bezier(0.45, 0, 0.55, 1) infinite,
    cardYank 0.34s var(--ease) 1;
}

@keyframes cardIdle {
  0%,
  100% {
    rotate: 0deg;
    translate: 0 0;
  }
  35% {
    rotate: 0.5deg;
    translate: 0 -1.8px;
  }
  70% {
    rotate: -0.4deg;
    translate: 0 1.2px;
  }
}

/* 挑牌：鼠标落进牌堆时，没被碰到的那几张轻轻退开一点、暗一点。
   不是「整叠一起动」，是「挑中了一张」——被碰的那张抬起来，
   其余让开，视线自然落到手上那一张。 */
.deck__stack:hover .deck__card:not(:hover) {
  --zoom: 0.985;
  opacity: 0.84;
}

/* 左上角日期角标 —— 牌堆里唯一露出来的部分 */
.deck__date {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  display: grid;
  justify-items: center;
  gap: 1px;
  padding: calc(var(--sy) * 0.11) calc(var(--sx) * 0.16);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-display);
  line-height: 1;
}

/* 日期角标必须严丝合缝地落在 --sx × --sy 那个角里，所以跟着错位量算，
   不跟着牌面算。两行：上面两位年份（等宽、红色），下面月份（大字）。
   完整的 1995.08 在卡片底部和浮层里——那里不受角标宽度限制。 */
.deck__yr {
  font-family: var(--font-mono);
  font-style: normal;
  font-size: calc(var(--sy) * 0.27);
  letter-spacing: 0.02em;
  color: var(--accent);
}

.deck__mo {
  font-size: calc(var(--sy) * 0.44);
  letter-spacing: -0.02em;
}

/* 内边距刻意留出 --sx / --sy 的空白：
   牌堆里每张牌只有左上角那一条 L 形露在外面，那里必须只有日期，不能有正文 */
.deck__body {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  padding: calc(var(--sy) + 2px) 0.8rem 10px calc(var(--sx) + 0.3rem);
}

.deck__cat {
  align-self: flex-start;
  font-family: var(--font-mono);
  font-size: max(8.5px, calc(var(--cw) * 0.03));
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1;
  padding: 0.28em 0.42em;
  background: var(--ink);
  color: var(--paper);
}

.deck__title {
  margin-top: 0.5rem;
  font-family: var(--font-display);
  font-size: max(12px, calc(var(--cw) * 0.068));
  line-height: 1.16;
  letter-spacing: -0.01em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.deck__text {
  margin-top: 0.42rem;
  font-size: max(10px, calc(var(--cw) * 0.042));
  line-height: 1.5;
  color: #4a4740;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.deck__foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding-top: 0.4rem;
  border-top: 2px solid rgba(11, 11, 12, 0.16);
  font-family: var(--font-mono);
  font-size: max(7px, calc(var(--cw) * 0.028));
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #6b675e;
}

/* 牌面变小的时候正文会撑爆卡片（高度按比例缩，字号有下限）。
   用容器查询按牌面自己的宽度减内容，而不是按视口猜。 */
.deck__card {
  container: deckcard / size;
}

@container deckcard (max-width: 224px) {
  .deck__text {
    -webkit-line-clamp: 1;
  }
}

@container deckcard (max-width: 196px) {
  .deck__cat {
    display: none;
  }
  .deck__title {
    margin-top: 0;
  }
}

.deck__sticker {
  position: absolute;
  right: 0.55rem;
  bottom: 0.5rem;
  width: calc(var(--cw) * 0.15);
  height: calc(var(--cw) * 0.15);
  transform: rotate(8deg);
  pointer-events: none;
}

/* 手写的 PICK ONE →。就这么多，不解释怎么用。
   （以前这里是「点开任意一张 ↗」，像产品新手引导，已删。） */
.deck__hint {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.32rem;
  margin-top: 0.6rem;
  font-family: var(--font-hand);
  font-size: 0.82rem;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--dim);
  transform: rotate(-2.2deg);
}

.deck__hint i {
  font-style: normal;
  font-size: 1.05rem;
  color: var(--accent);
}

/* ---------- 卡牌浮层 ---------- */

.cardView {
  position: fixed;
  inset: 0;
  z-index: 300;
}

.cardView__scrim {
  position: absolute;
  inset: 0;
  background: rgba(11, 11, 12, 0.88);
  opacity: 0;
  transition: opacity 0.42s var(--ease);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}

.cardView.is-open .cardView__scrim {
  opacity: 1;
}

.cardView__card {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background: var(--paper);
  color: var(--ink);
  border: 4px solid var(--ink);
  box-shadow: 12px 12px 0 var(--accent);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(1.4rem, 4vw, 2.6rem);
  will-change: transform;
}

.cardView__close {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  z-index: 3;
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.9rem;
  line-height: 1;
  color: var(--paper);
  background: var(--ink);
  border: 2px solid var(--ink);
  transition: background 0.25s var(--ease);
}

.cardView__close:hover {
  background: var(--accent);
}

.cardView__media {
  position: relative;
  margin: 0 0 1rem;
  aspect-ratio: 16 / 9;
  background: var(--ink);
  border: 3px solid var(--ink);
  overflow: hidden;
}

.cardView__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.cardView__meta {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.cardView__cat {
  padding: 0.32em 0.5em;
  background: var(--accent);
  color: var(--paper);
}

.cardView__date {
  color: #6b675e;
}

.cardView__title {
  margin: 0.7rem 0 0;
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 3.1vw, 1.95rem);
  line-height: 1.1;
  letter-spacing: -0.015em;
}

.cardView__body {
  margin-top: 0.75rem;
  font-size: clamp(0.82rem, 1.15vw, 0.94rem);
  line-height: 1.75;
  color: #3d3a34;
  max-width: 46em;
}

/* 前后翻：牌堆里只看得到 4 张，剩下 9 条在这里翻。
   放在卡片页脚位置，不压在正文上，也不会因为字号变化错位。 */
.cardView__navBar {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: 1.1rem;
}

.cardView__nav {
  flex: none;
  width: 2.1rem;
  height: 2.1rem;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 1rem;
  line-height: 1;
  color: var(--ink);
  background: rgba(11, 11, 12, 0.08);
  border: 2px solid var(--ink);
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}

.cardView__nav:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}

.cardView__idx {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: #6b675e;
}

/* 前后翻的时候内容轻轻推一下就走——不做第二次 FLIP，
   已经在最高层级了，再飞一次只会晕。 */
.cardView.is-swap .cardView__meta,
.cardView.is-swap .cardView__title,
.cardView.is-swap .cardView__body,
.cardView.is-swap .cardView__navBar,
.cardView.is-swap .cardView__foot {
  animation: cardSwap 0.38s var(--ease);
}

@keyframes cardSwap {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.cardView__foot {
  margin-top: 1.1rem;
  padding-top: 1rem;
  border-top: 2px solid rgba(11, 11, 12, 0.14);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #8a857a;
}

/* 内容在飞行途中不出现，落位后再淡入 */
.cardView__meta,
.cardView__title,
.cardView__body,
.cardView__navBar,
.cardView__foot,
.cardView__media {
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}

.cardView.is-settled .cardView__meta,
.cardView.is-settled .cardView__title,
.cardView.is-settled .cardView__body,
.cardView.is-settled .cardView__navBar,
.cardView.is-settled .cardView__foot,
.cardView.is-settled .cardView__media {
  opacity: 1;
}

/* ==========================================================================
   10  SKILLS 能力星球
   ========================================================================== */

.universe {
  position: relative;
  width: 100%;
  height: clamp(320px, 56vh, 520px); /* 兜底 */
  height: clamp(320px, 56svh, 520px);
  border: 2px solid var(--line);
  background-image: radial-gradient(rgba(242, 235, 221, 0.09) 1px, transparent 1.1px);
  background-size: 7px 7px;
  overflow: hidden;
  touch-action: pan-y;
}

.universe::after {
  content: 'PLAYGROUND';
  position: absolute;
  right: 0.7rem;
  bottom: 0.5rem;
  z-index: 4;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.24em;
  color: var(--dim);
  pointer-events: none;
}

.universe__halo {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 50%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 59, 31, 0.2), transparent 68%);
  pointer-events: none;
}

.universe__core {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) rotate(-4deg);
  display: grid;
  place-items: center;
  width: clamp(112px, 15vw, 158px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--accent);
  color: var(--paper);
  border: 3px solid var(--ink);
  box-shadow: 0 0 0 3px var(--accent);
  pointer-events: none;
  z-index: 1;
  /* 中心那颗红球是这一页的心脏：极慢地呼吸一下，让它看起来是活的。
     周期 5.6s、幅度 3.5%——足够被感觉到，又不至于跟标签抢戏。 */
  animation: coreBreathe 5.6s var(--ease) infinite;
}

@keyframes coreBreathe {
  0%,
  100% {
    transform: translate(-50%, -50%) rotate(-4deg) scale(1);
  }
  50% {
    transform: translate(-50%, -50%) rotate(-4deg) scale(1.035);
  }
}

.universe__coreText {
  display: grid;
  justify-items: center;
  line-height: 0.94;
  font-family: var(--font-display);
  font-size: clamp(0.74rem, 1.5vw, 1rem);
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

/* 单个能力标签 */
.skill {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  transform: translate3d(var(--x, 0px), var(--y, 0px), 0) rotate(var(--r, 0deg));
  will-change: transform;
}

.skill__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.46rem;
  padding: 0.62rem 0.98rem;
  font-family: var(--font-display);
  /* --fs 是纯视觉的节奏差，让 8 个标签有大有小；不代表任何熟练度 */
  font-size: calc(clamp(0.78rem, 1.3vw, 1.06rem) * var(--fs, 1));
  letter-spacing: 0.005em;
  text-transform: uppercase;
  line-height: 1;
  white-space: nowrap;
  background: var(--paper);
  color: var(--ink);
  border: 3px solid var(--ink);
  box-shadow: 5px 5px 0 var(--accent);
  cursor: grab;
  transition: background 0.28s var(--ease), color 0.28s var(--ease), box-shadow 0.28s var(--ease);
  touch-action: pan-y;
}

.skill__btn:active {
  cursor: grabbing;
}

.skill__btn:hover {
  box-shadow: 8px 8px 0 var(--accent);
}

.skill[data-color='yellow'] .skill__btn {
  box-shadow: 5px 5px 0 var(--yellow);
}
.skill[data-color='blue'] .skill__btn {
  box-shadow: 5px 5px 0 var(--blue);
}
.skill[data-color='orange'] .skill__btn {
  box-shadow: 5px 5px 0 var(--orange);
}

/* 按下去的瞬间：260ms 的挤压回弹。
   用独立的 scale 属性而不是 transform —— .skill 的 transform 每一帧
   都在按物理位置重写，两边都写 transform 会互相覆盖。 */
.skill.is-hit .skill__btn {
  animation: skillHit 0.26s var(--ease) both;
}

@keyframes skillHit {
  0% {
    scale: 1;
  }
  34% {
    scale: 1.09;
  }
  66% {
    scale: 0.97;
  }
  100% {
    scale: 1;
  }
}

.skill__sticker {
  width: 1.15em;
  height: 1.15em;
  object-fit: contain;
  flex: none;
}

/* ---------- 点击反馈：一个词，炸一下就没了 ----------
   这一块不是菜单。点标签不会展开任何文字——没有二级面板、没有 accordion、
   没有 modal、没有熟练度条。点了就在标签旁边炸出一个漫画拟声词，
   半秒后自己消失。标签上本来就写着 AI，旁边再写一个 AI 是废话，
   所以这里只说「炸了」这件事；连起来读就是 AI → BOOM!

   往右还是往左由 JS 按标签在容器里的位置决定（靠右就往左），
   否则会被 .universe 的 overflow: hidden 切掉半个词。 */
.skill__boom {
  position: absolute;
  top: 50%;
  left: 100%;
  z-index: 6;
  margin-left: 0.34rem;
  font-family: var(--font-display);
  font-size: calc(clamp(1.05rem, 1.9vw, 1.6rem) * var(--fs, 1));
  line-height: 1;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--yellow);
  /* 硬影而不是描边：和 hero 的 TANG.、各区块的 .secSub__b 是同一条排印语言 */
  text-shadow: 0.055em 0.055em 0 var(--accent);
  pointer-events: none;
  transform-origin: center;
  animation: skillBoom 0.52s var(--ease) both;
}

.skill__boom--l {
  left: auto;
  right: 100%;
  margin-left: 0;
  margin-right: 0.34rem;
}

/* 0 → 28%：0.4 弹到 1.18，就是「炸出来」的那一下；
   之后收一点、稳住，最后一边上浮一边淡掉。
   总长 520ms —— 短到不用等，长到看得清。 */
@keyframes skillBoom {
  0% {
    opacity: 0;
    transform: translateY(-50%) rotate(var(--br, -7deg)) scale(0.4);
  }
  28% {
    opacity: 1;
    transform: translateY(-50%) rotate(var(--br, -7deg)) scale(1.18);
  }
  46% {
    opacity: 1;
    transform: translateY(-50%) rotate(var(--br, -7deg)) scale(0.97);
  }
  74% {
    opacity: 1;
    transform: translateY(-50%) rotate(var(--br, -7deg)) scale(1.03);
  }
  100% {
    opacity: 0;
    transform: translateY(calc(-50% - 0.55rem)) rotate(var(--br, -7deg)) scale(1);
  }
}

/* ---------- SKILL INDEX：七个方向的完整清单 ----------
   杂志书末索引的读法：七栏，栏头是方向名 + 它自己的贴纸，下面是全部条目。
   常驻可见、不可点、不展开 —— 上一轮删掉的「点标签弹出二级标签」那一块
   换成了这个。区别是：面板是菜单（点一下才存在），索引是版面（一直在）。

   没有百分比、没有星级、没有熟练度条：data/skills.json 里没有等级字段，
   这里也不许凭空造一个出来。

   栏与栏之间靠 1px 竖线分，整块上下各一条横线 —— 这是报纸索引的做法。
   刻意不用「gap + 容器底色露出来当分隔线」那一招：条目数除不尽列数时
   （7 栏在 4 列断点下就是 3 个空位），空位会把底色露成一块亮斑。 */
.index {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  margin-top: clamp(1.1rem, 2.6vh, 1.8rem);
  border-top: 2px solid var(--line);
  border-bottom: 2px solid var(--line);
  /* 右边补一条竖线，把这个账本合上。
     每栏自己带 border-left，所以左边有 7 条线、右边一条都没有 ——
     上下两条横线铺到最右之后就凭空断在半空，看起来是漏了而不是留白。
     这一条补上，框才是闭合的。 */
  border-right: 1px solid var(--line);
}

.index__col {
  display: grid;
  align-content: start;
  gap: 0.45rem;
  min-width: 0;
  padding: 0.75rem 0.85rem 0.95rem;
  border-left: 1px solid var(--line);
}

.index__col[data-color='red'] {
  --col: var(--red);
}

.index__col[data-color='yellow'] {
  --col: var(--yellow);
}

.index__col[data-color='blue'] {
  --col: var(--blue);
  /* 纯 #1F4BFF 压在 #0B0B0C 上几乎看不出是蓝的，混一点米白提亮。
     不支持 color-mix 的浏览器拿上一行的纯蓝，只是暗一点，不影响读。 */
  --col: color-mix(in srgb, var(--blue) 58%, var(--paper));
}

.index__col[data-color='orange'] {
  --col: var(--orange);
}

.index__head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
  margin: 0;
  padding-bottom: 0.35rem;
  border-bottom: 3px solid var(--col, var(--accent));
}

.index__sticker {
  flex: none;
  width: 20px;
  height: 20px;
  object-fit: contain;
}

.index__name {
  min-width: 0;
  font-family: var(--font-display);
  font-size: 0.72rem;
  line-height: 1;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--paper);
}

/* 条目之间用「·」连成一段，不是 <ul> —— 这是一份索引，不是清单；
   七栏各自两三行，读起来是密度，不是条目数。 */
.index__list {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  line-height: 1.55;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--grey);
  /* PRODUCT 那栏八个词在七分栏里一行放不下，必须能断 */
  overflow-wrap: anywhere;
}

/* 七栏在窄屏上会挤成一列一个词。
   1100 → 4 栏，760 → 2 栏；再窄就保持 2 栏，因为「COMMERCIAL OPERATIONS」
   这类长词一栏放得下，反而比 1 栏更省纵向空间。 */
@media (max-width: 1100px) {
  .index {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (max-width: 760px) {
  .index {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .index__col {
    padding: 0.7rem 0.7rem 0.85rem;
  }
}

/* ==========================================================================
   11  FIND ME 地图 + 胶片条
   ========================================================================== */

/* 刊头已经统一到 .secSub（见 08 区块骨架）。
   这里原来是一套独立的 .atlas__title / .atlas__titleB，
   和 CONTACT、SKILLS 各写各的，三套规则说的是同一件事。 */

/* 1995 ────────── 2026：整页的主视觉，不是角落里的一行说明。
   左边空心、右边实心带红硬影，中间一条红色横轴。 */
.years {
  display: flex;
  align-items: center;
  gap: clamp(0.5rem, 1.6vw, 1.2rem);
  margin-bottom: clamp(0.5rem, 1.5vh, 0.95rem);
  /* --dx / --dy 由 js/main.js 按这一条在视口里的位置写进来（±10px 以内）。
     滚动时两个年份会轻轻错开一点，像时间轴被拉动。
     幅度刻意小到「说不清到底动没动」——是呼吸，不是动画。 */
  transform: translate3d(0, var(--dy, 0px), 0);
  will-change: transform;
}

.years__a,
.years__b {
  font-family: var(--font-display);
  font-size: clamp(2.3rem, 9.6vw, 6.8rem);
  line-height: 0.78;
  letter-spacing: -0.055em;
}

.years__a {
  color: transparent;
  -webkit-text-stroke: clamp(1.2px, 0.016em, 2.6px) var(--paper);
  transform: translate3d(calc(var(--dx, 0px) * -0.9), 0, 0);
}

.years__b {
  color: var(--paper);
  text-shadow: 0.05em 0.05em 0 var(--accent);
  transform: translate3d(var(--dx, 0px), 0, 0);
}

.years__line {
  position: relative;
  flex: 1;
  min-width: 2rem;
  height: 3px;
  background: var(--accent);
}

.years__mid {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 0.1rem 0.55rem;
  background: var(--ink);
  font-family: var(--font-mono);
  font-size: clamp(0.52rem, 1vw, 0.66rem);
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--paper);
  white-space: nowrap;
}

.atlas {
  display: grid;
  gap: clamp(0.55rem, 1.5vh, 0.95rem);
}

.atlas__map {
  position: relative;
  width: min(100%, 560px, 46vh); /* 兜底 */
  width: min(100%, 560px, 46svh);
  margin: 0 auto;
  aspect-ratio: 1000 / 1065;
}

.atlas__base,
.atlas__layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.atlas__layer {
  overflow: visible;
}

/* 弧线 */
.route {
  fill: none;
  stroke: var(--accent);
  stroke-width: 3.4;
  stroke-linecap: round;
  opacity: 0.68;
}

/* 用来量长度的隐藏路径：几何要在，但一笔都不画。
   用 transparent 而不是 stroke:none —— 保证元素有真实渲染几何，
   getPointAtLength 在各家浏览器上都不会拿到 0。 */
.routeTrail {
  fill: none;
  stroke: transparent;
  stroke-width: 0;
  pointer-events: none;
}

/* 沿着路线走的那个点。是这一页唯一在动的东西，
   所以给它的颜色也是全站最跳的黄 + 黑描边，像一颗掉在路上的弹珠。
   默认 opacity:0 —— 万一 rAF 没跑起来，图上不会多一个停着的点。 */
.routeDot {
  fill: var(--yellow);
  stroke: var(--ink);
  stroke-width: 2;
  opacity: 0;
  transition: opacity 0.7s var(--ease);
  pointer-events: none;
}

.routeDot.is-on {
  opacity: 1;
}

/* 点位 */
.pin__hit {
  fill: transparent;
  cursor: pointer;
}

.pin__dot {
  fill: var(--paper);
  stroke: var(--ink);
  stroke-width: 2.5;
  transition: fill 0.25s var(--ease);
}

.pin.is-on .pin__dot {
  fill: var(--accent);
}

.pin__ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  opacity: 0;
}

.pin.is-on .pin__ring {
  opacity: 1;
  animation: pinPulse 1.6s var(--ease) infinite;
}

@keyframes pinPulse {
  0% {
    r: 12;
    opacity: 0.9;
  }
  100% {
    r: 28;
    opacity: 0;
  }
}

.pin__burst {
  fill: var(--yellow);
  stroke: var(--ink);
  stroke-width: 2.5;
}

/* hover 卡片 */
.atlas__tip {
  position: absolute;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.42rem 0.6rem 0.42rem 0.42rem;
  background: var(--paper);
  color: var(--ink);
  border: 3px solid var(--ink);
  box-shadow: 5px 5px 0 var(--accent);
  transform: translate(-50%, calc(-100% - 14px));
  pointer-events: none;
  white-space: nowrap;
}

.atlas__tipThumb {
  width: 40px;
  height: 40px;
  flex: none;
  display: block;
  background: var(--ink-3);
  border: 2px solid var(--ink);
  overflow: hidden;
}

.atlas__tipThumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.atlas__tipText {
  display: grid;
  gap: 0.05rem;
  line-height: 1.2;
}

/* 地名：英文是主角（大字、粗、能一眼认出来），
   中文跟在下面做文化识别（小一号、灰一点，不跟英文抢行宽）。
   不要把「Shenzhen, China」拼成一行——那是一条信息，不是一个视觉元素。 */
.atlas__tipText b {
  font-family: var(--font-display);
  font-size: 0.92rem;
  letter-spacing: -0.015em;
  text-transform: uppercase;
}

.atlas__tipCn {
  font-family: var(--font-body);
  font-style: normal;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: #6b675e;
}

.atlas__tipMeta {
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 0.58rem;
  letter-spacing: 0.08em;
  color: #8a857a;
}

/* 胶片条 */
.strip {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding: 0.55rem 0.1rem 0.9rem;
  scrollbar-width: thin;
  scrollbar-color: var(--dim) transparent;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
}

.strip::-webkit-scrollbar {
  height: 5px;
}

.strip::-webkit-scrollbar-thumb {
  background: var(--dim);
}

.strip__item {
  flex: none;
  scroll-snap-align: start;
}

.strip__btn {
  display: grid;
  gap: 0.16rem;
  min-width: 118px;
  padding: 0.58rem 0.68rem;
  text-align: left;
  border: 2px solid var(--line);
  border-left: 4px solid var(--line);
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease), transform 0.3s var(--ease);
}

.strip__btn:hover,
.strip__btn.is-on {
  border-color: var(--accent);
  background: rgba(255, 59, 31, 0.08);
  transform: translateY(-2px);
}

.strip__n {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.12em;
  color: var(--dim);
}

/* 胶片条：英文大字负责整排的视觉节奏（粗、对齐、能扫），
   中文小字跟在下面做文化识别。数据层不动——placeCN 本来就在 JSON 里。 */
.strip__place {
  font-family: var(--font-display);
  font-size: 0.88rem;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.strip__cn {
  font-family: var(--font-body);
  font-style: normal;
  font-size: 0.68rem;
  line-height: 1.1;
  letter-spacing: 0.04em;
  color: var(--grey);
}

.strip__cn:empty {
  display: none;
}

.strip__date {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.06em;
  color: var(--grey);
}

.strip__btn.is-origin .strip__place {
  color: var(--yellow);
}

/* ==========================================================================
   12  CONTACT
   ========================================================================== */

.connect {
  border-bottom: 2px solid var(--line);
}

/* 刊头统一到 .secSub（见 08 区块骨架）。
   原来这里自己写了一套 .connect__title / .connect__titleB，
   和 FIND ME、SKILLS 各写各的，三套规则说的是同一件事。 */

.connect__row {
  position: relative;
  z-index: 0;
  display: grid;
  /* 四列：序号 / 键名 / 值 / 箭头。
     键名用 minmax(min-content, max-content)：装得下就取最宽的那个键
     （INSTAGRAM 在 25.6px 下要 168px），装不下才收到 min-content。
     值那一列 minmax(0, 1fr)——0 是关键，否则长邮箱会把整行顶宽、横向溢出。
     这样四个键永远左对齐，四个值也永远从同一条竖线上开始。 */
  grid-template-columns: auto minmax(min-content, max-content) minmax(0, 1fr) auto;
  align-items: baseline;
  gap: clamp(0.7rem, 2.4vw, 1.6rem);
  width: 100%;
  padding: 1.35rem 0.6rem;
  text-align: left;
  border-top: 2px solid var(--line);
  /* scale 也进过渡：整行被碰到时轻轻涨 0.3%（见下面的 :hover），
     瞬变看不出来，但要有，不然离开时会「啪」地弹回去。 */
  transition: padding-left 0.4s var(--ease), color 0.35s var(--ease), scale 0.4s var(--ease);
}

.connect__row::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.45s var(--ease);
}

.connect__row::after {
  position: absolute;
  right: 0.7rem;
  top: -0.6rem;
  z-index: -1;
  font-family: var(--font-hand);
  font-size: clamp(1rem, 2.2vw, 1.5rem);
  color: var(--yellow);
  transform: rotate(-8deg) scale(0.4);
  opacity: 0;
  transition: opacity 0.3s var(--ease), transform 0.4s var(--ease-back);
  pointer-events: none;
}

.connect li:nth-child(1) .connect__row::after {
  content: 'PING!';
}
.connect li:nth-child(2) .connect__row::after {
  content: 'YO!';
}
.connect li:nth-child(3) .connect__row::after {
  content: 'HEY!';
}
.connect li:nth-child(4) .connect__row::after {
  content: 'HELLO!';
}

.connect__row:hover,
.connect__row:focus-visible {
  padding-left: 1.3rem;
  color: var(--paper);
  /* 整行被碰到时轻轻涨一点：0.3%，肉眼量不出来，
     但它和左边那条红底刷过来、名字「叮」一下是同一个瞬间的事。
     用独立的 scale 属性，不碰 transform —— ::before 那条
     transform: scaleX() 的红底不能被搅进去。 */
  scale: 1.003;
}

.connect__row:hover::before,
.connect__row:focus-visible::before {
  transform: scaleX(1);
}

.connect__row:hover::after,
.connect__row:focus-visible::after {
  opacity: 1;
  transform: rotate(-8deg) scale(1);
  /* 冒出来之后再「叮」一下。抖动只动独立的 rotate / translate 属性，
     上面那条负责入场缩放的 transform 不参与，两边不打架。
     延迟 0.1s：先弹出来，再抖，顺序反了就像字在打哆嗦。 */
  animation: comicPop 0.5s var(--ease) 0.1s 1;
}

@keyframes comicPop {
  0%,
  100% {
    rotate: 0deg;
    translate: 0 0;
  }
  20% {
    rotate: 2.6deg;
    translate: 0 -1px;
  }
  45% {
    rotate: -1.9deg;
    translate: 0 1px;
  }
  70% {
    rotate: 1.1deg;
    translate: 0 -0.5px;
  }
}

.connect__num {
  font-family: var(--font-mono);
  font-style: normal;
  font-size: 0.64rem;
  letter-spacing: 0.1em;
  color: var(--grey);
  transition: color 0.3s var(--ease);
}

.connect__row:hover .connect__num {
  color: rgba(242, 235, 221, 0.8);
}

.connect__key {
  min-width: 0;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 2.2vw, 1.6rem);
  line-height: 1.1;
  letter-spacing: -0.015em;
  text-transform: uppercase;
}

/* 鼠标碰到某一行时，那一行的名字「叮」一下——漫画里打电话的那种抖。
   幅度小、只播一次，是反馈，不是装饰。 */
.connect__row:hover .connect__key,
.connect__row:focus-visible .connect__key {
  animation: comicShake 0.42s var(--ease);
}

@keyframes comicShake {
  0%,
  100% {
    transform: translate(0, 0) rotate(0deg);
  }
  20% {
    transform: translate(-2px, 1px) rotate(-0.9deg);
  }
  40% {
    transform: translate(2px, -1px) rotate(0.9deg);
  }
  60% {
    transform: translate(-1.5px, -1px) rotate(-0.5deg);
  }
  80% {
    transform: translate(1px, 1px) rotate(0.4deg);
  }
}

.connect__val {
  min-width: 0;
  font-family: var(--font-mono);
  font-size: clamp(0.76rem, 1.15vw, 0.92rem);
  letter-spacing: 0.02em;
  color: var(--grey);
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color 0.3s var(--ease);
}

.connect__row:hover .connect__val {
  color: var(--paper);
}

.connect__fx {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--grey);
  white-space: nowrap;
  transition: color 0.3s var(--ease), transform 0.3s var(--ease);
}

.connect__row:hover .connect__fx {
  color: var(--paper);
  transform: translate(3px, -3px);
}

.connect__fx.is-done {
  color: var(--yellow) !important;
}

/* 原来这里有一组 .footer / .footer__line：页脚那行「Explore. Experience.
   Stay yourself.」+ 版权行。两者都撤了 ——
   标语在 ABOUT 的主标语里已经说过一次，页脚再说一遍是同一句话说两次；
   版权行的信息并进了 .train__meta（见下面的 12b）。
   页脚文案现在只有六行，全部在 .train 里。 */

/* 窄屏：顶栏三段横排放不下。
   390 上量过：mark 18–152 + now 168–330 + meta 346–464，总宽 446px，
   比视口多出 56px。而 .topbar 是 position: fixed —— 溢出不会撑出横向滚动条，
   所以时钟就这么被静静裁掉了，任何溢出检测都抓不到。
   取舍：手机顶栏保住「EIGHTEEN TANG.」和时间。状态标签是锦上添花，
   先让位。（这不是隐藏信息：它不指向任何内容，只是一个状态角标。） */
@media (max-width: 640px) {
  .topbar__now {
    display: none;
  }
}

/* ==========================================================================
   12b  THE LAST TRAIN：一条出血的铁轨 + 一列停在尽头的火车
   --------------------------------------------------------------------------
   网站不该在版权行那里直接断掉，也不该用一个「小栏杆图标」收尾。
   这一页最后一样东西是一条横穿整个屏幕的铁轨：从屏幕最左铺到最右，
   横向把页面切断 —— 只有出血才读得成「路走到了尽头」，
   缩在正文 1140px 里的栅栏只是一个装饰图案。

   轨道是纯 CSS 画的（道砟网点 + 枕木 + 两条钢轨），不用图片：
   一条 100vw 的 SVG 会被 preserveAspectRatio 拉变形，宽屏上枕木
   会一根比一根宽；渐变按 px 排，任何屏宽下间距都一模一样。

   火车是一段自绘 SVG（见 index.html），进视口时从左开到右端停住，
   只跑一趟；离开视口再进来可以重跑（js/main.js 的 initTrain）。
   ========================================================================== */

.train {
  /* 火车渲染宽度。窄屏下限 210px —— 再小就只剩一个色块，
     看不出是车头还是车厢。 */
  --tw: clamp(210px, 54vw, 620px);
  /* 两条钢轨顶面之间的垂直距离 */
  --rail-gap: clamp(30px, 3.4vw, 46px);
  /* 上轨顶面离场景底边的高度。构成：道砟 0–18 / 枕木 8 起 /
     下轨 14–27 / 上轨 27+gap 起。三个数字必须和下面几条规则对齐。 */
  --rail-top: calc(27px + var(--rail-gap));
  /* 停靠位靠 left 表达，不靠 translate ——
     left 的百分比是相对轨道宽度算的，而轨道宽度 = .train 宽度 = 视口宽度，
     所以 calc(100% - ...) 是精确的。translate 的百分比只认元素自己的宽度，
     这里用不上。
     26px 是车头到屏幕右缘的余量：贴着边缘停会读成「被裁掉了」。 */
  --park-left: calc(100% - var(--tw) - 26px);
  /* 刹车距离：72% 处先退到这里，剩下 28% 慢慢蹭进站 */
  --brake: clamp(120px, 22vw, 300px);
  --train-dur: 10.5s;

  /* 火车的高度 = 宽度 × 200/920（SVG viewBox 的宽高比）。
     viewBox 里轮底正好落在下沿，所以 --rail-top 就是火车的 bottom，
     不需要再补偏移量。 */
  --th: calc(var(--tw) * 0.2174);

  /* 拟声词的字号。定义在这里而不是写在 .train__word 上，是因为下面的
     --head 要用它算高度，而 --head 挂在 .train 上（自定义属性向下继承，
     反过来取不到）。 */
  --word-fs: clamp(1rem, 2.4vw, 1.7rem);

  /* 车顶以上要留出的空：拟声词浮在车顶上方，不再压在车体上。
     为什么挪上去 —— 两个实际问题：
     1. 黄字压在米白色的驾驶室 / 车窗上读不清（黄和米白差得太少）；
     2. 原来从 left: 62% 起笔，390 上词右缘离屏幕边只剩 3px，
        再窄一点就被 overflow: clip 切掉。
     浮到车顶上方，那里是干净的深色背景，横向也有余量。

     --head 要覆盖：词高（= 字号，line-height: 1）+ 8px 间隙
     + 旋转后的外扩 + 字体上下溢出的余量。
     外扩量：词宽约 6.4 × 字号（实测 1440 与 390 两个宽度都是 6.4），
     rotate(-7deg) 绕中心转，AABB 上边比未旋转时高出
     (w·sin7° + h·cos7° − h) / 2 ≈ 0.39 × 字号。
     所以 1 + 0.39 ≈ 1.4 倍字号，再给 12px 的间隙 + 余量。 */
  --head: calc(var(--word-fs) * 1.45 + 12px);

  position: relative;
  margin-top: clamp(1.6rem, 4vh, 2.6rem);
  /* 出血：把 .wrap / .section / .page 三层留白一次抵掉。
     铁轨要从屏幕最左铺到最右，而正文容器只有 1140px；更要紧的是
     .page 给左侧数字导航留了 padding-left: var(--nav-w)，整个正文
     容器是偏右 46px 的 —— 只按「居中」去算出血量，铁轨会整体右移半格。
     抵完两边之后 .train 的宽度正好等于视口宽。
     --nav-w 在 1024 以下会被置 0，所以这里不需要额外的断点。 */
  margin-left: calc(-1 * (var(--nav-w) + var(--pad)));
  margin-right: calc(-1 * var(--pad));
  height: calc(var(--th) + var(--rail-top) + var(--head));
  /* 火车默认停在屏幕左边外面（见 .train__unit），这一层必须裁住它，
     否则整页会被它撑出上千像素的横向溢出。
     hidden 在前、clip 在后：老浏览器拿 hidden 兜底，新浏览器用 clip ——
     clip 不建立滚动容器，不会把将来的 sticky 之类带偏。 */
  overflow: hidden;
  overflow: clip;
}

/* 铁轨：铺满 .train 的整个宽度（= 视口宽）。
   这里不写 100vw，也不写 left: calc(50% - 50vw) ——
   那条路要求「父盒中心 = 视口中心」，而这一页的正文容器为了给左侧导航
   让位是偏右的，算出来会整体右移 46px。 */
.train__track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--rail-top);
}

/* 道砟：最底下一条网点，读成碎石。
   整页只有这一处和 SKILLS 那个宇宙底用了网点，两处都是「地面」的意思 ——
   不是随手撒的纹理。 */
.train__ballast {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 18px;
  background-image: radial-gradient(rgba(242, 235, 221, 0.17) 1px, transparent 1.2px);
  background-size: 6px 6px;
}

/* 枕木：一根根竖着的木条，粗黑描边 + 木色填充。
   一条 repeating-linear-gradient 画完四段（左边线 / 木面 / 右边线 / 间隔），
   比四层元素省事，也不会有对齐误差。 */
.train__ties {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8px;
  height: calc(var(--rail-gap) + 26px);
  background-image: repeating-linear-gradient(
    to right,
    var(--ink) 0 3px,
    var(--wood) 3px 25px,
    var(--ink) 25px 28px,
    transparent 28px 58px
  );
}

/* 钢轨：上下两条黑边夹一道米白，是轨面被压出来的高光。
   同样一条渐变画完，不用三层元素。 */
.train__rail {
  position: absolute;
  left: 0;
  right: 0;
  height: 13px;
  background-image: linear-gradient(
    to bottom,
    var(--ink) 0 3px,
    var(--paper-2) 3px 10px,
    var(--ink) 10px 13px
  );
}

.train__rail--b {
  bottom: 14px;
}

.train__rail--t {
  bottom: calc(14px + var(--rail-gap));
}

/* ---------- 火车 ---------- */

.train__unit {
  position: absolute;
  /* 静态位置就是停靠位：车头右缘离屏幕右缘 26px。
     100% = 轨道宽 = 视口宽，所以这个数是精确的。 */
  left: var(--park-left);
  bottom: var(--rail-top);
  width: var(--tw);
  /* 默认停在屏幕左边外面 —— 也就是「还没进站」。
     起点只要「足够远、看不见」就够了，所以这里用 100vw 是安全的：
     就算带滚动条时差了十几像素，也不影响它藏在画面外。
     这样离开视口把动画状态一清，它回到起点是看不见的，不会闪一下。 */
  translate: calc(-100vw - var(--tw)) 0;
}

.train__art {
  display: block;
  width: 100%;
  height: auto;
  /* 硬投影而不是模糊阴影：和全站的卡片、路牌是同一条语言。
     用 filter 而不是 box-shadow —— 火车的轮廓不是一个矩形。 */
  filter: drop-shadow(6px 6px 0 rgba(0, 0, 0, 0.45));
}

/* 驾驶室侧面的车号 */
.train__num {
  font-family: var(--font-mono);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: 0.02em;
  fill: var(--ink);
}

/* 车轮和烟圈要绕自己的中心转 / 缩放。
   SVG 的默认原点是用户坐标系的 (0,0)，不设 transform-box 的话
   scale 会把烟圈甩到画面外面去。 */
.train__smoke circle,
.train__spoke {
  transform-box: fill-box;
  transform-origin: center;
}

/* 拟声词：浮在车顶上方，只出现一次、几秒钟就走，
   不做成常驻 —— 页脚已经够满，不需要一个一直挂着的字。

   位置用 left: 40% 而不是 62%：
   - 62% 起笔时词右缘在 390 上离屏幕边只剩 3px，随时会被裁；
   - 40% 落在一节车厢的正上方，横向两边都有余量（1440 上离右缘 286px，
     390 上离右缘 71px）；
   - 也避开了烟囱：烟圈在车体右侧 92%~95% 那一带升起，词右缘在 79%，
     不会跟烟撞在一起。

   纵向靠 bottom: calc(100% + 8px)：.train__unit 是定位祖先，
   所以 100% 就是车高，词底边落在车顶上方 8px —— 在 .train 预留的
   --head 那一段里。 */
.train__word {
  position: absolute;
  left: 40%;
  top: auto;
  bottom: calc(100% + 8px);
  font-family: var(--font-hand);
  font-size: var(--word-fs);
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--yellow);
  /* 描边压在填充下面，字才不会糊成一坨黑 */
  -webkit-text-stroke: 4px var(--ink);
  paint-order: stroke fill;
  rotate: -7deg;
  opacity: 0;
  pointer-events: none;
}

/* ---------- 跑起来 ----------
   动画只在 .is-running 存在时挂上，而那一下是 JS 在元素进入视口时加的。
   火车本体不需要在别处写属性归位：它的默认 translate 本来就是「在屏幕外」，
   动画一撤它自己就回去了。 */

.train.is-running .train__unit {
  animation: trainRun var(--train-dur) 0.3s 1 both;
}

/* 出站加速 → 中段巡航 → 进站刹车。
   为什么要分两段缓动：一条 cubic-bezier 要么是「匀速滑过去」（不像火车），
   要么是「一路减速」（那起步就拖了）。真实的列车是加速、巡航、刹车，
   所以在 72% 处给它一个明确的位置，前后各配一条缓动。 */
@keyframes trainRun {
  0% {
    translate: calc(-100vw - var(--tw)) 0;
    animation-timing-function: cubic-bezier(0.5, 0, 0.72, 0.42);
  }
  72% {
    translate: calc(-1 * var(--brake)) 0;
    animation-timing-function: cubic-bezier(0.14, 0.72, 0.22, 1);
  }
  100% {
    translate: 0 0;
  }
}

/* 「车已经停好了」的静态位。开了减弱动态效果、或没有 IntersectionObserver
   时由 JS 挂上 —— 那列车就直接停在站里。它停着也是完整的画面，
   而不是「一个被禁用的动画」。 */
.train.is-parked .train__unit {
  translate: 0 0;
}

.train.is-running .train__smoke circle {
  animation: trainSmoke 2.8s linear infinite;
}

/* 烟不停 —— 这是刻意的，和上面轮子的处理不一样。
   轮子停是因为「车停了轮子还在转」是物理错误；
   蒸汽机车停在站里本来就该慢慢冒烟，而且这是页脚唯一常驻的环境动效，
   撤掉之后这块画面就彻底死了。 */

.train.is-running .train__smoke circle:nth-child(2) {
  animation-delay: 0.93s;
}

.train.is-running .train__smoke circle:nth-child(3) {
  animation-delay: 1.86s;
}

/* 三个烟圈共用一条关键帧，靠 delay 错开成一串。
   起点缩到 0.5、终点放到 1.45 并淡出 —— 烟是边升边散的。 */
@keyframes trainSmoke {
  0% {
    opacity: 0;
    translate: 0 6px;
    scale: 0.5;
  }
  24% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    translate: 16px -40px;
    scale: 1.45;
  }
}

/* 轮子只在「这一趟」里转。
   原来是 infinite —— 车已经停稳了，轮子还在原地打转，一辆停在站里的
   火车不该这样。改成按 --train-dur 整除：12 圈正好跑满 10.5s
   （0.875s 一圈，和 0.9s 看不出差别），配 both 把最后一帧定住，
   而最后一帧是 360deg —— 视觉上等于 0deg，所以不会突然扭一下。
   用 calc 而不是写死 0.875s：--train-dur 改了这里跟着走，
   不会出现「车都停了轮子还在转」或者反过来。 */
.train.is-running .train__spoke {
  animation: trainWheel calc(var(--train-dur) / 12) linear 0.3s 12 both;
}

@keyframes trainWheel {
  to {
    rotate: 360deg;
  }
}

/* 拟声词的出现时机必须跟着车走，不能凭感觉给一个「1.6s」。
   ------------------------------------------------------------
   原来写的是 `3.4s var(--ease) 1.6s` —— 词在 1.6s 淡入、5.0s 就没。
   而实测（_shots/probe-word.js 按真实时钟每 0.2s 采一次）：
   车头右缘要等到 t≈5.0s 才跨过屏幕左边，也就是说词**整个生命周期里
   车都还在屏幕外面** —— 62 个采样点里，词可见且车在画面里的有 0 个。
   换句话说这个拟声词在实际运行中一次都不会被看到。
   （之前的截图能截到，是因为脚本把 trainRun 和 trainWord 分别冻在了
   各自的峰值帧上，那个组合在真实时间轴上根本不存在。）

   现在按 --train-dur 的比例给：0.60 处起（此时车已经进画面、
   车厢正在经过），持续 0.40 个周期，正好在车停稳的前后淡出。
   实测这条时间轴上词从 t≈6.3s 亮到 t≈10.5s，而车从 t≈6.5s
   到 t≈10.4s 一直完整可见 —— 两者重叠。
   --train-dur 改了，这里跟着走。 */
.train.is-running .train__word {
  animation: trainWord calc(var(--train-dur) * 0.4) var(--ease)
    calc(var(--train-dur) * 0.6) 1 both;
}

@keyframes trainWord {
  0% {
    opacity: 0;
    scale: 0.5;
  }
  12% {
    opacity: 1;
    scale: 1.16;
  }
  22% {
    scale: 1;
  }
  72% {
    opacity: 1;
    scale: 1;
  }
  100% {
    opacity: 0;
    scale: 1.06;
  }
}

/* ---------- 文案 + 路牌 ---------- */

.train__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(1rem, 4vw, 2.4rem);
  margin-top: clamp(1.1rem, 2.8vh, 1.8rem);
}

.train__copy {
  display: grid;
  gap: 0.3rem;
  margin: 0;
  min-width: 0;
  text-transform: uppercase;
}

.train__title {
  font-family: var(--font-display);
  font-size: clamp(0.95rem, 2.4vw, 1.5rem);
  line-height: 1;
  letter-spacing: -0.012em;
  color: var(--paper);
}

/* 这一页最后一句话。给它字号和颜色 —— 不跟下面那行 0.62rem 的
   小字混在一起。只此一句，页脚不堆第二句文案。 */
.train__end {
  font-family: var(--font-display);
  font-size: clamp(0.84rem, 2.1vw, 1.35rem);
  line-height: 1.02;
  letter-spacing: -0.012em;
  color: var(--paper-2);
}

/* EIGHTEEN TANG · SHENZHEN, CHINA · © 2026。
   年份由 js/main.js 写进 #year，不写死 —— 写死的话过一年就是一句错话。 */
.train__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: clamp(1rem, 2.6vh, 1.6rem) 0 0;
  padding-top: 0.7rem;
  border-top: 2px solid var(--line);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
}

.train__meta i {
  font-style: normal;
  color: var(--line-strong);
}

/* ---------- 公路牌 ---------- */

.roadsign__wrap {
  position: relative;
  flex: none;
  display: grid;
  justify-items: center;
  /* 给下面那根柱子留位置，路牌才不会压在柱子上。
     40px = 柱高 42px 减去压在牌底后面的那 2px。 */
  padding-bottom: 40px;
}

.roadsign {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.66rem 1.2rem;
  font-family: var(--font-display);
  font-size: clamp(0.78rem, 1.5vw, 1rem);
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--yellow);
  border: 4px solid var(--ink);
  /* 双层黑框：外面一道 4px 实边，里面再收一道 —— 公路 / 铁路牌子上
     「字外面还有一圈框」的读法。两条 inset 阴影从外往内叠：
     先 2px 黄留白，再 3px 黑框，剩下的才是黄底。
     （第一条是黄的不是白搭 —— 它盖在第二条黑框的外圈上。）
     最外那条硬影用 --red 而不是 --ink：这一页的背景本来就是近黑的，
     黑色硬影投在近黑底上等于没有 —— hover 时把影子从 6px 放到 11px
     那个「抬起来」的反馈就完全看不见了。换成主题红之后它是真的看得见，
     也和 hero 那句 TANG. 的红色硬影是同一条语言。 */
  box-shadow:
    inset 0 0 0 2px var(--yellow),
    inset 0 0 0 5px var(--ink),
    6px 6px 0 var(--red);
  /* 微微歪着 —— 路边立着的牌子没有一块是真正水平的 */
  transform: rotate(-2.5deg);
  cursor: pointer;
  transition: box-shadow 0.28s var(--ease), scale 0.28s var(--ease),
    rotate 0.28s var(--ease);
}

/* hover：抬起 + 放大 + 再歪一点 + 蹭一下。
   四件事分开挂在四个不同的属性上，这是刻意的：
   常驻的歪角写在 transform 里、抬起写在 scale、再歪一点写在 rotate、
   蹭那一下写在 translate —— 各占一条通道，谁也压不掉谁。
   如果全塞进 transform，后面那条规则会把前面的整个覆盖掉。

   蹭的那一下只动 translate，不动 rotate：点击的摇晃动的是 rotate，
   两条动画属性不重叠，同时触发也不会互相打断。 */
.roadsign:hover,
.roadsign:focus-visible {
  scale: 1.04;
  rotate: -1.7deg;
  box-shadow:
    inset 0 0 0 2px var(--yellow),
    inset 0 0 0 5px var(--ink),
    11px 11px 0 var(--red);
  animation: signNudge 0.4s var(--ease) 1;
}

@keyframes signNudge {
  0%,
  100% {
    translate: 0 0;
  }
  30% {
    translate: -3px 0;
  }
  65% {
    translate: 2px 0;
  }
}

.roadsign:active {
  box-shadow:
    inset 0 0 0 2px var(--yellow),
    inset 0 0 0 5px var(--ink),
    3px 3px 0 var(--ink);
  scale: 0.99;
}

/* 点击时左右晃三下：-3 → +3 → -2 → 0，420ms。
   只动独立的 rotate 属性 —— 上面那条 transform 里的 -2.5deg 是常驻的歪角，
   写 transform 会把它抹掉，牌子会在晃完的瞬间「正过来」。
   时长落在 350–500ms 里：短于 350 像被电了一下，长于 500 就挡住回顶了。 */
.roadsign.is-shaking {
  animation: roadsignShake 0.42s var(--ease) 1;
}

@keyframes roadsignShake {
  0% {
    rotate: 0deg;
  }
  18% {
    rotate: -3deg;
  }
  42% {
    rotate: 3deg;
  }
  66% {
    rotate: -2deg;
  }
  100% {
    rotate: 0deg;
  }
}

/* 箭头很慢地往上顶：2.4s、3px。
   它是这块牌子上唯一常驻在动的东西，作用是提示「往上」这件事，
   不是装饰 —— 所以幅度小到刚够被看见。 */
.roadsign__arrow {
  font-size: 1.05em;
  line-height: 1;
  animation: signArrow 2.4s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

@keyframes signArrow {
  0%,
  100% {
    translate: 0 0;
  }
  50% {
    translate: 0 -3px;
  }
}

/* 牌子下面那根木柱：和铁轨的枕木同色，把牌子「种」在地上。
   尺寸是量出来的 —— 上一版是 9px 宽、露出 28px：左右各 3px 黑边之后
   只剩 3px 木色，在深色底上读起来就是一根飘着的细线，不像柱子。
   现在 15px 宽（木芯 9px）、露出 40px，才读得成一根柱子。
   padding-bottom 跟着给到 40px，柱子才不会伸到 .roadsign__wrap 外面去。 */
.roadsign__post {
  position: absolute;
  bottom: 0;
  left: 50%;
  z-index: 1;
  width: 15px;
  height: 42px;
  margin-left: -7.5px;
  background: var(--wood);
  border: 3px solid var(--ink);
  border-top: 0;
}

/* 窄屏：文案和路牌上下排。
   一行放不下两块东西时硬挤，路牌会压到文案上。 */
@media (max-width: 640px) {
  .train__foot {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.3rem;
  }
}

/* ==========================================================================
   13  Lightbox
   ========================================================================== */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 320;
  display: grid;
  place-items: center;
  background: rgba(11, 11, 12, 0.95);
  padding: clamp(1rem, 5vw, 3rem);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  animation: lbIn 0.3s var(--ease) both;
}

@keyframes lbIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.lightbox__stage {
  display: grid;
  gap: 0.7rem;
  justify-items: center;
  max-width: min(94vw, 1000px);
}

.lightbox__stage img {
  max-width: 100%;
  max-height: 74vh; /* 兜底 */
  max-height: 74svh;
  object-fit: contain;
  border: 4px solid var(--paper);
  box-shadow: 10px 10px 0 var(--accent);
  background: var(--ink-2);
}

.lightbox__cap {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.lightbox__cap b {
  font-family: var(--font-display);
  font-size: 0.9rem;
  letter-spacing: 0;
}

.lightbox__cap span {
  color: var(--grey);
}

.lightbox__close,
.lightbox__nav {
  position: absolute;
  z-index: 2;
  display: grid;
  place-items: center;
  color: var(--paper);
  background: rgba(11, 11, 12, 0.7);
  border: 2px solid var(--line-strong);
  transition: background 0.25s var(--ease), border-color 0.25s var(--ease);
}

.lightbox__close {
  top: clamp(0.8rem, 3vw, 1.6rem);
  right: clamp(0.8rem, 3vw, 1.6rem);
  width: 2.6rem;
  height: 2.6rem;
  font-family: var(--font-mono);
  font-size: 1rem;
}

.lightbox__nav {
  top: 50%;
  width: 3rem;
  height: 3.4rem;
  font-size: 1.6rem;
  line-height: 1;
  transform: translateY(-50%);
}

.lightbox__nav--prev {
  left: clamp(0.5rem, 2vw, 1.4rem);
}

.lightbox__nav--next {
  right: clamp(0.5rem, 2vw, 1.4rem);
}

.lightbox__close:hover,
.lightbox__nav:hover {
  background: var(--accent);
  border-color: var(--accent);
}

.lightbox__nav:disabled {
  opacity: 0.25;
  cursor: default;
}

.lightbox__nav:disabled:hover {
  background: rgba(11, 11, 12, 0.7);
  border-color: var(--line-strong);
}

/* 没有照片时的诚实空态 */
.lightbox__empty {
  display: grid;
  justify-items: center;
  gap: 0.55rem;
  text-align: center;
  max-width: 22rem;
}

.lightbox__empty .lightbox__sticker {
  width: 74px;
  height: 74px;
  margin-bottom: 0.4rem;
  transform: rotate(-6deg);
}

.lightbox__empty b {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 3vw, 1.7rem);
  letter-spacing: -0.01em;
}

.lightbox__empty span {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.lightbox__empty p {
  margin-top: 0.5rem;
  font-size: 0.84rem;
  line-height: 1.7;
  color: var(--grey);
}

/* ==========================================================================
   14  响应式
   ========================================================================== */

/* ---- 平板：导航移到底部 ---- */
@media (max-width: 1024px) {
  :root {
    --nav-w: 0px;
    --topbar-h: 52px;
  }

  .page {
    padding-left: 0;
  }

  .topbar {
    padding-left: var(--pad);
    padding-right: var(--pad);
  }

  .nav {
    top: auto;
    bottom: 0;
    width: 100%;
    height: auto;
    padding: 0.5rem var(--pad) calc(0.5rem + env(safe-area-inset-bottom));
    background: linear-gradient(to top, var(--ink) 64%, rgba(11, 11, 12, 0));
    justify-content: center;
  }

  .nav__list {
    display: flex;
    gap: clamp(0.6rem, 3vw, 1.6rem);
  }

  .nav__item {
    flex-direction: column;
    align-items: center;
    gap: 0.18rem;
    padding: 0.2rem 0.3rem;
  }

  .nav__item::before {
    display: none;
  }

  .nav__label {
    opacity: 1;
    transform: none;
    font-size: 0.6rem;
    letter-spacing: 0.02em;
  }

  /* 底部数字条上的短横线是多余的：标签已经一直在 */
  .nav__label::before {
    display: none;
  }

  .section {
    padding-bottom: clamp(3.4rem, 8vh, 4.4rem);
  }

  .aboutGrid {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(1.1rem, 3vh, 1.8rem);
  }

  /* 单列布局下 ABOUT 要塞下「大标题 + 角色声明 + 名片 + 牌堆」，
     牌堆按视口高度缩。下面这些 --cw 都是无 JS 时的兜底值，
     实际尺寸由 journal.js 的 layout() 写。 */
  .deck {
    --cw: clamp(150px, 19vh, 232px); /* 兜底 */
    --cw: clamp(150px, 19svh, 232px);
    justify-self: center;
  }

  .hero {
    font-size: clamp(2.4rem, 9vw, 5.2rem);
    margin-bottom: clamp(0.7rem, 2vh, 1.2rem);
  }

  .avatar {
    width: clamp(76px, 10vw, 108px);
  }

  .universe {
    height: clamp(320px, 50vh, 470px); /* 兜底 */
    height: clamp(320px, 50svh, 470px);
  }

  .atlas__map {
    width: min(100%, 460px, 38vh); /* 兜底 */
    width: min(100%, 460px, 38svh);
  }

  .fig--cat {
    left: -22px;
    --s: 98px;
  }

  .fig--coffee {
    right: -20px;
    --s: 58px;
  }

  .fig--bear {
    --s: 92px;
  }
}

/* ---- 手机 ---- */
@media (max-width: 640px) {
  .deck {
    --cw: clamp(140px, 17vh, 200px); /* 兜底 */
    --cw: clamp(140px, 17svh, 200px);
  }

  /* 手机上角色声明要缩一档，否则 INTERNET SURFER. 会顶到右边缘 */
  .statement {
    font-size: clamp(1.02rem, 4.6vw, 1.34rem);
  }

  .hero {
    font-size: clamp(2.2rem, 13.5vw, 3.6rem);
  }

  /* 手机上年份要占满整行，1995 / 2026 中间那根轴自然被压短 */
  .years__a,
  .years__b {
    font-size: clamp(2.2rem, 13vw, 4rem);
  }

  .years__mid {
    letter-spacing: 0.14em;
  }

  /* 手机只留 1 个角色，其余收起来——密度靠排版，不靠贴纸堆 */
  .fig--coffee,
  .fig--camera,
  .fig--cat2 {
    display: none;
  }

  .fig--cat {
    left: -14px;
    bottom: -10px;
    --s: 84px;
  }

  .fig--plane {
    --s: 60px;
  }

  .fig--bear {
    --s: 74px;
    right: 2%;
  }

  .fig--laptop {
    --s: 70px;
  }

  .fig--burst {
    --s: 56px;
  }

  .secHead {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
  }

  .eyebrow__note {
    text-align: left;
    white-space: normal;
  }

  .atlas__map {
    width: min(100%, 360px, 34vh); /* 兜底 */
    width: min(100%, 360px, 34svh);
  }

  /* 手机上换成显式的网格区域，不再靠自动放置。
     之前 key 写 grid-column:2、val 写 grid-column:2/-1，val 会一直伸进
     fx 那一列（实测重叠 12.2px），@eighteentang 和 ↗ 就贴在一起了。
     现在键名与账号各占一行、箭头独占一列，四条信息永远互不侵占。 */
  .connect__row {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      'num key fx'
      'num val fx';
    align-items: center;
    gap: 0.3rem 0.7rem;
    padding: 0.95rem 0.4rem;
  }

  .connect__num {
    grid-area: num;
  }

  .connect__key {
    grid-area: key;
  }

  .connect__val {
    grid-area: val;
    font-size: 0.78rem;
  }

  .connect__fx {
    grid-area: fx;
  }

  .universe {
    height: clamp(340px, 50vh, 460px); /* 兜底 */
    height: clamp(340px, 50svh, 460px);
  }

  /* 手机上标签要缩一点、核心要小一点，否则 8 个标签在椭圆环上会互相压住 */
  .universe__core {
    width: 88px;
  }

  .universe__coreText {
    font-size: 0.64rem;
  }

  .skill__btn {
    gap: 0.34rem;
    padding: 0.5rem 0.72rem;
    font-size: calc(clamp(0.7rem, 2.8vw, 0.9rem) * var(--fs, 1));
    border-width: 2.5px;
  }

  .lightbox__stage img {
    max-height: 60vh; /* 兜底 */
    max-height: 60svh;
    border-width: 3px;
    box-shadow: 6px 6px 0 var(--accent);
  }

  .lightbox__nav {
    width: 2.4rem;
    height: 2.8rem;
    font-size: 1.3rem;
  }
}

@media (max-width: 420px) {
  .deck {
    --cw: clamp(132px, 16vh, 170px); /* 兜底 */
    --cw: clamp(132px, 16svh, 170px);
  }
}

/* 矮屏：把纵向空间还回来 */
@media (max-height: 640px) {
  .universe {
    height: 300px;
  }

  .deck {
    --cw: clamp(130px, 15vh, 190px); /* 兜底 */
    --cw: clamp(130px, 15svh, 190px);
  }

  .section {
    padding-top: calc(var(--topbar-h) + 0.8rem);
  }

  .atlas__map {
    width: min(100%, 420px, 36vh); /* 兜底 */
    width: min(100%, 420px, 36svh);
  }
}

/* ==========================================================================
   15  prefers-reduced-motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
    scroll-snap-type: none;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .js [data-reveal] {
    opacity: 1;
    transform: none;
  }

  .pin.is-on .pin__ring {
    animation: none;
    opacity: 0.8;
    r: 17;
  }

  /* ---------- 常驻动效的显式关闭 ----------
     上面那条通配规则把 animation-duration 压到 0.001ms —— 对一次性入场够用，
     对「无限循环」的元素却不够干净：它们会瞬间跳到最后一帧再弹回基础态，
     而最后一帧不一定等于静止态。所以这里逐个关掉，并把被动画改过的属性
     显式归位。 */

  /* CRT glitch：幽灵层和扫描线本来平时就是不可见的，
     关掉动画之后剩下的是一行干干净净的 EIGHTEEN TANG. —— 正好是想要的结果。 */
  .hero__line,
  .hero__line::before,
  .hero__line::after,
  .hero::after {
    animation: none;
  }

  .hero__line::before,
  .hero__line::after,
  .hero::after {
    opacity: 0;
  }

  /* 漂移 / 巡航 / 快门 / 顶牌微动 / 箭头浮动 / 圆点呼吸：全部停住 */
  .hero,
  .rail__num,
  .fig--plane,
  .fig--camera,
  .deck__card.is-top,
  .roadsign__arrow,
  .now__dot {
    animation: none;
  }

  /* 基础态里没写 translate / rotate 的那几个，动画一停就会僵在
     最后一次算出来的位置上 —— 显式归零。 */
  .hero,
  .rail__num,
  .fig--plane,
  .deck__card.is-top,
  .roadsign__arrow {
    translate: none;
    rotate: none;
  }

  .now__dot {
    opacity: 1;
    transform: none;
  }

  /* 地图上那个巡游点：js/map.js 在减弱动态效果下压根不建它，
     这里再兜一层，免得以后改了那边忘了这一边。 */
  .routeDot {
    display: none;
  }

  /* 路牌不晃了，但「回顶」照常 —— 那是功能，不是动效。
     js/main.js 在减弱动态效果下直接 scrollTo，不经过这条规则。
     hover 的抬起 / 放大 / 再歪一点留着（那是状态反馈，不是装饰），
     只是不再「蹭那一下」。 */
  .roadsign.is-shaking,
  .roadsign:hover,
  .roadsign:focus-visible {
    animation: none;
  }

  /* ---------- ABOUT 卡片的「抽动」 ----------
     cardYank 是 hover 触发的。通配规则虽然已经把它压到 0.001ms，
     但显式关掉更干净：抬升、放大、换硬投影这些 hover 状态全部保留，
     只是不再抽那一下。 */
  .deck__card:hover,
  .deck__card.is-top:hover {
    animation: none;
  }

  /* ---------- THE LAST TRAIN ----------
     火车不动、烟不冒、轮子不转、拟声词不出现。
     但车要停在站里 —— 直接把静态位写上去，不依赖 JS 有没有挂 .is-parked。
     它停着也是一幅完整的画面，而不是「一个被禁用的动画」。 */
  .train__unit {
    animation: none;
    translate: 0 0;
  }

  .train__smoke circle,
  .train__spoke,
  .train__word {
    animation: none;
  }

  .train__word {
    opacity: 0;
  }
}
