/* ==========================================================
   kaixinbuilds — block print on paper
   One ink tone on a warm paper ground, one ochre accent used
   sparingly. Tone comes from carved line, never from gradient.
   The playful 16-bit register is quoted, never worn: it only
   appears inside .screen-bezel.
   ========================================================== */

:root {
  /* ── ink on paper ── */
  --paper:      #F6F1E6;
  --paper-sunk: #EFE8D7;
  --card:       #FCFAF4;
  --ink:        #1E2749;
  --ink-soft:   #4B5578;
  --ink-faint:  #626B86;
  --rule:       #DCD2BE;
  --glass-edge: rgba(255, 255, 255, .70);

  /* ── the single accent, drawn from the sunrise in the print ── */
  --ochre:      #C0703A;
  --ochre-deep: #96521F;
  --ochre-wash: #F0E2D2;

  --font-serif: "Spectral", "Songti SC", "Noto Serif CJK SC", "Source Han Serif SC",
                "SimSun", Georgia, serif;
  --font-sans:  "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
                "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;

  --wrap: 1080px;
  --wrap-narrow: 660px;
  --radius: 4px;              /* print is cut, not rounded */
}

/* Night print: the same block pulled in ink on dark stock. */
/* ── reset ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.68;
  -webkit-font-smoothing: antialiased;
}

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

/* ── the print, fixed behind every page ────────────────── */
/* A pseudo-element rather than background-attachment: fixed, which iOS
   Safari does not honour. It sits behind everything and never scrolls,
   so the pattern reads as paper stock rather than as content. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: url("assets/art/prints/payphone-background.jpg") center center / cover no-repeat;
  opacity: .30;
}
a { color: var(--ochre-deep); text-underline-offset: .18em; }

h1, h2, h3 {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: 600;
  line-height: 1.22;
  letter-spacing: -.012em;
}
/* Chinese in a serif face needs a touch more air than latin does. */
:lang(zh) h1, :lang(zh) h2, :lang(zh) h3 { letter-spacing: .01em; }

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

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--ink); color: var(--paper); padding: .6rem 1rem; text-decoration: none;
}
.skip-link:focus { left: 0; top: 0; }

/* ── glass ─────────────────────────────────────────────── */
/* Content floats on frosted panels so the print stays visible behind it
   without ever competing with the text on top. */
main > section > .wrap {
  background: color-mix(in srgb, var(--paper) 50%, transparent);
  -webkit-backdrop-filter: blur(30px) saturate(165%);
          backdrop-filter: blur(30px) saturate(165%);
  border: 1px solid var(--glass-edge);
  border-radius: 16px;
  padding: clamp(1.5rem, 4vw, 3rem);
  box-shadow: 0 12px 44px -20px rgba(30, 39, 73, .38);
}

/* Without backdrop-filter the panel would be bare translucency over a busy
   pattern, so thicken it to near-opaque instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  main > section > .wrap { background: color-mix(in srgb, var(--paper) 97%, transparent); }
}

/* The hero is the one place the print is worth seeing properly, so its panel
   drops the blur and thickens the wash instead. Blurring was what flattened
   the carving: at any opacity it turns the handsets into soft colour, while a
   plain wash leaves them readable as lines. Every other page keeps the frosted
   panel, where denser body copy runs over the pattern and needs the contrast.
   This outranks the @supports fallback above on specificity, which is correct:
   a browser without backdrop-filter never had the blur to lose. */
main > section.hero > .wrap {
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  box-shadow: 0 8px 30px -24px rgba(30, 39, 73, .32);
}

/* ── layout ────────────────────────────────────────────── */
.wrap { width: min(100% - 2.5rem, var(--wrap)); margin-inline: auto; }
.wrap-narrow { width: min(100% - 2.5rem, var(--wrap-narrow)); }
.section { padding-block: clamp(3.25rem, 7vw, 5.5rem); }
main > .section:first-child { padding-top: clamp(2.75rem, 6vw, 4.5rem); }
main > .section + .section { border-top: 1px solid var(--rule); }

/* Each nav item is its own page now, so every page opens with a title. */
.page-title {
  font-size: clamp(1.9rem, 4.4vw, 2.6rem);
  letter-spacing: -.025em;
}
.page-sub {
  margin: .85rem 0 clamp(2.25rem, 5vw, 3.25rem);
  color: var(--ink-soft);

  font-size: 1.04rem;
}
.section-title { font-size: clamp(1.5rem, 3.2vw, 1.95rem); }
.section-sub { margin: .7rem 0 2.5rem; color: var(--ink-soft); }

.eyebrow {
  margin: 0 0 .9rem;
  font-family: var(--font-sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--ochre-deep);
}

/* ── header ────────────────────────────────────────────── */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
          backdrop-filter: blur(18px) saturate(150%);
  border-bottom: 1px solid var(--rule);
}
/* The bar spans the window while the content panels stay centred at 1080px,
   so the wordmark sits at the left edge and the nav at the right instead of
   both bunching into the middle of a wide screen. */
.site-header .wrap {
  width: 100%;
  max-width: none;
  padding-inline: clamp(1.25rem, 3vw, 2.5rem);
}
.header-inner { display: flex; align-items: center; gap: 1.5rem; min-height: 60px; }

.wordmark {
  display: flex; align-items: center; gap: .6rem;
  margin-right: auto;
  color: var(--ink); text-decoration: none;
  font-family: var(--font-serif);
  font-size: 1.05rem; font-weight: 600; letter-spacing: -.01em;
}
.wordmark .mark {
  width: 22px; height: 22px; flex: none;
  border-radius: 3px;
}
.wordmark-text { white-space: nowrap; }
.wordmark-text :lang(zh-Hans) { letter-spacing: .04em; }
.wordmark-latin {
  margin-left: .55rem;
  font-family: var(--font-sans);
  font-size: .72rem;
  font-weight: 400;
  letter-spacing: .04em;
  color: var(--ink-faint);
}
.wordmark em { font-style: normal; color: var(--ochre-deep); }

.site-nav { display: flex; gap: 1.6rem; }
.site-nav a {
  color: var(--ink-soft); text-decoration: none;
  font-size: .92rem;
  padding-bottom: .15rem;
  border-bottom: 1px solid transparent;
}
.site-nav a:hover { color: var(--ink); border-bottom-color: var(--ochre); }
.site-nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ochre); }

@media (max-width: 700px) {
  /* The header was costing three rows on a phone and 17% of the viewport
     before a word of content: wordmark, then the language switch on a row of
     its own, then the nav. The switch only wrapped because the bilingual
     wordmark carries both 观象制器 and kaixinbuilds, and the pair missed
     sharing a row by 27px. Tightening the gutter, the column gap and the
     switch itself buys that back, so the switch sits beside the wordmark and
     the header drops to two rows. Both wordmark forms stay visible: neither
     is a translation of the other. */
  .site-header .wrap { padding-inline: .875rem; }
  .header-inner { flex-wrap: wrap; gap: .35rem .5rem; padding-bottom: .45rem; min-height: 0; }
  .wordmark { margin-right: auto; }
  .site-nav {
    order: 3;
    width: 100%;
    gap: 1.25rem;
    padding-top: .1rem;
  }
  .site-nav a { font-size: .88rem; }
}

/* ── hero ──────────────────────────────────────────────── */
.hero { padding-block: clamp(3.5rem, 10vw, 7rem) clamp(3rem, 8vw, 5rem); }

/* The couplet the flagship project is named from. Both languages always
   show: the pairing is the content, not a translation of it. */
.epigraph { margin: 0 0 2.25rem; }
.epigraph .zh {
  display: block;
  font-family: var(--font-serif);
  font-size: clamp(1.05rem, 2.4vw, 1.28rem);
  letter-spacing: .06em;
  color: var(--ink);
}
.epigraph .en {
  display: block;
  margin-top: .5rem;
  font-size: .88rem; font-style: italic;
  color: var(--ink-faint);
}
/* Chinese readers do not need the couplet glossed back to them, so the
   English line shows only in the English version. */


.epigraph .src {
  display: block; margin-top: .5rem;
  font-size: .76rem; line-height: 1.6;
  color: var(--ink-faint);

}

.hero-name {
  font-size: clamp(2.3rem, 6.5vw, 3.5rem);
  letter-spacing: -.028em;
}
.hero-tagline {
  margin: .85rem 0 0;
  font-size: clamp(.98rem, 2vw, 1.1rem);
  color: var(--ochre-deep); font-weight: 500;

}
.hero-intro { margin: 1.5rem 0 0; color: var(--ink-soft); }
/* stretch, not center: in bilingual mode the two buttons can hold a
   different number of lines, and a short one floating beside a tall one
   reads as a mistake rather than a choice. */
.hero-actions { display: flex; flex-wrap: wrap; align-items: stretch; gap: 1rem; margin: 2.4rem 0 0; }
.hero-actions .btn-primary {
  font-size: 1rem;
  padding: .9rem 1.9rem;
}
/* In bilingual mode a button holds two stacked labels, and the arrow was
   dropping onto a third line of its own. Taking it out of the text flow and
   centring it against the whole button keeps it beside the pair no matter
   how many lines the labels run to, and leaves the one-language case
   looking exactly as it did before. */
.hero-actions .btn-primary {
  position: relative;
  padding-right: 3.1rem;
}
.hero-actions .btn-primary::after {
  content: "\2192";
  position: absolute;
  right: 1.9rem;
  top: 50%;
  transform: translateY(-50%);
  transition: transform .16s;
}
.hero-actions .btn-primary:hover::after { transform: translateY(-50%) translateX(3px); }

/* ── buttons ───────────────────────────────────────────── */
.btn {
  display: inline-block; font-family: var(--font-sans);
  font-size: .92rem; font-weight: 500;
  padding: .66rem 1.35rem;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  text-decoration: none; cursor: pointer;
  transition: background .14s, color .14s;
}
.btn-primary { background: var(--ink); color: var(--paper); }
.btn-primary:hover { background: transparent; color: var(--ink); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--rule); }
.btn-ghost:hover { border-color: var(--ink); }
.btn-accent { background: var(--ochre); border-color: var(--ochre); color: #FFF8F0; }
.btn-accent:hover { background: var(--ochre-deep); border-color: var(--ochre-deep); }
.btn[disabled] { opacity: .55; cursor: progress; }

/* ── screen bezel: the one place pixel content is allowed ─ */
.screen-bezel {
  border-radius: 8px;
  padding: 11px;
  background: var(--ink);
  box-shadow: inset 0 1px 4px rgba(0, 0, 0, .35);
}
.screen-inner {
  position: relative;
  border-radius: 3px;
  overflow: hidden;
  background: #12162A;
  aspect-ratio: 16 / 10;
}
.screen-inner img {
  width: 100%; height: 100%;
  object-fit: cover; image-rendering: pixelated;
  opacity: 0; transition: opacity .2s;
}
.shot.is-loaded .screen-inner img { opacity: 1; }
.shot.is-loaded .screen-inner::after { content: none; }
.screen-inner::after {
  content: attr(data-missing);
  position: absolute; inset: 0;
  display: grid; place-items: center; text-align: center;
  padding: 1rem;
  color: rgba(242, 235, 219, .42);
  font-size: .72rem; line-height: 1.8; font-family: var(--font-sans);
}

/* ── featured project ──────────────────────────────────── */
.featured-titlerow { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; }
.featured-title { font-size: clamp(1.75rem, 4vw, 2.35rem); letter-spacing: -.025em; }
.featured-subtitle { margin: .45rem 0 0; color: var(--ink-soft); font-size: 1.04rem; }
.featured-summary { margin: 1.35rem 0 0; color: var(--ink-soft); }

.badge-live {
  font-family: var(--font-sans);
  font-size: .64rem; font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ochre-deep);
  border: 1px solid var(--ochre);
  border-radius: var(--radius);
  padding: .16rem .5rem;
}

/* specimen-label discipline: figures stated plainly, ruled, not boxed */
.stats {
  display: flex; flex-wrap: wrap; gap: 1.5rem 2.5rem;
  margin: 2.1rem 0 0; padding: 1.3rem 0 0;
  border-top: 1px solid var(--rule);
  list-style: none;
}
.stats .value {
  display: block;
  font-family: var(--font-serif);
  font-size: clamp(1.5rem, 3.4vw, 1.95rem);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.stats .label {
  display: block; margin-top: .2rem;
  font-size: .78rem; letter-spacing: .06em;
  color: var(--ink-faint);
}

.highlight-box {
  margin: 2.1rem 0 0;
  padding-left: 1.2rem;
  border-left: 2px solid var(--ochre);

}
.highlight-box .label {
  display: block;
  font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ochre-deep); margin-bottom: .45rem;
}
.highlight-box p { margin: 0; color: var(--ink-soft); font-size: .97rem; }
.theory-refs { margin: .8rem 0 0; padding-left: 1.1rem; display: grid; gap: .45rem; font-size: .86rem; line-height: 1.5; color: var(--ink-soft); }
.theory-refs a { overflow-wrap: anywhere; }
.theory-note { display: block; color: var(--ink-faint); }

.featured-cta { margin: 2rem 0 0; }

/* A prompt meant to be copied out verbatim rather than read as prose, so it
   is set in mono on the card ground and allowed to scroll on its own. */
.copy-block {
  margin: 1.3rem 0 0;
  padding: 1rem 1.15rem;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow-x: auto;
}
.copy-block pre {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas,
               "PingFang SC", "Microsoft YaHei", monospace;
  font-size: .86rem;
  line-height: 1.75;
  white-space: pre-wrap;      /* long Chinese lines wrap instead of scrolling */
  word-break: break-word;
  color: var(--ink);
}
.copy-block .cap {
  display: block;
  margin-bottom: .6rem;
  font-family: var(--font-sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ochre-deep);
}

/* screenshots read as captured artifacts: plenty of air, ruled captions */
.shots {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1.5rem;
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
}
.shot { margin: 0; }
.shot-wide { grid-column: 1 / -1; }
.shot-wide .screen-inner { aspect-ratio: 1600 / 826; }
.shot figcaption {
  margin-top: .75rem; padding-top: .5rem;
  border-top: 1px solid var(--rule);
  font-size: .81rem; color: var(--ink-faint);
}

/* ── project grid ──────────────────────────────────────── */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  gap: clamp(1.5rem, 3vw, 2.25rem);
  align-items: start;
}
.card {
  display: flex; flex-direction: column;
  background: color-mix(in srgb, var(--card) 92%, transparent);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.6rem;
  transition: border-color .15s;
}
.card:hover { border-color: var(--ink); }
.card-body { display: flex; flex-direction: column; flex: 1; }

/* Name on top, the thing itself in the middle, details underneath. */
.card-media { gap: 0; }
.card-media > h3 { margin-bottom: 1rem; }
.card-media > .shot { margin: 0 0 1.25rem; }
.card-media .screen-inner { aspect-ratio: 16 / 10; }
.card-media .shot figcaption { margin-top: .6rem; padding-top: .45rem; }
.card h3 { font-size: 1.16rem; }

/* A project with a playable embed spans the whole grid row. */
.card-wide {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: 1.8rem;
  align-items: center;
}
@media (max-width: 760px) { .card-wide { grid-template-columns: 1fr; gap: 1.3rem; } }

.embed .screen-inner { background: #12162A; }
.embed .screen-inner::after { content: none; }
.card-wide .screen-inner { aspect-ratio: 16 / 10; }
.embed-note { margin: .7rem 0 0 !important; font-size: .78rem; color: var(--ink-faint); }

.embed-play {
  position: absolute; inset: 0;
  display: grid; place-content: center; gap: .7rem; justify-items: center;
  width: 100%; height: 100%;
  border: 0; cursor: pointer;
  font-family: var(--font-sans); color: #F2EBDB;
  background: radial-gradient(circle at 50% 45%, #2A2F52 0%, #12162A 70%);
  transition: background .2s;
}
.embed-play:hover { background: radial-gradient(circle at 50% 45%, #353B66 0%, #12162A 70%); }
.embed-play-icon {
  display: grid; place-content: center;
  width: 54px; height: 54px; border-radius: 50%;
  border: 1px solid rgba(242, 235, 219, .45);
  font-size: 1.1rem; padding-left: 4px;
}
.embed-play:hover .embed-play-icon { border-color: var(--ochre); color: var(--ochre); }
.embed-play-label { font-size: .88rem; font-weight: 500; letter-spacing: .02em; }

.embed-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.card p { margin: .7rem 0 1.4rem; color: var(--ink-soft); font-size: .94rem; }
.card-foot { margin-top: auto; display: flex; align-items: center; justify-content: flex-end; }

.card-link { font-size: .9rem; font-weight: 500; text-decoration: none; white-space: nowrap; }
.card-link:hover { text-decoration: underline; }
.card-link[aria-disabled="true"] { color: var(--ink-faint); pointer-events: none; }

/* ── connect ─────────────────────────────────────────────── */
/* An accordion rather than a stack: the page reads as a scannable list of
   titles, and a visitor opens only what they came for. Built on <details>
   so it works with the keyboard and without JavaScript. */
/* Upcoming and completed are separate groups, each in its own order. */
#connect-list > .connect-group:first-child { margin-top: clamp(2rem, 5vw, 3rem); }
.connect-group + .connect-group { margin-top: 3.25rem; }
.connect-group-title {
  font-family: var(--font-sans);
  font-size: .7rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .6rem;
}
.connect-list { list-style: none; margin: 0; padding: 0; }
.connect-list li { border-top: 1px solid var(--rule); }
.connect-list li:last-child { border-bottom: 1px solid var(--rule); }

.connect-head {
  display: grid;
  grid-template-columns: 8.5rem 1fr 1rem;
  align-items: baseline;
  gap: .5rem 1.25rem;
  padding: 1.15rem .25rem;
  cursor: pointer;
  list-style: none;
  transition: background .15s;
}
.connect-head::-webkit-details-marker { display: none; }
.connect-head:hover { background: color-mix(in srgb, var(--ochre) 7%, transparent); }
.connect-head:focus-visible { outline: 2px solid var(--ochre); outline-offset: -2px; }

.connect-date { font-size: .84rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.connect-title {
  font-family: var(--font-serif);
  font-size: 1.04rem; font-weight: 600;
  line-height: 1.35;
  color: var(--ink);
}
.connect-chevron {
  justify-self: end;
  width: 8px; height: 8px;
  border-right: 1.5px solid var(--ink-faint);
  border-bottom: 1.5px solid var(--ink-faint);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .2s;
}
.connect-item[open] .connect-chevron { transform: rotate(-135deg) translate(-2px, -2px); }

.connect-body {
  padding: 0 1rem 1.4rem 9.75rem;
  animation: connect-open .22s ease-out;
}
@keyframes connect-open { from { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .connect-body { animation: none; } }

.connect-venue { margin: 0; font-size: .87rem; color: var(--ink-faint); }
.connect-venue a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
}
.connect-venue a:hover { color: var(--ochre-deep); border-bottom-color: var(--ochre); }
.connect-summary { margin: .6rem 0 0; color: var(--ink-soft); font-size: .95rem; }
.connect-links { display: flex; flex-wrap: wrap; gap: 1.4rem; margin: .9rem 0 0; }
.connect-link { font-size: .89rem; }

/* Recognition earned by an entry, set apart from its description. */
.connect-award {
  margin: .6rem 0 0;
  font-size: .84rem; font-weight: 500;
  color: var(--ochre-deep);
}
.connect-award::before { content: "\25c6"; margin-right: .45rem; font-size: .7em; vertical-align: .1em; }

@media (max-width: 660px) {
  .connect-head { grid-template-columns: 1fr auto; gap: .3rem .7rem; }
  .connect-date    { grid-row: 1; grid-column: 1; }
  .connect-chevron { grid-row: 1; grid-column: 2; align-self: center; }
  .connect-title   { grid-row: 2; grid-column: 1 / -1; }
  .connect-body { padding-left: .25rem; padding-right: .25rem; }
}

/* ── approach ──────────────────────────────────────────── */
.lede { margin: 1.1rem 0 0; color: var(--ink-soft); }
.approach .epigraph { margin-bottom: 2rem; }

/* The loop is a real sequence, so it is a real ordered list. */
.loop {
  list-style: none;
  counter-reset: loop;
  margin: clamp(2.5rem, 6vw, 3.5rem) 0 0;
  padding: 0;
  display: grid;
  gap: clamp(1.9rem, 4vw, 2.6rem);
}
.loop > li { counter-increment: loop; position: relative; padding-left: 2.9rem; min-width: 0; }
.loop > li::before {
  content: counter(loop);
  position: absolute; left: 0; top: .1rem;
  width: 1.9rem; height: 1.9rem;
  display: grid; place-content: center;
  border: 1px solid var(--ochre);
  border-radius: 50%;
  font-family: var(--font-sans);
  font-size: .78rem; font-weight: 600;
  color: var(--ochre-deep);
}
/* a line joining the stages, so it reads as one loop rather than five notes */
.loop > li:not(:last-child)::after {
  content: "";
  position: absolute; left: .95rem; top: 2.3rem; bottom: -1.5rem;
  border-left: 1px dashed var(--rule);
}
.loop h2 { font-size: 1.08rem; line-height: 1.35; }
.loop-cycle {
  margin: .25rem 0 0;
  font-size: .72rem; letter-spacing: .12em; text-transform: lowercase;
  font-style: italic;
  color: var(--ink-faint);
}
/* Each stage is answered in points rather than prose: they are notes taken
   from a real build, and a bullet cannot pretend to be an argument. */
.loop-points {
  margin: .7rem 0 0;
  padding-left: 1.05rem;
  display: grid;
  gap: .4rem;
  color: var(--ink-soft);
  font-size: .96rem;
}
.loop-points li { padding-left: .15rem; min-width: 0; }
.loop-points li::marker { color: var(--ochre); }
/* Numbers need more room than a bullet does, and read better held apart
   from the text with the figures aligned. */
.loop-points-num { padding-left: 1.6rem; }
.loop-points-num li::marker { font-variant-numeric: tabular-nums; font-weight: 600; }

/* The loop has no last stage, so the list is closed by a line saying so. */
.loop-close {
  margin: clamp(1.9rem, 4vw, 2.6rem) 0 0;
  padding-left: 2.9rem;
  color: var(--ink-soft);
  font-size: .96rem;
}
@media (max-width: 480px) { .loop-close { padding-left: 0; } }

/* ── the tools behind a role ────────────────────────────── */
/* Each hired role names a real product, so the name carries a way to reach
   it: the link sits beside the heading, in the open. The tutorial is one
   step further out, folded away, because six open guides would bury the
   six roles they belong to. */
.tool-head {
  display: flex; flex-wrap: wrap;
  align-items: baseline;
  gap: .4rem .6rem;
}
.tool-link {
  font-family: var(--font-sans);
  font-size: .72rem; font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
  color: var(--ochre-deep);
  max-width: 100%;
  overflow-wrap: anywhere;
  text-decoration: none;
  padding: .18rem .55rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  transition: background .15s, border-color .15s;
}
.tool-link:hover { background: var(--ochre-wash); border-color: var(--ochre); }
.tool-link:focus-visible { outline: 2px solid var(--ochre); outline-offset: 2px; }

/* Folded, so the list still reads as six roles rather than twelve links. */
.tool-guide { margin: .6rem 0 0; }
.tool-guide > summary {
  display: inline-flex; align-items: center; gap: .4rem;
  cursor: pointer; list-style: none;
  font-family: var(--font-sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-faint);
}
.tool-guide > summary::-webkit-details-marker { display: none; }
.tool-guide > summary::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  border-right: 1.5px solid var(--ink-faint);
  border-bottom: 1.5px solid var(--ink-faint);
  transform: rotate(-45deg) translate(-1px, -1px);
  transition: transform .2s;
}
.tool-guide[open] > summary::before { transform: rotate(45deg) translate(-1px, -1px); }
.tool-guide > summary:hover { color: var(--ochre-deep); }
.tool-guide > summary:focus-visible { outline: 2px solid var(--ochre); outline-offset: 2px; }
.tool-guide-body { margin: .45rem 0 0; font-size: .9rem; }
.tool-guide-body a {
  color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
}
.tool-guide-body a:hover { color: var(--ochre-deep); border-bottom-color: var(--ochre); }

/* A step that names a page carries the page, on its own line under the
   instruction so the sentence still reads as a sentence. */
.step-item .tool-link { display: inline-block; margin-top: .35rem; }

/* Screenshots of somebody else's interface are evidence, not artwork:
   shown whole rather than cropped to a tidy ratio. */
.step-shot { max-width: 30rem; margin: 1rem 0 .25rem; }
.step-shot .screen-inner { aspect-ratio: auto; background: var(--paper); }
.step-shot .screen-inner img { height: auto; object-fit: contain; image-rendering: auto; }
/* The guide's screenshots are whole GitHub screens, so they take the
   column's width: at 30rem the buttons they point at would be unreadable. */
.guide-shot { max-width: 100%; }
.guide-shot .copy-block, .loop-points-num .copy-block { margin-top: .8rem; }

/* The second half of the Practice page: what the loop is aimed at.
   Deliberately not numbered. The loop is a sequence and reads as one; the
   three needs are simultaneous, so they are set as three plates side by side
   in the reading order, each held by a left rule rather than a counter. */
.needs-block {
  margin: clamp(3.25rem, 7vw, 4.75rem) 0 0;
  padding-top: clamp(2.5rem, 6vw, 3.5rem);
  border-top: 1px solid var(--rule);
}
.needs {
  display: grid;
  gap: clamp(2rem, 4vw, 2.75rem);
  margin: clamp(2rem, 5vw, 2.75rem) 0 0;
}
.need {
  padding-left: clamp(1.1rem, 3vw, 1.5rem);
  border-left: 2px solid var(--rule);
}
.need-name {
  margin: 0 0 .55rem;
  font-family: var(--font-sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .2em; text-transform: uppercase;
  color: var(--ochre-deep);
}
/* The student's own sentence, quoted. It is the thing being answered, so it
   is set as speech rather than as body copy. */
.need-voice {
  margin: 0 0 .5rem;
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 1.06rem;
  line-height: 1.5;
  color: var(--ink);
}
.need h3 { margin: 0; font-size: 1.02rem; line-height: 1.4; color: var(--ink-soft); }
.need-points { margin-top: .85rem; }
.needs-close {
  margin: clamp(2rem, 4vw, 2.6rem) 0 0;
  color: var(--ink-faint);
  font-size: .9rem;
}

/* Chinese does not italicise, so the quoted line leans on the corner brackets
   the copy already carries and drops the slant. */
html[data-lang-mode="zh"] .need-voice,
.need-voice .bi-zh { font-style: normal; }

/* ── contact layout ───────────────────────────────────── */
.contact-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 30rem);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
  margin-top: clamp(2rem, 5vw, 3rem);
}
@media (max-width: 860px) {
  .contact-layout { grid-template-columns: 1fr; }
  .specimen { justify-self: start; max-width: 30rem; }
}
/* ── the print, hung and labelled ──────────────────────── */
/* Gallery conventions: a deep mat, a thin frame, and a wall label naming the
   artist first. The work is credited, not used as wallpaper. */
.specimen { display: none; margin: 0; }
.specimen.is-loaded { display: block; }

.specimen-plate {
  background: #FFFDF8;
  border: 1px solid var(--rule);
  padding: clamp(.9rem, 2.5vw, 1.4rem);
  box-shadow: 0 14px 30px -18px rgba(30, 39, 73, .5);
}
.specimen-plate img { width: 100%; height: auto; }

.specimen-label {
  display: grid;
  gap: .12rem;
  margin-top: 1rem;
  padding-left: .1rem;
  border-left: 0;
}
.specimen-artist {
  font-family: var(--font-serif);
  font-size: .94rem;
  color: var(--ink-soft);
}
.specimen-title {
  font-family: var(--font-serif);
  font-size: 1.08rem; font-weight: 600; font-style: italic;
  color: var(--ink);
}
.specimen-meta {
  margin-top: .2rem;
  font-size: .82rem;
  color: var(--ink-faint);
}
/* ── the name, glossed ─────────────────────────────────── */
/* The epigraph device again, but closing the last page rather than opening
   one, so it sits under a rule and keeps to a reading measure. */
.colophon {
  max-width: 34rem;
  margin: clamp(3rem, 8vw, 4.5rem) 0 0;
  padding-top: 1.4rem;
  border-top: 1px solid var(--rule);
}
.colophon .zh { letter-spacing: .1em; }

/* ── data-load failure notice ──────────────────────────── */
.data-error { border: 1px solid var(--rule); border-radius: var(--radius); padding: 1.5rem; background: var(--card); }
.data-error h3 { font-size: 1rem; margin-bottom: .5rem; }
.data-error p { margin: 0; color: var(--ink-soft); font-size: .92rem; }

/* ── lightbox ──────────────────────────────────────────── */
/* <dialog> so the browser supplies the focus trap, the Escape key and the
   inertness of everything behind it. */
/* An always-visible control, not just a hover cursor: on a touch screen
   there is no hover, so the affordance has to be drawn. */
.shot img, .specimen img { cursor: zoom-in; }
.shot img:focus-visible, .specimen img:focus-visible { outline: 2px solid var(--ochre); outline-offset: 3px; }

.screen-bezel, .specimen-plate { position: relative; }

.screen-bezel::after,
.specimen-plate::after {
  content: "\2921\FE0E";           /* diagonal arrows */
  position: absolute;
  right: .85rem; bottom: .85rem;
  width: 1.85rem; height: 1.85rem;
  display: grid; place-content: center;
  border-radius: 5px;
  font-size: .82rem; line-height: 1;
  pointer-events: none;
  transition: opacity .16s, transform .16s;
}
/* dark device frame: light chip */
.screen-bezel::after {
  color: #F2EBDB;
  background: rgba(242, 235, 219, .16);
  border: 1px solid rgba(242, 235, 219, .32);
  opacity: .85;
}
/* light gallery mat: ink chip */
.specimen-plate::after {
  right: .55rem; bottom: .55rem;
  color: var(--ink);
  background: rgba(30, 39, 73, .07);
  border: 1px solid rgba(30, 39, 73, .18);
  opacity: .8;
}
.shot:hover .screen-bezel::after,
.specimen:hover .specimen-plate::after {
  opacity: 1;
  transform: scale(1.08);
}
/* the play button owns that corner instead */
.embed .screen-bezel::after { content: none; }

.lightbox {
  border: 0;
  padding: clamp(1rem, 4vw, 2.5rem);
  max-width: 100vw;
  max-height: 100vh;
  width: 100%;
  height: 100%;
  background: transparent;
  overflow: hidden;
}
.lightbox::backdrop {
  background: rgba(18, 22, 42, .88);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}
.lightbox img {
  max-width: 100%;
  max-height: calc(100vh - 8rem);
  margin: auto;
  object-fit: contain;
  border-radius: 3px;
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .8);
  cursor: zoom-out;
}
.lightbox-caption {
  margin: 1rem auto 0;
  max-width: 60ch;
  text-align: center;
  font-size: .86rem;
  color: rgba(242, 235, 219, .8);
}
.lightbox-close {
  position: absolute; top: .9rem; right: 1rem;
  width: 2.4rem; height: 2.4rem;
  display: grid; place-content: center;
  background: rgba(242, 235, 219, .12);
  border: 1px solid rgba(242, 235, 219, .3);
  border-radius: 50%;
  color: #F2EBDB;
  font-size: 1.1rem; line-height: 1;
  cursor: pointer;
}
.lightbox-close::before { content: "\00d7"; }
.lightbox-close:hover { background: rgba(242, 235, 219, .22); }


/* ── the canonical URL, shown in full ──────────────────── */
/* Visible and linked, so the address is both readable to a person and a
   real signal pointing search engines at the project itself. */
.project-url {
  margin: .9rem 0 0;
  font-size: .82rem;
  word-break: break-word;
}
.project-url a {
  color: var(--ink-faint);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: .1rem;
}
.project-url a:hover { color: var(--ochre-deep); border-bottom-color: var(--ochre); }
.card .project-url { margin: 0 0 .6rem; }

.project-links { display: grid; gap: .35rem; margin: 0 0 1.2rem; }
.project-links a {
  font-size: .86rem;
  color: var(--ochre-deep);
  text-decoration: none;
}
.project-links a:hover { text-decoration: underline; }

/* ── three language modes ──────────────────────────────── */
.lang-switch {
  display: flex;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  flex: none;
}
.lang-switch button {
  font-family: var(--font-sans);
  font-size: .8rem;
  color: var(--ink-soft);
  background: transparent;
  border: 0;
  border-left: 1px solid var(--rule);
  padding: .36rem .7rem;
  cursor: pointer;
  white-space: nowrap;
  transition: background .14s, color .14s;
}
.lang-switch button:first-child { border-left: 0; }
.lang-switch button:hover { color: var(--ink); background: rgba(192, 112, 58, .08); }
.lang-switch button.is-on {
  background: var(--ochre);
  color: #FFF8F0;
  font-weight: 500;
}

/* In bilingual mode both languages are shown, Chinese leading and English
   set quieter beneath it, so neither reads as a caption on the other. */
.bi-zh, .bi-en { display: block; }
.bi-en {
  margin-top: .18em;
  font-size: .92em;
  color: var(--ink-faint);
}
/* inside a heading the pairing should stay tight */
h1 .bi-en, h2 .bi-en, h3 .bi-en { font-size: .82em; font-weight: 400; }
/* small metadata reads better on one line */
.stats .label .bi-zh, .stats .label .bi-en,
.connect-date .bi-zh, .connect-date .bi-en,
.specimen-meta .bi-zh, .specimen-meta .bi-en { display: block; }
.badge-live .bi-en { display: none; }

/* the epigraph gloss hides only in Chinese-only mode */
html[data-lang-mode="zh"] .epigraph .en { display: none; }

/* ── work page: an index beside one open project ───────── */
.work-layout {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: clamp(1.75rem, 4vw, 3rem);
  align-items: start;
  /* The page title sits directly above this on both pages that use it, and
     a two-line bilingual title needs air under it before the index and the
     content start. Without this the whole layout crowds the masthead. */
  margin-top: clamp(2.25rem, 5vw, 3.5rem);
}
/* A grid item's min-width defaults to auto, which floors it at its content's
   min-content width. One long unbreakable line, or a strip that scrolls on its
   own axis, would otherwise widen the column past the viewport and drag the
   whole page sideways on a phone. */
.work-layout > * { min-width: 0; }
@media (max-width: 820px) {
  .work-layout { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
}

.project-nav { position: sticky; top: 5.5rem; }
/* The connect index is grouped now, so it needs headings of its own. Quiet
   ones: they label the list rather than compete with it. */
.nav-group {
  margin: 1.6rem 0 .5rem;
  font-family: var(--font-sans);
  font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ochre-deep);
}
.nav-group:first-child { margin-top: 0; }
@media (max-width: 820px) { .project-nav { position: static; } }

.project-nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .1rem; }
.project-nav a {
  display: block;
  padding: .55rem .7rem;
  border-left: 2px solid var(--rule);
  color: var(--ink-soft);
  font-size: .92rem;
  line-height: 1.4;
  text-decoration: none;
  transition: color .14s, border-color .14s, background .14s;
}
.project-nav a:hover { color: var(--ink); background: color-mix(in srgb, var(--ochre) 7%, transparent); }
.project-nav a.is-on {
  color: var(--ink);
  border-left-color: var(--ochre);
  background: color-mix(in srgb, var(--ochre) 10%, transparent);
  font-weight: 500;
}
.project-nav .bi-en { font-size: .84em; }

/* A long reference page reads as one document with a standing index, so the
   blocks are ruled apart rather than boxed. */
.doc-block + .doc-block {
  margin-top: clamp(2.75rem, 6vw, 4rem);
  padding-top: clamp(2.75rem, 6vw, 4rem);
  border-top: 1px solid var(--rule);
}
.doc-block > .section-title { scroll-margin-top: 6rem; }
.doc-block .loop h3 { font-size: 1.08rem; line-height: 1.35; font-family: var(--font-serif); font-weight: 600; }
.doc-block { scroll-margin-top: 5.5rem; }

/* Folded blocks (session pages, see wireCollapse): the heading is the
   toggle, and a closed page reads as a short outline, so the rules between
   blocks tighten from document spacing to list spacing. */
.doc-block.is-collapsible + .doc-block.is-collapsible { margin-top: 0; padding-top: 1.35rem; }
.doc-block.is-collapsible { padding-bottom: 1.35rem; }
/* Block, not flex: in bilingual mode the heading holds two stacked spans,
   and a flex row would set them side by side. */
.doc-block.is-collapsible > .section-title {
  position: relative;
  padding-right: 2rem;
  margin-bottom: 0;
  cursor: pointer;
}
.doc-block.is-collapsible > .section-title::after {
  content: "+";
  position: absolute; right: 0; top: 0;
  font-family: var(--font-sans, inherit); font-weight: 400;
  color: var(--ink-faint);
}
.doc-block.is-open > .section-title::after { content: "−"; }
.doc-block.is-collapsible > .section-title:hover::after { color: var(--ink); }
.doc-block.is-collapsible > .section-title:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
.doc-block.is-open > .doc-body { padding-top: 1.1rem; }

/* Entries carry a date above the title in the index. */
.project-nav .nav-date {
  display: block;
  font-size: .72rem; letter-spacing: .04em;
  color: var(--ink-faint);
  margin-bottom: .1rem;
}

.project-panel[hidden] { display: none; }
.project-panel > .eyebrow { margin-top: 0; }
.project-detail .shots { margin-top: clamp(2rem, 4vw, 2.75rem); }

/* ==========================================================
   Cut figures
   Diagrams drawn the way the rest of the site is printed:
   flat ink blocks, one ochre, carved hatching where a
   gradient would otherwise be reached for. Every label is
   real HTML rather than SVG <text>, so bilingual mode can
   stack the two languages the way it does everywhere else.
   ========================================================== */
.cut { display: block; width: 100%; height: auto; }
.cut .ink   { fill: var(--ink); }
.cut .ink-2 { fill: var(--ink-soft); }
.cut .ink-3 { fill: color-mix(in srgb, var(--ink) 16%, transparent); }
.cut .och   { fill: var(--ochre); }
.cut .och-w { fill: var(--ochre-wash); }
.cut .paper { fill: var(--paper); }
.cut .carve { stroke: var(--paper); fill: none; stroke-width: 2; stroke-linecap: round; }
.cut .rule  { stroke: var(--rule); fill: none; stroke-width: 1.5; }
.cut .line  { stroke: var(--ink); fill: none; stroke-width: 2; stroke-linecap: round; }
.cut .line-o{ stroke: var(--ochre); fill: none; stroke-width: 2; stroke-linecap: round; }

/* The banner under the page title. Decorative: the title already
   says "splitting heaven and earth", so the drawing repeats it in
   the other language rather than adding a claim of its own. */
.figure-hero {
  margin: clamp(1.75rem, 4vw, 2.5rem) 0 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: clamp(1rem, 2.5vw, 1.6rem) 0;
}

/* ── the loop, as three cut stamps ─────────────────────── */
/* Three moves and a return arrow. Set as an <ol> because the
   order is the whole point, and the arrow says it repeats. */
.cycle {
  list-style: none;
  margin: 1.6rem 0 0; padding: 0;
  display: grid; gap: 1rem;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 620px) { .cycle { grid-template-columns: 1fr; } }
.cycle > li {
  position: relative;
  display: grid; gap: .7rem; justify-items: center; text-align: center;
  padding: 1.2rem .9rem;
  background: color-mix(in srgb, var(--card) 92%, transparent);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.cycle-glyph { width: 74px; }
.cycle-step {
  font-size: .68rem; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--ochre-deep);
}
.cycle-label { font-size: .94rem; color: var(--ink-soft); line-height: 1.55; }
/* The chevron between stamps, and nothing after the last one. */
.cycle > li:not(:last-child)::after {
  content: "";
  position: absolute; z-index: 1;
  right: -.63rem; top: 50%;
  width: .7rem; height: .7rem;
  margin-top: -.35rem;
  border-top: 2px solid var(--ochre);
  border-right: 2px solid var(--ochre);
  transform: rotate(45deg);
  background: var(--paper);
}
@media (max-width: 620px) {
  .cycle > li:not(:last-child)::after {
    right: 50%; top: auto; bottom: -.63rem;
    margin: 0 -.35rem 0 0;
    transform: rotate(135deg);
  }
}
.cycle-return {
  margin: .8rem 0 0;
  font-size: .8rem; letter-spacing: .04em;
  color: var(--ink-faint); text-align: center;
}

/* ── the roster: one person, five hires ────────────────── */
/* The point of the picture is the arrow direction. Judgement
   goes one way only; nothing in the bottom row decides. */
.roster {
  margin: 1.7rem 0 0;
  display: grid; gap: 1.1rem;
  justify-items: center;
}
.roster-tools { width: 100%; }
.roster-you {
  width: min(100%, 20rem);
  display: grid; gap: .55rem; justify-items: center; text-align: center;
  padding: 1.1rem;
  background: var(--ink); color: var(--paper);
  border-radius: var(--radius);
}
.roster-you .roster-glyph { width: 40px; }
.roster-you .roster-glyph .ink { fill: var(--paper); }
.roster-you .roster-name { font-family: var(--font-serif); font-size: 1.08rem; }
.roster-you .roster-role { font-size: .82rem; opacity: .75; }
/* The fan spans the roster, so its five endpoints can sit over the five
   column centres of the row beneath. Its own viewBox carries the vertical
   air, which is why the flow row adds none of its own. */
.roster-flow { display: grid; justify-items: center; width: 100%; }
.roster-fan { width: 100%; }
.roster-stem { display: none; width: 26px; }
@media (max-width: 700px) {
  /* Two columns down here, so a five-way fan would land nowhere. */
  .roster-fan { display: none; }
  .roster-stem { display: block; }
}
.roster-tools {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: .7rem;
  grid-template-columns: repeat(5, minmax(0, 1fr));
}
.roster-tools-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 700px) { .roster-tools { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.roster-tools li {
  display: grid; gap: .4rem; justify-items: center; text-align: center;
  padding: .85rem .5rem;
  background: color-mix(in srgb, var(--card) 92%, transparent);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-size: .82rem; color: var(--ink-soft);
}
.roster-tools .roster-glyph { width: 30px; }
.roster-note {
  margin: .9rem 0 0;
  font-size: .84rem; color: var(--ink-faint); text-align: center;
}

/* ── the climb: four stages on one path ────────────────── */
/* The stage titles live in the list below, so the map carries
   numerals only — the one kind of label that needs no
   translating. Hidden from the reader who is using a screen
   reader, who gets the ordered list instead. */
.climb { margin: 1.5rem 0 0; }
.climb .cut { max-height: 210px; }
/* The map's key: four stops in a row under the four markers, two by two
   on a phone. */
.climb-key {
  list-style: none; margin: .4rem 0 0; padding: 0;
  display: grid; gap: .9rem 1.2rem;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 700px) { .climb-key { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.climb-key li { display: grid; gap: .2rem; align-content: start; font-size: .84rem; color: var(--ink-soft); line-height: 1.45; }
.climb-key b { font-family: var(--font-serif); font-size: 1.02rem; font-weight: 600; color: var(--ink); }

/* ── the evolution gallery ─────────────────────────────── */
/* A strip that scroll-snaps. Without JS it is still a strip you
   can drag; the buttons and dots are the enhancement, not the
   mechanism. */
/* The gallery is a size container so the arrows can be pinned to the
   middle of the screenshot itself: the frame is 14:9 of the gallery's
   width, so half its height is a fixed share of that width, whatever the
   caption below it runs to. */
.evo { margin: 1.6rem 0 0; position: relative; container-type: inline-size; }
.evo-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  gap: 1.2rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
  padding-bottom: .3rem;
}
.evo-track::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) { .evo-track { scroll-behavior: auto; } }
.evo-slide { margin: 0; scroll-snap-align: center; }
.evo-slide .screen-inner { aspect-ratio: 14 / 9; }
.evo-slide .screen-inner img { image-rendering: auto; object-fit: cover; object-position: top center; }
.evo-slide .evo-cap {
  margin-top: .8rem; padding-top: .55rem;
  border-top: 1px solid var(--rule);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem .9rem;
}
.evo-slide .evo-date {
  font-variant-numeric: tabular-nums;
  font-size: .72rem; font-weight: 600; letter-spacing: .1em;
  color: var(--ochre-deep); white-space: nowrap;
}
.evo-slide .evo-name { font-family: var(--font-serif); font-size: 1rem; }
.evo-slide .evo-note { flex: 1 1 14rem; font-size: .84rem; color: var(--ink-faint); line-height: 1.5; }

.evo-controls {
  margin-top: 1rem;
  display: flex; align-items: center; gap: .9rem; flex-wrap: wrap;
}
.evo-btn {
  display: grid; place-content: center;
  width: 2.1rem; height: 2.1rem; padding: 0;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  color: var(--ink);
  cursor: pointer;
  transition: border-color .14s, background .14s;
}
.evo-btn:hover:not([disabled]) { border-color: var(--ink); background: var(--card); }
.evo-btn[disabled] { opacity: .35; cursor: default; }
.evo-btn svg { width: 9px; }
/* Prev and next sit on the left and right edges of the screenshot,
   on paper so they read against a dark screen. */
.evo-btn[data-evo] {
  position: absolute;
  top: calc(50cqw * 9 / 14);
  transform: translateY(-50%);
  z-index: 2;
  width: 2.6rem; height: 2.6rem;
  border-radius: 50%;
  background: var(--paper);
  border-color: var(--ink-faint);
  box-shadow: 0 2px 10px rgb(0 0 0 / .18);
}
.evo-btn[data-evo]:hover:not([disabled]) { background: var(--paper); border-color: var(--ink); }
.evo-btn[data-evo="prev"] { left: .7rem; }
.evo-btn[data-evo="next"] { right: .7rem; }
.evo-btn[data-evo][disabled] { opacity: 0; pointer-events: none; }
.evo-btn[data-evo] svg { width: 10px; }
.evo-dots { display: flex; gap: .45rem; margin: 0; padding: 0; list-style: none; }
.evo-dots button {
  width: 1.5rem; height: 3px; padding: 0;
  border: 0; border-radius: 0;
  background: var(--rule);
  cursor: pointer;
  transition: background .14s;
}
.evo-dots button:hover { background: var(--ink-faint); }
.evo-dots button[aria-current="true"] { background: var(--ochre); }
.evo-count {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font-size: .78rem; color: var(--ink-faint);
}

/* The first version, playable. Taller than a screenshot frame
   because it is the actual page, and a login card needs room. */
.evo-embed { margin: 2.1rem 0 0; }
.evo-embed .screen-inner { aspect-ratio: 4 / 3; }
@media (min-width: 700px) { .evo-embed .screen-inner { aspect-ratio: 16 / 10; } }
.evo-embed .screen-bezel::after { content: none; }

/* ── phone overrides, kept last on purpose ─────────────── */
/* Each rule below targets a selector defined earlier in this file. A media
   query carries no extra specificity, so these only win by being last: an
   identical block written higher up is silently overridden by the desktop
   rule that follows it. Moving any of this upward will quietly undo it. */
@media (max-width: 700px) {
  /* lets the language switch share the wordmark's row instead of taking one
     of its own; the pair missed fitting by 27px */
  .lang-switch button { font-size: .76rem; padding: .3rem .5rem; }

  /* In bilingual mode every hero block is twice the height it is in one
     language, so desktop spacing compounds until the calls to action fall
     off an 812px screen. Phone values only; the desktop hero is untouched. */
  .hero { padding-block: 2rem 2.5rem; }
  .epigraph { margin-bottom: 1.5rem; }
  .hero-actions { margin-top: 1.6rem; }
}

/* ── Nüwa: the page told as you scroll ─────────────────── */
/* wireReveal adds .reveal only when motion is welcome, so a reader
   without script, or who has asked for less motion, gets the page whole. */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .7s ease, transform .7s cubic-bezier(.2, .7, .2, 1);
  transition-delay: var(--reveal-delay, 0s);
}
.reveal.is-in { opacity: 1; transform: none; }

/* The first message, typed out: a caret while it is still being written. */
pre.is-typing::after {
  content: "▍";
  color: var(--ochre);
  animation: caret 1s steps(1) infinite;
}
@keyframes caret { 50% { opacity: 0; } }

/* The banner on arrival: the tear opens, then five stones are set into it
   one at a time, which is the title drawn as it happens. */
#nuwa > .wrap > .eyebrow,
#nuwa > .wrap > .page-title { animation: rise .8s cubic-bezier(.2, .7, .2, 1) both; }
#nuwa > .wrap > .page-title { animation-delay: .12s; }
#nuwa .figure-hero .tear { animation: open-tear 1s ease .35s both; }
#nuwa .figure-hero .stone {
  transform-box: fill-box;
  transform-origin: center;
  animation: set-stone .75s cubic-bezier(.2, .8, .25, 1.15) var(--d, 1s) both;
}
@keyframes rise { from { opacity: 0; transform: translateY(14px); } }
@keyframes open-tear { from { opacity: 0; } }
@keyframes set-stone {
  from { opacity: 0; transform: translateY(-46px) rotate(-14deg) scale(.85); }
}
@media (prefers-reduced-motion: reduce) {
  #nuwa > .wrap > .eyebrow, #nuwa > .wrap > .page-title,
  #nuwa .figure-hero .tear, #nuwa .figure-hero .stone { animation: none; }
  pre.is-typing::after { animation: none; }
}

/* ── Nüwa: spotting the gap, told with the platform's own art ── */
.sky { margin: 1.6rem 0 0; }
.sky-legend { list-style: none; margin: .8rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: .4rem 1.2rem; font-size: .84rem; color: var(--ink-soft); }
.sky-legend li { display: flex; align-items: center; gap: .45rem; }
.sky-legend i { display: inline-block; width: 1.4rem; height: 3px; border-radius: 2px; }
.sky-legend i.dashed { background: repeating-linear-gradient(90deg, currentColor 0 6px, transparent 6px 11px); }
.sky-chart { width: 100%; height: auto; margin-top: .4rem; overflow: visible; }
.sky-grid { stroke: var(--rule); stroke-width: 1; }
.sky-tick { font-size: 11px; fill: var(--ink-faint); font-family: var(--font-sans); text-anchor: end; }
.sky-tick[text-anchor="middle"] { text-anchor: middle; }
.sky-end { font-size: 12px; font-weight: 600; fill: var(--ink); font-family: var(--font-sans); }
.sky-chart circle { cursor: default; }
.sky figcaption { display: grid; gap: .5rem; margin-top: .4rem; }
.sky-take { margin: 0; font-size: .95rem; color: var(--ink); }
.sky-data summary { cursor: pointer; font-size: .82rem; color: var(--ink-faint); }
.sky-data table { margin-top: .5rem; border-collapse: collapse; font-size: .82rem; font-variant-numeric: tabular-nums; }
.sky-data th, .sky-data td { padding: .25rem .8rem .25rem 0; text-align: left; border-bottom: 1px solid var(--rule); }

.gap-trio, .arc-trio {
  list-style: none; margin: 1.4rem 0 0; padding: 0;
  display: grid; gap: .9rem; grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 700px) { .gap-trio, .arc-trio { grid-template-columns: 1fr; } }
.gap-trio li {
  padding: 1rem 1.1rem; background: var(--card); border: 1px solid var(--rule);
  border-top: 3px solid var(--ochre); border-radius: var(--radius);
}
.gap-tag { font-size: .74rem; font-weight: 600; letter-spacing: .12em; color: var(--ochre-deep); }
.gap-trio .need-voice { margin: .45rem 0 0; font-family: var(--font-serif); font-size: 1.05rem; line-height: 1.5; color: var(--ink); }
.arc-title { margin: 2.2rem 0 0; font-family: var(--font-serif); font-size: 1.25rem; font-weight: 600; color: var(--ink); }
.arc-card {
  display: grid; gap: .35rem; align-content: start; padding: 1.1rem;
  background: var(--ink); color: var(--paper); border-radius: var(--radius);
}
.arc-card .need-name { margin: 0; font-size: .74rem; letter-spacing: .14em; font-weight: 600; opacity: .8; }
.arc-line { margin: 0; font-family: var(--font-serif); font-size: 1.12rem; line-height: 1.45; }
.arc-q { margin: .3rem 0 0; font-size: .84rem; line-height: 1.5; opacity: .85; }
.arc-letter {
  display: inline-grid; place-content: center; width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: var(--ochre); color: var(--paper); font-family: var(--font-serif); font-size: 1.15rem; font-weight: 600;
}
.arc-turn { margin: 2.4rem 0 0; font-family: var(--font-serif); font-size: 1.2rem; color: var(--ink); }
.arc-fill { margin: 1.2rem 0 0; padding: 1.2rem 1.2rem 1.4rem; background: var(--card); border: 1px solid var(--rule); border-radius: var(--radius); }
.arc-fill-head { display: flex; align-items: center; gap: .7rem; margin: 0; font-family: var(--font-serif); font-size: 1.1rem; font-weight: 600; color: var(--ink); }
.arc-note { margin: .8rem 0 0; color: var(--ink-soft); }
.arc-modes { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: .7rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 560px) { .arc-modes { grid-template-columns: 1fr; } }
.arc-modes li { display: grid; gap: .2rem; padding: .8rem .9rem; background: var(--paper); border-radius: var(--radius); font-size: .86rem; color: var(--ink-soft); }
.arc-modes b { font-family: var(--font-serif); font-size: 1rem; color: var(--ink); }
.arc-board { max-width: 100%; margin: 1rem 0 0; }

/* A row of the platform's own art, captioned underneath. */
.sprites { margin: 1.1rem 0 0; }
.sprites ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem .6rem; }
.sprites li { display: grid; justify-items: center; gap: .25rem; width: 4.4rem; text-align: center; font-size: .72rem; line-height: 1.35; color: var(--ink-soft); }
.sprites img { width: 3.6rem; height: 3.6rem; object-fit: contain; }
.sprites.pixel img { image-rendering: pixelated; }
.sprites.big li { width: 6rem; }
.sprites.big img { width: 5.4rem; height: 5.4rem; }
.sprites figcaption { margin-top: .5rem; font-size: .86rem; color: var(--ink-soft); }
/* Light on dark: the English half of each line keeps the card's paper
   colour instead of the page's faint grey, which vanishes on ink. */
.arc-card .bi-en { color: var(--paper); opacity: .78; }
/* SingStat's live dashboard under the chart. Power BI lays itself out at
   roughly 16:10, so the frame keeps that shape at every width. */
.sky-live { margin: 1.6rem 0 0; }
.sky-frame { margin-top: .7rem; aspect-ratio: 16 / 10.5; border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; background: var(--card); }
.sky-frame iframe { width: 100%; height: 100%; border: 0; display: block; }
/* Screens inside the 卡住了 box: two side by side, one per row on a phone. */
.stuck-shots { display: grid; gap: .8rem; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: .7rem 0 .4rem; }
@media (max-width: 560px) { .stuck-shots { grid-template-columns: 1fr; } }
.stuck-shots .shot { margin: 0; }
.stuck-shots .screen-inner { aspect-ratio: auto; background: var(--paper); }
.stuck-shots .screen-inner img { height: auto; object-fit: contain; }
.stuck-shots figcaption { font-size: .78rem; }
.stuck-more { margin-top: .4rem; }
/* The tech FAQ: each question set apart from its answer. */
.faq .faq-q { display: block; margin-bottom: .2rem; font-weight: 600; color: var(--ink); }

/* ── Nüwa: the sky, mended, section by section ─────────── */
.cut .och-deep { fill: var(--ochre-deep); }
.mend { display: block; width: 15rem; max-width: 100%; height: auto; margin: 0 0 .6rem; overflow: visible; }
.mend-crack { fill: none; stroke: var(--ink-faint); stroke-width: 1.4; stroke-linejoin: round; opacity: .55; }
.mend-stone { transform-box: fill-box; transform-origin: center; }
/* Hidden only once wireReveal has marked it, so without script or with
   less motion asked for, the stone simply sits in the crack. */
.mend.reveal .mend-stone { opacity: 0; transform: translateY(-16px) rotate(-18deg); }
.mend.reveal.is-in .mend-stone { animation: mend-drop .75s cubic-bezier(.2, .8, .25, 1.15) .2s both; }
.mend.reveal.is-in .mend-crack { animation: mend-glow 1.4s ease .55s both; }
@keyframes mend-drop {
  from { opacity: 0; transform: translateY(-16px) rotate(-18deg); }
  to   { opacity: 1; transform: none; }
}
@keyframes mend-glow { 40% { stroke: var(--ochre); opacity: 1; } }

.mended { margin: 2.6rem 0 0; }
.mended .rays path { stroke-width: 2; fill: none; stroke-dasharray: 1; stroke-dashoffset: 0; }
.mended.reveal .rays path { stroke-dashoffset: 1; }
.mended.reveal.is-in .rays path { animation: ray-draw 1.1s ease var(--d, 1s) forwards; }
.mended .stone-set { transform-box: fill-box; transform-origin: center; }
.mended.reveal .stone-set { opacity: 0; }
.mended.reveal.is-in .stone-set { animation: stone-in .7s cubic-bezier(.2, .8, .25, 1.15) var(--d, 0s) both; }
@keyframes stone-in {
  from { opacity: 0; transform: translateY(-46px) rotate(-14deg) scale(.85); }
  to   { opacity: 1; transform: none; }
}
@keyframes ray-draw { to { stroke-dashoffset: 0; } }

/* The two firings, each bar as wide as its commit count. */
.evo-phases { display: flex; gap: .5rem; margin: 1.4rem 0 0; padding: 0; list-style: none; }
.evo-phases li { flex: var(--n) 1 0; min-width: 0; }
.evo-phase-bar { display: block; height: .55rem; margin-bottom: .55rem; border-radius: 0 4px 4px 0; background: var(--ink); transform-origin: left; }
.evo-phases li + li .evo-phase-bar { background: var(--ochre); }
.evo-phases b { display: block; font-size: .82rem; font-weight: 600; color: var(--ink); }
.evo-phases .value, .evo-total .value {
  display: block; margin-top: .15rem;
  font-family: var(--font-serif); font-size: clamp(1.35rem, 3vw, 1.7rem);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.evo-phases .label, .evo-total .label { display: block; font-size: .76rem; letter-spacing: .04em; color: var(--ink-faint); }
.evo-total { margin: .9rem 0 1.4rem; padding-top: .7rem; border-top: 1px solid var(--rule); }
.evo-phases.reveal .evo-phase-bar { transform: scaleX(0); }
.evo-phases.reveal.is-in .evo-phase-bar { animation: phase-grow .9s ease .2s forwards; }
.evo-phases.reveal.is-in li + li .evo-phase-bar { animation-delay: .9s; }
@keyframes phase-grow { to { transform: scaleX(1); } }

/* The ending: the next crack is already open, and the next stone rising. */
.mended .stone-next { transform-box: fill-box; transform-origin: center; }
.mended.reveal.is-in .stone-next { animation: stone-rise 3.2s ease-in-out 2.2s infinite alternate; }
@keyframes stone-rise { to { transform: translateY(-34px) rotate(10deg); } }
.mend-on { max-width: 34rem; margin: 1.6rem 0 0; font-size: .95rem; line-height: 1.8; color: var(--ink-soft); }
@media (prefers-reduced-motion: reduce) {
  .evo-phases.reveal .evo-phase-bar { transform: none; animation: none; }
  .mended .stone-next { animation: none; }
}

/* More than one hole: the sky torn in four places, each tear numbered
   to its card. The tears open one by one as the strip arrives. */
.holes-sky { display: block; width: 100%; height: auto; margin: 1rem 0 0; }
.holes-sky .hole { transform-box: fill-box; transform-origin: top center; }
.holes-sky.reveal .hole { transform: scaleY(0); }
.holes-sky.reveal.is-in .hole { animation: hole-open .6s cubic-bezier(.3, .9, .3, 1.2) var(--d, 0s) forwards; }
@keyframes hole-open { to { transform: scaleY(1); } }
.hole-n { font-family: var(--font-serif); font-size: 42px; font-weight: 600; fill: var(--ochre-deep); }
.holes {
  list-style: none; margin: .4rem 0 0; padding: 0;
  display: grid; gap: .9rem; grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 700px) { .holes { grid-template-columns: 1fr; } }
.holes li {
  position: relative; padding: 1rem 1.1rem 1rem 3rem;
  background: var(--card); border: 1px solid var(--rule); border-radius: var(--radius);
}
.hole-tag {
  position: absolute; left: 1rem; top: .95rem;
  font-family: var(--font-serif); font-size: 1.3rem; font-weight: 600; line-height: 1; color: var(--ochre-deep);
}
.hole-title { margin: 0; font-family: var(--font-serif); font-size: 1.05rem; line-height: 1.45; color: var(--ink); }
.hole-note { margin: .35rem 0 0; font-size: .88rem; line-height: 1.65; color: var(--ink-soft); }
@media (prefers-reduced-motion: reduce) { .holes-sky.reveal .hole { transform: none; animation: none; } }

/* Stone for hole: each row pairs a pain point (muted, left) with what
   the platform does about it (right), joined by the stone that fills it. */
.mend-map { list-style: none; margin: 1.4rem 0 0; padding: 0; display: grid; gap: .8rem; }
.mend-map li {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; background: var(--card);
}
.mend-hole {
  display: flex; gap: .7rem; align-items: baseline; padding: 1rem 1.1rem;
  background: var(--ochre-wash); border-right: 1px dashed var(--rule);
}
.mend-hole p { margin: 0; font-size: .9rem; line-height: 1.5; color: var(--ink-soft); }
.mend-tag { font-family: var(--font-serif); font-size: 1.2rem; font-weight: 600; line-height: 1; color: var(--ochre-deep); }
.mend-fix { position: relative; padding: 1rem 1.1rem 1rem 3.3rem; }
.mend-glyph { position: absolute; left: 1rem; top: 1rem; width: 1.7rem; height: auto; overflow: visible; }
.mend-glyph path { transform-box: fill-box; transform-origin: center; }
.mend-fix-title { margin: 0; font-family: var(--font-serif); font-size: 1.05rem; line-height: 1.45; color: var(--ink); }
.mend-fix-note { margin: .35rem 0 0; font-size: .88rem; line-height: 1.65; color: var(--ink-soft); }
.mend-map li.reveal.is-in .mend-glyph path { animation: mend-drop .7s cubic-bezier(.2, .8, .25, 1.15) calc(var(--reveal-delay, 0s) + .35s) both; }
@media (max-width: 700px) {
  .mend-map li { grid-template-columns: 1fr; }
  .mend-hole { border-right: 0; border-bottom: 1px dashed var(--rule); }
}

/* Recent research, each card dated first so the year is never in doubt. */
.research { list-style: none; margin: 1.2rem 0 0; padding: 0; display: grid; gap: .8rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 700px) { .research { grid-template-columns: 1fr; } }
.research li { display: grid; gap: .45rem; align-content: start; padding: 1rem 1.1rem; background: var(--card); border: 1px solid var(--rule); border-left: 3px solid var(--ink); border-radius: var(--radius); }
.res-date { font-family: var(--font-serif); font-size: 1.1rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--ochre-deep); }
.res-find { margin: 0; font-size: .92rem; line-height: 1.65; color: var(--ink); }
.research .tool-link { font-size: .8rem; }

/* Nüwa hands-on: the annotated screenshots at the full column width, one
   to a row, so every red ring is readable on a projector and a laptop. */
#nuwa #hands-on .step-shot { max-width: none; margin: .9rem 0 .4rem; }
#nuwa #hands-on .stuck-shots { grid-template-columns: 1fr; }
#nuwa #hands-on .step-shot figcaption, #nuwa #hands-on .stuck-shots figcaption { font-size: .82rem; }
.research li { min-width: 0; }
.research .tool-link { white-space: normal; overflow-wrap: anywhere; }
#nuwa #hands-on a.shot-full { display: block; cursor: zoom-in; }
