/* ==========================================================================
   AndyJingLiu — editorial stylesheet
   Fonts are self-hosted because the app sends `font-src 'self'` in its CSP.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts
   -------------------------------------------------------------------------- */

/* Source Serif 4 — display + long-form reading (Latin) */
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("fonts/source-serif-4-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Source Serif 4";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("fonts/source-serif-4-latin-wght-italic.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("fonts/source-serif-4-latin-ext-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Inter — interface, labels, metadata (Latin) */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/inter-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --------------------------------------------------------------------------
   2. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Type stacks.
     Chinese is progressive: we name only high-quality screen faces, so a
     machine without them falls through to its system sans rather than to
     SimSun, which is unreadable at display sizes. */
  --han-sans: "PingFang SC", "HarmonyOS Sans SC", "Noto Sans CJK SC",
    "Source Han Sans SC", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
  --han-serif: "Source Han Serif SC", "Noto Serif CJK SC", "Noto Serif SC",
    "Songti SC", var(--han-sans);

  --font-display: "Source Serif 4", var(--han-serif), Georgia, "Times New Roman",
    serif;
  --font-body: "Source Serif 4", Georgia, serif;
  --font-ui: "Inter", var(--han-sans), -apple-system, BlinkMacSystemFont,
    "Segoe UI", sans-serif;
  /* Chinese running text pairs Han sans with a Latin sans; putting a Latin
     serif next to sans Han glyphs reads as a mismatch mid-sentence. */
  --font-body-zh: "Inter", var(--han-sans), sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    monospace;

  /* Ink & paper — "酒红". Laid paper that leans warm, a warm blue-free black
     for ink, and exactly one colour: a deep burgundy. Links, the seal, and the
     accent stroke on generated covers are all the same hue — one colour is the
     limit, and the restraint is the point. The whole ramp is warm-biased — no
     neutral or cool grey appears anywhere, which is what keeps it reading as
     printed matter rather than as an interface. */
  --paper: #f6f2ec;
  --paper-raised: #fcfaf6;
  --paper-sunk: #eae3d9;
  --ink: #1c1714;
  /* Running text sits a touch lighter than headings — pure --ink over a long
     column reads as harsh. */
  --ink-body: #2b2521;
  /* Grey ramp is spaced so all three levels stay distinguishable AND clear
     WCAG AA (4.5:1) against --paper — metadata and the section numbers are
     real text, not decoration. Ratios: 7.5 / 5.8 / 4.9. */
  --ink-2: #554c46;
  --ink-3: #675d56;
  --ink-4: #726860;
  --accent: #6b2233;
  --accent-wash: rgba(107, 34, 51, 0.15);
  /* The seal takes the accent rather than a second colour: one hue for the
     whole site. Kept as its own token so it can be pulled back out to a
     contrasting gold (#8c6a3f) by changing this line alone. White on it
     is 11:1, which is what lets the mark hold at 20px. */
  --seal: #6b2233;
  --seal-ink: #ffffff;
  --rule: rgba(28, 23, 20, 0.15);
  /* Pulled well away from --accent in both hue and value; a burgundy site
     cannot afford an error state that reads as decoration. */
  --danger: #8e2b20;
  --success: #2e6b48;
  --code-bg: #1c1714;
  --code-ink: #f6f2ec;
  /* Portraits are treated, not tinted: full monochrome so a casual photograph
     reads as an intentional author portrait rather than a snapshot. */
  --portrait-filter: grayscale(1) contrast(1.06) brightness(1.02);

  /* The seal is one physical object at one physical size, so it is set here
     and never per-context. 20px: 18 went weak standing on its own under the
     portrait, 24 shouted over the 1.2rem wordmark it sits beside. */
  --seal-size: 1.25rem;

  /* Rhythm. One spacing scale, used everywhere. */
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;
  --space-9: 6rem;
  --space-10: 8rem;

  /* Type scale — a ~1.25 ratio, fluid at the display end. */
  --step--2: 0.75rem;
  --step--1: 0.875rem;
  --step-0: 1rem;
  --step-1: 1.125rem;
  --step-2: clamp(1.25rem, 0.4vw + 1.15rem, 1.4rem);
  --step-3: clamp(1.5rem, 0.8vw + 1.3rem, 1.85rem);
  --step-4: clamp(1.85rem, 1.4vw + 1.5rem, 2.5rem);
  --step-5: clamp(2.3rem, 2.6vw + 1.7rem, 3.5rem);
  --step-6: clamp(2.9rem, 4.4vw + 1.8rem, 5rem);

  /* Measure — the single most important number in an editorial layout. */
  --measure: 34em;
  --measure-wide: 42em;

  --shell: 1180px;
  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  /* Left margin column holding section numbers. Every numbered section
     hangs off this one value, so their headings share a vertical edge.
     "01" renders ~16px wide, so this leaves ~24px between number and title. */
  --rail: 2.5rem;

  --radius: 3px;
  color-scheme: light dark;
}

/* --------------------------------------------------------------------------
   2b. Dark mode
   Only the ink/paper tokens flip. Every component reads through them, so
   nothing below this block needs a dark-mode variant. The accent lightens
   because #1c3f5f on a dark ground has almost no contrast, and the seal warms
   for the same reason.
   -------------------------------------------------------------------------- */

@media (prefers-color-scheme: dark) {
  :root {
    --paper: #171310;
    --paper-raised: #1f1a16;
    --paper-sunk: #282119;
    --ink: #efe9e0;
    --ink-body: #d8d1c7;
    /* Ratios against --paper: 8.3 / 6.3 / 4.9 — same three-step separation as
       the light ramp, measured in the other direction. */
    --ink-2: #b5ada2;
    --ink-3: #9e958a;
    --ink-4: #8b8278;
    /* Burgundy cannot survive on a dark ground — it goes to mud. The accent
       lifts to the same hue at porcelain value instead. */
    --accent: #d9a0a8;
    --accent-wash: rgba(217, 160, 168, 0.2);
    /* Follows the accent here too — a pressed seal in reverse, dark glyph on
       a light ground. 8.4:1. */
    --seal: #d9a0a8;
    --seal-ink: #171310;
    --rule: rgba(239, 233, 224, 0.16);
    --danger: #e0917f;
    --success: #7bc49a;
    --code-bg: #100d0b;
    --code-ink: #d8d1c7;
    /* Photographs need to sit back a little on a dark ground, or they glow. */
    --portrait-filter: grayscale(1) contrast(1.02) brightness(0.9);
  }

  /* Images and embeds carry their own light; take the edge off. */
  .featured-image:not(.cover-generated) img,
  .article-card > a:not(.cover-generated) .thumb,
  .article-row-image:not(.cover-generated) img,
  .article-hero:not(.cover-generated) img,
  .video-thumb,
  .article-body img {
    filter: brightness(0.9) contrast(1.02);
  }
}

/* Alternate palette — "森绿".
   To try it, add data-palette="forest" to the <html> tag in base.html:
       <html lang="{{ locale }}" data-palette="forest">
   Remove the attribute to go back to 酒红. Nothing else needs to change.
   Scoped to light mode so it cannot outrank the dark-mode block above. */
@media (prefers-color-scheme: light) {
  :root[data-palette="forest"] {
    --paper: #f1f4ef;
    --paper-raised: #fbfcfa;
    --paper-sunk: #e3e8e1;
    --ink: #161a17;
    --ink-body: #262c28;
    --ink-2: #4a504b;
    --ink-3: #5c635e;
    --ink-4: #666f68;
    --accent: #2d5040;
    --accent-wash: rgba(45, 80, 64, 0.15);
    --rule: rgba(22, 26, 23, 0.15);
    --code-bg: #161a17;
    --code-ink: #f1f4ef;
  }
}

/* --------------------------------------------------------------------------
   3. Base
   -------------------------------------------------------------------------- */

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

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

body {
  margin: 0;
  overflow-x: hidden;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 17px;
  font-weight: 400;
  line-height: 1.7;
  font-optical-sizing: auto;
  font-variant-numeric: oldstyle-nums;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Chinese needs a slightly larger body size, looser leading, and no
   old-style figures (they do not harmonise with Han glyphs). */
html[lang="zh"] body {
  font-family: var(--font-body-zh);
  font-size: 16.5px;
  line-height: 1.9;
  font-variant-numeric: normal;
  letter-spacing: 0.01em;
}

img,
iframe,
video {
  max-width: 100%;
}

img {
  display: block;
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
  transition: color 160ms ease;
}

a:hover {
  color: var(--accent);
}

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

h1,
h2,
h3,
h4 {
  margin-top: 0;
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.014em;
  text-wrap: balance;
}

/* Chinese headings are sans, not serif — and this is deliberate.
   var(--han-serif) leads with Source Han Serif / Noto Serif SC, which almost
   no reader actually has installed; in practice the stack fell through to
   Songti SC, whose hairline horizontals disintegrate on screen and whose
   letterforms do not sit well beside Source Serif 4's Latin. PingFang and
   HarmonyOS Sans, by contrast, ship on the machines that matter and hold up at
   display size. Han faces have no true light/regular contrast, so headings
   need extra weight and much looser leading than Latin. */
html[lang="zh"] h1,
html[lang="zh"] h2,
html[lang="zh"] h3,
html[lang="zh"] h4 {
  font-family: "Inter", var(--han-sans), sans-serif;
  font-weight: 600;
  line-height: 1.34;
  letter-spacing: 0;
}

p {
  margin-top: 0;
  margin-bottom: var(--space-4);
}

hr {
  height: 0;
  margin: var(--space-7) 0;
  border: 0;
  border-top: 1px solid var(--rule);
}

::selection {
  background: var(--accent-wash);
}

/* --------------------------------------------------------------------------
   4. Shared type utilities
   -------------------------------------------------------------------------- */

/* Small uppercase label — the workhorse of editorial hierarchy. */
.eyebrow,
.section-kicker,
.article-meta,
.section-index,
.article-number,
.portrait-caption {
  margin-bottom: var(--space-3);
  color: var(--ink-3);
  font-family: var(--font-ui);
  font-size: var(--step--2);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.11em;
  line-height: 1.4;
  text-transform: uppercase;
}

html[lang="zh"] .eyebrow,
html[lang="zh"] .section-kicker,
html[lang="zh"] .article-meta {
  letter-spacing: 0.08em;
}

.eyebrow,
.section-kicker {
  color: var(--accent);
}

.section-index,
.article-number {
  margin: 0;
  color: var(--ink-4);
  font-feature-settings: "tnum";
}

.article-meta {
  margin-bottom: var(--space-2);
  color: var(--ink-4);
}

/* The seal — 名章.
   A person owns one seal, cut once, at one size. So this is sized in absolute
   units and anchors its own font-size: every instance is the same object no
   matter what type it happens to sit next to. Sizing it in `em` is what let
   the three on the homepage drift apart — the stamp inherited the caption's
   0.75rem and the glyph shrank to a third of its square while the box stayed
   put. Nothing below may be overridden per-context. */
.seal {
  display: inline-grid;
  width: var(--seal-size);
  height: var(--seal-size);
  margin-left: 0.28em;
  place-items: center;
  border-radius: 2px;
  background: var(--seal);
  color: var(--seal-ink);
  /* The anchor. Descendant `em` now resolves against the seal, not the page. */
  font-size: var(--seal-size);
  /* Set slightly off square, the way a hand-pressed seal lands. */
  transform: rotate(-3.5deg) translateY(0.04em);
  vertical-align: baseline;
}

.seal-glyph {
  font-family: var(--han-serif);
  font-size: 0.68em;
  font-weight: 500;
  line-height: 1;
  /* Han glyphs carry their own sidebearings; without this the character sits
     visually low and left inside the square. */
  transform: translateY(0.02em);
}

/* The only permitted per-context change: standing alone under the portrait
   there is no wordmark to sit against, so the optical gap is dropped. */
.portrait-caption .seal {
  margin-left: 0;
}

/* Inline "read more" link with a rule that grows on hover. */
.text-link {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4em;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.01em;
  transition:
    color 160ms ease,
    border-color 160ms ease;
}

.text-link:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.text-link span,
.btn span {
  transition: transform 160ms ease;
}

.text-link:hover span,
.btn:hover span {
  transform: translateX(3px);
}

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  padding: 0.6rem 1.15rem;
  background: var(--ink);
  color: var(--paper);
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.01em;
  transition:
    background 160ms ease,
    border-color 160ms ease,
    color 160ms ease;
}

.btn:hover {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}

.btn:disabled {
  border-color: var(--ink-4);
  background: var(--ink-4);
  cursor: not-allowed;
}

.btn-secondary {
  border-color: var(--rule);
  background: transparent;
  color: var(--ink);
}

.btn-secondary:hover {
  border-color: var(--ink);
  background: transparent;
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   6. Shell — header, container, footer
   -------------------------------------------------------------------------- */

.container {
  width: min(var(--shell), 100% - var(--gutter) * 2);
  min-height: calc(100vh - 260px);
  margin: 0 auto;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  backdrop-filter: blur(16px) saturate(150%);
}

@supports (background: color-mix(in srgb, red 50%, transparent)) {
  .site-header {
    background: color-mix(in srgb, var(--paper) 86%, transparent);
  }
}

.site-nav {
  display: flex;
  width: min(var(--shell), 100% - var(--gutter) * 2);
  min-height: 66px;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  margin: 0 auto;
}

.site-brand,
.footer-brand {
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 500;
  letter-spacing: -0.015em;
  white-space: nowrap;
}

.site-brand:hover,
.footer-brand:hover {
  color: var(--ink);
}

.site-nav-links {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 1.6rem);
}

.site-nav-links a,
.nav-link-button {
  position: relative;
  color: var(--ink-2);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 450;
  letter-spacing: 0.005em;
}

.site-nav-links a::after {
  position: absolute;
  right: 0;
  bottom: -0.4rem;
  left: 0;
  height: 1px;
  background: currentColor;
  content: "";
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 180ms ease;
}

.site-nav-links a:hover,
.nav-link-button:hover {
  color: var(--accent);
}

.site-nav-links a:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

.nav-social {
  color: var(--ink-3);
}

.language-switch {
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  font-variant-numeric: tabular-nums;
}

.language-switch::after {
  display: none;
}

.nav-logout-form,
.inline-form {
  display: inline;
  margin: 0;
}

.nav-link-button,
.delete-link-button {
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  font: inherit;
}

.site-footer {
  width: min(var(--shell), 100% - var(--gutter) * 2);
  margin: 0 auto;
  padding: var(--space-8) 0 var(--space-6);
  border-top: 1px solid var(--rule);
}

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

.footer-identity {
  text-align: left;
}

.footer-inner p {
  max-width: 32ch;
  margin: var(--space-2) 0 0;
  color: var(--ink-3);
  font-size: var(--step--1);
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 450;
}

.footer-links a {
  color: var(--ink-2);
}

.copyright {
  margin: var(--space-7) 0 0;
  color: var(--ink-4);
  font-family: var(--font-ui);
  font-size: var(--step--2);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  text-align: center;
}

/* --------------------------------------------------------------------------
   7. Page frames
   -------------------------------------------------------------------------- */

.home-page,
.editorial-page,
.about-page {
  padding: clamp(2rem, 5vw, 4rem) 0 var(--space-10);
}

.content-section {
  padding: var(--space-9) 0 0;
  margin-top: var(--space-9);
  border-top: 1px solid var(--rule);
}

/* A heading row: number in the rail, title, and a trailing "see all" link.
   Baseline alignment (not box-top) is what makes the number sit on the same
   line as a heading many times its size. */
.section-heading-row {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 0;
  margin-bottom: var(--space-7);
}

.section-heading-row > .text-link {
  margin-left: var(--space-6);
}

.section-heading-row h2 {
  margin: 0;
  font-size: var(--step-4);
}

/* --------------------------------------------------------------------------
   8. Home — masthead
   -------------------------------------------------------------------------- */

/* The copy is centred against the portrait rather than sitting on its
   baseline. Bottom alignment is the tidier idea on paper — both columns meet
   the rule — but the portrait is a 4:5 block and the copy is barely half its
   height, so in practice it left a large hole above the headline and pushed
   the name most of the way down the screen. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 0.44fr);
  align-items: center;
  gap: clamp(2rem, 5vw, 4.5rem);
  padding-bottom: var(--space-8);
  border-bottom: 1px solid var(--ink);
}

.hero h1 {
  margin: 0 0 var(--space-5);
  font-size: var(--step-6);
  font-weight: 400;
  line-height: 0.98;
  letter-spacing: -0.028em;
}

html[lang="zh"] .hero h1 {
  font-size: clamp(2.6rem, 4vw + 1.4rem, 4.2rem);
  font-weight: 600;
  line-height: 1.22;
  /* Han glyphs are set on a fixed em square, so at display size the built-in
     sidebearings read as gaps. A little negative tracking closes them. */
  letter-spacing: -0.02em;
}

.hero-subtitle {
  max-width: var(--measure);
  margin-bottom: var(--space-6);
  color: var(--ink-2);
  font-size: var(--step-2);
  line-height: 1.62;
}

html[lang="zh"] .hero-subtitle {
  line-height: 1.85;
}

.social-links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.hero-visual {
  position: relative;
}

.portrait-frame {
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--paper-sunk);
}

.hero-img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 34%;
  /* Fully monochrome, and it stays monochrome. A partial desaturation reads
     as a dirty photograph rather than a decision, and revealing the colour on
     hover is a trick, not a treatment. */
  filter: var(--portrait-filter);
}

.portrait-caption {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.6em;
  margin: var(--space-3) 0 0;
  color: var(--ink-4);
  text-align: right;
}

/* --------------------------------------------------------------------------
   9. Home — about preview
   -------------------------------------------------------------------------- */

.about-preview {
  margin-top: var(--space-9);
  padding-top: var(--space-9);
  border-top: 1px solid var(--rule);
}

/* Flush to the section's left edge, matching the article and video blocks in
   sections 02 and 03. The number hangs alone in the margin. */
.about-preview-body {
  margin-left: 0;
}

.about-lead {
  max-width: var(--measure-wide);
  margin-bottom: var(--space-5);
  color: var(--ink-2);
  font-size: var(--step-2);
  line-height: 1.68;
}

html[lang="zh"] .about-lead {
  line-height: 1.95;
}

/* --------------------------------------------------------------------------
   10. Home — featured article + article grid
   -------------------------------------------------------------------------- */

.featured-article {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
  padding-bottom: var(--space-8);
  border-bottom: 1px solid var(--rule);
}

.featured-image {
  display: block;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--paper-sunk);
}

.featured-image img {
  width: 100%;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.featured-image:hover img {
  transform: scale(1.02);
}

.featured-copy {
  max-width: var(--measure);
}

.featured-copy h3 {
  margin: 0 0 var(--space-4);
  font-size: var(--step-5);
  line-height: 1.06;
}

html[lang="zh"] .featured-copy h3 {
  line-height: 1.3;
}

.featured-copy > p:not(.eyebrow):not(.article-meta) {
  margin-bottom: var(--space-5);
  color: var(--ink-2);
}

/* Borderless cards separated by hairlines rather than boxes. */
.article-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-7) clamp(1.5rem, 3vw, 2.5rem);
  padding-top: var(--space-7);
}

/* A three-column grid holding one or two cards leaves visible empty cells,
   which reads as missing content rather than as space. Below three, the
   columns widen to fill the row instead. */
.article-grid-sparse {
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
}

/* And a single remaining card turned full width would out-shout the featured
   article above it, so it lies down into a row. */
.article-grid-sparse:has(> :only-child) .article-card {
  display: grid;
  align-items: center;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(1.5rem, 4vw, 3rem);
}

.article-grid-sparse:has(> :only-child) .card-copy {
  padding-top: 0;
}

.article-card {
  overflow: hidden;
}

.article-card .thumb,
.article-row-image img {
  width: 100%;
  aspect-ratio: 5 / 4;
  border-radius: var(--radius);
  object-fit: cover;
  background: var(--paper-sunk);
  transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.article-card > a,
.article-row-image {
  display: block;
  overflow: hidden;
  border-radius: var(--radius);
}

.article-card:hover .thumb,
.article-row-image:hover img {
  transform: scale(1.02);
}

/* Generated covers.
   These are transparent line drawings, not photographs: the panel behind them
   is the page's own sunk paper, so they follow light and dark mode with no
   second artwork. Each composition runs off all four edges by design, so the
   cover reads as a crop of something larger. They do not scale on hover —
   magnifying vector rules thickens them and the effect looks like an error. */
.cover-generated {
  background: var(--paper-sunk);
}

.cover-generated img {
  background: transparent;
}

.cover-generated:hover img {
  transform: none;
}

.card-copy {
  padding-top: var(--space-4);
}

.article-card h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--step-3);
  line-height: 1.16;
}

html[lang="zh"] .article-card h3 {
  line-height: 1.4;
}

.article-card p:not(.article-meta) {
  margin: 0;
  color: var(--ink-3);
  font-size: var(--step-0);
  line-height: 1.6;
}

/* --------------------------------------------------------------------------
   11. Videos
   -------------------------------------------------------------------------- */

.home-video-grid,
.video-list-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-7) clamp(1.5rem, 3vw, 2.5rem);
}

.video-wrapper {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  background: #111;
  cursor: pointer;
}

.video-thumb,
.video-iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}

.video-thumb {
  transition: transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.video-card:hover .video-thumb,
.home-video-card:hover .video-thumb {
  transform: scale(1.02);
}

.video-iframe {
  z-index: 3;
  opacity: 0;
  transition: opacity 200ms ease;
}

.video-iframe.is-ready {
  opacity: 1;
}

.video-play-button {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  display: grid;
  width: 56px;
  height: 56px;
  transform: translate(-50%, -50%);
  place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 50%;
  background: rgba(20, 20, 22, 0.5);
  color: #fff;
  cursor: pointer;
  font-size: 1rem;
  backdrop-filter: blur(8px);
  transition:
    background 180ms ease,
    transform 180ms ease;
}

.video-play-button:hover {
  background: var(--accent);
  transform: translate(-50%, -50%) scale(1.05);
}

.home-video-card h3 {
  margin: var(--space-4) 0 0;
  font-size: var(--step-2);
  line-height: 1.3;
}

.video-card-copy {
  padding-top: var(--space-4);
}

.video-card-copy h2 {
  margin: 0 0 var(--space-2);
  font-size: var(--step-3);
  line-height: 1.18;
}

html[lang="zh"] .home-video-card h3,
html[lang="zh"] .video-card-copy h2 {
  line-height: 1.45;
}

.video-card-copy > p:not(.article-meta) {
  max-width: var(--measure);
  color: var(--ink-3);
  font-size: var(--step-0);
  line-height: 1.6;
}

/* --------------------------------------------------------------------------
   12. Elsewhere — the closing line
   This replaced a full-width dark call-to-action panel. That panel was the
   grammar of a SaaS landing page: a personal site does not need to shout at
   its reader on the way out, and the same two links already sit in the footer.
   A kicker and two text links say the same thing without breaking the page's
   quiet.
   -------------------------------------------------------------------------- */

.elsewhere {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-7);
  margin-top: var(--space-9);
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule);
}

.elsewhere .section-kicker {
  margin-bottom: 0;
}

.elsewhere-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
}

/* --------------------------------------------------------------------------
   13. Index pages — page header, article list
   -------------------------------------------------------------------------- */

.page-header {
  max-width: 46rem;
  margin-bottom: var(--space-9);
  padding-bottom: var(--space-7);
  border-bottom: 1px solid var(--ink);
}

.page-header h1 {
  margin-bottom: var(--space-5);
  font-size: var(--step-6);
  line-height: 0.98;
  letter-spacing: -0.028em;
}

html[lang="zh"] .page-header h1 {
  font-size: clamp(2.6rem, 4vw + 1.4rem, 4.2rem);
  line-height: 1.22;
  letter-spacing: -0.02em;
}

.page-header > p:not(.eyebrow),
.page-header-with-action > div > p:not(.eyebrow) {
  max-width: var(--measure);
  margin-bottom: 0;
  color: var(--ink-2);
  font-size: var(--step-2);
  line-height: 1.62;
}

html[lang="zh"] .page-header > p:not(.eyebrow),
html[lang="zh"] .page-header-with-action > div > p:not(.eyebrow) {
  line-height: 1.85;
}

.page-header-with-action {
  display: flex;
  max-width: none;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-7);
}

.page-header-with-action > div {
  max-width: 46rem;
}

.article-list {
  display: flex;
  flex-direction: column;
}

.article-row {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr) minmax(0, 15rem);
  align-items: start;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding: var(--space-7) 0;
  border-bottom: 1px solid var(--rule);
}

.article-row:first-child {
  padding-top: 0;
}

.article-row-copy {
  max-width: var(--measure-wide);
}

.article-row-copy h2 {
  margin: 0 0 var(--space-3);
  font-size: var(--step-4);
  line-height: 1.1;
}

html[lang="zh"] .article-row-copy h2 {
  line-height: 1.35;
}

.article-row-copy > p:not(.article-meta) {
  max-width: var(--measure);
  margin-bottom: var(--space-4);
  color: var(--ink-2);
}

/* --------------------------------------------------------------------------
   14. Article detail
   -------------------------------------------------------------------------- */

.article-detail {
  padding: clamp(2rem, 5vw, 4rem) 0 var(--space-10);
}

.article-header {
  max-width: 44rem;
  margin: 0 auto var(--space-8);
  text-align: center;
}

.article-header .eyebrow {
  margin-bottom: var(--space-5);
}

.article-title {
  margin-bottom: var(--space-5);
  font-size: var(--step-6);
  line-height: 1.02;
  letter-spacing: -0.026em;
}

html[lang="zh"] .article-title {
  font-size: clamp(2.4rem, 3.6vw + 1.3rem, 3.8rem);
  line-height: 1.28;
  letter-spacing: -0.02em;
}

.article-deck {
  max-width: var(--measure);
  margin: 0 auto var(--space-5);
  color: var(--ink-2);
  font-size: var(--step-2);
  line-height: 1.6;
}

html[lang="zh"] .article-deck {
  line-height: 1.85;
}

.article-header .article-meta {
  margin-bottom: 0;
}

.article-hero {
  overflow: hidden;
  max-width: 62rem;
  margin: 0 auto var(--space-9);
  border-radius: var(--radius);
  background: var(--paper-sunk);
}

.article-hero img {
  width: 100%;
  max-height: 34rem;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.article-body {
  max-width: var(--measure);
  margin: 0 auto;
  color: var(--ink-body);
  font-size: var(--step-1);
  line-height: 1.72;
}

html[lang="zh"] .article-body {
  font-family: var(--font-body-zh);
  font-size: var(--step-0);
  line-height: 1.95;
  letter-spacing: 0.012em;
}

.article-body > * + * {
  margin-top: var(--space-4);
}

.article-body h2,
.article-body h3,
.article-body h4 {
  margin-top: var(--space-8);
  margin-bottom: var(--space-3);
}

.article-body h2 {
  font-size: var(--step-4);
}

.article-body h3 {
  font-size: var(--step-3);
}

.article-body a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.article-body a:hover {
  text-decoration-color: transparent;
}

.article-body ul,
.article-body ol {
  padding-left: 1.3em;
}

.article-body li + li {
  margin-top: var(--space-2);
}

.article-body img {
  width: 100%;
  border-radius: var(--radius);
}

.article-body figcaption {
  color: var(--ink-4);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  text-align: center;
}

.article-body blockquote {
  margin: var(--space-7) 0;
  padding-left: var(--space-5);
  border-left: 2px solid var(--accent);
  color: var(--ink-2);
  font-style: italic;
}

html[lang="zh"] .article-body blockquote {
  font-style: normal;
}

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

/* Code uses its own two tokens rather than inverting ink and paper, which in
   dark mode would have produced a bright block in the middle of the column. */
.article-body pre {
  overflow-x: auto;
  padding: var(--space-5);
  border-radius: var(--radius);
  background: var(--code-bg);
  color: var(--code-ink);
  font-size: var(--step--1);
  line-height: 1.6;
}

.article-body code {
  padding: 0.1em 0.35em;
  border-radius: 2px;
  background: var(--paper-sunk);
  font-family: var(--font-mono);
  font-size: 0.9em;
}

.article-body pre code {
  padding: 0;
  background: transparent;
  font-size: inherit;
}

.article-body table {
  display: block;
  overflow-x: auto;
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step-0);
}

.article-body th,
.article-body td {
  border-bottom: 1px solid var(--rule);
  padding: var(--space-3) var(--space-4);
  text-align: left;
}

.article-body th {
  font-family: var(--font-ui);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.article-back {
  display: flex;
  justify-content: space-between;
  max-width: var(--measure);
  margin: var(--space-9) auto 0;
  padding-top: var(--space-5);
  border-top: 1px solid var(--rule);
}

/* --------------------------------------------------------------------------
   15. About
   -------------------------------------------------------------------------- */

.about-page {
  display: grid;
  grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr);
  align-items: start;
  gap: clamp(2.5rem, 7vw, 6rem);
}

.about-portrait {
  position: sticky;
  top: 8rem;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--paper-sunk);
}

.about-portrait img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 34%;
  filter: var(--portrait-filter);
}

.about-copy {
  max-width: var(--measure-wide);
}

.about-copy h1 {
  margin-bottom: var(--space-5);
  font-size: var(--step-5);
  line-height: 1.04;
  letter-spacing: -0.024em;
}

html[lang="zh"] .about-copy h1 {
  line-height: 1.28;
  letter-spacing: -0.015em;
}

.about-statement {
  color: var(--ink);
  font-size: var(--step-3);
  line-height: 1.58;
}

html[lang="zh"] .about-statement {
  font-weight: 400;
  line-height: 1.85;
}

.about-copy > p:not(.eyebrow):not(.about-statement) {
  color: var(--ink-2);
}

.about-rule {
  width: 3rem;
  height: 2px;
  margin: var(--space-6) 0;
  background: var(--accent);
}

.about-copy .social-links {
  margin-top: var(--space-6);
}

/* --------------------------------------------------------------------------
   16. Empty / utility states
   -------------------------------------------------------------------------- */

.empty-panel {
  grid-column: 1 / -1;
  padding: var(--space-8);
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
  color: var(--ink-3);
  text-align: center;
}

.empty-panel p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   17. Admin, forms, error pages
   -------------------------------------------------------------------------- */

.page,
.auth-card,
.error-page {
  max-width: 60rem;
  margin: var(--space-8) auto var(--space-10);
}

.auth-card,
.error-page {
  max-width: 34rem;
}

.page h1,
.auth-card h1,
.error-page h1 {
  margin-bottom: var(--space-4);
  font-size: var(--step-5);
}

.page-intro,
.auth-card > p,
.error-page p {
  max-width: var(--measure);
  color: var(--ink-2);
}

.article-form {
  display: flex;
  max-width: 46rem;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-6);
}

.article-form label {
  margin-top: var(--space-4);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 550;
  letter-spacing: 0.02em;
}

.article-form input[type="text"],
.article-form input[type="password"],
.article-form input[type="url"],
.article-form select,
.article-form textarea {
  width: 100%;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 0.7rem 0.85rem;
  background: var(--paper-raised);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--step-0);
  transition: border-color 160ms ease;
}

.article-form input:hover,
.article-form select:hover,
.article-form textarea:hover {
  border-color: var(--ink-4);
}

.article-form input:focus,
.article-form select:focus,
.article-form textarea:focus {
  border-color: var(--ink);
}

.article-form textarea {
  min-height: 16rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.7;
  resize: vertical;
}

.article-form .btn {
  align-self: flex-start;
  margin-top: var(--space-5);
}

.form-section-heading {
  margin: var(--space-7) 0 0;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
  font-size: var(--step-2);
}

.admin-actions,
.admin-section {
  margin-top: var(--space-8);
}

.admin-actions-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.table-scroll {
  overflow-x: auto;
}

.admin-table {
  width: 100%;
  min-width: 40rem;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}

.admin-table th,
.admin-table td {
  border-bottom: 1px solid var(--rule);
  padding: var(--space-3) var(--space-4);
  text-align: left;
}

.admin-table th {
  border-bottom-color: var(--ink);
  color: var(--ink-3);
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.admin-table tbody tr:hover {
  background: var(--paper-sunk);
}

.admin-table-actions {
  width: 5rem;
  text-align: right;
}

.delete-link-button,
.link-quiet {
  color: var(--ink-2);
  font-size: inherit;
}

.link-quiet:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.delete-link-button {
  color: var(--danger);
}

.admin-list {
  padding-left: 1.3em;
  color: var(--ink-2);
}

.error-message,
.setup-notice,
.alert {
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-ui);
  font-size: var(--step--1);
}

.error-message,
.alert-error {
  border: 1px solid rgba(155, 44, 30, 0.28);
  background: rgba(155, 44, 30, 0.06);
  color: var(--danger);
}

.setup-notice {
  border: 1px solid var(--rule);
  background: var(--paper-sunk);
  color: var(--ink-2);
}

.alert-success {
  border: 1px solid rgba(30, 107, 63, 0.28);
  background: rgba(30, 107, 63, 0.06);
  color: var(--success);
}

.flash-messages {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-5);
}

/* --------------------------------------------------------------------------
   18. Responsive
   -------------------------------------------------------------------------- */

@media (max-width: 1000px) {
  :root {
    --rail: 2.25rem;
  }

  .nav-social {
    display: none;
  }

  .hero {
    grid-template-columns: minmax(0, 1fr) minmax(210px, 0.4fr);
  }

  .article-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .article-row {
    grid-template-columns: 2.5rem minmax(0, 1fr) minmax(0, 11rem);
  }

  .about-page {
    grid-template-columns: minmax(0, 1fr);
  }

  .about-portrait {
    position: static;
    max-width: 26rem;
  }
}

@media (max-width: 720px) {
  :root {
    --gutter: 1.25rem;
    --rail: 2rem;
  }

  /* The "see all" link drops under the heading and lines up with it. */
  .section-heading-row {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    row-gap: var(--space-4);
  }

  .section-heading-row > .text-link {
    grid-column: 2;
    justify-self: start;
    margin-left: 0;
  }

  body {
    font-size: 16px;
  }

  .site-nav {
    min-height: auto;
    align-items: flex-start;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) 0;
  }

  .site-nav-links {
    width: 100%;
    overflow-x: auto;
    gap: var(--space-4);
    padding-bottom: 2px;
    scrollbar-width: none;
    white-space: nowrap;
  }

  .site-nav-links::-webkit-scrollbar {
    display: none;
  }

  .site-nav-links a::after {
    display: none;
  }

  .home-page,
  .editorial-page,
  .about-page,
  .article-detail {
    padding: var(--space-5) 0 var(--space-8);
  }

  .content-section,
  .about-preview {
    padding-top: var(--space-8);
    margin-top: var(--space-8);
  }

  .hero,
  .featured-article {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-6);
  }

  .hero {
    padding-bottom: var(--space-6);
  }

  /* Portrait first on small screens: the face is the hook. */
  .hero-visual {
    order: -1;
  }

  .hero-img {
    aspect-ratio: 3 / 2;
    object-position: 50% 32%;
  }

  .portrait-caption {
    justify-content: flex-start;
    text-align: left;
  }

  .page-header-with-action,
  .elsewhere,
  .footer-inner {
    align-items: flex-start;
    flex-direction: column;
    gap: var(--space-5);
  }

  /* Stacked, a centred identity block would float oddly above left-aligned
     links, so it goes back to flush left. */
  .footer-identity,
  .footer-inner p {
    min-width: 0;
    margin-left: 0;
    text-align: left;
  }

  .article-grid,
  .home-video-grid,
  .video-list-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  /* One column means the sparse-grid row layout has nothing to lie down into. */
  .article-grid-sparse:has(> :only-child) .article-card {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .article-grid-sparse:has(> :only-child) .card-copy {
    padding-top: var(--space-4);
  }

  .page-header {
    max-width: none;
    margin-bottom: var(--space-8);
    padding-bottom: var(--space-6);
  }

  .article-row {
    grid-template-columns: 2rem minmax(0, 1fr);
    gap: var(--space-4);
    padding: var(--space-6) 0;
  }

  /* Image leads the row on small screens, copy follows. */
  .article-row-image {
    grid-row: 1;
    grid-column: 2;
    max-width: 22rem;
    margin-bottom: var(--space-4);
  }

  .article-row-copy {
    grid-row: 2;
    grid-column: 2;
  }

  .article-number {
    grid-row: 1;
    grid-column: 1;
  }

  .article-header {
    max-width: none;
    text-align: left;
  }

  .article-deck {
    margin-left: 0;
  }

  .article-hero {
    margin-bottom: var(--space-8);
  }

  .article-hero img {
    aspect-ratio: 3 / 2;
  }

  .elsewhere {
    margin-top: var(--space-8);
  }

  .page,
  .auth-card,
  .error-page {
    margin: var(--space-6) auto var(--space-8);
  }
}

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

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
