/* ==========================================================================
   site.css — Ying Jiang, Product Designer
   Sections: 1 Reset · 2 Typography · 3 Layout · 4 Header/Footer
             5 Home · 6 Work index · 7 Case study · 8 Figures & diagrams
             9 About/Craft/Résumé/Contact · 10 Utilities · 11 Print
   ========================================================================== */

/* ==========================================================================
   1 · RESET
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--s-5));
}

/* Column flex so the footer is pinned to the bottom of the viewport on pages
   too short to fill it — thanks.html and an error page, mostly. dvh rather
   than vh so a mobile browser's collapsing toolbar can't leave a strip of
   paper below the dark footer. The skip link and the progress bar are taken
   out of flow, so they never become flex items. */
body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  color: var(--ink-2);
  font: 400 var(--fs-body)/var(--lh-body) var(--font-body);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
main { flex: 1 0 auto; }

h1, h2, h3, h4, p, figure, blockquote, dl, dd, ul, ol { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, video { display: block; max-width: 100%; }
img, video { height: auto; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
table { border-collapse: collapse; width: 100%; }

:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-sm);
}
:focus:not(:focus-visible) { outline: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   2 · TYPOGRAPHY
   ========================================================================== */

h1, h2, h3, h4 {
  color: var(--ink);
  font-family: var(--font-ui);
  font-weight: 700;
  letter-spacing: -0.018em;
  line-height: var(--lh-tight);
  text-wrap: balance;
}

/* The display voice: serif, for anything that carries an argument.
   h3 and h4 stay sans so the hierarchy has two distinct registers, not
   six sizes of the same thing. */
.display, h1, .h1, h2, .h2 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: var(--track-display);
}

.display { font-size: var(--fs-display); }
h1, .h1  { font-size: var(--fs-h1); }
h2, .h2  { font-size: var(--fs-h2); line-height: var(--lh-snug); }
h3, .h3  { font-size: var(--fs-h3); line-height: var(--lh-snug); letter-spacing: -0.012em; }

p { text-wrap: pretty; }
p + p { margin-top: var(--s-5); }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: var(--measure-lead);
}

/* Sits directly under an h1 and belongs to it, so the gap stays tight. */
.page-subtitle {
  margin-top: var(--s-3);
  font: 400 var(--fs-h3)/1.25 var(--font-display);
  letter-spacing: -0.006em;
  color: var(--ink-3);
}

/* The label voice: mono. Eyebrows, metadata, anything the page says about
   itself rather than to you. */
.eyebrow {
  display: block;
  font: 500 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-3);
}
.eyebrow--accent { color: var(--accent-deep); }

.meta {
  font-size: var(--fs-meta);
  color: var(--ink-3);
}

strong, b { color: var(--ink); font-weight: 600; }
em, i { font-style: italic; }

a {
  color: inherit;
  text-decoration-color: var(--ink-4);
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
  transition: text-decoration-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
a:hover { text-decoration-color: var(--accent); }

/* Arrow link */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font: 600 var(--fs-small)/1 var(--font-ui);
  color: var(--ink);
  text-decoration: none;
}
.link-arrow::after {
  content: "→";
  transition: transform var(--dur-2) var(--ease-out);
}
.link-arrow:hover::after { transform: translateX(4px); }

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0.8rem 1.35rem;
  border-radius: var(--r-md);
  font: 600 var(--fs-small)/1 var(--font-ui);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.btn:active { transform: translateY(1px); }
/* A disabled button has to look disabled, or it reads as broken. It also has
   to stay legible: this is 4.6:1 on the ink fill, so it is still readable
   rather than a gray smear. */
.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.btn--primary { background: var(--ink); color: var(--paper); }
/* Deep accent, not the base one: 8.6:1 on white keeps the label legible. */
.btn--primary:hover { background: var(--accent-deep); }

.btn--ghost {
  border: var(--border);
  color: var(--ink);
  background: var(--paper);
}
.btn--ghost:hover { border-color: var(--accent); color: var(--accent-deep); background: var(--accent-wash); }

/* ==========================================================================
   3 · LAYOUT
   ========================================================================== */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.shell--wide {
  width: min(
    var(--shell-wide),
    calc(100% - max(0px, calc((100% - var(--shell)) / 2)))
  );
  max-width: none;
  margin-inline: max(0px, calc((100% - var(--shell)) / 2)) 0;
}

.section { padding-block: var(--section); }
.section--tight { padding-block: calc(var(--section) * 0.55); }
/* A background change already reads as a break; the section before it needn't pay for one too. */
.section--flush { padding-bottom: calc(var(--section) * 0.5); }
/* A page header and the content it introduces are one unit — only the content
   section below pays for the gap between them. */
.section--head { padding-bottom: 0; }
/* The mirror of --head: this section continues the one above, which owns the gap. */
.section--follow { padding-top: 0; }
.section--panel { background: var(--panel); }

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

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

/* A section's own heading, sitting directly above its content. */
.section-head { margin-bottom: var(--s-6); }

.skip-link {
  position: absolute;
  left: var(--s-4);
  top: var(--s-4);
  z-index: 200;
  padding: var(--s-3) var(--s-4);
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-md);
  font: 600 var(--fs-small)/1 var(--font-ui);
  text-decoration: none;
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

/* ==========================================================================
   4 · HEADER / FOOTER
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(255, 255, 255, 0.86);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid var(--rule);
}

.site-header__inner {
  min-height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
}

/* Logo — the mark carries the identity. Name and role live in About and the footer. */
.logo {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  border-radius: var(--r-md);
}
.logo__mark {
  display: block;
  height: 48px;
  width: auto;
  transition: transform var(--dur-2) var(--ease);
}
.logo:hover .logo__mark { transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) { .logo:hover .logo__mark { transform: none; } }

/* Nav */
.nav { display: flex; align-items: center; gap: var(--s-5); }
.nav__list { display: flex; align-items: center; gap: var(--s-5); }
.nav__link {
  position: relative;
  font: 500 var(--fs-small)/1 var(--font-ui);
  color: var(--ink-2);
  text-decoration: none;
  padding-block: var(--s-2);
  transition: color var(--dur-1) var(--ease);
}
.nav__link:hover { color: var(--accent-deep); }
.nav__link[aria-current="page"] { color: var(--ink); font-weight: 600; }
.nav__link[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 2px;
  background: var(--accent);
  border-radius: 2px;
}

.nav__toggle { display: none; }

@media (max-width: 860px) {
  .nav__toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    font: 600 var(--fs-small)/1 var(--font-ui);
    color: var(--ink);
    padding: var(--s-2);
  }
  .nav__panel {
    position: fixed;
    inset: var(--header-h) 0 auto 0;
    background: var(--paper);
    border-bottom: var(--border);
    padding: var(--s-5) var(--gutter) var(--s-6);
    display: none;
    box-shadow: var(--shadow-2);
  }
  .nav__panel[data-open="true"] { display: block; }
  .nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .nav__list li + li { border-top: var(--border); }
  .nav__link { display: block; padding: var(--s-4) 0; font-size: 1.05rem; }
  .nav__link[aria-current="page"]::after { display: none; }
}
@media (min-width: 861px) {
  .nav__panel { display: contents; }
}

/* Footer */
/* This used to carry a sitemap, a contact column and a name block. The nav is
   sticky and four items long, so all three were restating the visible page.
   What is left is the colophon: provenance, and a per-page footnote.

   Dark, like the old site. The color change is what separates the footer from
   the page, so the hairline rule that used to do that job is gone. Everything
   sits on one line where there is room, which keeps the block near 56px. */
.site-footer {
  padding-block: var(--s-4);
  background: var(--ink);
  color: var(--ink-4);
}
.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-5);
  justify-content: space-between;
  font: 400 var(--fs-meta)/1.5 var(--font-mono);
  color: var(--ink-4);
}
/* Icon plus label, never the icon alone: a bare glyph is a guessing game, and
   Behance in particular is not widely recognized. The label gives each link a
   real hit area, so the box only needs to clear the 24px of WCAG 2.5.8 rather
   than being padded out to 44. Marks are sized in em so they track the label
   instead of needing their own breakpoint. */
.footer-social {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  margin: 0;
  padding: 0;
  list-style: none;
}
.footer-social a {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 24px;
  color: var(--ink-4);
  text-decoration: none;
}
/* The dark mirror of .nav__link:hover — that one goes to full ink on paper,
   this one goes to full paper on ink. Accent is 3:1 here and cannot be used. */
.footer-social a:hover { color: var(--paper); }
.footer-social__mark {
  width: 1.3em;
  height: 1.3em;
  flex: none;
  fill: currentColor;
}

/* ==========================================================================
   5 · HOME
   ========================================================================== */

.hero {
  position: relative;
  padding-block: clamp(2.25rem, 0.75rem + 4.5vw, 5rem) var(--s-7);
  overflow: hidden;
}
.hero__art {
  position: absolute;
  inset: 0 -10% 0 auto;
  width: min(70%, 900px);
  opacity: 0.3;
  pointer-events: none;
  mask-image: linear-gradient(to left, #000 30%, transparent 92%);
}
@media (max-width: 900px) { .hero__art { opacity: 0.16; inset: 0; width: 100%; } }
.hero__inner { position: relative; display: grid; gap: var(--s-7); }
.hero__claim {
  font-size: var(--fs-h1);
  max-width: 20ch;
  margin-bottom: var(--s-5);
}
.hero__body { max-width: 48ch; }

/* Wide: the strip becomes the second column and stands up on end, which is the
   only thing tall enough to balance the claim. Narrow: it falls back under the
   intro as a rule of names. */
@media (min-width: 900px) {
  .hero__inner {
    grid-template-columns: minmax(0, auto) auto;
    justify-content: start;
    gap: calc(var(--s-9) * 1.5);
    align-items: center;
  }
}

/* Credibility strip */
/* No horizontal rules: each entry now has its own two-line hierarchy, so the
   group reads as a unit without being boxed. The vertical rule stays — it is
   the only thing marking the hero as two columns. */
.logos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--s-5);
}
.logos__org {
  display: block;
  font: 600 var(--fs-body)/1.2 var(--font-ui);
  color: var(--ink);
}
.logos__what {
  display: block;
  margin-top: var(--s-1);
  font-size: var(--fs-meta);
  color: var(--ink-3);
}
@media (min-width: 900px) {
  .logos {
    grid-template-columns: 1fr;
    gap: var(--s-4);
    padding: var(--s-2) 0 var(--s-2) var(--s-7);
    border-left: var(--border);
  }
}

/* Featured cards */
.featured { display: grid; gap: var(--s-8); }
@media (min-width: 900px) {
  .featured { grid-template-columns: repeat(3, 1fr); gap: var(--s-7) var(--s-6); }
}

.card { display: flex; flex-direction: column; position: relative; isolation: isolate; }
.card__media {
  background: var(--panel);
  border: var(--border);
  border-radius: var(--r-lg);
  aspect-ratio: 16 / 10;
  overflow: hidden;
  margin-bottom: var(--s-5);
  transition: border-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease-out);
}
.card__media img, .card__media svg { width: 100%; height: 100%; object-fit: cover; }
.card:hover .card__media { border-color: var(--ink-4); transform: translateY(-3px); }
.card__media.designops-thumb,
.work-row__media.designops-thumb {
  aspect-ratio: 16 / 9;
  background: #e9eef1;
}
.card__media.designops-thumb img,
.work-row__media.designops-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Diagrams are much wider than the frame, so fit the whole thing rather than crop it. */
.card__media.diagram-thumb,
.work-row__media.diagram-thumb {
  display: flex;
  align-items: center;
}
.card__media.diagram-thumb img,
.work-row__media.diagram-thumb img {
  height: auto;
  border-block: 1px solid var(--rule);
}
/* Screenshots whose subject sits at the top, so any crop should fall at the bottom. */
.card__media.crop-top img,
.work-row__media.crop-top img {
  object-position: top;
}
/* Painted behind the card and bled into the gutter, so the hover target reads
   as the whole block rather than only the image. */
.card::before {
  content: "";
  position: absolute;
  inset: calc(var(--s-4) * -1);
  z-index: -1;
  border-radius: var(--r-lg);
  background: var(--panel);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}
.card:hover::before,
.card:focus-within::before { opacity: 1; }
.card__eyebrow { margin-bottom: var(--s-3); }
.card__title { font-size: var(--fs-h3); margin-bottom: var(--s-3); }
.card__title a { text-decoration: none; }
.card__title a::after { content: ""; position: absolute; inset: 0; }
.card__summary { font-size: var(--fs-small); }

/* Tag chips */
.tags { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-4); }
.tag {
  font: 400 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  padding: 0.45rem 0.7rem;
}

/* CTA band */
.cta { text-align: left; }
.cta h2 { max-width: 36ch; margin-bottom: var(--s-4); }
.cta__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }

/* ==========================================================================
   6 · WORK INDEX
   ========================================================================== */

.work-list { display: grid; gap: var(--s-9); }

.work-row {
  position: relative;
  isolation: isolate;
  display: grid;
  gap: var(--s-5);
  align-items: center;
}
@media (min-width: 860px) {
  .work-row { grid-template-columns: 1.15fr 1fr; gap: var(--s-8); }
  .work-row:nth-child(even) .work-row__media { order: 2; }
}
.work-row__media {
  position: relative;
  background: var(--panel);
  border: var(--border);
  border-radius: var(--r-lg);
  aspect-ratio: 8 / 5;
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease);
}
.work-row__media > img, .work-row__media > svg { width: 100%; height: 100%; object-fit: cover; }
.work-row:hover .work-row__media { border-color: var(--ink-4); }
/* On hover the whole row becomes a link to a case the reader can't open yet, so
   the blur answers the click before it happens. The lock badge beside the title
   carries the state at rest. */
.work-row--protected .work-row__scrim {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(241, 244, 246, 0.75);
  background: color-mix(in srgb, var(--panel) 75%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
  pointer-events: none;
}
.work-row--protected:hover .work-row__scrim,
.work-row--protected:focus-within .work-row__scrim { opacity: 1; }
.work-row__scrim svg {
  width: 40px;
  height: 40px;
  color: var(--ink);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.work-row::before {
  content: "";
  position: absolute;
  inset: calc(var(--s-6) * -1) calc(var(--s-4) * -1);
  z-index: -1;
  border-radius: var(--r-lg);
  background: var(--panel);
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease);
}
.work-row:hover::before,
.work-row:focus-within::before { opacity: 1; }

.work-row__title {
  display: block;
  font-size: var(--fs-h2);
  margin-block: var(--s-3) var(--s-4);
}
.work-row__title a { text-decoration: none; }
.work-row__title a::after { content: ""; position: absolute; inset: 0; }
/* The at-rest signal that a case is gated, since the scrim only appears on
   hover. Decorative in markup — the scrim carries the accessible name. */
.work-row__lock {
  display: inline-grid;
  place-items: center;
  width: 1.02em;
  height: 1.02em;
  margin-left: 0.3em;
  vertical-align: -0.02em;
  border-radius: var(--r-pill);
  background: var(--accent-wash);
  border: 1px solid var(--accent-edge);
  color: var(--accent-deep);
}
.work-row__lock svg {
  width: 0.62em;
  height: 0.62em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* An aside in voice, but it is argument rather than metadata — it does not
   belong at the metadata size. */
.note {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-3);
  font-style: italic;
  max-width: var(--measure-note);
}

/* ==========================================================================
   7 · CASE STUDY
   ========================================================================== */

.cs-hero { padding-block: var(--s-8) var(--s-7); }
.cs-hero__eyebrow { margin-bottom: var(--s-5); }
.cs-hero h1 { max-width: 34ch; margin-bottom: var(--s-3); }
/* The h1 names the thing; the subtitle says what was interesting about it. */
.cs-hero__sub {
  max-width: 38ch;
  margin-bottom: var(--s-5);
  font: 400 var(--fs-h2)/var(--lh-snug) var(--font-display);
  letter-spacing: var(--track-display);
  color: var(--ink);
}
.cs-hero__sub--wide { max-width: none; }
.cs-hero .shot {
  background: var(--panel-2);
  box-shadow: var(--shadow-2);
}
.cs-hero .shot--wide { margin-inline: auto; }
/* The glance strip is metadata for the hero shot, not a new section — drop the
   section padding so it reads as attached to it. */
.cs-hero + .section { padding-top: 0; }

/* At a glance */
.glance {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  padding-block: var(--s-6);
  border-block: var(--border);
  margin-block: var(--s-7);
}
.glance dt {
  font: 500 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--s-3);
}
.glance dd { font-size: var(--fs-small); color: var(--ink); margin: 0; }

/* Body prose */
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--s-5); }
/* The 2px rule does the dividing, so the gap above it does not have to. */
.prose h2 {
  margin-top: var(--s-8);
  padding-top: var(--s-5);
  border-top: 2px solid var(--ink);
  font-size: var(--fs-h2);
}
.prose h2:first-child { margin-top: 0; }
.prose h3 { margin-top: var(--s-7); }
.prose ul, .prose ol { display: grid; gap: var(--s-4); padding-left: 0; }
.prose li { position: relative; padding-left: var(--s-6); }
.prose ul li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.72em;
  width: 10px; height: 2px;
  background: var(--accent);
}
.prose ol { counter-reset: n; }
.prose ol li { counter-increment: n; }
.prose ol li::before {
  content: counter(n, decimal-leading-zero);
  position: absolute;
  left: 0; top: 0.15em;
  font: 700 var(--fs-meta)/1.6 var(--font-ui);
  color: var(--accent-deep);
}

/* Pull quote / key idea */
.keyline {
  margin-block: var(--s-7);
  padding: var(--s-2) 0 var(--s-2) var(--s-5);
  border-left: 2px solid var(--accent);
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 1.05rem + 0.9vw, 1.75rem);
  line-height: 1.32;
  letter-spacing: -0.008em;
  color: var(--ink);
  max-width: 48ch;
}

/* Tradeoff blocks */
.tradeoff {
  padding: var(--s-5);
  background: var(--panel);
  border-radius: var(--r-lg);
}
.tradeoff + .tradeoff { margin-top: var(--s-4); }
/* margin-top 0 or the card inherits the .prose h3 rhythm on top of its own padding */
.tradeoff h3 { font-size: 1.0625rem; margin-top: 0; margin-bottom: var(--s-3); }
.tradeoff p { font-size: var(--fs-small); }
.tradeoff__alt {
  display: block;
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--rule);
  font-size: var(--fs-meta);
  color: var(--ink-3);
}

/* Outcomes */
.outcomes {
  display: grid;
  gap: var(--s-6) var(--s-8);
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  margin-top: var(--s-6);
}
.outcome {
  padding-top: var(--s-4);
  border-top: 2px solid var(--accent);
}
.outcome__num {
  display: block;
  font: 700 clamp(1.3rem, 0.96rem + 1.08vw, 1.8rem)/1 var(--font-display);
  letter-spacing: -0.018em;
  color: var(--ink);
  margin-bottom: var(--s-3);
}
.outcome p { font-size: var(--fs-small); }

/* Next / prev nav */
.next-nav {
  display: grid;
  gap: var(--s-5);
  margin-top: var(--s-8);
  padding-block: var(--s-8);
  border-top: var(--border);
}
@media (min-width: 760px) {
  .next-nav { grid-template-columns: 1fr 1fr; }
  /* Keeps Next on the right even when there is no Previous beside it. */
  .next-nav__item--next { grid-column: 2; }
}
.next-nav__item { position: relative; }
.next-nav__item a { text-decoration: none; }
.next-nav__item a::after { content: ""; position: absolute; inset: 0; }
.next-nav__item h3 { margin-top: var(--s-3); font-size: var(--fs-h3); }
.next-nav__item--end { text-align: right; }

/* The whole item is the hit area (see the ::after overlay above), so the hover
   state has to fire from the item, not from the link. */
.next-nav__item .eyebrow,
.next-nav__item h3 a {
  transition: color var(--dur-1) var(--ease);
}
.next-nav__item:hover h3 a,
.next-nav__item:hover .eyebrow,
.next-nav__item:focus-within h3 a,
.next-nav__item:focus-within .eyebrow { color: var(--accent); }

/* Arrows are generated content so a screen reader never announces them as
   "leftwards arrow" on top of the word Previous. */
.next-nav__item--prev .eyebrow::before,
.next-nav__item--next .eyebrow::after {
  display: inline-block;
  transition: transform var(--dur-2) var(--ease-out);
}
.next-nav__item--prev .eyebrow::before { content: "\2190"; margin-right: 0.6em; }
.next-nav__item--next .eyebrow::after  { content: "\2192"; margin-left: 0.6em; }
.next-nav__item--prev:hover .eyebrow::before { transform: translateX(-4px); }
.next-nav__item--next:hover .eyebrow::after  { transform: translateX(4px); }

/* Reading progress — the one live, moving thing on the page, so it gets the
   ginger. The spark is used once per page or not at all. */
.progress {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  width: 100%;
  transform-origin: 0 50%;
  transform: scaleX(0);
  background: var(--spark);
  z-index: 120;
}

/* ==========================================================================
   8 · FIGURES & DIAGRAMS
   ========================================================================== */

/* Top margin stays under the h3 step so a figure reads as attached to the
   paragraph that introduces it, not as a division of its own. */
.shot {
  margin-block: var(--s-6);
  background: var(--panel);
  border: var(--border);
  border-radius: var(--r-lg);
  padding: clamp(1rem, 0.4rem + 2.4vw, 2rem);
}
.shot--plain { background: none; border: 0; padding: 0; }
.shot--stack { display: grid; gap: var(--s-5); }
.shot > img, .shot > svg, .shot > video { width: 100%; border-radius: var(--r-sm); }

/* Two supporting shots that make one point. Half width each, so they read as
   evidence rather than as two figures competing for the argument. */
.shot--pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-5); align-items: start; }
.shot--pair > figcaption { grid-column: 1 / -1; }
@media (max-width: 720px) {
  .shot--pair { grid-template-columns: 1fr; }
}

/* Pointer pins reveal the real controls that connect the shipped journey. */
.journey-step__image { position: relative; }
.journey-clicker {
  position: absolute;
  width: clamp(36px, 5vw, 60px);
  aspect-ratio: 1;
  pointer-events: none;
}
.journey-clicker img {
  display: block;
  width: 100%;
  filter: drop-shadow(0 2px 2px rgb(22 21 19 / 0.5));
}
.journey-clicker--fleet { left: 94.5%; top: 64%; }
.journey-clicker--journey { left: 78%; top: calc(87.3% + 10px); }
.journey-clicker--devices { left: 94.5%; top: calc(35% + 10px); }
@media (max-width: 520px) {
  .journey-clicker--fleet,
  .journey-clicker--devices { transform: translateX(-25%); }
}

/* Injected by main.js around any oversized figure image. Transparent — its only
   job is to make the image a real, focusable control. */
.shot__zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}
.shot__zoom > img { display: block; width: 100%; border-radius: var(--r-sm); }

/* A captured plugin panel is a 1× artifact. Upscaling it past its own pixels
   makes it look like a mockup of a screenshot, so cap it at native width. */
.shot--capture { display: grid; justify-items: start; }
.shot--capture > img { width: auto; max-width: 100%; }

/* Wide figure inside a .prose column; keep its left edge on the reading grid. */
.shot--wide { max-width: none; width: min(100%, 1100px); }
@media (min-width: 1180px) {
  .prose .shot--wide { width: 1100px; margin-left: 0; }
}

/* No character cap: the figure already sets the measure, so a caption under a
   wide figure runs the full width of the thing it describes. */
.shot figcaption {
  margin-top: var(--s-4);
  font-size: var(--fs-small);
  line-height: 1.55;
  color: var(--ink-3);
}
.shot figcaption > * + * { margin-top: var(--s-3); }
.shot figcaption b { color: var(--ink-2); font-weight: 600; }

/* Placeholder for assets not yet produced */
.shot--placeholder {
  display: grid;
  place-content: center;
  gap: var(--s-3);
  min-height: 260px;
  text-align: center;
  border-style: dashed;
  border-color: var(--ink-4);
}
.shot--placeholder .ph__id {
  font: 500 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--accent-deep);
}
.shot--placeholder .ph__what {
  font: 600 1.05rem/1.4 var(--font-ui);
  color: var(--ink);
  max-width: 40ch;
}
.shot--placeholder .ph__spec { font-size: var(--fs-meta); color: var(--ink-3); }

/* Three-up sequence. Sibling artifacts that read as one beat rather than three
   separate figures — used where the progression matters more than any one shot. */
.trio { display: grid; gap: var(--s-4); }
@media (min-width: 760px) {
  .trio { grid-template-columns: repeat(3, 1fr); }
  .trio--pair { grid-template-columns: repeat(2, 1fr); }
}
.trio__item {
  display: grid;
  align-content: start;
  gap: var(--s-3);
  padding: var(--s-4);
  background: var(--panel);
  border: var(--border);
  border-radius: var(--r-lg);
}
/* Captured panels differ in aspect. Contain rather than crop — the counts and
   verdicts are the point, and a crop would eat them — and let the shared ratio
   do the work of making three unlike shots read as a set. */
.trio__item > img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: contain;
  border-radius: var(--r-sm);
}
.trio--native-media { gap: var(--s-2); }
.trio--native-media .trio__item { padding: var(--s-2); }
.trio--native-media .trio__item > img {
  aspect-ratio: auto;
  height: auto;
  object-fit: initial;
}
.trio__step {
  font: 500 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--accent-deep);
}
.trio__name { font: 600 1rem/1.35 var(--font-ui); color: var(--ink); }
.trio__note { font-size: var(--fs-meta); line-height: 1.5; color: var(--ink-3); }

/* Document excerpt. Deliberately unhighlighted — this is a quoted artifact,
   not a code sample to copy. */
.excerpt {
  margin-block: var(--s-6);
  background: var(--panel);
  border: var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.excerpt__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: baseline;
  justify-content: space-between;
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--rule);
}
.excerpt__file { font: 500 var(--fs-meta)/1.4 var(--font-mono); color: var(--ink-2); }
.excerpt__tag {
  font: 500 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-3);
}
.excerpt pre {
  margin: 0;
  padding: var(--s-5);
  overflow-x: auto;
  font: 400 var(--fs-small)/1.7 var(--font-mono);
  color: var(--ink-2);
  white-space: pre-wrap;
  tab-size: 2;
}
.excerpt pre b { color: var(--ink); font-weight: 600; }
.shot .excerpt { margin-block: 0; }

/* A prompt is the agent's interface the way a panel was the plugin's. */
.prompts { display: grid; gap: var(--s-3); margin: 0; padding: 0; list-style: none; }
.prompts li {
  padding: var(--s-4);
  background: var(--paper);
  border: 1px solid var(--accent-edge);
  border-radius: var(--r-sm);
  font: 400 var(--fs-small)/1.6 var(--font-mono);
  color: var(--ink-2);
}
/* Customer voice. Evidence, not ornament — no oversized quotation marks, and
   the attribution carries as much weight as the sentence. */
.voices { display: grid; gap: var(--s-5); margin-block: var(--s-6); }
.voice { padding-left: var(--s-5); border-left: 3px solid var(--accent); }
.voice p {
  font: 400 var(--fs-lead)/1.5 var(--font-display);
  letter-spacing: var(--track-display);
  color: var(--ink);
  max-width: 46ch;
}
.voice footer {
  margin-top: var(--s-3);
  font: 500 var(--fs-eyebrow)/1.4 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Data table */
.table-wrap { overflow-x: auto; margin-block: var(--s-6); }
.data-table { font-size: var(--fs-small); min-width: 560px; }
.data-table th, .data-table td {
  text-align: left;
  padding: var(--s-4) var(--s-4) var(--s-4) 0;
  border-bottom: var(--border);
  vertical-align: top;
}
.data-table th {
  font: 500 var(--fs-eyebrow)/1.5 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom-color: var(--ink);
}
.data-table td strong { color: var(--ink); }
/* Opt-in: only for tables whose widest row fits the desktop content column on one line. */
@media (min-width: 1000px) {
  .data-table--nowrap th,
  .data-table--nowrap td { white-space: nowrap; }
  .data-table--nowrap th:first-child,
  .data-table--nowrap td:first-child { padding-right: var(--s-5); }
  .data-table--nowrap th:last-child,
  .data-table--nowrap td:last-child { padding-right: 0; }
}

/* ==========================================================================
   9 · ABOUT / CRAFT / RÉSUMÉ / CONTACT
   ========================================================================== */

.portrait {
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--panel);
  /* Source is 960×640; trim 190px of empty ceiling and the bottom 1/5 of floor,
     leaving 960×322. The 59.75% keeps that window in place. */
  aspect-ratio: 480 / 161;
}
.portrait .shot__zoom { height: 100%; }
.portrait img { width: 100%; height: 100%; object-fit: cover; object-position: center 59.75%; display: block; }
.portrait figcaption {
  margin-top: var(--s-3);
  font-size: var(--fs-meta);
  color: var(--ink-3);
}

/* --------------------------------------------------------------------------
   Illustrations
   Her own vector drawings, carried over from the previous site. One world,
   one character, five scenes. They are the only thing on this site that
   nobody else could have, so they are used deliberately and not often.
   -------------------------------------------------------------------------- */
.illus {
  display: block;
  width: 100%;
  height: auto;
  background: var(--panel-2);
  border-radius: var(--r-lg);
}

/* Full-bleed signature band — one per page, at most. */
.illus-band {
  margin: 0;
  background: var(--panel-2);
  overflow: hidden;
}
.illus-band img {
  display: block;
  width: 100%;
  height: auto;
  /* The lamp cord runs off the top edge and the floor below the shadow is
     empty, so a crop is only ever safe from the bottom. The 26vw floor keeps
     that crop under 7%, which stops the viewport height eating her feet. */
  max-height: max(26vw, 46vh);
  object-fit: cover;
  object-position: center top;
}

/* Scene + caption pairs, e.g. "off the clock" */
.scenes {
  display: grid;
  gap: var(--s-6);
  margin-top: var(--s-6);
}
@media (min-width: 780px) { .scenes { grid-template-columns: repeat(3, 1fr); } }
.scene { margin: 0; }
.scene img {
  display: block;
  width: 100%;
  height: auto;
  background: var(--panel-2);
  border-radius: var(--r-lg);
}
.scene figcaption {
  margin-top: var(--s-4);
  font-size: var(--fs-small);
  color: var(--ink-2);
}
.scene figcaption b {
  display: block;
  font: 500 var(--fs-eyebrow)/1 var(--font-mono);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--s-3);
}

/* A single list with a bold lead-in: the eye runs down the labels, not across a grid. */
.principles { display: grid; gap: var(--s-4); font-size: var(--fs-small); color: var(--ink-2); }
.principles b { color: var(--ink); font-weight: 600; }
.principle { padding-top: var(--s-4); border-top: var(--border); }
/* The rule separates items; the first one has nothing above it to separate from. */
.principle:first-child { padding-top: 0; border-top: 0; }
.principle h3 { font-size: 1.0625rem; margin-bottom: var(--s-3); }
.principle p { font-size: var(--fs-small); }

/* Craft */
.play-grid {
  columns: 1;
  column-gap: var(--s-5);
}
@media (min-width: 620px) { .play-grid { columns: 2; } }
@media (min-width: 1000px) { .play-grid { columns: 3; } }
.play-item {
  break-inside: avoid;
  margin-bottom: var(--s-5);
}
@media (min-width: 1000px) {
  .play-grid[data-masonry] {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-auto-flow: dense;
    grid-auto-rows: 1px;
    row-gap: 0;
  }
  .play-item--wide {
    grid-column: span 2;
  }
  .play-item--baking { grid-column: 1; }
  .play-item--interface-left { grid-column: 1; }
  .play-item--systems { grid-column: 2; }
  .play-item--interface-middle { grid-column: 2; }
  .play-item--bass,
  .play-item--systems-tail,
  .play-item--interface-right { grid-column: 3; }
  .play-item--motion-side { grid-column: 3; }
}
.play-item__media {
  display: block;
  width: 100%;
  padding: 0;
  border: var(--border);
  background: var(--panel);
  border-radius: var(--r-md);
  overflow: hidden;
  cursor: zoom-in;
  transition: border-color var(--dur-2) var(--ease);
}
.play-item__media img { display: block; width: 100%; height: auto; }
.play-item__media--tea { aspect-ratio: 5 / 4; }
.play-item__media--tea picture { display: block; height: 100%; }
.play-item__media--tea img { height: 100%; object-fit: cover; object-position: bottom; }
.play-item__media:hover { border-color: var(--ink-4); }
/* Motion clips play in place, so they are the media box rather than a trigger for it. */
.play-item__media--video { cursor: auto; }
.play-item__cap { margin-top: var(--s-3); font: 400 var(--fs-meta)/1.5 var(--font-mono); color: var(--ink-3); }

/* Résumé */
.resume-grid { display: grid; gap: var(--s-8); }
@media (min-width: 960px) { .resume-grid { grid-template-columns: 280px minmax(0, 1fr); } }
.resume-aside { position: sticky; top: calc(var(--header-h) + var(--s-5)); align-self: start; }
.resume-section + .resume-section { margin-top: var(--s-8); }
.resume-section > h2 {
  font-size: var(--fs-h3);
  padding-bottom: var(--s-3);
  border-bottom: 2px solid var(--ink);
  margin-bottom: var(--s-5);
}
.resume-job + .resume-job { margin-top: var(--s-6); }
.resume-job__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-3); }
.resume-job__title { font: 700 1.0625rem/1.3 var(--font-ui); color: var(--ink); }
.resume-job__org { font-weight: 600; color: var(--ink-2); }
.resume-job__dates { font: 400 var(--fs-meta)/1.3 var(--font-mono); color: var(--ink-3); margin-left: auto; }
.resume-job__context { font-size: var(--fs-meta); color: var(--ink-3); margin-block: var(--s-2) var(--s-4); }
.resume-job ul { display: grid; gap: var(--s-3); font-size: var(--fs-small); }
.resume-job li { position: relative; padding-left: var(--s-5); }
.resume-job li::before {
  content: ""; position: absolute; left: 0; top: 0.66em;
  width: 8px; height: 2px; background: var(--accent);
}
.kv { display: grid; gap: var(--s-2); font-size: var(--fs-small); }
.kv dt { font-weight: 600; color: var(--ink); }
.kv dd { margin: 0; color: var(--ink-2); }

/* Contact */

/* The copy and illustration share a row so the contact header stays compact. */
.contact-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--s-7);
}
.contact-head__copy { align-self: start; }
.contact-head__copy h1 { max-width: 19ch; }

/* The lean. The character sits flush on the seam where the page turns gray —
   the same move as the 2010–2024 site. The gray is the next section's own
   .section--panel background, not something baked into the artwork, so the two
   can never drift out of match when a token changes. --panel is used rather
   than --paper-2 because a 2% step off white doesn't read as a surface, and
   she has to be leaning on something. She is in normal flow, so she cannot
   land on top of the form at any width. */
.contact-lean {
  display: flex;
  justify-content: flex-end;
}
.contact-lean img {
  display: block;
  width: clamp(190px, 26vw, 321px);
  height: auto;
  /* Her elbows and the mug rest on the line; her hands hang 12 of the artwork's
     229 rows past it. That overhang is the lean. A percentage translate
     resolves against the element's own height, so it holds at any width, and
     transforms don't affect layout, so the overhang spills into the gray
     instead of pushing the section open. */
  transform: translateY(5.24%);
}

@media (max-width: 700px) {
  .contact-head { grid-template-columns: 1fr; gap: var(--s-5); }
  .contact-lean { justify-content: center; }
}

/* One column, now that the social links live in the footer instead. Capped so
   the fields don't run the full width of the shell and become hard to scan. */
.contact-single { max-width: var(--measure); }

/* The lean already marks the seam, so the form doesn't need a full section of
   air above it — and this is the one page where getting to the fields without
   scrolling matters more than the rhythm. */
.contact-panel { padding-top: var(--s-6); }

.field { display: grid; gap: var(--s-2); }
.field + .field { margin-top: var(--s-5); }
.field label { font: 600 var(--fs-small)/1 var(--font-ui); color: var(--ink); }
.field input, .field textarea {
  font: inherit;
  font-size: var(--fs-small);
  color: var(--ink);
  padding: var(--s-3) var(--s-4);
  background: var(--paper);
  border: var(--border);
  border-radius: var(--r-md);
  width: 100%;
}
.field input:focus, .field textarea:focus { border-color: var(--accent); }
.field textarea { min-height: 140px; resize: vertical; }
/* Error is carried by color AND wording, never color alone. --spark-deep is
   5.2:1, so it is legible as text; the bare --spark is not. */
.field__error { font-size: var(--fs-meta); color: var(--spark-deep); }
.field__error[hidden] { display: none; }
.field input[aria-invalid="true"] { border-color: var(--spark-deep); }
.field input[aria-invalid="true"]:focus { border-color: var(--spark-deep); }

.gate-form { max-width: 22rem; }

/* Honeypot. Off-canvas rather than display:none, because some bots skip
   fields they can tell are hidden. Nothing here is reachable by keyboard
   or announced by a screen reader. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.contact-form { position: relative; }
.form-status {
  margin-bottom: var(--s-5);
  padding: var(--s-4) var(--s-5);
  border-left: 2px solid var(--accent);
  background: var(--accent-wash);
  font-size: var(--fs-small);
  color: var(--ink);
}
.form-status[hidden] { display: none; }
.form-status[data-state="error"] { border-left-color: var(--spark); background: var(--panel); }

/* Lightbox (built by main.js, styled here) */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-4);
  padding: clamp(1rem, 0.5rem + 3vw, 3rem);
  background: rgba(17, 19, 21, 0.94);
}
.lightbox[hidden] { display: none; }
.lightbox__figure { min-width: 0; text-align: center; }
.lightbox__img {
  max-width: 100%;
  max-height: 78vh;
  margin-inline: auto;
  border-radius: var(--r-md);
  cursor: zoom-in;
}
.lightbox--zoom .lightbox__figure {
  overflow: auto;
  max-height: 88vh;
  overscroll-behavior: contain;
}
.lightbox--zoom .lightbox__img {
  max-width: none;
  max-height: none;
  cursor: zoom-out;
}
.lightbox--zoom .lightbox__cap { display: none; }
.lightbox__cap {
  margin-top: var(--s-4);
  font-size: var(--fs-meta);
  color: var(--ink-4);
}
.lightbox__tools {
  position: absolute;
  top: var(--s-5); right: var(--s-5);
  display: flex;
  gap: var(--s-3);
}
.lightbox__btn {
  padding: var(--s-3) var(--s-4);
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: var(--r-md);
  color: #fff;
  font: 600 var(--fs-meta)/1 var(--font-ui);
}
.lightbox__nav {
  width: 48px; height: 48px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.24);
  color: #fff;
  font-size: 1.25rem;
}
.lightbox__nav[hidden] { display: none; }
.lightbox__btn:hover, .lightbox__nav:hover { background: rgba(255, 255, 255, 0.12); }
.lightbox :focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* ==========================================================================
   10 · UTILITIES
   ========================================================================== */

.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;
}

.mt-0 { margin-top: 0 !important; }
.mt-5 { margin-top: var(--s-5); }
.mt-7 { margin-top: var(--s-7); }
.measure { max-width: var(--measure); }

/* Reveal on scroll — opt-in, and a no-op when motion is reduced */
.reveal { opacity: 0; transform: translateY(14px); }
.reveal[data-shown="true"] {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
}
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; }
}
.no-js .reveal { opacity: 1; transform: none; }

/* ==========================================================================
   11 · PRINT
   ========================================================================== */

@media print {
  .site-header, .site-footer, .next-nav, .progress, .hero__art, .contact-lean, .nav { display: none !important; }
  body { color: #000; font-size: 11pt; }
  a { text-decoration: none; }
  .prose { max-width: none; }
  .shot { break-inside: avoid; border: 1px solid #ccc; }
  .section { padding-block: 1rem; }
}
