/* ==========================================================================
   Jinxiu Chen — Portfolio
   Design system: "Index / Catalogue"
   Editorial grid + archival typography + restrained motion.
   No framework. Hand-written.
   ========================================================================== */

/* ---------- 1. Tokens ---------------------------------------------------- */
:root {
  /* Colour — pure white ground, neutral ink */
  --paper:      #FFFFFF;
  --paper-deep: #F1F1F1;
  --ink:        #111111;   /* emphasis: headings, cursor, labels, hover */
  --text:       #2E2E2E;   /* body copy */
  --ink-2:      #525252;   /* secondary */
  --ink-3:      #767676;   /* meta — 4.5:1 on white (WCAG AA) */
  --rule:       rgba(0, 0, 0, 0.13);
  --rule-soft:  rgba(0, 0, 0, 0.06);
  --accent:     #111111;

  /* Type */
  /* Neither Marcellus nor Switzer carries Chinese glyphs, so the CJK face is
     named explicitly and deliberately as a SANS: a Song/Ming serif fallback
     reads heavy and traditional next to the latin, whereas Source Han Sans /
     PingFang keeps the same neutral, modern voice. Latin still resolves to
     Marcellus/Switzer because those come first in the stack. */
  --cjk:   'PingFang SC', 'HarmonyOS Sans SC', 'Source Han Sans SC',
           'Noto Sans CJK SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  --serif: 'Marcellus', var(--cjk), 'Times New Roman', serif;
  --sans:  'Switzer', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', var(--cjk);

  --fs-display: clamp(3.2rem, 10.6vw, 10.5rem);
  --fs-h1:      clamp(2.1rem, 5.4vw, 4.6rem);
  --fs-h2:      clamp(1.6rem, 3.2vw, 2.7rem);
  --fs-h3:      clamp(1.15rem, 1.7vw, 1.5rem);
  --fs-body:    clamp(0.95rem, 1.02vw, 1.075rem);
  --fs-lead:    clamp(1.05rem, 1.35vw, 1.35rem);
  --fs-micro:   0.656rem;   /* 10.5px — matched to the reference */

  /* Space */
  --gutter:   clamp(1.1rem, 3.4vw, 3.4rem);
  --gap:      clamp(1rem, 2.6vw, 2.6rem);
  --bay:      clamp(3.5rem, 9vw, 9rem);   /* vertical section rhythm */
  --bay-sm:   clamp(2rem, 4.5vw, 4rem);
  --maxw:     1680px;

  /* Motion */
  --ease:     cubic-bezier(0.16, 1, 0.3, 1);
  --ease-io:  cubic-bezier(0.65, 0, 0.35, 1);
  --dur:      0.9s;
  --dur-fast: 0.42s;

  --bar-h:    clamp(56px, 6vw, 72px);
}

/* ---------- 2. Reset ---------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 300;
  line-height: 1.62;
  letter-spacing: 0.005em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "kern" 1, "liga" 1;
  overflow-x: hidden;
}

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

h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
h1, h2, h3, h4 { color: var(--ink); }
ul, ol { margin: 0; padding: 0; list-style: none; }

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

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

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

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

/* ---------- 3. Primitives ---------------------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.micro {
  font-size: var(--fs-micro);
  font-weight: 400;
  letter-spacing: 0.015em;   /* the reference tracks uppercase almost not at all */
  text-transform: uppercase;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

.rule { height: 1px; background: var(--rule); border: 0; }

.serif { font-family: var(--serif); font-weight: 400; color: var(--ink); }

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: 0.85rem 1.2rem;
}
.skip:focus { left: 0; }

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

/* ---------- 4. Top bar -------------------------------------------------- */
.bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 90;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  background: var(--paper);
  border-bottom: 1px solid transparent;
  transition: border-color 0.5s var(--ease);
}
.bar.is-stuck { border-bottom-color: var(--rule); }
.bar__in {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: baseline;
  gap: 1.5rem;
}
.bar__mark {
  color: var(--ink);
  font-family: var(--serif);
  font-size: clamp(0.95rem, 1.15vw, 1.15rem);
  letter-spacing: 0.01em;
  line-height: 1;
  white-space: nowrap;
}
.bar__mark span { transition: opacity 0.4s var(--ease); }

.nav {
  display: flex;
  gap: clamp(1.4rem, 2.8vw, 2.8rem);
  align-items: baseline;
}

/* language switch, set off from the page links by a hairline */
.lang {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  padding-left: clamp(0.7rem, 1.8vw, 1.5rem);
  border-left: 1px solid var(--rule);
}
.lang__opt {
  color: var(--ink-3);
  transition: color 0.3s var(--ease);
}
.lang__opt.is-on { color: var(--ink); }
.lang__opt:hover { color: var(--ink); }
.lang__sep { color: var(--ink-3); opacity: 0.4; }
@media (max-width: 560px) {
  .lang { border-left: 0; padding-left: 0; }
}

.bar__index { margin-left: auto; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js .bar, .js .screen, .js .page-portfolio,
  .js .page-about, .js .page-contact, .js .notfound { animation: none !important; }
  .js [data-reveal], .js [data-veil], .js .line > span {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    transition: none !important;
  }
}

/* still mode (?still=1) — freezes every animation for screenshots, audits
   and anyone who asks the OS for reduced motion */
.still *, .still *::before, .still *::after {
  animation: none !important;
  transition: none !important;
}
.still [data-reveal], .still [data-veil], .still .line > span {
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
}
.still .m-letter { transform: none !important; }
.still .plate__layer img { transform: none !important; }
.still .tint { opacity: 0 !important; }

/* ---------- 6. Home — image-led showcase -------------------------------- */
.page-home {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

.screen {
  flex: 1;
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: var(--bar-h) var(--gutter) 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* the work wall: one large plate + a thumbnail rail */
/* ---------- 6b. Standing deck of works -------------------------------- */
.deck {
  --card-w: clamp(190px, 24.5vw, 366px);
  flex: 1;
  position: relative;
  /* a real backdrop is required for the label's difference blending — with a
     transparent deck the blend has nothing to invert against and the white
     text simply vanishes */
  background: var(--paper);
  min-height: 0;
  overflow: hidden;
  perspective: 900px;
  perspective-origin: 50% 48%;
  cursor: grab;
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;
}
.deck.is-dragging { cursor: grabbing; }
.deck.is-dragging .deck__item { cursor: grabbing; }
.deck__row {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: translateY(2%) rotateX(-9deg);
  /* The row's plane sits in front of the plates in 3D hit-testing and would
     swallow every hover. Keep it out of hit-testing; plates opt back in. */
  pointer-events: none;
}

/* cards stand on their bottom edge, in a shallow arc */
.deck__item {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--card-w);
  aspect-ratio: 16 / 10;
  display: block;
  border-radius: 10px;
  background: var(--paper-deep);
  /* NOTE: no overflow here — clipping would flatten the 3D context and the
     hover label's counter-rotation would stop working. The inner .deck__fig
     does the clipping instead. */
  transform-style: preserve-3d;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  pointer-events: auto;
  will-change: transform, opacity;
  /* three stacked shadows: a tight one that draws the edge, a mid one that
     seats the card, and a wide one for depth — the plates are translucent,
     so without this they melt into the white page */
  box-shadow:
    0 1px 2px rgba(0, 0, 0, calc(0.06 + 0.04 * var(--sel, 0))),
    0 10px 22px -12px rgba(0, 0, 0, calc(0.08 + 0.09 * var(--sel, 0))),
    0 34px 60px -30px rgba(0, 0, 0, calc(0.10 + 0.12 * var(--sel, 0)));
  transition: box-shadow 0.3s var(--ease), opacity 0.2s linear;
}
.deck__fig {
  position: absolute;
  inset: 0;
  display: block;
  overflow: hidden;
  border-radius: inherit;
  /* hairline inside the crop so the card edge reads even on white artwork */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.07);
}
.deck__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  border-radius: inherit;
}
/* Plain type, no plate behind it. difference-blending means the name is
   always the inverse of whatever artwork sits underneath — dark on light
   covers, light on dark ones — so it stays legible without a black bar. */
.deck__tag {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 9;
  padding: 0;
  background: none;
  color: #ffffff;
  mix-blend-mode: difference;
  /* matched to the reference: 10.5px, weight 400, near-zero tracking.
     Its whole site uses just two sizes — 10.5px and 16px. */
  font-size: 0.656rem;          /* 10.5px */
  font-weight: 400;
  letter-spacing: 0.015em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transform: translate(-9999px, -9999px);
  transition: opacity 0.22s var(--ease);
}
.deck__tag.is-on { opacity: 1; }

/* ---------- chosen-work note: sits BELOW the stack, never over it -------- */
.deck__note {
  position: absolute;
  left: 50%;
  /* a share of the deck, not a hard 0: anchoring to the very bottom left a
     220px hole between the stack and the title on a tall viewport */
  bottom: 20%;
  z-index: 7;
  width: min(46ch, 62%);
  transform: translate(-50%, 14px);
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease), transform 0.62s var(--ease);
}
.deck__note.is-on {
  opacity: 1;
  transform: translate(-50%, 0);
}
.deck__note-title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.2rem, 2.1vw, 1.9rem);
  line-height: 1.12;
  color: var(--ink);
  margin-bottom: 0.4rem;
}
.deck__note-meta { color: var(--ink-3); margin-bottom: 0.7rem; }
@media (max-width: 900px) {
  .deck__note { width: 82%; }
}

.deck__hint {
  position: absolute;
  z-index: 6;
  left: 0;
  bottom: clamp(0.4rem, 1.5vh, 1rem);
  padding: 0.45rem 0.8rem;
  background: var(--paper);
  color: var(--ink-2);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}
.deck:hover .deck__hint { opacity: 0.92; }
@media (hover: none) { .deck__hint { display: none; } }

/* the row is wider than the frame when spread: fade the ends */
.deck::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background:
    linear-gradient(90deg, var(--paper), rgba(255, 255, 255, 0) 9%),
    linear-gradient(270deg, var(--paper), rgba(255, 255, 255, 0) 9%);
}

@media (max-width: 900px) {
  .deck { --card-w: clamp(170px, 46vw, 330px); perspective: 1250px; }
}
@media (max-width: 620px) {
  .deck { --card-w: clamp(150px, 66vw, 260px); perspective: 1000px; }
  .screen__caption { flex-direction: column; gap: 0.15rem; }
}


.screen__caption {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  border-top: 1px solid var(--rule);
  padding-block: clamp(0.55rem, 1.3vh, 0.85rem);
  color: var(--ink-3);
}
.screen__caption b { font-weight: 400; color: var(--ink); }

/* role line lives in the bar to keep the wall free of copy */
.bar__role {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--ink-3);
  margin-left: 1rem;
}
.bar__role .caret {
  display: inline-block;
  width: 0.42em;
  height: 0.95em;
  background: var(--ink);
  translate: 0 0.06em;
  animation: caret 1.05s steps(1, end) infinite;
}
@keyframes caret { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
@media (prefers-reduced-motion: reduce) { .bar__role .caret { animation: none } }
@media (max-width: 860px) { .bar__role { display: none; } }

@media (max-width: 900px) {
  .ring-stage {
    --item-w: clamp(160px, 34vw, 300px);
    perspective: 1100px;
    perspective-origin: 50% 46%;
  }
}
@media (max-width: 620px) {
  .ring-stage {
    --item-w: clamp(146px, 48vw, 240px);
    perspective: 900px;
  }
  .ring-hint { display: none; }
  .screen__caption { flex-direction: column; gap: 0.15rem; }
}

/* home keeps the footer inside the single screen */
.page-home .foot {
  margin-top: 0;
  padding-block: clamp(0.7rem, 1.6vh, 1rem);
  border-top: 1px solid var(--rule);
}

/* ---------- 7c. Morph overlay (page transition) ------------------------ */
.morph {
  position: fixed;
  top: 0; left: 0;
  width: 100vw; height: 100vh;
  z-index: 300;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  will-change: transform;
  /* Was 0.9s: the old view sat there for most of a second before navigating,
     which reads as a hang. The morph only needs to register as motion — the
     destination loads on its own. */
  transition: transform 0.46s var(--ease), opacity 0.26s linear;
}
/* After the card fills the frame the page is still loading underneath. A slow
   drift keeps the image alive so the wait reads as progress, not a hang. */
.morph.is-loading {
  transition: transform 7s cubic-bezier(0.16, 0.84, 0.44, 1);
}

/* Indeterminate hairline — the only chrome, one pixel, deliberately quiet.
   `fixed` on its own layer so the overlay's drift scale cannot move it. */
.morph__sweep {
  position: fixed;
  left: 0; bottom: 0;
  width: 100vw; height: 1px;
  z-index: 310;                    /* above .morph's 300 */
  /* difference-blended like the hover label, so the line stays visible
     whether the full-bleed artwork behind it is light or dark */
  background: rgba(255, 255, 255, 0.16);
  mix-blend-mode: difference;
  overflow: hidden;
  pointer-events: none;
  animation: morph-sweep-in 0.3s linear both;
}
@keyframes morph-sweep-in { from { opacity: 0; } to { opacity: 1; } }
.morph__sweep::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 34%; height: 100%;
  background: #ffffff;
  transform: translateX(-100%);
  animation: morph-sweep 1.25s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
@keyframes morph-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(394%); }
}
/* while a navigation is in flight the custom dot must not sit on top of it */
.is-navigating .cursor { opacity: 0 !important; }
.morph img { width: 100%; height: 100%; object-fit: cover; }

/* Entrance. This used to be a 0.55s fade of the whole <body> from opacity 0,
   which read as a blank hang: first paint landed at ~556ms on an idle local
   server. It is now shorter AND staged, so the eye reads motion rather than
   a wait. */
@keyframes enterBar {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: none; }
}
@keyframes enterMain {
  from { opacity: 0; transform: translateY(16px) scale(0.988); }
  to   { opacity: 1; transform: none; }
}
/* Deliberately NOT a fade of the whole <body>: with body at opacity 0 the
   browser has nothing opaque to paint, so first paint was pushed from 36ms to
   556ms — which is exactly the "it hangs on open" feeling. The page surface
   paints immediately and only the content is staged in. */
.js .bar { animation: enterBar 0.44s var(--ease) 0.04s both; }
.js .screen,
.js .page-portfolio,
.js .page-about,
.js .page-contact,
.js .notfound { animation: enterMain 0.6s var(--ease) 0.1s both; }

/* an arriving morph hand-off owns the frame; do not stack the entrance on top */
.js.is-arriving .bar,
.js.is-arriving .screen,
.js.is-arriving .page-portfolio,
.js.is-arriving .page-about,
.js.is-arriving .page-contact { animation: none; }

/* ---------- 8. Work grid (portfolio) ----------------------------------- */
.page-head {
  padding-top: calc(var(--bar-h) + clamp(3rem, 11vh, 8rem));
  padding-bottom: clamp(2rem, 5vw, 4rem);
}
.page-head h1 {
  font-family: var(--serif);
  font-size: var(--fs-h1);
  line-height: 0.98;
  letter-spacing: -0.018em;
  max-width: 22ch;
}
.page-head__meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
  margin-top: clamp(1.6rem, 4vw, 2.6rem);
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
  color: var(--ink-2);
}

.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gap);
  row-gap: var(--bay);
  padding-bottom: var(--bay);
}
.card { grid-column: span 6; }
.card--wide { grid-column: span 7; }
.card--narrow { grid-column: span 5; }
.card--right { grid-column: 7 / span 6; }
.card--inset { grid-column: 3 / span 8; }

.card__media {
  position: relative;
  overflow: hidden;
  background: var(--paper-deep);
}
.card__media img {
  width: 100%;
  transition: transform 1.1s var(--ease), filter 0.6s var(--ease);
  will-change: transform;
}
@media (hover: hover) {
  .card:hover .card__media img { transform: scale(1.035); }
}
.card__body {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding-top: 1rem;
  margin-top: 1rem;
  border-top: 1px solid var(--rule);
}
.card__title { font-family: var(--serif); font-size: var(--fs-h3); line-height: 1.15; color: var(--ink); }
.card__meta { color: var(--ink-3); text-align: right; }
.card__media::after {
  content: attr(data-label);
  position: absolute;
  left: 0; bottom: 0;
  padding: 0.5rem 0.7rem;
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  background: var(--ink);
  color: var(--paper);
  transform: translateY(101%);
  transition: transform 0.55s var(--ease);
}
@media (hover: hover) { .card:hover .card__media::after { transform: none; } }

@media (max-width: 900px) {
  .card, .card--wide, .card--narrow, .card--right, .card--inset { grid-column: span 12; }
  .grid { row-gap: clamp(2.4rem, 8vw, 4rem); }
}

/* ---------- 9. Project page -------------------------------------------- */
.proj { padding-top: var(--bar-h); }

.proj__head {
  padding-top: clamp(2.5rem, 9vh, 7rem);
  padding-bottom: clamp(1.6rem, 4vw, 3rem);
}
.proj__back { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--ink-2); }
.proj__back:hover { color: var(--ink); }

.proj__title {
  font-family: var(--serif);
  font-size: var(--fs-h1);
  line-height: 0.98;
  letter-spacing: -0.018em;
  max-width: 24ch;
  margin-top: clamp(1.2rem, 3vw, 2.2rem);
}
.proj__meta {
  display: flex;
  gap: clamp(1.5rem, 5vw, 5rem);
  flex-wrap: wrap;
  margin-top: clamp(1.4rem, 3.5vw, 2.4rem);
  padding-top: 1rem;
  border-top: 1px solid var(--rule);
}
.proj__meta div { display: flex; gap: 0.7rem; align-items: baseline; }
.proj__meta dt { color: var(--ink-3); }
.proj__meta dd { margin: 0; color: var(--ink); }

.bleed { width: 100%; }
.bleed img { width: 100%; }

.proj__lead { margin-top: clamp(1.5rem, 4vw, 3rem); }

/* body: sticky label column + flowing figures */
.body-flow {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gap);
  padding-bottom: var(--bay);
}

.section {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gap);
  grid-column: 1 / -1;
  margin-top: var(--bay);
  scroll-margin-top: calc(var(--bar-h) + 1.5rem);
}
.section:first-child { margin-top: clamp(2.5rem, 7vw, 5rem); }

.section__head {
  position: sticky;
  top: calc(var(--bar-h) + 1.2rem);
  z-index: 5;
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding-block: 0.7rem;
  background: linear-gradient(var(--paper) 78%, transparent);
  grid-column: 1 / span 3;
}
.section__num { color: var(--ink-3); }
.section__title {
  font-family: var(--serif);
  font-size: var(--fs-h3);
  line-height: 1.2;
  text-transform: none;
  color: var(--ink);
}
.section__body { grid-column: 4 / span 9; }

@media (max-width: 980px) {
  .section__head { position: static; grid-column: 1 / -1; background: none; border-top: 1px solid var(--rule); padding-top: 0.9rem; }
  .section__body { grid-column: 1 / -1; }
}

/* figures */
.figs { display: grid; gap: clamp(0.8rem, 2vw, 1.6rem); }
.figs--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 700px) { .figs--2 { grid-template-columns: 1fr; } }

.fig { margin: 0; }
.fig__media { position: relative; overflow: hidden; background: var(--paper-deep); }
.fig__media img { width: 100%; }

/* image load-in (only when JS is present, so no-JS never hides content) */
.js .fig__media img,
.js .card__media img,
.js .bleed img { opacity: 0; transition: opacity 0.8s var(--ease); }
.js img.is-loaded { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .js .fig__media img, .js .card__media img, .js .bleed img { opacity: 1 !important; }
}

.text-block {
  max-width: 62ch;
  font-size: var(--fs-lead);
  font-weight: 200;
  line-height: 1.66;
  color: var(--ink-2);
  padding-block: clamp(1.4rem, 3.5vw, 2.6rem);
}
.text-block p + p { margin-top: 1em; }
.text-block strong { font-weight: 400; color: var(--ink); }

.fig-cap {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 0.55rem;
  color: var(--ink-3);
}

/* next project */
.next {
  border-top: 1px solid var(--ink);
  padding-block: clamp(2.5rem, 7vw, 5rem);
}
.next__link {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
}
.next__title {
  font-family: var(--serif);
  font-size: var(--fs-h2);
  line-height: 1.05;
  transition: transform 0.7s var(--ease);
}
@media (hover: hover) { .next__link:hover .next__title { transform: translateX(0.6rem); } }

/* ---------- 10. Footer -------------------------------------------------- */
.foot {
  border-top: 1px solid var(--rule);
  padding-block: clamp(1.8rem, 4vw, 3rem) clamp(1.5rem, 3vw, 2.5rem);
  margin-top: var(--bay);
}
.foot__in {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1.2rem;
  flex-wrap: wrap;
  color: var(--ink-2);
}
.foot a { position: relative; }
.foot a::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -2px;
  height: 1px; background: currentColor;
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 0.5s var(--ease);
}
.foot a:hover::after { transform: scaleX(1); }
.foot__links { display: flex; gap: 1.4rem; }
.foot__sign { color: var(--ink-3); }

/* ---------- 11. Cursor -------------------------------------------------- */
/* The custom dot is a replacement, not an overlay: without this the native
   pointer kept showing underneath it. Scoped to .has-cursor, which JS adds
   only when it has successfully taken over. */
html.has-cursor,
html.has-cursor * { cursor: none !important; }
html.has-cursor input,
html.has-cursor textarea,
html.has-cursor select { cursor: auto !important; }

.cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: 150;
  pointer-events: none;
  display: grid;
  place-items: center;
  width: 10px; height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: var(--ink);
  color: var(--paper);
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0;
  transition: width 0.42s var(--ease), height 0.42s var(--ease),
              margin 0.42s var(--ease), background-color 0.42s var(--ease),
              opacity 0.3s var(--ease);
}
.cursor.is-on { opacity: 1; }
.cursor.is-view {
  width: 74px; height: 74px; margin: -37px 0 0 -37px;
  background: var(--ink);
}
.cursor.is-view span { opacity: 1; }
/* Width is the wrong signal: a narrow desktop window still has a mouse, and
   hiding the dot there while JS hides the native pointer leaves NO cursor. */
@media (hover: none) { .cursor { display: none; } }
.cursor span { opacity: 0; transition: opacity 0.25s var(--ease) 0.08s; }
/* ---------- 12. Progress rail ------------------------------------------ */
.rail {
  position: fixed;
  right: var(--gutter);
  top: 50%;
  translate: 0 -50%;
  z-index: 80;
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: flex-end;
}
.rail button {
  width: 16px; height: 2px;
  background: var(--rule);
  transition: width 0.5s var(--ease), background-color 0.5s var(--ease);
}
.rail button.is-on { width: 30px; background: var(--ink); }
@media (max-width: 1180px) { .rail { display: none; } }

/* ---------- 13. Misc ---------------------------------------------------- */
.notfound { min-height: 70svh; display: grid; place-items: center; text-align: center; }
.notfound h1 { font-family: var(--serif); font-size: var(--fs-h1); }

/* still mode: never animate scrolling */
.still, .still * { scroll-behavior: auto !important; }


/* ---------- 15. Title roll --------------------------------------------- */
.roll {
  position: relative;
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}
.roll i {
  display: block;
  font-style: inherit;
  transition: transform 0.6s var(--ease);
  transition-delay: var(--rd, 0s);
}
.roll i + i {
  position: absolute;
  left: 0;
  top: 100%;
}
@media (hover: hover) {
  .row:hover .roll i,
  a:hover > .roll i,
  .card:hover .roll i { transform: translateY(-100%); }
}
@media (prefers-reduced-motion: reduce) {
  .roll i { transition: none; }
}
.still .roll i { transform: none !important; }

/* ---------- 16. Lightbox ------------------------------------------------ */
.fig__media {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  cursor: zoom-in;
  text-align: left;
}
.fig__zoom {
  position: absolute;
  right: 0; bottom: 0;
  padding: 0.45rem 0.65rem;
  background: var(--paper);
  color: var(--ink);
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}
.fig__media:hover .fig__zoom,
.fig__media:focus-visible .fig__zoom { opacity: 1; transform: none; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: var(--paper);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s var(--ease);
}
.lightbox.is-open { opacity: 1; pointer-events: auto; }

.lightbox__bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 0.9rem var(--gutter);
  border-bottom: 1px solid var(--rule);
  color: var(--ink-2);
}
.lightbox__stage {
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(0.8rem, 2.4vw, 2rem) var(--gutter);
  overflow: hidden;
}
.lightbox__stage img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  opacity: 0;
  transform: scale(0.985);
  transition: opacity 0.45s var(--ease), transform 0.6s var(--ease);
}
.lightbox.is-ready .lightbox__stage img { opacity: 1; transform: none; }
.lightbox__foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 0.9rem var(--gutter);
  border-top: 1px solid var(--rule);
  color: var(--ink-2);
}
.lightbox button {
  color: var(--ink);
  padding: 0.2rem 0.1rem;
  border-bottom: 1px solid transparent;
  transition: border-color 0.35s var(--ease), opacity 0.35s var(--ease);
}
.lightbox button:hover:not(:disabled) { border-bottom-color: currentColor; }
.lightbox button:disabled { opacity: 0.3; cursor: default; }
.lightbox__nav { display: flex; gap: 1.4rem; }
.lightbox__count { font-variant-numeric: tabular-nums; }
@media (max-width: 700px) {
  .lightbox__bar .lightbox__hint { display: none; }
}

/* ---------- 13. About & Contact ---------------------------------------- */
.page-about,
.page-contact {
  padding-bottom: clamp(3rem, 8vh, 7rem);
}

.page-head__eyebrow { color: var(--ink-3); margin-bottom: 0.7rem; }

.about__lead {
  margin-top: clamp(1.4rem, 3.5vw, 2.8rem);
}
.about__lead-text {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.5rem, 3.5vw, 2.9rem);
  line-height: 1.18;
  letter-spacing: -0.01em;
  color: var(--ink);
  /* measure in em so it scales with THIS font size, not the container's */
  max-width: 20em;
}
.about__standfirst {
  margin-top: clamp(1rem, 2.4vw, 1.7rem);
  color: var(--ink-3);
}

.about__body {
  margin-top: clamp(2.4rem, 6vw, 4.6rem);
}
.about__prose {
  max-width: 66ch;
  display: grid;
  gap: 1.5rem;
  font-size: var(--fs-body);
  line-height: 1.68;
  color: var(--text);
}

.about__figs {
  margin-top: clamp(2.6rem, 6vw, 4.6rem);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2.6vw, 2.4rem);
}
.about__fig { margin: 0; }
.about__fig img {
  width: 100%;
  height: auto;
  border-radius: 10px;
  background: var(--paper-deep);
}
.about__fig figcaption {
  margin-top: 0.7rem;
  color: var(--ink-3);
}
@media (max-width: 720px) { .about__figs { grid-template-columns: 1fr; } }

.about__lists {
  margin-top: clamp(2.8rem, 6vw, 5rem);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.6rem, 4vw, 4rem);
  border-top: 1px solid var(--rule);
  padding-top: clamp(1.4rem, 3vw, 2.2rem);
}
.about__col-title { color: var(--ink-3); margin-bottom: 0.9rem; }
.about__list {
  display: grid;
  gap: 0.5rem;
  font-size: var(--fs-body);
  color: var(--text);
}
@media (max-width: 720px) { .about__lists { grid-template-columns: 1fr; } }

.about__cta {
  margin-top: clamp(2.6rem, 6vw, 4.4rem);
  border-top: 1px solid var(--rule);
  padding-top: clamp(1.4rem, 3vw, 2.2rem);
  display: grid;
  gap: 0.9rem;
  color: var(--text);
}
.about__cta-links { display: flex; flex-wrap: wrap; gap: 1.6rem; }

.link-underline {
  color: var(--ink);
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 100% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  padding-bottom: 2px;
  transition: background-size 0.4s var(--ease);
}
.link-underline:hover { background-size: 0% 1px; }

/* contact: two big rows, nothing else */
.contact__rows {
  margin-top: clamp(1.8rem, 4.5vw, 3.4rem);
  border-top: 1px solid var(--rule);
}
.contact__row {
  display: grid;
  grid-template-columns: 10rem 1fr auto;
  align-items: baseline;
  gap: 1rem;
  padding: clamp(1.1rem, 2.6vw, 1.8rem) 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink);
  transition: opacity 0.4s var(--ease);
}
.contact__row:hover { opacity: 0.62; }
.contact__label { color: var(--ink-3); }
.contact__value {
  /* sans, not the serif: Marcellus draws its zero as a plain oval, which made
     the email address read as "j.chenO42O225@..." */
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(1.05rem, 2.4vw, 1.9rem);
  line-height: 1.2;
  word-break: break-word;
  font-variant-numeric: slashed-zero;
}
.contact__go { color: var(--ink-3); white-space: nowrap; }
@media (max-width: 720px) {
  .contact__row { grid-template-columns: 1fr; gap: 0.35rem; }
  .contact__go { display: none; }
}

.contact__note {
  margin-top: clamp(2rem, 5vw, 3.4rem);
  display: grid;
  gap: 0.9rem;
  max-width: 62ch;
  color: var(--text);
}
.contact__note .micro { color: var(--ink-3); }
.contact__back { display: flex; flex-wrap: wrap; gap: 1.6rem; margin-top: 0.4rem; }

/* explicit entry point under the stack — the plate itself is still clickable,
   this just names the action */
.deck__note-cta {
  margin-top: clamp(0.7rem, 1.6vw, 1.05rem);
  color: var(--ink-3);
}
.deck__note-link {
  pointer-events: auto;                 /* .deck__note is inert by default */
  display: inline-block;
  color: var(--ink);
  padding-bottom: 3px;
  border-bottom: 1px solid currentColor;
  transition: opacity 0.32s var(--ease), border-color 0.32s var(--ease);
}
.deck__note-link:hover { opacity: 0.5; }
.deck__note-link span { display: inline-block; transition: transform 0.32s var(--ease); }
.deck__note-link:hover span { transform: translateX(3px); }

/* ---------- 15. Chinese typography ------------------------------------- */
/* CJK glyphs are already square and even, so the latin tracking that gives the
   English UI its air makes Chinese look cramped, and uppercase is a no-op.
   Chinese also sets slightly looser: the faces have a larger x-height than
   Switzer, so identical line-height reads tighter. */
html[lang^="zh"] .micro { letter-spacing: 0.06em; }

html[lang^="zh"] h1,
html[lang^="zh"] .about__lead-text,
html[lang^="zh"] .proj__title,
html[lang^="zh"] .deck__note-title {
  font-weight: 400;
  letter-spacing: 0.005em;
  line-height: 1.3;
}
/* CJK needs a shorter measure than latin at the same width before it feels
   like a wall of characters */
html[lang^="zh"] .about__prose { max-width: 46ch; }
html[lang^="zh"] .about__prose p,
html[lang^="zh"] .body-flow p { line-height: 1.95; }
html[lang^="zh"] .about__lead-text { max-width: 15em; }
html[lang^="zh"] .contact__value { font-family: var(--sans); }

/* ---------- 17. Arriving from a card morph ---------------------------- */
/* The overlay that covered the departing page cannot fade — the document is
   gone. So the arriving page paints the same frame (handed over through
   sessionStorage) and fades THAT out, which makes the two halves of the
   transition read as one continuous move instead of a cut. */
html.is-arriving::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 320;
  background: var(--paper) var(--jx-morph-img, none) center / cover no-repeat;
  animation: veil-out 0.52s cubic-bezier(0.22, 0.61, 0.36, 1) both;
  pointer-events: none;
}
@keyframes veil-out {
  from { opacity: 1; transform: scale(1.085); }
  to   { opacity: 0; transform: scale(1.115); }
}
@media (prefers-reduced-motion: reduce) {
  html.is-arriving::before { animation-duration: 0.01s; }
}

/* ---------- 18. Category filter ----------------------------------------- */
.cats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.35rem;
  padding-top: 0.4rem;
  padding-bottom: clamp(1.4rem, 3vw, 2.4rem);
}
.cats__item {
  color: var(--ink-3);
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  padding-bottom: 3px;
  transition: color 0.3s var(--ease), background-size 0.4s var(--ease);
}
.cats__item:hover { color: var(--ink); background-size: 100% 1px; }
.cats__item.is-on { color: var(--ink); background-size: 100% 1px; }
.cats__empty { color: var(--ink-3); padding: 2rem 0 3rem; }
