/*
  Design tokens and base styles.

  Strict monochrome: links are distinguished by underline and weight, never by hue.
  Images are the only colour on the site. Hairline rules, no shadows, no radii.

  Inter and Noto Sans SC are self-hosted — see css/fonts.css and wwwroot/fonts/.
  The system-stack fallbacks below only matter if a font file fails to load.
*/

:root {
  /* Locked palette — see DESIGN.md */
  --bg: #fafaf8;
  --ink: #1a1a1a;
  --muted: #6b6b68;
  --rule: #e2e2de;
  --code-bg: #f2f2ee;

  --sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --cjk: "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --measure: 68ch;
  --gap: 1.5rem;
  --step: 1.5;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #121212;
    --ink: #e8e8e4;
    --muted: #9a9a96;
    --rule: #2a2a2a;
    --code-bg: #1c1c1c;
  }
}

:root[data-theme="dark"] {
  --bg: #121212;
  --ink: #e8e8e4;
  --muted: #9a9a96;
  --rule: #2a2a2a;
  --code-bg: #1c1c1c;
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1rem;
  line-height: var(--step);
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

/* Chinese posts get a matched CJK stack rather than a browser fallback. */
[lang="zh"],
[lang^="zh-"] {
  font-family: var(--cjk);
  line-height: 1.9;
}

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

a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

a:hover {
  text-decoration-thickness: 2px;
}

hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: calc(var(--gap) * 2) 0;
}

/* ---------- layout ---------- */

.shell {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0 var(--gap);
}

.site-header {
  border-bottom: 1px solid var(--rule);
  margin-bottom: calc(var(--gap) * 2);
}

.site-header .shell {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap);
  padding-block: var(--gap);
}

.site-header a {
  text-decoration: none;
}

.site-header a:hover,
.site-header a[aria-current] {
  text-decoration: underline;
  text-decoration-thickness: 1px;
}

.wordmark {
  font-weight: 600;
  letter-spacing: -0.01em;
}

.site-nav {
  display: flex;
  gap: var(--gap);
  font-size: 0.9375rem;
}

.site-footer {
  border-top: 1px solid var(--rule);
  margin-top: calc(var(--gap) * 3);
  padding-block: var(--gap);
  color: var(--muted);
  font-size: 0.875rem;
}

.site-footer .shell {
  display: flex;
  justify-content: space-between;
  gap: var(--gap);
  flex-wrap: wrap;
}

main {
  min-height: 60vh;
}

/* ---------- typography ---------- */

h1,
h2,
h3,
h4 {
  line-height: 1.25;
  letter-spacing: -0.015em;
  font-weight: 600;
  margin: calc(var(--gap) * 1.5) 0 0.5rem;
}

h1 {
  font-size: 1.75rem;
  margin-top: 0;
}

h2 {
  font-size: 1.25rem;
}

h3 {
  font-size: 1.0625rem;
}

p,
ul,
ol,
blockquote,
pre,
table {
  margin: 0 0 1rem;
}

ul,
ol {
  padding-left: 1.25rem;
}

blockquote {
  margin-left: 0;
  padding-left: var(--gap);
  border-left: 1px solid var(--rule);
  color: var(--muted);
}

small,
.meta {
  color: var(--muted);
  font-size: 0.875rem;
}

.meta {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  font-variant-numeric: tabular-nums;
}

/* A citation line reads as one sentence, so it must not be a flex row: .meta puts a
   0.75rem gap between every child, which includes bare text nodes, and splits
   "<em>Journal</em>, 2023" into two items with a hole between them. */
.citation {
  color: var(--muted);
  font-size: 0.875rem;
}

.citation em {
  font-style: italic;
}

/* ---------- code ---------- */

code,
kbd,
samp {
  font-family: var(--mono);
  font-size: 0.875em;
}

:not(pre) > code {
  background: var(--code-bg);
  padding: 0.1em 0.35em;
}

pre {
  background: var(--code-bg);
  border: 1px solid var(--rule);
  padding: var(--gap);
  overflow-x: auto;
  line-height: 1.5;
}

pre code {
  background: none;
  padding: 0;
}

/* ---------- syntax highlighting ----------
   ColorCode.HtmlClassFormatter emits these class names. Strict monochrome per
   DESIGN.md: tokens are differentiated by weight, opacity and italic only — never
   hue. To add colour later, these three rules are the only thing to change. */

pre .keyword,
pre .controlKeyword,
pre .preprocessorKeyword,
pre .type,
pre .typeVariable,
pre .className,
pre .constructor,
pre .predefined,
pre .pseudoKeyword,
pre .builtinFunction,
pre .builtinValue,
pre .cssPropertyName,
pre .cssSelector,
pre .htmlElementName,
pre .htmlAttributeName,
pre .xmlName,
pre .xmlAttribute,
pre .jsonKey,
pre .sqlSystemFunction,
pre .powershellCommand,
pre .powershellType,
pre .markdownHeader,
pre .bold {
  font-weight: 600;
}

pre .comment,
pre .htmlComment,
pre .xmlComment,
pre .xmlDocComment,
pre .attribute,
pre .italic {
  color: var(--muted);
  font-style: italic;
}

pre .string,
pre .stringCSharpVerbatim,
pre .stringEscape,
pre .cssPropertyValue,
pre .htmlAttributeValue,
pre .xmlAttributeValue,
pre .jsonString,
pre .jsonConst,
pre .number,
pre .jsonNumber,
pre .namespace,
pre .powershellVariable,
pre .powershellParameter {
  color: var(--muted);
}

/* ---------- math ---------- */
/* A display equation can be wider than a phone column and KaTeX never wraps.
   The formula scrolls inside its own box rather than pushing the page wide. */

.prose .katex-display {
  overflow-x: auto;
  overflow-y: hidden;
}

/* ---------- media embeds ---------- */
/* Markup produced by Core/Media/MediaEmbeds.cs: :::youtube and :::pdf directives,
   the archive's legacy ![pdf](...) convention, and a bare YouTube URL on its own
   line. picture/img already get max-width:100% + display:block above, so they need
   nothing further here. */

.embed {
  margin: 0 0 1rem;
}

.embed-video {
  position: relative;
  aspect-ratio: 16 / 9;
}

.embed-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.embed-pdf object {
  width: 100%;
  min-height: 70vh;
  border: 1px solid var(--rule);
  background: var(--code-bg);
}

.embed-pdf figcaption {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: var(--muted);
}

/* ---------- tables ---------- */

/* Wraps every rendered table — see SyntaxHighlighting.cs's ScrollableTableRenderer.
   No margin here: the table inside already carries it via the generic block-margin
   rule above, and adding it here too would double the gap after a table. */
.table-scroll {
  overflow-x: auto;
}

table {
  border-collapse: collapse;
  width: 100%;
  /* A table that fits stretches to 100% exactly as before (short, prose-heavy
     tables like the licenses page are unaffected). A table whose columns need
     more room than that — the regex cheatsheet's dense, bilingual columns —
     claims the width it actually wants instead of being squeezed into
     mid-character wraps, and scrolls horizontally via .table-scroll instead. */
  min-width: max-content;
  font-size: 0.9375rem;
}

th,
td {
  border-bottom: 1px solid var(--rule);
  padding: 0.5rem 0.75rem 0.5rem 0;
  text-align: left;
  vertical-align: top;
}

th {
  font-weight: 600;
}

/* ---------- content index ---------- */

.entry-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.entry {
  border-bottom: 1px solid var(--rule);
  padding-block: 1rem;
  display: grid;
  gap: 0.25rem;
}

.entry:last-child {
  border-bottom: 0;
}

.entry-title {
  font-weight: 600;
  text-decoration: none;
}

/* Scoped to links: publication titles use .entry-title on a span and should not
   suggest they are clickable. */
a.entry-title:hover {
  text-decoration: underline;
}

.entry-desc {
  color: var(--muted);
  font-size: 0.9375rem;
}

.tags {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: 0.8125rem;
  color: var(--muted);
  font-family: var(--mono);
}

/* ---------- search form ---------- */
/* Shared by /writing's tier search and the 404 page's search prompt. */

.search {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  font-size: 0.9375rem;
}

.search input {
  flex: 1;
  min-width: 0;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--rule);
  padding: 0.4rem 0.6rem;
  font: inherit;
}

.search input:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: -1px;
}

.search button {
  background: none;
  border: 1px solid var(--rule);
  color: var(--ink);
  padding: 0.4rem 0.75rem;
  font: inherit;
  cursor: pointer;
}

.search button:hover {
  border-color: var(--ink);
}

/* ---------- utility ---------- */

.stack > * + * {
  margin-top: var(--gap);
}

.lede {
  font-size: 1.0625rem;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (max-width: 34rem) {
  :root {
    --gap: 1.125rem;
  }

  .site-header .shell {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }
}

/* ---------- moved from component-scoped styles ---------- */
/* These lived in <style> blocks inside Blazor components. The pages are now
   generated, so they belong with the rest of the site's rules. */

.prose > :first-child {
  margin-top: 0;
}

.prose h2,
.prose h3 {
  margin-top: calc(var(--gap) * 2);
}

.tabs {
  display: flex;
  gap: var(--gap);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 0.5rem;
  font-size: 0.9375rem;
}

.tabs a {
  text-decoration: none;
  color: var(--muted);
}

.tabs a[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
}

.theme-toggle {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

.theme-toggle:hover {
  text-decoration-thickness: 2px;
}

/* ---------- illustrations ----------
   site/art/*.svg, drawn by site/art/generate.py and inlined by the generator so these
   rules reach inside them. The drawings carry classes, never colours, so one file
   serves both themes. --signal is the only hue on the site, and it appears only here:
   it marks the moment that matters in each drawing (the alarm, the front, the chosen
   route). The interface itself stays monochrome. */

:root {
  --signal: #d9480f;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --signal: #ff8a4c;
  }
}

:root[data-theme="dark"] {
  --signal: #ff8a4c;
}

.art {
  display: block;
  width: 100%;
  height: auto;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.25;
  overflow: visible;
}

.art text {
  stroke: none;
  font-family: var(--mono);
  font-size: 12.5px;
  fill: var(--muted);
}

.art .a-ink { stroke: var(--ink); }
.art .a-mute { stroke: var(--muted); }
.art .a-thin { stroke: var(--ink); stroke-opacity: 0.16; stroke-width: 1; }
.art .a-tree { stroke-opacity: 0.45; stroke-width: 0.9; }
.art .a-sig { stroke: var(--signal); }
.art .a-bold { stroke-width: 2.25; }
.art .a-dash { stroke-dasharray: 5 5; }
.art .a-fill-ink { fill: var(--ink); stroke: none; }
.art .a-fill-sig { fill: var(--signal); stroke: none; }
.art .a-fill-soft { fill: var(--code-bg); stroke: none; }
.art .a-band { fill: var(--ink); fill-opacity: 0.07; stroke: none; }
.art .a-sigband { fill: var(--signal); fill-opacity: 0.14; stroke: none; }
.art .a-label-sig { fill: var(--signal); }
.art .a-label-ink { fill: var(--ink); font-weight: 600; }

/* The main line of each drawing draws itself in once; the signal dot breathes. Cards
   show the finished drawing; only the large hero animates. The hidden state lives only
   in the keyframes, so a browser that runs no animation still shows the whole line. */
.art-live .a-draw {
  stroke-dasharray: 1;
  animation: art-draw 2.4s cubic-bezier(0.6, 0.1, 0.2, 1) 0.15s both;
}

.art-live .a-pulse {
  transform-box: fill-box;
  transform-origin: center;
  animation: art-pulse 2.4s ease-in-out 2.4s infinite;
}

@keyframes art-draw {
  from { stroke-dashoffset: 1; }
  to { stroke-dashoffset: 0; }
}

@keyframes art-pulse {
  50% { transform: scale(1.6); opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  .art-live .a-draw,
  .art-live .a-pulse {
    animation: none;
  }
}

/* ---------- portfolio: home, /projects, /skills and their hero pages ----------
   Same rules as the rest of the site (hairlines, no shadows, no radii, monochrome
   chrome), on a wider column so drawings and card grids have room. Writing pages keep
   the 68ch reading measure. */

.shell.wide {
  max-width: 74rem;
}

.site-nav {
  flex-wrap: wrap;
  row-gap: 0.25rem;
}

/* Doubled class: the spacing must beat the margin: 0 on lists such as .cards. */
.stack.stack > * + * {
  margin-top: var(--gap);
}

.stack-lg.stack-lg > * + * {
  margin-top: calc(var(--gap) * 3);
}

.eyebrow {
  display: block;
  margin: 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.display {
  font-size: clamp(1.9rem, 1.2rem + 2.6vw, 3.1rem);
  line-height: 1.08;
  letter-spacing: -0.03em;
  font-weight: 650;
  margin: 0.5rem 0 1rem;
  text-wrap: balance;
}

.page-head {
  max-width: 48rem;
}

.page-head .lede {
  color: var(--muted);
}

.section-title {
  font-size: 1.0625rem;
  margin: 0;
}

.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--gap);
  border-bottom: 1px solid var(--ink);
  padding-bottom: 0.5rem;
  font-size: 0.9375rem;
}

/* hero ---------------------------------------------------------------------- */

.hero {
  display: grid;
  gap: calc(var(--gap) * 1.5);
  align-items: center;
}

.hero-text .lede {
  color: var(--muted);
  max-width: 40rem;
  text-wrap: pretty;
}

.hero-art {
  margin: 0;
  border: 1px solid var(--rule);
  padding: clamp(0.75rem, 2vw, 1.5rem);
  background: var(--bg);
}

@media (min-width: 56rem) {
  .hero {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    gap: calc(var(--gap) * 2.5);
  }
}

.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  margin: 1.5rem 0 0;
}

.button {
  display: inline-block;
  background: var(--ink);
  color: var(--bg);
  padding: 0.55rem 1rem;
  text-decoration: none;
  font-weight: 600;
}

.button:hover {
  text-decoration: underline;
}

.crumbs {
  font-size: 0.875rem;
  color: var(--muted);
  margin-bottom: calc(var(--gap) * 1.5);
}

.crumbs span {
  margin-left: 0.35rem;
}

/* numbers -------------------------------------------------------------------- */

.stats {
  list-style: none;
  padding: 0;
  margin: calc(var(--gap) * 2) 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9.5rem), 1fr));
  border-top: 1px solid var(--ink);
}

.stats li {
  display: grid;
  align-content: start;
  gap: 0.35rem;
  padding: 1rem 1.25rem 1rem 0;
  border-bottom: 1px solid var(--rule);
}

.stat-value {
  font-size: clamp(1.75rem, 1.3rem + 1.5vw, 2.5rem);
  font-weight: 650;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat-label {
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.4;
}

/* cards ---------------------------------------------------------------------- */

.cards {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr));
  gap: var(--gap);
}

/* Six skill areas: three across on a wide screen, never a lonely last row of two. */
.cards-compact {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr));
}

.item > section {
  margin-top: calc(var(--gap) * 3);
}

.card {
  display: flex;
  flex-direction: column;
  height: 100%;
  border: 1px solid var(--rule);
  text-decoration: none;
  transition: border-color 0.15s ease;
}

.card:hover,
.card:focus-visible {
  border-color: var(--ink);
}

.card:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 2px;
}

.card-art {
  display: block;
  padding: 1rem;
  border-bottom: 1px solid var(--rule);
  background: var(--bg);
  overflow: hidden;
}

/* Labels are written for the large drawing; at card size they would be noise. */
.card-art .art text {
  display: none;
}

.card-art .art {
  transition: transform 0.35s ease;
}

.card:hover .card-art .art {
  transform: scale(1.03);
}

.card-text {
  display: grid;
  gap: 0.4rem;
  padding: 1rem 1rem 1.25rem;
}

.card-title {
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

.card:hover .card-title {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

.card-desc {
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1.45;
}

.cards-compact .card-desc {
  font-size: 0.875rem;
}

/* chips ---------------------------------------------------------------------- */

.chips {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.75rem;
}

.chips > * {
  border: 1px solid var(--rule);
  padding: 0.2rem 0.5rem;
  text-decoration: none;
}

.chips a:hover {
  border-color: var(--ink);
}

.page-head .chips {
  margin-top: 1.25rem;
}

/* item pages ------------------------------------------------------------------- */

.item-body {
  display: grid;
  gap: calc(var(--gap) * 2);
  margin-top: calc(var(--gap) * 2.5);
}

.item-body .prose {
  max-width: var(--measure);
}

@media (min-width: 56rem) {
  .item-body {
    grid-template-columns: minmax(0, 1fr) 16rem;
    gap: calc(var(--gap) * 3);
  }

  .item-body-single {
    grid-template-columns: minmax(0, 1fr);
  }

  .item-side {
    position: sticky;
    top: var(--gap);
    align-self: start;
  }
}

.item-side {
  display: grid;
  gap: 0.6rem;
  align-content: start;
  font-size: 0.9375rem;
  border-top: 1px solid var(--ink);
  padding-top: 0.75rem;
}

.item-side .eyebrow + * {
  margin-bottom: 0.75rem;
}

.plain {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.35rem;
}

/* Numbered section headings turn each write-up into a short sequence of steps. */
.item .prose {
  counter-reset: step;
}

.item .prose h2 {
  counter-increment: step;
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  border-top: 1px solid var(--rule);
  padding-top: 1rem;
}

.item .prose h2:first-child {
  margin-top: 0;
}

.item .prose h2::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--muted);
}

.pager {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  margin-top: calc(var(--gap) * 3);
  border-top: 1px solid var(--ink);
}

.pager a {
  display: grid;
  gap: 0.35rem;
  align-content: start;
  padding-top: 1rem;
  text-decoration: none;
  font-weight: 600;
}

.pager a:hover {
  text-decoration: underline;
}

.pager-next,
.pager-next .eyebrow {
  text-align: right;
}

/* skill pages: tool table ------------------------------------------------------ */

.tools ul {
  list-style: none;
  padding: 0;
  margin: calc(var(--gap) * 2) 0 0;
  border-top: 1px solid var(--ink);
}

.tools li {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 2fr);
  gap: 0.25rem var(--gap);
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--rule);
  font-size: 0.9375rem;
}

.tool-name {
  font-weight: 600;
}

.tool-level {
  font-family: var(--mono);
  font-size: 0.8125rem;
}

.tool-where {
  color: var(--muted);
}

@media (max-width: 40rem) {
  .tools li {
    grid-template-columns: 1fr;
  }
}

/* timeline --------------------------------------------------------------------- */

.timeline {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--gap);
}

.timeline li {
  position: relative;
  display: grid;
  align-content: start;
  gap: 0.35rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--ink);
}

.timeline li::before {
  content: "";
  position: absolute;
  top: -5px;
  left: 0;
  width: 9px;
  height: 9px;
  background: var(--ink);
}

.timeline li.timeline-now::before {
  background: var(--signal);
}

.timeline-year {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--muted);
}

.timeline-title {
  font-weight: 600;
  line-height: 1.3;
}

.timeline-desc {
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.45;
}

.split {
  display: grid;
  gap: calc(var(--gap) * 2);
}

@media (min-width: 56rem) {
  .split {
    grid-template-columns: 1fr 1fr;
    gap: calc(var(--gap) * 3);
  }
}
