/* Yunhan Fang — portfolio site
   Design system: design/design-system.md (Option 1 "Monograph", 26 Sep 2026;
   header, footer and ruled cards revised 15 Oct 2026, see design/decisions.md).
   Order: tokens → phone rungs (one media block) → base → components in page order.
   Breakpoint: narrow 51.25rem (820 px). CSS cannot read custom properties in
   media queries, so the value is repeated below with a comment. */

@font-face { font-family: "Cabinet Grotesk"; src: url("fonts/CabinetGrotesk-Light.woff2") format("woff2"); font-weight: 300; font-style: normal; font-display: swap; }
@font-face { font-family: "Cabinet Grotesk"; src: url("fonts/CabinetGrotesk-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Cabinet Grotesk"; src: url("fonts/CabinetGrotesk-Medium.woff2") format("woff2"); font-weight: 500; font-style: normal; font-display: swap; }

:root {
  /* colour roles (all text pairs measured ≥ 5.9:1 on the canvas) */
  --canvas: #f3f1ec;
  --ink: #1c1b18;
  --muted: #5c5955;
  --accent: #2743b8;   /* one accent, one job: arrows, the email, focus */
  --hair: #cfcbc2;        /* retired 2026-10-16: no grey rules on the page */
  --tile-ground: #ffffff;
  --chip: #e9e6df;        /* boxed chips in the footer */

  /* type: one family, three weights. Regular (400) for the reading text (lede, body
     paragraphs); Light (300) for the rest (captions, tags, chips, footer, card meta);
     Medium (500) for the name, the nav, headings and card titles */
  --font: "Cabinet Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
  --weight-body: 300;   /* the root: small and secondary text */
  --weight-text: 400;   /* reading paragraphs */
  --weight-head: 500;

  /* type ladder: one scale at every width (2026-10-27; the fluid clamps and the phone rung
     were retired so the page reads the same on a phone as on a desk). Headings are one
     ladder in Medium: h1 the name in the header, h2 every section heading, h3 every item
     title (card titles, the contact invitation) */
  --text-nav: 1.5rem;       /* 24 px, header links */
  --text-h2: 2rem;          /* 32 px, section headings and the menu rows */
  --text-h3: 1.5rem;        /* 24 px, item titles */
  --text-lede: 2rem;        /* 32 px, the hero statement (Regular, not a heading) */
  --text-body: 1.125rem;    /* 18 px */
  --text-small: 0.9375rem;  /* 15 px */
  --text-label: 0.875rem;   /* 14 px */
  --leading-lede: 1.35;  /* wraps to 3+ lines */
  --leading-head: 1.15;  /* h2 */
  --leading-tight: 1.2;  /* h3 */
  --leading-body: 1.55;
  --leading-small: 1.45;

  /* spacing scale, 4 px base */
  --sp-2xs: 0.125rem;
  --sp-xs: 0.25rem;
  --sp-sm: 0.5rem;
  --sp-md: 0.75rem;
  --sp-lg: 1rem;
  --sp-xl: 1.5rem;
  --sp-2xl: 2rem;
  --sp-3xl: 3rem;
  --sp-4xl: 4rem;

  /* spacing roles */
  --inset-section: var(--sp-3xl);
  --inset-hero-top: var(--sp-4xl);
  --stack-head: var(--sp-2xl);        /* h2 to its content */
  --stack-tile: var(--sp-md);         /* frame to caption */
  --gap-tiles: var(--sp-3xl) var(--sp-2xl);
  --gap-hero: var(--sp-3xl);
  --gutter: clamp(1rem, 4vw, 2.5rem);

  /* layout */
  --page-max: 72rem;
  --measure-lede: 24em;
  --measure-body: 34em;
  --target: 2.75rem;    /* 44 px */
  --header-h: 4rem;     /* the sticky phone header band, 64 px plus its rule */
  --focus-ring: 3px solid var(--accent);
  --focus-offset: 3px;
}

/* phone rungs of the spacing roles, applied at 820 px and below (type does not step) */
@media (max-width: 51.25rem) {
  :root {
    --inset-section: var(--sp-2xl);
    --stack-head: var(--sp-xl);
    --inset-hero-top: var(--sp-2xl);
    --gap-tiles: var(--sp-2xl);
    --gap-hero: var(--sp-xl);
  }
}

/* ---------- base ---------- */

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

html {
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font);
  font-weight: var(--weight-body);
  font-size: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: auto;
}

body {
  margin: 0;
  font-size: var(--text-body);
  line-height: var(--leading-body);
  padding-inline: var(--gutter);
}

.page {
  max-width: var(--page-max);
  margin-inline: auto;
}

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

a {
  color: inherit;
  text-decoration: none;
  transition-property: color;
  transition-duration: 150ms;
}

a:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

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

[hidden] { display: none !important; }

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

.skip-link {
  position: absolute;
  left: var(--gutter);
  top: -100%;
  padding: var(--sp-sm) var(--sp-md);
  background: var(--ink);
  color: var(--canvas);
  font-family: var(--font);
  font-size: var(--text-label);
  z-index: 10;
}
.skip-link:focus-visible { top: var(--sp-sm); outline-color: var(--canvas); outline-offset: -6px; }

/* ---------- header ---------- */

/* A ruled band: the name and the section links at display size (after gracesportfolio.com) */
.site-header {
  display: flex;
  align-items: center;   /* the mark beside the name makes baseline alignment drift; centre instead */
  flex-wrap: wrap;
  gap: var(--sp-lg) var(--sp-3xl);
  padding-block: 3.5rem;
  border-bottom: 1px solid var(--ink);
  font-weight: var(--weight-head);
  line-height: 1.1;
}

/* The name at the nav size with the cat mark (cropped from the portrait) beside it */
.site-name {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-md);
  min-height: var(--target);
  font-size: var(--text-nav);
  letter-spacing: -0.01em;
}
.site-mark { width: auto; height: 2rem; flex: none; }

.site-nav {
  margin-left: auto;
  display: flex;
  gap: var(--sp-3xl);
  font-size: var(--text-nav);
}

@media (hover: hover) {
  .site-nav a:hover,
  .site-name:hover,
  .menu-toggle:hover,
  .menu a:hover { color: var(--accent); }
}

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

.hero {
  display: grid;
  grid-template-columns: 55fr 45fr;
  gap: var(--gap-hero);
  align-items: center;
  padding-block: var(--inset-hero-top) var(--inset-section);
}

/* The figure comes first in the source (it is decorative, so reading order is unaffected)
   and sits to the right of the lede on wide screens, centred above it on phones. */
.lede {
  grid-column: 1;
  grid-row: 1;
  font-weight: var(--weight-text);
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  letter-spacing: -0.01em;
  max-width: var(--measure-lede);
  text-wrap: pretty;
}

.hero-figure {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  width: min(100%, 17rem);
}
/* The portrait: a still image by default; the looping clip (hair, tail tip and a blink move,
   the rest frozen) replaces it only where the reader has not asked for reduced motion. */
.portrait-live { display: none; width: 100%; height: auto; }
@media (prefers-reduced-motion: no-preference) {
  .portrait-still { display: none; }
  .portrait-live { display: block; }
}

/* ---------- sections ---------- */

/* Sections are separated by space alone (no rules); each opens with an h2 */
.section {
  padding-block: var(--inset-section);
  scroll-margin-top: var(--sp-lg);
}

/* ---------- headings: one ladder ---------- */

.site-name, .section-title, .tile-title, .contact-title {
  font-weight: var(--weight-head);
  letter-spacing: -0.01em;
}
.section-title, .contact-title { color: var(--ink); }   /* card titles inherit the card's colour */
.section-title {
  font-size: var(--text-h2);
  line-height: var(--leading-head);
  text-wrap: balance;
  margin-bottom: var(--stack-head);
}
.tile-title, .contact-title {
  font-size: var(--text-h3);
  line-height: var(--leading-tight);
  text-wrap: balance;
}

.body {
  font-weight: var(--weight-text);
  max-width: var(--measure-body);
  text-wrap: pretty;
}


/* services: two ruled cards, the same object as the work cards (below), each with a two-ink
   scene in the cover slot, the number as a small label in the title row, and a description row */

.tile-art {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 16 / 10;
  padding: 6%;
  border-bottom: 1px solid var(--ink);
}
.tile-art img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }

.tile-desc {
  flex: 1 1 auto;   /* takes up the difference, so the meta rows of a pair line up */
  padding: 0.875rem 1.125rem 1rem;
  border-bottom: 1px solid var(--ink);
  font-weight: var(--weight-text);
  font-size: 1rem;
  line-height: 1.5;
}

/* work cards: ruled. One ink frame, rows divided by rules (cover / title / client and
   role), and a second frame offset behind it, filled with a colour of the card's own. */

.work-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap-tiles);
  list-style: none;
  padding: 0 var(--sp-md) var(--sp-md) 0;   /* room for the offset frames */
}

.tile { position: relative; display: flex; flex-direction: column; }   /* the card fills the row */

.tile { --tint: var(--chip); }   /* each tile sets its own --tint inline, from work.json */
.tile-shadow {
  position: absolute;
  inset: var(--sp-md) calc(-1 * var(--sp-md)) calc(-1 * var(--sp-md)) var(--sp-md);
  border: 1px solid var(--ink);
  background: var(--tint, var(--chip));
}

.tile-card {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ink);
  background: var(--canvas);
  transition-property: translate, color;
  transition-duration: 150ms;
}
/* Focus: the cobalt ring with canvas on both sides of it (a 3 px gap inside, a 3 px halo
   outside), so it reads at 7.2:1 even where it crosses a vermilion offset frame. */
.tile-card:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  box-shadow: 0 0 0 9px var(--canvas);
}

.tile-cover {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-bottom: 1px solid var(--ink);
}

.tile-ground {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 16 / 10;
  border-bottom: 1px solid var(--ink);
}
.tile-ground img { width: 58%; }

.tile-title {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-lg);
  padding: 0.875rem 1.125rem;
  border-bottom: 1px solid var(--ink);
}

.tile-meta {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--sp-xs) var(--sp-xl);
  padding: var(--sp-md) 1.125rem 0.875rem;
  font-weight: var(--weight-text);
  font-size: var(--text-small);
  line-height: var(--leading-small);
}
.tile-title { transition: color 150ms; }   /* only the title takes the tint on hover (2026-10-27) */
.tile-client { grid-column: 1; color: var(--muted); }
.tile-role { grid-column: 2; grid-row: 1; white-space: nowrap; text-align: right; }
.tile-what { grid-column: 1; grid-row: 2; color: var(--muted); }
.services-list .tile-client { color: var(--muted); }

.ext {
  color: var(--accent);
  font-weight: var(--weight-body);
  font-size: 0.8em;
}

/* Hover and keyboard focus: the card lifts and only its title takes the card's own tint;
   the arrow, client and role keep their colours. Every tint is darkened until it reads at
   4.6:1 or better on the canvas. */
a.tile-card:focus-visible .tile-title { color: var(--tint); }
@media (hover: hover) {
  a.tile-card:hover .tile-title { color: var(--tint); }
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  a.tile-card:hover { translate: -2px -2px; }   /* the lift is motion, so it is opt-in */
}

/* contact: the cat on the post beside a first-person invitation and the email as a chip */

.contact {
  display: grid;
  grid-template-columns: 18.75rem 1fr;
  gap: var(--sp-4xl);
  align-items: center;
}
.contact-art { width: 18.75rem; }
.contact-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl);
  max-width: 34em;
}
.contact-email {
  color: var(--accent);
  font-weight: var(--weight-head);
  text-decoration: underline;
  text-underline-position: from-font;
  text-decoration-thickness: from-font;
  text-decoration-skip-ink: auto;
  text-underline-offset: 0.12em;
  overflow-wrap: anywhere;
}
/* only a chip that is a link responds to the pointer; the plain chips stay still */
@media (hover: hover) {
  a.chip:hover { color: var(--accent); }
}

/* ---------- footer ---------- */

/* A ruled band with boxed chips (after gracesportfolio.com) */
.site-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-lg) var(--sp-2xl);
  padding-block: var(--sp-2xl) var(--sp-3xl);
  border-top: 1px solid var(--ink);
  font-size: 1rem;
  line-height: 1.3;
}
.chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  padding: var(--sp-sm) 0.875rem;
  border: 1px solid var(--ink);
  background: var(--chip);
}

/* ---------- phone menu: a hamburger in the sticky header and a full-screen index sheet ---------- */

/* Hidden above the breakpoint, where the header carries the links. Below it the header is
   sticky, the hamburger links to #menu, and the sheet shows beneath the header while it is
   the :target (no script: see the note in index.html). After Zara and Retool (icon in a fixed
   band, the close icon in its place), Open and BitcoinOS on Mobbin. */
.menu, .menu-toggle { display: none; }

/* ---------- narrow: 51.25rem = 820 px (same value as the rung block) ---------- */
@media (max-width: 51.25rem) {
  html { scroll-padding-top: calc(var(--header-h) + 1px + var(--sp-lg)); }   /* anchors and focus land below the sticky band */

  .site-nav { display: none; }

  /* the band: full-bleed across the gutters, the name and the icon on one line */
  .site-header {
    position: sticky;
    top: 0;
    z-index: 30;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--sp-lg);
    min-height: var(--header-h);
    margin-inline: calc(-1 * var(--gutter));
    padding: var(--sp-sm) var(--gutter);
    background: var(--canvas);
  }
  .site-name { min-width: 0; }
  .menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: var(--target);
    height: var(--target);
    margin-inline: auto -0.625rem;   /* the 24 px glyph sits on the gutter edge; the 44 px target overhangs it */
  }
  .menu-toggle:focus-visible { outline-offset: -3px; }
  .menu-toggle-close { display: none; }
  html:has(.menu:target) .menu-toggle-open { display: none; }
  html:has(.menu:target) .menu-toggle-close { display: inline-flex; }

  .hero {
    grid-template-columns: 1fr;
    align-items: start;
  }
  .hero-figure { grid-column: 1; grid-row: 1; justify-self: center; width: min(100%, 14rem); }
  .lede { grid-row: 2; }

  /* services: ruled rows instead of cards (after Vucko and Koto): the drawing across the full
     width, then numeral and title, the description, the tags; one ink rule between items */
  .services-list { display: block; padding: 0; border-top: 1px solid var(--ink); }
  .services-list .tile-shadow { display: none; }
  .services-list .tile-card {
    display: flex;
    flex-direction: column;
    padding-block: var(--sp-xl);
    border: 0;
    border-bottom: 1px solid var(--ink);
  }
  .services-list .tile-art { padding: 0; border: 0; aspect-ratio: 16 / 10; margin-bottom: var(--sp-lg); }
  .services-list .tile-title { padding: 0; border: 0; }
  .services-list .tile-desc { padding: var(--sp-lg) 0 0; border: 0; }
  .services-list .tile-meta { padding: var(--sp-md) 0 0; }

  .work-list { grid-template-columns: 1fr; }

  .tile-meta { grid-template-columns: 1fr; }
  .tile-role, .tile-what { grid-column: 1; grid-row: auto; white-space: normal; text-align: left; }
  .tile-role { color: var(--ink); }

  .contact { grid-template-columns: 1fr; gap: var(--sp-2xl); }
  .contact-art { width: 14rem; }

  .site-footer { flex-direction: column; align-items: flex-start; gap: var(--sp-lg); }

  /* the sheet: the page ground beneath the sticky band, three ruled index rows, then the
     email chip (and, where the icon cannot swap, a Close chip) in the bottom row */
  .menu:target {
    display: flex;
    flex-direction: column;
    position: fixed;
    inset: 0;
    z-index: 20;
    padding: calc(var(--header-h) + 1px) var(--gutter) 0;
    background: var(--canvas);
    overflow-y: auto;
    overscroll-behavior: contain;
    outline: none;
  }
  html:has(.menu:target) { overflow: hidden; }   /* the page behind does not scroll (where :has is supported) */

  .menu-list {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  .menu-list a {
    display: flex;
    align-items: center;
    min-height: 4rem;                  /* 64 px rows */
    padding-block: var(--sp-lg);
    border-bottom: 1px solid var(--ink);
    font-size: var(--text-nav);        /* the header links' size, 24 px (was h2, 2026-10-27) */
    font-weight: var(--weight-head);
    letter-spacing: -0.01em;
    line-height: var(--leading-tight);
  }
  .menu-list a:focus-visible { outline-offset: -4px; }
  .menu-foot {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: var(--sp-md);
    flex-wrap: wrap;
    margin-top: auto;
    padding-block: var(--sp-2xl) calc(var(--sp-lg) + env(safe-area-inset-bottom, 0px));
  }
  .menu-foot .chip { font-weight: var(--weight-head); font-size: var(--text-body); line-height: 1.2; }
  .menu-close { margin-left: auto; }
  @supports selector(:has(a)) { .menu-close { display: none; } }   /* the close icon in the header does this job */
}

@media print {
  .menu, .menu-toggle, .skip-link { display: none !important; }
  .site-header { position: static; }
}
