/* ===========================================================================
   mina zavary — portfolio
   one stylesheet. tokens at the top; change those and the whole site moves.
   =========================================================================== */

/* --- tokens --------------------------------------------------------------- */

:root {
  /* surfaces */
  --paper: #faf7f0;
  --paper-2: #f4efe4;
  --card: #fffdf8;
  --line: #e6ded0;

  /* text */
  --ink: #2e2a26;
  --ink-2: #6b6058;
  --muted: #94897e;

  /* category accents — these carry the whole identity */
  --research: #7f9a80;
  --research-soft: #e4ece2;
  --design: #c4805f;
  --design-soft: #f6e6da;
  --teaching: #8b93c4;
  --teaching-soft: #e6e7f4;

  /* type */
  --display: 'Fraunces', 'Iowan Old Style', Georgia, serif;
  --body: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* rhythm */
  --gutter: clamp(1.25rem, 5vw, 5rem);
  --measure: 62ch;
  --radius: 18px;
  --radius-lg: 28px;

  /* the drifting hero blobs — much softer on a dark background, where they
     read as light haze rather than a tint */
  --blob-a: 0.28;
  --blob-b: 0.4;
  --blob-c: 0.2;
  /* the sage blob reads greener than the category colour, which is flattened
     by the blur — this is only used for that blob */
  --blob-green: #6d9c73;

  /* the page-level colour washes. one place to turn them up or down. */
  --wash-sage: color-mix(in srgb, var(--blob-green) 30%, transparent);
  --wash-clay: color-mix(in srgb, var(--design) 24%, transparent);
  --wash-peri: color-mix(in srgb, var(--teaching) 22%, transparent);

  /* motion */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 0.55s;
  --dur-fast: 0.25s;
}

/* dark tokens live in one place and get applied by two selectors below:
   the system preference, and an explicit choice from the toggle. */
@media (prefers-color-scheme: dark) {
  /* system is dark AND the visitor hasn't explicitly asked for light */
  :root:not([data-theme='light']) {
    --paper: #17151a;
    --paper-2: #1e1b22;
    --card: #201d26;
    --line: #322d3a;

    --ink: #f2ece4;
    --ink-2: #b9b0a8;
    --muted: #8d8379;

    --research: #9dbb9e;
    --research-soft: #26302a;
    --design: #dda07f;
    --design-soft: #33261f;
    --teaching: #a9b0e0;
    --teaching-soft: #26273a;

    --blob-a: 0.13;
    --blob-b: 0.2;
    --blob-c: 0.09;
    --blob-green: #7fb488;

    --wash-sage: color-mix(in srgb, var(--blob-green) 11%, transparent);
    --wash-clay: color-mix(in srgb, var(--design) 9%, transparent);
    --wash-peri: color-mix(in srgb, var(--teaching) 9%, transparent);
  }
}

/* the visitor explicitly chose dark, whatever their system says */
:root[data-theme='dark'] {
  --paper: #17151a;
  --paper-2: #1e1b22;
  --card: #201d26;
  --line: #322d3a;

  --ink: #f2ece4;
  --ink-2: #b9b0a8;
  --muted: #8d8379;

  --research: #9dbb9e;
  --research-soft: #26302a;
  --design: #dda07f;
  --design-soft: #33261f;
  --teaching: #a9b0e0;
  --teaching-soft: #26273a;

  --blob-a: 0.13;
  --blob-b: 0.2;
  --blob-c: 0.09;
  --blob-green: #7fb488;

  --wash-sage: color-mix(in srgb, var(--blob-green) 11%, transparent);
  --wash-clay: color-mix(in srgb, var(--design) 9%, transparent);
  --wash-peri: color-mix(in srgb, var(--teaching) 9%, transparent);
}

/* --- reset ---------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* load-bearing: without this, body's background propagates to the canvas
     and gets sized to the viewport, which pins the colour washes to the
     screen instead of letting them run down the document. */
  background: var(--paper);
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

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

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
h4 {
  font-family: var(--display);
  font-weight: 400;
  font-variation-settings: 'SOFT' 40, 'WONK' 1, 'opsz' 40;
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0 0 1em;
  text-wrap: pretty;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

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

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

.wrap {
  width: 100%;
  max-width: 1180px;
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: clamp(4rem, 9vw, 8rem);
}

.section--tight {
  padding-block: clamp(2.5rem, 6vw, 5rem);
}

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

/* --- skip link ------------------------------------------------------------ */

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: 0.75rem 1.25rem;
  border-radius: 0 0 12px 0;
}

.skip:focus {
  left: 0;
}

/* --- nav ------------------------------------------------------------------ */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  backdrop-filter: saturate(180%) blur(14px);
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-fast) var(--ease);
}

.nav.is-stuck {
  border-bottom-color: var(--line);
}

.nav__inner {
  display: flex;
  /* the mark is a serif and the links a sans, at different sizes — centring
     their boxes leaves the baselines ~1.5px apart. align the text properly. */
  align-items: baseline;
  justify-content: space-between;
  /* same gap the nav links use, so the toggle sits at a consistent distance */
  gap: clamp(0.75rem, 2.5vw, 2rem);
  padding-block: 1.1rem;
}

.nav__mark {
  font-family: var(--display);
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 20;
}

.nav__mark span {
  color: var(--muted);
}

.nav__links {
  display: flex;
  gap: clamp(0.75rem, 2.5vw, 2rem);
  font-size: 0.95rem;
  color: var(--ink-2);
  /* the toggle is a third flex child, so without this space-between would
     strand the links in the middle of the bar */
  margin-left: auto;
}

.nav__links a {
  position: relative;
  padding-block: 0.2rem;
  transition: color var(--dur-fast) var(--ease);
}

.nav__links a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  height: 1px;
  width: 100%;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--dur-fast) var(--ease);
}

.nav__links a:hover,
.nav__links a[aria-current='page'] {
  color: var(--ink);
}

.nav__links a:hover::after,
.nav__links a[aria-current='page']::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* --- theme toggle --------------------------------------------------------- */

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
  flex: none;
  transition: color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.theme-toggle:hover {
  color: var(--design);
  border-color: color-mix(in srgb, var(--design) 45%, var(--line));
  transform: scale(1.12) rotate(-12deg);
}

.theme-toggle svg {
  width: 13px;
  height: 13px;
  display: block;
}

/* show the sun in dark mode (click to go light) and the moon in light mode */
.theme-toggle .icon-sun {
  display: none;
}

:root[data-theme='dark'] .theme-toggle .icon-sun {
  display: block;
}
:root[data-theme='dark'] .theme-toggle .icon-moon {
  display: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .theme-toggle .icon-sun {
    display: block;
  }
  :root:not([data-theme='light']) .theme-toggle .icon-moon {
    display: none;
  }
}

/* the mark and the links start crowding each other on small phones */
@media (max-width: 430px) {
  .nav__mark {
    font-size: 1rem;
  }

  .nav__links {
    font-size: 0.85rem;
    gap: 0.7rem;
  }
}

/* --- section rail --------------------------------------------------------- */

.rail {
  position: fixed;
  right: clamp(0.9rem, 2.2vw, 2rem);
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  align-items: flex-end;
}

.rail__item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--muted);
  font-size: 0.76rem;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 0.15rem 0;
}

.rail__label {
  opacity: 0;
  transform: translateX(6px);
  transition: opacity var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
  white-space: nowrap;
  pointer-events: none;
}

.rail__item:hover .rail__label,
.rail__item:focus-visible .rail__label,
.rail__item[aria-current='true'] .rail__label {
  opacity: 1;
  transform: none;
}

.rail__item[aria-current='true'] .rail__label {
  color: var(--design);
}

.rail__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--line);
  flex: none;
  transition: background var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.rail__item:hover .rail__dot {
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--design) 60%, var(--line));
}

.rail__item[aria-current='true'] .rail__dot {
  background: var(--design);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--design) 14%, transparent);
  transform: scale(1.25);
}

/* not enough room beside the content on narrow screens */
@media (max-width: 1080px) {
  .rail {
    display: none;
  }
}

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

.hero {
  position: relative;
  padding-block: clamp(3.5rem, 11vw, 8.5rem) clamp(3rem, 7vw, 6rem);
  overflow: hidden;
}

/* the slow-drifting soft blobs behind the headline */
.blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
  animation: drift 26s ease-in-out infinite alternate;
}

.blob--a {
  opacity: var(--blob-a);
  width: 42vw;
  height: 42vw;
  max-width: 520px;
  max-height: 520px;
  background: var(--design);
  top: -12%;
  right: -6%;
}

.blob--b {
  opacity: var(--blob-b);
  width: 34vw;
  height: 34vw;
  max-width: 420px;
  max-height: 420px;
  background: var(--blob-green);
  bottom: -18%;
  left: -8%;
  animation-duration: 34s;
  animation-direction: alternate-reverse;
}

.blob--c {
  opacity: var(--blob-c);
  width: 24vw;
  height: 24vw;
  max-width: 300px;
  max-height: 300px;
  background: var(--teaching);
  top: 34%;
  right: 24%;
  animation-duration: 41s;
}

@keyframes drift {
  0% {
    transform: translate3d(0, 0, 0) scale(1);
  }
  50% {
    transform: translate3d(3%, -5%, 0) scale(1.09);
  }
  100% {
    transform: translate3d(-4%, 4%, 0) scale(0.95);
  }
}

/* overflow:hidden slices the blurred blobs off in a hard horizontal line at the
   section boundary. this fades them into the page background instead. */
.hero::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(90px, 18vh, 200px);
  background: linear-gradient(to bottom, transparent, var(--paper));
  pointer-events: none;
  z-index: 0;
}

.hero__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.22fr) minmax(0, 0.78fr);
  gap: clamp(1.5rem, 5vw, 4rem);
  align-items: center;
}

/* a hero with no photo beside it — one full-width column */
.hero__inner--solo {
  grid-template-columns: 1fr;
}

@media (max-width: 880px) {
  .hero__inner {
    grid-template-columns: 1fr;
  }

  .hero__photo {
    max-width: 300px;
    order: -1; /* portrait first on a phone, before the wall of text */
  }
}

.hero__photo {
  width: 100%;
}

.hero h1 {
  font-size: clamp(2.4rem, 5.6vw, 4.4rem);
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 100;
  max-width: 22ch;
  margin-bottom: 1.5rem;
}

/* the greeting owns its own line, so the three roles always start on the
   next one rather than dragging "phd candidate," up beside "mina," */
.hero h1 .greet {
  display: block;
}

.hero h1 em {
  font-style: italic;
  color: var(--design);
}

/* fraunces draws the right single quote as a raised comma — at display size
   that reads as punctuation rather than an apostrophe. sharpen it (drop the
   softness and the wonky alternate), shrink it, and lift it off the baseline.
   to try a different look, adjust this one rule:
     rounder  -> 'SOFT' 60
     quirkier -> 'WONK' 1
     higher   -> raise the vertical-align value */
.hero h1 .apos {
  font-variation-settings: 'SOFT' 0, 'WONK' 0, 'opsz' 144;
  font-size: 0.78em;
  vertical-align: 0.1em;
  letter-spacing: 0;
}

/* the three roles pick up the same colors as the work filters below */
.hero h1 em.role-research {
  color: var(--research);
}
.hero h1 em.role-design {
  color: var(--design);
}
.hero h1 em.role-teaching {
  color: var(--teaching);
}

.hero__lede {
  font-size: clamp(1.05rem, 0.98rem + 0.5vw, 1.3rem);
  color: var(--ink-2);
  max-width: 54ch;
  margin-bottom: 2rem;
}

/* a deliberate break between the two lede sentences — closer than a full
   paragraph gap, but a clear pause */
.hero__lede--tight {
  margin-bottom: 0.9rem;
}

/* the two-identity strip — the "at a glance" promise */
.identities {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 2.25rem;
}

.identity {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.95rem;
  border-radius: 999px;
  font-size: 0.9rem;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink-2);
}

.identity::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--dot, var(--muted));
}

.identity[data-kind='research'] {
  --dot: var(--research);
}
.identity[data-kind='design'] {
  --dot: var(--design);
}
.identity[data-kind='teaching'] {
  --dot: var(--teaching);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: center;
}

/* --- buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 1.4rem;
  border-radius: 999px;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--body);
  font-size: 0.95rem;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.btn:hover {
  transform: translateY(-2px);
}

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--line);
}

.btn--ghost:hover {
  border-color: var(--ink);
}

.btn__arrow {
  transition: transform var(--dur-fast) var(--ease);
}

.btn:hover .btn__arrow {
  transform: translateX(3px);
}

/* --- section headings ----------------------------------------------------- */

.eyebrow {
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.9rem;
  display: block;
}

.section__head {
  margin-bottom: clamp(1.75rem, 4vw, 3rem);
}

.section__head h2 {
  font-size: clamp(1.9rem, 4vw, 3rem);
  margin-bottom: 0.75rem;
}

.section__head p {
  color: var(--ink-2);
  max-width: var(--measure);
  margin: 0;
}

/* --- research statement --------------------------------------------------- */

.statement {
  /* slightly translucent so the section's colour wash reads through the panel
     instead of being hidden behind it */
  background: color-mix(in srgb, var(--paper-2) 80%, transparent);
  border-radius: var(--radius-lg);
  padding: clamp(1.75rem, 5vw, 3.5rem);
}

.statement__lead {
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.6vw, 2rem);
  line-height: 1.3;
  font-variation-settings: 'SOFT' 50, 'WONK' 1, 'opsz' 40;
  margin-bottom: 1.5rem;
  max-width: 26ch;
}

.statement__body {
  columns: 2;
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
  color: var(--ink-2);
}

.statement__body p:last-child {
  margin-bottom: 0;
}

@media (max-width: 720px) {
  .statement__body {
    columns: 1;
  }
}

/* the contexts where measurement decides access — a scannable tile row that
   replaces a paragraph of examples */
.contexts {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  margin-block: 1.5rem 2rem;
}

.context {
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 0.9rem 1rem;
  background: var(--card);
  transition: border-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.context:hover {
  border-color: color-mix(in srgb, var(--research) 45%, var(--line));
  transform: translateY(-2px);
}

.context__k {
  font-family: var(--display);
  font-size: 1.02rem;
  line-height: 1.2;
  margin-bottom: 0.25rem;
}

.context__v {
  font-size: 0.82rem;
  color: var(--muted);
  line-height: 1.45;
}

.context--now {
  border-color: color-mix(in srgb, var(--research) 55%, var(--line));
  background: var(--research-soft);
}

.context--now .context__v {
  color: color-mix(in srgb, var(--research) 75%, var(--ink));
}

/* generic pill row, used for methods */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-block: 0.75rem 0;
}

.chip {
  font-size: 0.85rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.35rem 0.85rem;
  color: var(--ink-2);
  background: var(--card);
}

/* short scannable list used on the homepage + about, tighter than .prose ul */
.points {
  display: grid;
  gap: 0.7rem;
  margin-block: 1.25rem;
}

.points li {
  position: relative;
  padding-left: 1.35rem;
  color: var(--ink-2);
  font-size: 0.97rem;
}

.points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent, var(--research));
}

.points strong {
  color: var(--ink);
  font-weight: 500;
}

.questions {
  margin-top: 2rem;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
}

.question {
  border-left: 2px solid var(--research);
  padding-left: 1.1rem;
  font-size: 0.98rem;
  color: var(--ink-2);
}

.question strong {
  display: block;
  font-family: var(--display);
  font-size: 1.05rem;
  color: var(--ink);
  font-weight: 400;
  margin-bottom: 0.35rem;
}

/* the design thread through the research — clay-accented so a ux reader can
   see it at a glance against the sage of the research questions above */
.implications {
  margin-top: 2rem;
  padding: clamp(1.25rem, 3vw, 2rem);
  border-radius: var(--radius);
  background: var(--design-soft);
  border: 1px solid color-mix(in srgb, var(--design) 24%, transparent);
}

.implications .eyebrow {
  color: color-mix(in srgb, var(--design) 78%, var(--ink));
  margin-bottom: 0.4rem;
}

.implications__line {
  font-family: var(--display);
  font-size: clamp(1.15rem, 2vw, 1.5rem);
  line-height: 1.35;
  font-variation-settings: 'SOFT' 50, 'WONK' 1, 'opsz' 40;
  color: var(--ink);
  max-width: 34ch;
  margin: 0;
}

/* --- filters -------------------------------------------------------------- */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 2rem;
  align-items: center;
}

.filter {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1.05rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-2);
  font-family: var(--body);
  font-size: 0.92rem;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease);
}

.filter::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--dot, var(--muted));
  transition: transform var(--dur-fast) var(--ease);
}

.filter[data-filter='research'] {
  --dot: var(--research);
}
.filter[data-filter='design'] {
  --dot: var(--design);
}
.filter[data-filter='teaching'] {
  --dot: var(--teaching);
}
.filter[data-filter='all'] {
  --dot: var(--ink-2);
}

.filter:hover {
  border-color: var(--ink-2);
  color: var(--ink);
}

.filter[aria-pressed='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.filter[aria-pressed='true']::before {
  transform: scale(1.35);
}

.filter__count {
  font-size: 0.8rem;
  opacity: 0.6;
  font-variant-numeric: tabular-nums;
}

/* --- work feed ------------------------------------------------------------ */

.feed {
  display: grid;
  gap: clamp(1rem, 2.2vw, 1.5rem);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
}

/* pinned to three on desktop so the running order of the cards is predictable
   — the third item is reliably the last one in the first row */
@media (min-width: 1000px) {
  .feed {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.25rem, 2.6vw, 1.75rem);
  overflow: hidden;
  transition: transform var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

/* `display: flex` above beats the browser's own [hidden] { display: none },
   so filtering out a card did nothing without this rule. */
.card[hidden] {
  display: none;
}

/* the category hairline along the top edge */
.card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--accent, var(--muted));
  transform: scaleX(0.18);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

.card[data-kind='research'] {
  --accent: var(--research);
  --accent-soft: var(--research-soft);
}
.card[data-kind='design'] {
  --accent: var(--design);
  --accent-soft: var(--design-soft);
}
.card[data-kind='teaching'] {
  --accent: var(--teaching);
  --accent-soft: var(--teaching-soft);
}

.card:hover,
.card:focus-within {
  transform: translateY(-4px);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
  box-shadow: 0 12px 32px -18px color-mix(in srgb, var(--accent) 70%, transparent);
}

.card:hover::before,
.card:focus-within::before {
  transform: scaleX(1);
}

.card__kind {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.76rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.85rem;
}

.card__kind::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.card h3 {
  font-size: clamp(1.2rem, 1.6vw, 1.45rem);
  margin-bottom: 0.5rem;
}

.card h3 a::after {
  /* makes the whole card clickable without nesting interactive elements */
  content: '';
  position: absolute;
  inset: 0;
}

.card__meta {
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.9rem;
}

.card__meta span + span::before {
  content: ' · ';
}

.card__sub {
  color: var(--ink-2);
  font-size: 0.95rem;
  margin-bottom: 1.1rem;
  flex: 1;
}

.card__note {
  display: inline-block;
  font-size: 0.82rem;
  padding: 0.28rem 0.7rem;
  border-radius: 999px;
  background: var(--accent-soft);
  color: color-mix(in srgb, var(--accent) 78%, var(--ink));
  margin-bottom: 1rem;
  align-self: flex-start;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-bottom: 0;
}

.tag {
  font-size: 0.78rem;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
}

.card__more {
  margin-top: 1rem;
  font-size: 0.88rem;
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.card__more span {
  transition: transform var(--dur-fast) var(--ease);
}

.card:hover .card__more span {
  transform: translateX(4px);
}

.feed__empty {
  color: var(--muted);
  padding: 2rem 0;
}

/* teaching sits below the feed: one card, with the context beside it */
.teaching-strip {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: start;
}

@media (max-width: 860px) {
  .teaching-strip {
    grid-template-columns: 1fr;
  }
}

/* a single card shouldn't stretch to three columns wide */
.teaching-strip .feed {
  grid-template-columns: minmax(0, 1fr);
}

/* --- lists (publications, talks) ------------------------------------------ */

.split {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
}

.entry {
  padding-block: 1.1rem;
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  gap: 1rem;
  align-items: start;
}

.entry:last-child {
  border-bottom: 1px solid var(--line);
}

.entry__when {
  font-size: 0.85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  padding-top: 0.15rem;
}

.entry__title {
  font-family: var(--display);
  font-size: 1.05rem;
  line-height: 1.35;
  margin-bottom: 0.25rem;
}

.entry__link {
  border-bottom: 1px solid var(--line);
  transition: color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

.entry__link:hover {
  color: var(--design);
  border-color: var(--design);
}

.entry__ext {
  font-size: 0.75em;
  opacity: 0.55;
}

.entry__sub {
  font-size: 0.88rem;
  color: var(--ink-2);
}

.entry__sub em {
  font-style: normal;
  color: var(--muted);
}

@media (max-width: 520px) {
  .entry {
    grid-template-columns: 1fr;
    gap: 0.3rem;
  }
}

/* --- about teaser --------------------------------------------------------- */

.teaser {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: clamp(1.5rem, 5vw, 4rem);
  align-items: center;
}

@media (max-width: 800px) {
  .teaser {
    grid-template-columns: 1fr;
  }
}

/* photo slots — soft gradient until a real image is dropped in */
.photo {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: linear-gradient(
    145deg,
    var(--research-soft),
    var(--design-soft) 55%,
    var(--teaching-soft)
  );
  aspect-ratio: var(--ratio, 4 / 5);
  display: grid;
  place-items: center;
}

.photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.photo__hint {
  font-size: 0.82rem;
  color: var(--muted);
  text-align: center;
  padding: 1rem;
  line-height: 1.5;
}

.photo--arch {
  border-radius: 999px 999px var(--radius-lg) var(--radius-lg);
}

.photo-row {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

/* --- contact -------------------------------------------------------------- */

.contact {
  background: var(--paper-2);
}

.contact h2 {
  font-size: clamp(2rem, 5.5vw, 3.6rem);
  margin-bottom: 1.25rem;
}

.contact__links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin-top: 1.75rem;
  font-size: 0.98rem;
}

.contact__links a {
  color: var(--ink-2);
  border-bottom: 1px solid var(--line);
  padding-bottom: 2px;
  transition: color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease);
}

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

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

.footer {
  padding-block: 2.5rem;
  font-size: 0.85rem;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
}

/* --- case study pages ----------------------------------------------------- */

.case-hero {
  padding-block: clamp(2.5rem, 7vw, 5rem) clamp(1.5rem, 4vw, 3rem);
}

.back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
  color: var(--muted);
  margin-bottom: 2rem;
  transition: color var(--dur-fast) var(--ease);
}

.back:hover {
  color: var(--ink);
}

.back span {
  transition: transform var(--dur-fast) var(--ease);
}

.back:hover span {
  transform: translateX(-3px);
}

.case-hero h1 {
  font-size: clamp(2.1rem, 5.5vw, 3.9rem);
  font-variation-settings: 'SOFT' 55, 'WONK' 1, 'opsz' 80;
  max-width: 20ch;
  margin-bottom: 1.25rem;
}

.case-hero__lede {
  font-size: clamp(1.05rem, 1.5vw, 1.25rem);
  color: var(--ink-2);
  max-width: 58ch;
}

.case-hero__image {
  margin-top: clamp(1.75rem, 4vw, 2.75rem);
}

/* --- case study figures --------------------------------------------------- */

/* sits outside .prose so screenshots get the full page width */
.shot {
  margin: clamp(2rem, 4vw, 3rem) 0;
}

.shot__frame {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  padding: clamp(0.6rem, 1.5vw, 1rem);
  overflow: hidden;
}

.shot__frame video {
  width: 100%;
  height: auto;
  display: block;
  border-radius: inherit;
}

/* a phone-shot clip is very tall — don't let it run the length of the page */
.shot--portrait .shot__frame {
  max-width: 320px;
}

.shot__frame img {
  width: 100%;
  border-radius: calc(var(--radius) - 6px);
  display: block;
}

.shot figcaption {
  font-size: 0.85rem;
  color: var(--muted);
  margin-top: 0.7rem;
  max-width: 60ch;
}

/* ---- browser frame -------------------------------------------------------
   these screenshots are web pages, and they're wildly different heights. the
   frame gives every one the same footprint and lets the tall ones scroll
   inside their own window instead of stretching the page. */

.browser {
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  background: var(--card);
  box-shadow: 0 14px 34px -24px rgba(0, 0, 0, 0.45);
}

.browser__bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 0.85rem;
  height: 30px;
  background: color-mix(in srgb, var(--paper-2) 82%, var(--line));
  border-bottom: 1px solid var(--line);
}

.browser__bar span {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--muted) 45%, transparent);
  flex: none;
}

.browser__screen {
  height: var(--screen-h, 440px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: #fff;
}

.browser__screen img {
  width: 100%;
  display: block;
  border-radius: 0;
}

/* the concept gallery — several screens side by side, all the same height */
.shots {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  margin-bottom: 0.7rem;
}

/* three per row so six concepts land as two even rows, not five and an orphan */
@media (min-width: 720px) {
  .shots {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.shots .browser__screen {
  --screen-h: 300px;
}

/* a deck of slides — uniform 16:9, so a plain grid is enough */
.slides {
  display: grid;
  gap: 0.9rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  margin-bottom: 1rem;
}

.slide {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--card);
  transition: transform var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}

.slide:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 26px -20px rgba(0, 0, 0, 0.4);
}

.slide img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
}

/* text on one side, screenshot on the other */
.aside-figure {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
  margin-block: clamp(2rem, 4vw, 3rem);
}

@media (max-width: 820px) {
  .aside-figure {
    grid-template-columns: 1fr;
  }
}

.aside-figure .shot {
  margin: 0;
}

/* two screenshots beside each other */
.pair-figure {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  margin-bottom: 0.7rem;
}

/* embedded prototype video */
.embed {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--paper-2);
}

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

/* a phone prototype is portrait — 16:9 would letterbox it into nothing */
.embed--tall {
  aspect-ratio: auto;
  height: 620px;
}

@media (max-width: 640px) {
  .embed--tall {
    height: 480px;
  }
}

/* student quotes / testimonials on the teaching pages */
.quotes {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  margin-block: 1.5rem 2.5rem;
}

.quote {
  margin: 0; /* these are <blockquote>s — drop the ua margin */
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.25rem 1.4rem;
  font-size: 0.95rem;
  color: var(--ink-2);
}

.quote p {
  margin-bottom: 0.75rem;
}

.quote footer {
  font-size: 0.82rem;
  color: var(--muted);
}

/* ---- student voices ------------------------------------------------------
   a wall of course-evaluation quotes, grouped by theme. uses css columns so it
   reads as a masonry the eye can skim rather than a block of text. breaks out
   of .prose so it can use the full page width. */

.voices {
  margin-block: clamp(2rem, 5vw, 3.5rem);
}

.voices__theme {
  margin-bottom: clamp(1.75rem, 4vw, 2.75rem);
}

.voices__label {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent, var(--muted));
  margin-bottom: 1rem;
}

.voices__label::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.voices__label::after {
  content: '';
  height: 1px;
  background: var(--line);
  flex: 1;
}

.voices__grid {
  columns: 3;
  column-gap: 1rem;
}

@media (max-width: 900px) {
  .voices__grid {
    columns: 2;
  }
}

@media (max-width: 620px) {
  .voices__grid {
    columns: 1;
  }
}

.voices__grid .quote {
  break-inside: avoid;
  margin-bottom: 1rem;
  display: inline-block;
  width: 100%;
}

.pull__cite {
  display: block;
  font-family: var(--body);
  font-size: 0.85rem;
  color: var(--muted);
  margin-top: 0.9rem;
  letter-spacing: 0;
}

/* a compact list of course materials / artifacts */
.materials {
  display: grid;
  gap: 0.6rem;
  margin-block: 1.25rem 2rem;
}

.material {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  align-items: baseline;
  padding: 0.85rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
}

.material__name {
  font-family: var(--display);
  font-size: 1rem;
  flex: 1 1 16ch;
}

.material__what {
  font-size: 0.88rem;
  color: var(--muted);
  flex: 2 1 24ch;
}

.facts {
  display: grid;
  gap: 1.25rem 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
  padding-block: 1.75rem;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-block: clamp(2rem, 5vw, 3.5rem);
}

.fact__label {
  font-size: 0.76rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.4rem;
}

.fact__value {
  font-size: 0.95rem;
  color: var(--ink);
}

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

.prose h2 {
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  margin-top: 3rem;
  margin-bottom: 1rem;
}

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

.prose h3 {
  font-size: 1.2rem;
  margin-top: 2rem;
  margin-bottom: 0.6rem;
}

.prose p,
.prose li {
  color: var(--ink-2);
}

.prose ul {
  list-style: none;
  margin-bottom: 1.25rem;
}

.prose ul li {
  position: relative;
  padding-left: 1.4rem;
  margin-bottom: 0.5rem;
}

.prose ul li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.65em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent, var(--muted));
}

.prose strong {
  color: var(--ink);
  font-weight: 500;
}

.pull {
  font-family: var(--display);
  font-size: clamp(1.3rem, 2.4vw, 1.8rem);
  line-height: 1.35;
  font-variation-settings: 'SOFT' 50, 'WONK' 1, 'opsz' 40;
  color: var(--ink);
  border-left: 3px solid var(--accent, var(--muted));
  padding-left: clamp(1rem, 2.5vw, 1.75rem);
  margin-block: 2.5rem;
  max-width: 34ch;
}

.next {
  border-top: 1px solid var(--line);
  padding-top: 2rem;
  margin-top: 4rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
  align-items: center;
}

/* case pages inherit their accent from a data attribute on <body> */
body[data-kind='research'] {
  --accent: var(--research);
  --accent-soft: var(--research-soft);
}
body[data-kind='design'] {
  --accent: var(--design);
  --accent-soft: var(--design-soft);
}
body[data-kind='teaching'] {
  --accent: var(--teaching);
  --accent-soft: var(--teaching-soft);
}

/* --- cv page -------------------------------------------------------------- */

.cv-section {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 3rem);
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  border-top: 1px solid var(--line);
}

.cv-section h2 {
  font-size: 1.3rem;
  position: sticky;
  top: 5.5rem;
  align-self: start;
}

@media (max-width: 760px) {
  .cv-section {
    grid-template-columns: 1fr;
  }
  .cv-section h2 {
    position: static;
  }
}

.cv-item {
  margin-bottom: 1.75rem;
}

.cv-item:last-child {
  margin-bottom: 0;
}

.cv-item__head {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 0.35rem;
}

.cv-item__title {
  font-family: var(--display);
  font-size: 1.08rem;
  line-height: 1.3;
  /* grow to fill the row so a title that wraps to two lines keeps the date
     beside it rather than pushing it onto its own line */
  flex: 1 1 20ch;
}

.cv-item__when {
  font-size: 0.84rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  margin-left: auto;
}

.cv-item__role {
  font-size: 0.9rem;
  color: var(--ink-2);
  margin-bottom: 0.5rem;
}

.cv-item ul li {
  position: relative;
  padding-left: 1.2rem;
  margin-bottom: 0.4rem;
  font-size: 0.93rem;
  color: var(--ink-2);
}

.cv-item ul li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--line);
}

.pairs {
  display: grid;
  gap: 0.75rem;
}

.pair {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: 1rem;
  font-size: 0.93rem;
}

.pair dt {
  color: var(--muted);
}

.pair dd {
  margin: 0;
  color: var(--ink-2);
}

@media (max-width: 520px) {
  .pair {
    grid-template-columns: 1fr;
    gap: 0.15rem;
  }
}

/* --- motion --------------------------------------------------------------- */

/* scroll reveal: elements start shifted, js adds .is-in when they enter view.
   scoped to .js (set by an inline script in <head>) so that if javascript is
   disabled or fails to load, the content is simply visible rather than blank. */
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  transition-delay: var(--delay, 0ms);
}

.js .reveal.is-in {
  opacity: 1;
  transform: none;
}

/* display lines reveal word by word. the hero headline plays on load; every
   other [data-split] sits inside a .reveal and stays paused until that
   ancestor is in view, so a line never plays out below the fold. */
.word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.5em);
  animation: word-in 0.75s var(--ease) forwards;
  animation-delay: var(--delay, 0ms);
}

.js .reveal .word {
  animation-play-state: paused;
}

.js .reveal.is-in .word {
  animation-play-state: running;
}

/* no js means no .is-in ever arrives — don't leave the text invisible */
html:not(.js) .word {
  opacity: 1;
  transform: none;
  animation: none;
}

@keyframes word-in {
  to {
    opacity: 1;
    transform: none;
  }
}

/* cards animate out and back in when the filter changes */
.card.is-leaving {
  opacity: 0;
  transform: scale(0.97);
  transition: opacity 0.18s ease, transform 0.18s ease;
}

/* no fill-mode on purpose: outside the animation the card sits at its normal
   (visible) style, so a card is never left invisible if the animation doesn't
   get to run */
/* backwards fill so the card holds its `from` state through --delay rather
   than flashing at full opacity first. site.js clears .is-entering on
   animationend (and on a timer), so nothing can stay stuck invisible. */
.card.is-entering {
  animation: card-in 0.32s var(--ease) var(--delay, 0ms) backwards;
}

@keyframes card-in {
  from {
    opacity: 0;
    transform: scale(0.98) translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --- colour washes --------------------------------------------------------
   one continuous layer behind the entire page rather than a shape per
   section. the first version clipped each blob to its own section with
   overflow:hidden, which drew a hard horizontal line across every soft edge
   exactly at the section boundary — the opposite of flowing.

   these are radial gradients in body's background, so there is nothing to
   clip and nothing to overflow. the positions are percentages of the whole
   document, so the colour drifts down the page across section boundaries
   instead of restarting at each one.

   the catch: a background set on <body> propagates to the canvas and is then
   sized to the viewport, which would pin these to the screen like a fixed
   backdrop. giving <html> its own background stops that propagation, so
   body's background box becomes the full document height. that's what the
   html rule further up is for — don't remove it. */

body {
  /* the vertical radius is in vh, not vw. the body box is as tall as the
     whole document — nearly 10,000px here — so a vw-sized vertical radius
     would scatter small spots down an enormous page. in vh each one is most
     of a screen tall however long the page gets. the positions stay
     percentages of the document, which is what carries the colour across
     section boundaries.

     four, and large. earlier versions used six and then eleven, and both
     read as busy: at that count the shapes are necessarily smaller, they sit
     closer together, and a lot of small soft ellipses becomes texture rather
     than composition. fewer and bigger means each one covers a screen or
     more, so you're inside a field of colour rather than passing a row of
     spots. the sage that used to sit at the top left came out — it crowded
     the portrait, which has the hero's own blobs behind it already.

     still nothing regular: the vertical gaps are 19, 28 and 22 percent,
     three of the four anchor well off the page while one sits inboard at
     22%, and the falloff ranges from 62% to 78% so they aren't all equally
     diffuse. the last one is the largest of the four — it sits at 91%, which
     is inside the teaching section. */
  background-image:
    radial-gradient(86vw 70vh at 108% 22%, var(--wash-clay), transparent 76%),
    radial-gradient(62vw 56vh at 22% 41%, var(--wash-peri), transparent 62%),
    radial-gradient(94vw 76vh at 106% 69%, var(--wash-sage), transparent 78%),
    radial-gradient(108vw 88vh at -10% 91%, var(--wash-clay), transparent 74%);
  background-repeat: no-repeat;
}

/* the contact band and the statement panel both sit on paper-2. at full
   opacity they'd block the layer underneath, so they let it through. */
.contact {
  background: color-mix(in srgb, var(--paper-2) 78%, transparent);
}

/* --- depth and life -------------------------------------------------------
   everything here animates `translate`, which is its own property in css, so
   it composes with the `transform` already on .blob (the drift keyframes),
   .btn (the hover lift) and .card (the hover rise) instead of clobbering it.
   the inputs are custom properties written by site.js:
     --px / --py   pointer position, -1..1 from the centre of the hero
     --out         how far the hero has scrolled away, 0..1                  */

.blob {
  --depth: 20px;
  translate: calc(var(--px, 0) * var(--depth)) calc(var(--py, 0) * var(--depth));
  /* slow and heavily eased: the blobs should lag behind the cursor like
     something seen through water, not track it */
  transition: translate 1.4s cubic-bezier(0.16, 0.84, 0.44, 1);
}
.blob--a {
  --depth: 30px;
}
.blob--b {
  --depth: 18px;
}
.blob--c {
  --depth: 46px;
}

/* the hero hands off as you scroll past it — the copy lifts and dims faster
   than the portrait, so the two planes visibly separate */
.hero__copy {
  translate: 0 calc(var(--out, 0) * -44px);
  opacity: calc(1 - var(--out, 0) * 0.7);
}

.hero__photo {
  translate: 0 calc(var(--out, 0) * -16px);
}

/* the buttons lean toward the cursor. --mx/--my are set per button in js and
   cleared on pointerleave, so they spring back. the base .btn rule owns
   `transform` for the hover lift; this one only adds translate to the
   transition list, which means restating it. */
.hero__actions .btn {
  translate: calc(var(--mx, 0) * 1px) calc(var(--my, 0) * 1px);
  transition: transform var(--dur-fast) var(--ease),
    background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
    translate 0.35s var(--ease);
}

/* --- scroll cue ----------------------------------------------------------- */

.cue {
  position: absolute;
  left: 50%;
  bottom: clamp(0.5rem, 2vh, 1.25rem);
  translate: -50% 0;
  z-index: 1;
  display: grid;
  justify-items: center;
  gap: 0.45rem;
  font-size: 0.68rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  pointer-events: none;
  /* gone almost immediately — it has done its job after the first nudge */
  opacity: clamp(0, calc(1 - var(--out, 0) * 6), 1);
  transition: opacity 0.3s var(--ease);
}

.cue__line {
  width: 1px;
  height: 32px;
  background: linear-gradient(var(--muted), transparent);
  transform-origin: top center;
  animation: cue-drop 2.6s var(--ease) infinite;
}

@keyframes cue-drop {
  0% {
    transform: scaleY(0);
    opacity: 0;
  }
  30% {
    transform: scaleY(1);
    opacity: 1;
  }
  100% {
    transform: scaleY(1) translateY(32px);
    opacity: 0;
  }
}

@media (max-width: 880px) {
  /* the hero is already tall on a phone; the cue is just clutter */
  .cue {
    display: none;
  }
}

/* --- a rule under every section heading ----------------------------------- */

.section__head h2 {
  position: relative;
}

.section__head h2::after {
  content: '';
  display: block;
  width: clamp(40px, 6vw, 72px);
  height: 2px;
  margin-top: 0.8rem;
  background: var(--ink);
  opacity: 0.45;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.7s var(--ease) 0.12s;
}

/* .section__head also carries .reveal, so this lands with the heading */
.section__head.is-in h2::after {
  transform: scaleX(1);
}

/* without js nothing ever adds .is-in — draw it straight away */
html:not(.js) .section__head h2::after {
  transform: scaleX(1);
}

/* --- card life ------------------------------------------------------------ */

/* the accent hairline grows to its resting width as the card reveals, rather
   than being there from the start */
.js .card.reveal:not(.is-in)::before {
  transform: scaleX(0);
}

.card .photo img {
  transition: transform 0.7s var(--ease);
}

.card:hover .photo img,
.card:focus-within .photo img {
  transform: scale(1.05);
}

.filter:active {
  transform: scale(0.97);
}

/* --- a live dot on the two running projects ------------------------------- */

.context--now .context__k {
  display: flex;
  align-items: center;
  gap: 0.45rem;
}

.context--now .context__k::before {
  content: '';
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--research);
  animation: live 2.8s ease-out infinite;
}

@keyframes live {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--research) 55%, transparent);
  }
  70%,
  100% {
    box-shadow: 0 0 0 9px transparent;
  }
}

/* --- contact links: the underline draws in -------------------------------- */

/* the border-bottom stays as the resting rail; this paints a second, inked
   line over it from the left. restates the transition to add background-size. */
.contact__links a {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 1px;
  transition: color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    background-size 0.45s var(--ease);
}

.contact__links a:hover,
.contact__links a:focus-visible {
  background-size: 100% 1px;
}

/* --- rules that draw ------------------------------------------------------ */

/* the research questions: the sage rule runs down the side as each one
   arrives, instead of being there from the start. moved off border-left so
   it has something to scale. */
.question {
  border-left: 0;
  position: relative;
}

/* drawn by default, and only held back while it's waiting on a reveal — so a
   .question used outside a .reveal still gets its rule. */
.question::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--research);
  transform-origin: top;
  transition: transform 0.65s var(--ease) 0.1s;
}

.js .reveal:not(.is-in) .question::before {
  transform: scaleY(0);
}

/* the two live-project tiles get the same treatment along the top edge */
.context {
  position: relative;
  overflow: hidden;
}

.context::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--research);
  transform-origin: left;
  transition: transform 0.7s var(--ease) 0.2s;
}

.js .reveal:not(.is-in) .context::before {
  transform: scaleX(0);
}

/* --- card spotlight ------------------------------------------------------- */

/* sits under the card's own content. --cx/--cy are written by site.js; with
   no pointer they're unset and the gradient parks in the corner at zero
   opacity, so nothing shows until you're actually over a card. */
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  background: radial-gradient(
    240px circle at var(--cx, 50%) var(--cy, 50%),
    color-mix(in srgb, var(--accent, var(--muted)) 14%, transparent),
    transparent 70%
  );
}

.card:hover::after {
  opacity: 1;
}

/* the card's content has to sit above the gradient */
.card > * {
  position: relative;
  z-index: 1;
}

/* and the category hairline above both, or the spotlight tints it */
.card::before {
  z-index: 2;
}

/* --- the portrait ---------------------------------------------------------
   it sits still until you touch it. three properties, so nothing collides:
     transform   the hover lift, on the frame
     translate   the scroll handoff, set further up
     scale       the hover push-in, on the image — whose own `transform` is
                 already taken by the slow drift.                          */

/* lifts on hover. `transform` here, because `translate` is already carrying
   the scroll handoff further up. */
.hero__photo {
  transform: scale(var(--pscale, 1)) translateY(var(--plift, 0));
  transition: transform 0.45s var(--ease), box-shadow 0.45s var(--ease);
}

.hero__photo:hover {
  --pscale: 1.015;
  --plift: -7px;
  box-shadow: 0 34px 64px -34px
    color-mix(in srgb, var(--design) 65%, transparent);
}

/* the image pushes in a little behind the frame. `scale` rather than
   `transform`, which the slow drift already owns. */
.hero__photo img {
  transition: scale 0.6s var(--ease);
}

.hero__photo:hover img {
  scale: 1.05;
}

/* a very slow drift inside the arch — the frame holds still, the image moves.
   on the image, not the container, so it can't fight the tilt or the scroll. */
.hero__photo img {
  animation: breathe 22s ease-in-out infinite alternate;
}

@keyframes breathe {
  from {
    transform: scale(1) translateY(0);
  }
  to {
    transform: scale(1.045) translateY(-1.2%);
  }
}


/* the one switch that turns all of it off */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .reveal,
  .word {
    opacity: 1 !important;
    transform: none !important;
  }

  .blob {
    animation: none !important;
  }

  /* the new translate-based layers, the cue, and the live dots */
  .blob,
  .hero__copy,
  .hero__photo,
  .hero__actions .btn {
    translate: none !important;
    opacity: 1 !important;
  }

  .cue {
    display: none;
  }

  .section__head h2::after {
    transform: scaleX(1) !important;
  }

  .context--now .context__k::before {
    animation: none !important;
  }

  .card .photo img,
  .hero__photo img {
    transform: none !important;
  }

  .hero__photo {
    transform: none !important;
  }

  .hero__photo img {
    scale: 1 !important;
  }

  .question::before,
  .context::before {
    transform: scaleY(1) scaleX(1) !important;
  }

  .card::after {
    display: none;
  }
}

/* --- print ---------------------------------------------------------------- */

@media print {
  .nav,
  .footer,
  .blob,
  .hero__actions,
  .filters {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }
}

/* --- work-in-progress lock ------------------------------------------------
   see assets/js/gate.js. delete this block along with the script when the
   site goes public. */

html.is-locked,
html.is-locked body {
  overflow: hidden;
}

/* the page stays on screen but heavily blurred, so this reads as an overlay
   popping up rather than a separate blank page. it also can't be read,
   selected, or clicked through while the gate is up. */
html.is-locked body > *:not(.gate) {
  filter: blur(16px);
  pointer-events: none;
  user-select: none;
}

.gate {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  /* a veil rather than a wall — enough to keep the text legible over the
     blurred page, not so much that it stops feeling like an overlay */
  background: color-mix(in srgb, var(--paper) 62%, transparent);
  backdrop-filter: blur(6px);
}

.gate__card {
  position: relative;
  width: 100%;
  max-width: 27rem;
  text-align: center;
  padding: clamp(1.5rem, 4vw, 2.25rem);
}

.gate__close {
  position: absolute;
  top: 0;
  right: 0;
  width: 2rem;
  height: 2rem;
  font-size: 1.4rem;
  line-height: 1;
  border: 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: 999px;
  transition: color var(--dur-fast) var(--ease);
}

.gate__close:hover {
  color: var(--ink);
}

.gate__back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 1.5rem;
  padding: 0;
  border: 0;
  background: transparent;
  font-family: var(--body);
  font-size: 0.9rem;
  color: var(--muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease);
}

.gate__back:hover {
  color: var(--ink);
}

.gate__back span {
  transition: transform var(--dur-fast) var(--ease);
}

.gate__back:hover span {
  transform: translateX(-3px);
}

.gate__mark {
  font-family: var(--display);
  font-size: 1.4rem;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 30;
  color: var(--ink);
  margin-bottom: 0.6rem;
}

.gate__mark span {
  color: var(--muted);
}

.gate__note {
  color: var(--ink-2);
  font-size: 0.98rem;
  margin-bottom: 1.75rem;
}

.gate__form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  justify-content: center;
}

.gate__label {
  position: absolute;
  left: -9999px;
}

.gate__input {
  flex: 1 1 13rem;
  min-width: 0;
  font-family: var(--body);
  font-size: 1rem;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.7rem 1.2rem;
  transition: border-color var(--dur-fast) var(--ease);
}

.gate__input:focus {
  outline: none;
  border-color: var(--design);
}

.gate__btn {
  flex: none;
}

.gate__error {
  margin-top: 1rem;
  font-size: 0.9rem;
  color: var(--design);
}

/* ---- hcde 417: teaching hero ---------------------------------------------
   the clip of class is the first thing on the page. it's shot on a phone, so
   it's very tall — cap it and let the course facts sit alongside rather than
   underneath, or the fold is nothing but video. */

.teach-hero {
  display: grid;
  grid-template-columns: minmax(0, 270px) 1fr;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
  margin-block: clamp(2rem, 5vw, 3.25rem);
}

@media (max-width: 720px) {
  .teach-hero {
    grid-template-columns: 1fr;
  }
  .teach-hero__clip {
    max-width: 240px;
    margin-inline: auto;
  }
}

.teach-hero__clip {
  margin: 0;
}

.teach-hero__clip video {
  width: 100%;
  display: block;
  border-radius: var(--radius-lg);
  background: var(--card);
}

.teach-hero__clip figcaption {
  font-size: 0.82rem;
  color: var(--muted);
  text-align: center;
  margin-top: 0.7rem;
}

/* the facts already carry rules of their own; inside the split they'd stack
   three borders in a row, so strip them back to a single left hairline. */
.teach-hero__meta .facts {
  margin-block: 0;
  padding-block: 0;
  border-top: 0;
  border-bottom: 0;
  gap: 1.4rem 2rem;
  /* four facts: 2x2 reads better here than auto-fit, which drops the fourth
     onto a row of its own and leaves a hole next to it. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (max-width: 460px) {
  .teach-hero__meta .facts {
    grid-template-columns: 1fr;
  }
}

@media (min-width: 721px) {
  .teach-hero__meta {
    border-left: 1px solid var(--line);
    padding-left: clamp(1.5rem, 3vw, 2.5rem);
  }
}

/* ---- student voices: rotating spotlight ----------------------------------
   one quote at a time, crossfading. every slide is stacked in the same grid
   cell so the box is as tall as the longest quote and never jumps on change. */

.spot {
  margin-block: clamp(1.5rem, 4vw, 2.5rem);
}

.spot__stage {
  display: grid;
  align-items: center;
  min-height: 1px;
}

.spot__quote {
  grid-area: 1 / 1;
  margin: 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease),
    visibility 0s linear var(--dur);
}

.spot__quote.is-on {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease),
    visibility 0s;
}

.spot__quote p {
  font-family: var(--display);
  font-size: clamp(1.25rem, 2.6vw, 1.85rem);
  line-height: 1.35;
  color: var(--ink);
  margin-bottom: 0.9rem;
  text-wrap: balance;
}

.spot__quote footer {
  font-size: 0.85rem;
  color: var(--muted);
}

.spot__dots {
  display: flex;
  gap: 0.5rem;
  margin-top: 1.4rem;
}

.spot__dot {
  width: 26px;
  height: 4px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--line);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), width var(--dur-fast) var(--ease);
}

.spot__dot:hover {
  background: var(--ink-2);
}

.spot__dot.is-on {
  width: 44px;
  background: var(--teaching);
}

/* ---- student voices: filterable wall -------------------------------------- */

.voices__title {
  font-family: var(--display);
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  margin-bottom: 0.75rem;
}

.voices__intro {
  color: var(--ink-2);
  max-width: 58ch;
  margin-bottom: 1.75rem;
}

.voices__filters {
  margin-bottom: 1.75rem;
}

.voices__filters .filter {
  --dot: var(--teaching);
}

.voices__wall {
  columns: 3;
  column-gap: 1rem;
}

@media (max-width: 900px) {
  .voices__wall {
    columns: 2;
  }
}

@media (max-width: 620px) {
  .voices__wall {
    columns: 1;
  }
}

.voices__wall .quote {
  break-inside: avoid;
  margin-bottom: 1rem;
  display: inline-block;
  width: 100%;
  border-left: 2px solid var(--teaching-soft);
  transition: border-color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}

.voices__wall .quote:hover {
  border-left-color: var(--teaching);
  transform: translateY(-2px);
}

/* the rule above sets display:inline-block, which beats the browser's own
   [hidden] { display: none } — without this, filtering a quote out does
   nothing at all. same trap as .card[hidden]. */
.voices__wall .quote[hidden] {
  display: none;
}

.voices__wall .quote.is-entering {
  animation: quote-in var(--dur) var(--ease) both;
}

@keyframes quote-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .spot__quote {
    transition: none;
  }
  .voices__wall .quote.is-entering {
    animation: none;
  }
  .voices__wall .quote:hover {
    transform: none;
  }
}

/* a poster is portrait and dense — it needs more room than .shot--portrait's
   320px phone-clip cap, but not the full page width either. */
.shot--poster .shot__frame {
  max-width: 720px;
  margin-inline: auto;
}

/* ---- severity findings ----------------------------------------------------
   a usability result set is a ranked list, not prose. every block below reads
   its level off --sev (1..5) so the colour ramp, the meter and the card accent
   all stay in step from one declaration. */

.finding {
  /* clay at the top of the scale, sage at the bottom — warm reads as urgent */
  --sev-color: color-mix(in srgb, var(--design) calc(var(--sev) * 20%), var(--research));
}

/* -- the meter -- */

.sev-meter {
  display: inline-flex;
  gap: 3px;
  flex: none;
}

.sev-meter i {
  width: 5px;
  height: 16px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--sev-color) 22%, transparent);
}

/* light up as many pips as the severity, counting from the left */
.sev-meter i:nth-child(-n + 1) { background: var(--sev-color); }
[style*="--sev: 1"] .sev-meter i:nth-child(-n + 1),
[style*="--sev: 2"] .sev-meter i:nth-child(-n + 2),
[style*="--sev: 3"] .sev-meter i:nth-child(-n + 3),
[style*="--sev: 4"] .sev-meter i:nth-child(-n + 4),
[style*="--sev: 5"] .sev-meter i:nth-child(-n + 5) {
  background: var(--sev-color);
}
[style*="--sev: 1"] .sev-meter i:nth-child(n + 2),
[style*="--sev: 2"] .sev-meter i:nth-child(n + 3),
[style*="--sev: 3"] .sev-meter i:nth-child(n + 4),
[style*="--sev: 4"] .sev-meter i:nth-child(n + 5) {
  background: color-mix(in srgb, var(--sev-color) 22%, transparent);
}

/* -- a finding card -- */

.finding {
  margin-block: clamp(1.25rem, 3vw, 1.75rem);
  padding: clamp(1.25rem, 3vw, 1.9rem);
  border-radius: var(--radius);
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--sev-color);
  scroll-margin-top: 90px;
}

.finding__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 0.85rem;
  margin-bottom: 1.1rem;
}

.finding__sev {
  font-size: 0.74rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--sev-color);
}

.finding__head h3 {
  flex-basis: 100%;
  font-family: var(--display);
  font-size: clamp(1.2rem, 2.4vw, 1.5rem);
  line-height: 1.25;
  margin: 0;
}

/* -- the numbers, pulled out of the prose -- */

.stats {
  list-style: none;
  margin: 0 0 1.2rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.stats li {
  flex: 1 1 140px;
  padding: 0.7rem 0.85rem;
  border-radius: 12px;
  background: color-mix(in srgb, var(--sev-color) 9%, transparent);
}

.stats b {
  display: block;
  font-family: var(--display);
  font-size: 1.3rem;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.stats span {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--ink-2);
}

.finding__body > p {
  margin-bottom: 0.9rem;
  color: var(--ink-2);
  max-width: 62ch;
}

.finding__body > p:last-child {
  margin-bottom: 0;
}

.finding__quote {
  margin: 1.1rem 0;
  padding-left: 1rem;
  border-left: 2px solid color-mix(in srgb, var(--sev-color) 45%, transparent);
  font-family: var(--display);
  font-size: clamp(1rem, 1.9vw, 1.2rem);
  line-height: 1.4;
  color: var(--ink);
}

/* the marks go on the text, not the blockquote — <cite> is a block, so an
   ::after on the blockquote lands underneath it on its own line. */
.finding__quote p {
  margin: 0;
}

.finding__quote p::before { content: '\201C'; }
.finding__quote p::after { content: '\201D'; }

.finding__quote cite {
  display: block;
  margin-top: 0.45rem;
  font-family: var(--body);
  font-size: 0.8rem;
  font-style: normal;
  color: var(--muted);
}

.finding__rec {
  margin: 1.3rem 0 0;
  padding-top: 1rem;
  border-top: 1px dashed var(--line);
  font-size: 0.92rem;
  color: var(--ink-2);
}

.finding__rec b {
  display: inline-block;
  margin-right: 0.5rem;
  font-size: 0.72rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--sev-color);
}

/* evidence shown inside a finding card — sits with the claim it supports */
.finding__fig {
  margin: 1.2rem 0 0;
}

.finding__fig img {
  width: 100%;
  display: block;
  border-radius: 12px;
  border: 1px solid var(--line);
}

.finding__fig figcaption {
  margin-top: 0.55rem;
  font-size: 0.82rem;
  color: var(--muted);
}
