@charset "UTF-8";
/* ===========================================================================
   Sultanul Ovi - portfolio styles
   One stylesheet for every page. Neutral palette, one card primitive,
   spacing from a fixed scale.
   =========================================================================== */

:root {
  --bg: #fafafa;
  --surface: #ffffff;
  --surface-soft: #f5f5f5;
  --border: #e5e5e5;
  --border-strong: #a3a3a3;
  --text: #171717;
  --text-secondary: #525252;
  --text-muted: #737373;
  --accent: #171717;
  --accent-contrast: #fafafa;
  --overlay: rgba(10, 10, 10, 0.82);
  --header-bg: rgba(250, 250, 250, 0.8);

  /* Type scale. Every size below 1.125rem comes from here, so the small
     end of the scale cannot drift selector by selector. */
  --text-xs: 0.75rem;     /* 12px - uppercase micro labels, tertiary meta */
  --text-sm: 0.8125rem;   /* 13px - chips, pills, badges */
  --text-md: 0.875rem;    /* 14px - dates, locations, section descriptions */
  --text-base: 0.9375rem; /* 15px - card body copy, bullets, author lines */
  --text-lg: 1rem;        /* 16px - card titles */
  --text-xl: 1.125rem;    /* 18px - summary figures, hero role */

  --radius: 12px;
  --radius-sm: 8px;
  /* Page width. */
  --measure: 960px;
  /* Reading width for running prose: hero copy and page intros. Kept narrow
     on purpose, long lines are harder to read. */
  --measure-prose: 42rem;
  /* Text inside cards: bullets, descriptions. Wider, because it sits in a
     grid column that has already been narrowed by the date column. */
  --measure-card: 48rem;
}

[data-theme='dark'] {
  --bg: #171717;
  --surface: #262626;
  --surface-soft: #303030;
  --border: #404040;
  --border-strong: #737373;
  --text: #fafafa;
  --text-secondary: #d4d4d4;
  --text-muted: #a3a3a3;
  --accent: #fafafa;
  --accent-contrast: #171717;
  --overlay: rgba(0, 0, 0, 0.88);
  --header-bg: rgba(23, 23, 23, 0.82);
}

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

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

/* A column body plus a growing main is what pins the footer to the bottom of
   the viewport on a short page (a filtered project list, say) without pulling
   it out of flow on a long one. dvh follows so mobile browsers measure the
   visible viewport rather than the tallest one. */
body {
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    sans-serif;
  font-size: 16px;
  line-height: 1.6;
  color: var(--text);
  background-color: var(--bg);
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

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

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

/* The browser hides [hidden] with display: none, but that rule loses to any
   author rule that sets display. .project is display: flex, so the project
   filter set the attribute and nothing disappeared. Restate it so hiding
   works on flex and grid elements too. */
[hidden] {
  display: none !important;
}

/* Offscreen until focused, so a keyboard user can jump the nav. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: 0 0 var(--radius-sm) 0;
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-md);
  font-weight: 500;
}

.skip-link:focus {
  left: 0;
}

/* The skip target should not draw a ring when it receives focus. */
main:focus {
  outline: none;
}

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

.container {
  width: 100%;
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0 1.5rem;
}

main.container {
  margin-top: 8rem;
  margin-bottom: 5rem;
  display: flex;
  flex-direction: column;
  gap: 3.5rem;
  flex: 1 0 auto;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

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

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  background: var(--header-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
}

.site-header nav {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0.75rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.brand {
  font-weight: 600;
  font-size: var(--text-lg);
  padding: 0.4rem 0.5rem;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

.brand:hover {
  background: var(--surface-soft);
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 0.125rem;
  list-style: none;
}

.nav-links a {
  display: block;
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  color: var(--text-secondary);
  transition: background-color 0.15s, color 0.15s;
}

.nav-links a:hover {
  background: var(--surface-soft);
  color: var(--text);
}

.nav-links a[aria-current='page'] {
  color: var(--text);
  font-weight: 500;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color 0.15s, color 0.15s;
}

.icon-btn:hover {
  background: var(--surface-soft);
  color: var(--text);
}

.icon-btn svg {
  width: 20px;
  height: 20px;
}

.menu-toggle {
  display: none;
}

.mobile-nav {
  display: none;
  flex-direction: column;
  gap: 0.125rem;
  padding: 0.5rem 1.5rem 1rem;
  border-top: 1px solid var(--border);
  list-style: none;
}

.mobile-nav.open {
  display: flex;
}

.mobile-nav a {
  display: block;
  padding: 0.6rem 0.5rem;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
}

.mobile-nav a:hover {
  background: var(--surface-soft);
  color: var(--text);
}

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

.eyebrow {
  font-size: var(--text-sm);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text-muted);
}

/* Eyebrow with a leading icon, used for the location line in the hero. */
.eyebrow-pin {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.eyebrow-pin svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  /* Optical alignment: the pin reads slightly low against uppercase text. */
  margin-top: -1px;
}

.page-title {
  margin-top: 0.5rem;
  font-size: 2.25rem;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.15;
}

.page-intro {
  margin-top: 0.75rem;
  max-width: var(--measure-prose);
  color: var(--text-secondary);
  line-height: 1.7;
}

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
}

.section-head h2 {
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.02em;
}

.section-head p {
  margin-top: 0.25rem;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.section-head .more {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex-shrink: 0;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.section-head .more svg {
  width: 15px;
  height: 15px;
  transition: transform 0.15s;
}

.section-head .more:hover {
  color: var(--text);
}

/* Underline the words only, so the arrow is not dragged through a line. */
.section-head .more:hover span {
  text-decoration: underline;
}

.section-head .more:hover svg {
  transform: translateX(2px);
}

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

.hero {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding-bottom: 2.5rem;
  border-bottom: 1px solid var(--border);
}

.hero-top {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

/* Large portrait, matching the placement on the previous site: full width on
   mobile, sitting beside the text on wide screens. */
/* Wraps the portrait so it can be clicked to swap. Carries the sizing the
   image used to own, so the image just fills it. */
.hero-photo-toggle {
  display: block;
  width: 100%;
  /* Single value for every breakpoint, so there is one number to tune. */
  max-width: 252px;
  padding: 0;
  border: none;
  background: none;
  border-radius: var(--radius);
  cursor: pointer;
  flex-shrink: 0;
  line-height: 0;
}

.hero-photo {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
  transition: box-shadow 0.15s, transform 0.15s;
}

.hero-photo-toggle:hover .hero-photo {
  transform: translateY(-2px);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.18);
}

.hero-photo-toggle:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 4px;
}

[data-theme='dark'] .hero-photo {
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
}

.hero-text {
  max-width: var(--measure-prose);
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.hero-text h1 {
  font-size: 2.25rem;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.1;
}

.hero-role {
  font-size: var(--text-xl);
  font-weight: 400;
  color: var(--text-secondary);
}

.hero-role a:hover {
  text-decoration: underline;
}

/* Time sensitive availability, sits directly under the role line. */
.status-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-top: 0.25rem;
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.25rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-soft);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

/* The one piece of colour on the site, and it carries meaning rather than
   decoration: this role is actively open. */
.status-dot {
  width: 7px;
  height: 7px;
  flex-shrink: 0;
  border-radius: 999px;
  background: #16a34a;
  box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.16);
}

[data-theme='dark'] .status-dot {
  background: #4ade80;
  box-shadow: 0 0 0 3px rgba(74, 222, 128, 0.18);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-top: 0.25rem;
}

/* Separates the social icons from the document buttons below them. */
.button-row + .button-row {
  padding-top: 0.75rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-secondary);
  font-size: var(--text-md);
  transition: border-color 0.15s, color 0.15s;
}

.btn:hover {
  border-color: var(--border-strong);
  color: var(--text);
}

.btn svg {
  width: 16px;
  height: 16px;
}

.btn-icon {
  padding: 0.45rem 0.6rem;
}

.prose {
  /* Wider than the strict prose measure so the block does not stop short
     under the hero image, but still short of the full container: full width
     would push lines past 110 characters, which is hard to read. */
  max-width: var(--measure-card);
  color: var(--text-secondary);
  line-height: 1.75;
}

.prose p + p {
  margin-top: 1rem;
}

/* Lettered list for the research areas, as on the previous site.
   Generous space above and below so it reads as a distinct block. */
.alpha-list {
  margin: 1.5rem 0;
  padding-left: 1.6rem;
  list-style-type: lower-alpha;
}

.alpha-list li {
  margin-bottom: 0.6rem;
}

.alpha-list li:last-child {
  margin-bottom: 0;
}

/* Research summary: a single line of figures, wrapping on narrow screens. */
.summary {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 2rem;
  padding: 1.25rem;
}

.stat {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
}

.stat-label {
  font-size: var(--text-base);
  color: var(--text-muted);
}

.stat-value {
  /* One step above the label rather than three. At text-xl the figures
     sat well clear of the words they belong to and read as separate. */
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text);
}

/* ------------------------------------------------------------ primitives -- */

/* The single card primitive. Everything on the site is this or a list of it. */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* A card containing divided rows. */
.list {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0 1.25rem;
}

.list > * + * {
  border-top: 1px solid var(--border);
}

.row {
  padding: 1.25rem 0;
}

.chip {
  display: inline-block;
  padding: 0.15rem 0.625rem;
  border-radius: 999px;
  background: var(--surface-soft);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}

.link-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.25rem 0.7rem;
  border-radius: var(--radius-sm);
  background: var(--surface-soft);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  transition: background-color 0.15s, color 0.15s;
}

.link-chip svg {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

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

.grid-2 {
  display: grid;
  gap: 1rem;
}

.grid-3 {
  display: grid;
  gap: 0.75rem;
}

/* Project cards need their own steps. grid-3 jumps straight from one column
   to three at 768px, which is too narrow for a card carrying a cover image,
   a description and two chip rows. This stops at two until 1024px. */
.project-grid {
  display: grid;
  gap: 1rem;
}

.meta {
  font-size: var(--text-md);
  color: var(--text-muted);
}

.meta-small {
  font-size: var(--text-xs);
  color: var(--text-muted);
  margin-top: 0.25rem;
}

.tag-type {
  text-align: center;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  align-self: start;
}

/* ---------------------------------------------------------- publications -- */

.pub {
  display: grid;
  gap: 0.5rem;
  padding: 1.25rem 0;
}

.pub-venue {
  font-size: var(--text-md);
  font-weight: 600;
}

.pub-title {
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: 1.5;
}

.pub-title a:hover {
  text-decoration: underline;
}

.pub-authors {
  margin-top: 0.25rem;
  font-size: var(--text-base);
  color: var(--text-muted);
}

.pub-authors strong {
  font-weight: 600;
  color: var(--text);
}

.pub-source {
  margin-top: 0.25rem;
  font-size: var(--text-base);
  font-style: italic;
  color: var(--text-muted);
}

.pub-links {
  margin-top: 0.75rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* ------------------------------------------------------------- experience -- */

.exp {
  display: grid;
  gap: 0.5rem;
  padding: 1.25rem 0;
}

.exp h3 {
  font-size: var(--text-lg);
  font-weight: 600;
}

.exp-org {
  font-size: var(--text-base);
  color: var(--text-muted);
}

.exp-org a:hover {
  text-decoration: underline;
}

.exp-desc {
  margin-top: 0.5rem;
  max-width: var(--measure-card);
  font-size: var(--text-base);
  line-height: 1.65;
  color: var(--text-secondary);
}

.exp-work {
  margin-top: 0.75rem;
  max-width: var(--measure-card);
  padding: 0.75rem;
  border-radius: var(--radius-sm);
  background: var(--surface-soft);
}

.exp-work-title {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text);
}

.exp-work-title a:hover {
  text-decoration: underline;
}

.exp-work p {
  margin-top: 0.25rem;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-secondary);
}

.exp-bullets {
  margin-top: 0.5rem;
  padding-left: 1.1rem;
  max-width: var(--measure-card);
  font-size: var(--text-base);
  line-height: 1.65;
  color: var(--text-secondary);
}

.exp-bullets li + li {
  margin-top: 0.25rem;
}

/* Discourage a single trailing word from wrapping onto its own line.
   Browsers without support just ignore this. */
.exp-bullets li,
.exp-desc,
.exp-work p,
.page-intro,
.project-body p,
.section-head p {
  text-wrap: pretty;
}

.courses {
  margin-top: 0.75rem;
  max-width: var(--measure-card);
}

.courses-label {
  margin-bottom: 0.45rem;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.exp-when {
  font-size: var(--text-md);
  color: var(--text-muted);
}

.simple-row {
  display: grid;
  gap: 0.25rem;
  padding: 1rem 0;
}

.simple-row p {
  font-size: var(--text-base);
  color: var(--text-secondary);
}

.simple-row strong {
  font-weight: 600;
  color: var(--text);
}

/* ---------------------------------------------------------------- skills -- */

.skill-card {
  padding: 1.25rem;
}

.skill-card h3 {
  font-size: var(--text-lg);
  font-weight: 600;
}

.skill-card .chip-row,
.skill-card .exp-desc {
  margin-top: 0.75rem;
}

/* ---------------------------------------------------------- updates -- */

.updates {
  padding: 0.25rem 1.25rem 0.85rem;
}

.update {
  display: grid;
  gap: 0.15rem;
  padding: 0.8rem 0;
  font-size: var(--text-base);
  color: var(--text-secondary);
}

.update + .update {
  border-top: 1px solid var(--border);
}

.update-date {
  color: var(--text-muted);
  font-size: var(--text-md);
}

/* A linked entry reads exactly like an unlinked one. The row colour is the
   same either way, and hover is the only thing that marks it clickable. */
.update a {
  color: inherit;
}

.update a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Sits after the text it belongs to, on the text baseline. */
.update .flag {
  width: 18px;
  height: 10px;
  display: inline-block;
  margin-left: 0.4rem;
  vertical-align: baseline;
  border: 1px solid var(--border);
  border-radius: 1px;
}

@media (min-width: 768px) {
  .update {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 2rem;
    align-items: baseline;
  }

  .update-date {
    text-align: right;
  }
}

/* ------------------------------------------------------------ modal -- */

/* A browser that does not know <dialog> treats it as an unknown element and
   shows its contents inline, which would dump all 17 updates onto the page.
   Supporting browsers get this from their own stylesheet anyway. */
.modal:not([open]) {
  display: none;
}

.modal {
  /* The global reset zeroes margin on everything, which also removes the
     auto margin a dialog relies on to centre itself. Put it back. */
  margin: auto;
  width: min(100% - 2rem, 42rem);
  max-height: min(80vh, 40rem);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  overflow: hidden;
}

.modal::backdrop {
  background: var(--overlay);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 1.25rem;
  border-bottom: 1px solid var(--border);
}

.modal-head h2 {
  font-size: var(--text-xl);
  font-weight: 600;
}

/* The list scrolls, the header stays put. */
.modal-body {
  max-height: calc(min(80vh, 40rem) - 4rem);
  overflow-y: auto;
}

/* A button rather than a link, so it needs the anchor reset too. */
button.more {
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
}

/* --------------------------------------------------------------- projects -- */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.filter-btn {
  padding: 0.375rem 0.875rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-secondary);
  font-size: var(--text-md);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s, background-color 0.15s, color 0.15s;
}

.filter-btn:hover {
  border-color: var(--border-strong);
}

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

.filter-count {
  font-size: var(--text-md);
  color: var(--text-muted);
}

.project {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: border-color 0.15s;
}

.project:hover {
  border-color: var(--border-strong);
}

.project-media {
  position: relative;
  /* A fixed height, not aspect-ratio. Grid rows stretch every card to the
     tallest one, and an aspect-ratio box is still a shrinkable flex item, so
     cards with long titles or many chips squashed their own cover. An
     explicit height plus flex-shrink: 0 makes every cover identical. */
  height: 200px;
  flex-shrink: 0;
  overflow: hidden;
  background: var(--surface-soft);
}

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

.status {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  padding: 0.1rem 0.6rem;
  border-radius: 999px;
  background: var(--header-bg);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

.project-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.25rem;
}

.project-body h3 {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.45;
}

.project-body > div > p {
  margin-top: 0.5rem;
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-secondary);
}

.project-foot {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* A rule between what the project is built with and where it lives. Only
   drawn when the links follow something, so a card with no tech chips does
   not open on a stray line. */
.project-links:not(:first-child) {
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}

/* ----------------------------------------------------------- education -- */

.edu {
  display: grid;
  gap: 1rem;
  padding: 1.25rem 0;
}

/* Institution marks carry their own dark brand colours, so they sit on a
   white tile. On the light theme the tile blends into the card and only the
   padding shows; on the dark theme it keeps the logos readable. */
.edu-logo {
  width: 56px;
  height: 56px;
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: #ffffff;
  padding: 5px;
}

.edu h3 {
  font-size: var(--text-lg);
  font-weight: 600;
}

.edu-concentration {
  display: inline-block;
  margin-top: 0.4rem;
  padding: 0.15rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-soft);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.edu-concentration strong {
  font-weight: 600;
  color: var(--text);
}

.edu .exp-org {
  margin-top: 0.4rem;
}

.edu-grade {
  margin-top: 0.25rem;
  font-size: var(--text-base);
  font-weight: 500;
}

.cert {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: border-color 0.15s;
}

.cert:hover {
  border-color: var(--border-strong);
}

.cert-media {
  position: relative;
  /* Same reason as .project-media: a flex item may shrink below its ratio
     when the grid row stretches the card. */
  aspect-ratio: 4 / 3;
  flex-shrink: 0;
  border: none;
  padding: 0;
  background: var(--surface-soft);
  cursor: zoom-in;
  width: 100%;
}

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

.cert-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 1.25rem;
}

.cert-body h3 {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.5;
}

.cert-provider {
  margin-top: 0.25rem;
  font-size: var(--text-base);
  color: var(--text-muted);
}

/* Filled pill with a small leading icon, carried over from the previous site. */
.cert-link {
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.75rem;
  border-radius: var(--radius-sm);
  background: var(--surface-soft);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  transition: background-color 0.15s, color 0.15s;
}

.cert-link:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

.cert-link svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

/* --------------------------------------------------------------- lightbox -- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: var(--overlay);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.lightbox.open {
  display: flex;
}

.lightbox img {
  max-width: min(100%, 1100px);
  max-height: 86vh;
  width: auto;
  border-radius: var(--radius);
  object-fit: contain;
}

.lightbox-close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  cursor: pointer;
}

.lightbox-close:hover {
  background: rgba(255, 255, 255, 0.24);
}

.lightbox-close svg {
  width: 22px;
  height: 22px;
}

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

.site-footer {
  border-top: 1px solid var(--border);
  padding: 2rem 0;
  margin-top: 1rem;
  flex-shrink: 0;
}

.site-footer .container {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  justify-content: space-between;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.site-footer a:hover {
  color: var(--text);
  text-decoration: underline;
}

.site-footer .heart {
  color: #e0504a;
}

/* ------------------------------------------------------------ responsive -- */

@media (min-width: 768px) {
  .container {
    padding: 0 2rem;
  }

  .hero-top {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 3rem;
  }

  .hero-text h1 {
    font-size: 3rem;
  }

  .page-title {
    font-size: 2.5rem;
  }

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

  /* grid-3 stays single column here. Three columns inside a 704px tablet
     leaves 227px each, which turns the text-heavy Research Focus cards into
     tall thin strips. It steps up at 1024 instead. */

  /* Certificates are the exception: six cards divide evenly into two, and
     each one is mostly image rather than prose. */
  .cert-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

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

  /* max-content on the badge column, not auto: auto lets grid squeeze the
     track to min-content and break a label like "Conference Workshop"
     onto one word per line. */
  .pub {
    grid-template-columns: 110px minmax(0, 1fr) min-content;
    column-gap: 1.5rem;
  }

  /* Manuscripts under review drop the left column only. */
  .pub.no-venue {
    grid-template-columns: minmax(0, 1fr) min-content;
  }

  .exp,
  .simple-row {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 2rem;
  }

  .exp-when,
  .simple-row .exp-when {
    text-align: right;
  }

  .edu {
    grid-template-columns: 56px minmax(0, 1fr) auto;
    column-gap: 1.5rem;
    align-items: start;
  }
}

@media (min-width: 1024px) {
  /* Covers Research Focus and the certificate grid, which also carries
     grid-3, so both reach three columns at the same width. */
  .grid-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

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

@media (max-width: 1023px) {
  .nav-links {
    display: none;
  }

  .menu-toggle {
    display: inline-flex;
  }
}

@media (max-width: 640px) {
  main.container {
    margin-top: 6.5rem;
    gap: 2.5rem;
  }

  .hero-text h1 {
    font-size: 2rem;
  }

  .page-title {
    font-size: 1.875rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
  }
}

/* ------------------------------------------------- academic service -- */

.service-groups {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.service-group {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.service-group > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.9rem 1.25rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius);
}

/* Replace the default marker with a chevron that rotates when open. */
.service-group > summary::-webkit-details-marker {
  display: none;
}

.service-group > summary::after {
  content: '';
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.15s;
}

.service-group[open] > summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.service-group > summary:hover {
  background: var(--surface-soft);
}

.service-count {
  padding: 0.05rem 0.5rem;
  border-radius: 999px;
  background: var(--surface-soft);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-muted);
}

.service-rows {
  padding: 0 1.25rem;
  border-top: 1px solid var(--border);
}

.service-item {
  display: grid;
  gap: 0.15rem;
  padding: 0.7rem 0;
}

.service-item + .service-item {
  border-top: 1px solid var(--border);
}

.service-item > p:first-child {
  font-size: var(--text-base);
  color: var(--text-secondary);
}

.service-role {
  font-weight: 600;
  color: var(--text);
}

.service-role::after {
  content: ', ';
  font-weight: 400;
  color: var(--text-secondary);
}

.service-place {
  color: var(--text-muted);
}

.service-place::before {
  content: ', ';
}

@media (min-width: 768px) {
  .service-item {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 2rem;
  }

  .service-item .exp-when {
    text-align: right;
  }
}

.page-intro strong {
  font-weight: 600;
  color: var(--text);
}

/* ----------------------------------------------------------------- notes -- */

/* Every category carries one accent, set on an ancestor and read by the dot,
   the card edge, and the series rule. Colour is the only place the site uses
   any, so it is kept to small marks rather than fills. Dark values are lifted
   because the same hue reads muddy on a dark surface. */
.cat-blue   { --cat: #2563eb; --cat-soft: rgba(37, 99, 235, 0.09); }
.cat-violet { --cat: #7c3aed; --cat-soft: rgba(124, 58, 237, 0.09); }
.cat-green  { --cat: #15803d; --cat-soft: rgba(21, 128, 61, 0.09); }
.cat-amber  { --cat: #b45309; --cat-soft: rgba(180, 83, 9, 0.09); }

[data-theme='dark'] .cat-blue   { --cat: #60a5fa; --cat-soft: rgba(96, 165, 250, 0.13); }
[data-theme='dark'] .cat-violet { --cat: #a78bfa; --cat-soft: rgba(167, 139, 250, 0.13); }
[data-theme='dark'] .cat-green  { --cat: #4ade80; --cat-soft: rgba(74, 222, 128, 0.13); }
[data-theme='dark'] .cat-amber  { --cat: #fbbf24; --cat-soft: rgba(251, 191, 36, 0.13); }

.note-dot {
  display: inline-block;
  flex-shrink: 0;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--cat, var(--border-strong));
}

/* Says the section is unfinished, attached to the word "Notes" wherever it
   appears. Sized in em so the one rule serves both the page heading and the
   much smaller breadcrumb. Outlined rather than filled: it should read as a
   note to the reader, not as an error. */
.draft-tag {
  display: inline-block;
  margin-left: 0.5em;
  padding: 0.15em 0.55em;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text-muted);
  font-size: 0.4em;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 1.7;
  text-transform: uppercase;
  vertical-align: middle;
  white-space: nowrap;
}

/* In the breadcrumb the surrounding text is already at the small end of the
   scale and uppercase, so the em ratio that suits an h1 would be unreadable. */
.eyebrow .draft-tag {
  font-size: 0.85em;
  letter-spacing: 0.06em;
}

/* -- hub ------------------------------------------------------------------ */

.cat-grid {
  gap: 1rem;
}

.cat-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1.5rem;
  transition: border-color 0.15s, transform 0.15s;
}

.cat-card:hover {
  border-color: var(--cat);
  transform: translateY(-2px);
}

.cat-card-top {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.cat-tagline {
  color: var(--cat);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.cat-card h3 {
  font-size: 1.125rem;
  font-weight: 600;
}

.cat-desc {
  color: var(--text-secondary);
  font-size: var(--text-base);
}

/* Pushed to the bottom edge so cards of unequal text still line their counts
   up with each other. */
.cat-count {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: auto;
  padding-top: 0.75rem;
  color: var(--text-muted);
  font-size: var(--text-md);
}

.cat-count svg {
  width: 15px;
  height: 15px;
  margin-left: auto;
  transition: transform 0.15s;
}

.cat-card:hover .cat-count svg {
  transform: translateX(3px);
}

.cat-count .dot {
  margin: 0 0.1rem;
}

/* -- category page -------------------------------------------------------- */

/* The accent class wraps the whole page so --cat reaches the series cards,
   which means this element sits between .container and the sections that
   were spaced by it. It has to pass that spacing through. */
.cat-page {
  display: flex;
  flex-direction: column;
  gap: 3.5rem;
}

.cat-page-head .page-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.cat-page-head .note-dot {
  width: 10px;
  height: 10px;
}

.stack-tight {
  display: grid;
  gap: 1rem;
}

/* A series is one card: the book or topic on top, its parts listed below in
   reading order rather than by date, because order is the whole point. */
.series {
  overflow: hidden;
}

.series-head {
  padding: 1.5rem;
  border-bottom: 1px solid var(--border);
  background: var(--cat-soft);
}

.series-head h3 {
  font-size: 1.125rem;
  font-weight: 600;
}

.series-author {
  margin-top: 0.15rem;
  color: var(--text-muted);
  font-size: var(--text-md);
}

.series-desc {
  max-width: var(--measure-card);
  margin-top: 0.6rem;
  color: var(--text-secondary);
  font-size: var(--text-base);
}

.series-count {
  margin-top: 0.75rem;
  color: var(--cat);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.part-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.5rem 1rem;
  padding: 0.85rem 1.5rem;
  transition: background-color 0.15s;
}

.part-row + .part-row {
  border-top: 1px solid var(--border);
}

.part-row:hover {
  background: var(--surface-soft);
}

.part-label {
  color: var(--cat);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.part-title {
  color: var(--text);
  font-size: var(--text-base);
}

.part-row:hover .part-title {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.part-time {
  color: var(--text-muted);
  font-size: var(--text-md);
  white-space: nowrap;
}

@media (max-width: 599px) {
  .part-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .part-label {
    grid-column: 1 / -1;
  }
}

/* -- listing rows --------------------------------------------------------- */

/* Same two column shape as the publication and service rows: content left,
   date and reading time right, stacking on narrow screens. */
.note-row {
  display: grid;
  gap: 0.5rem;
  padding: 1.25rem 0;
}

/* The category mark above a row title, so a mixed list stays readable
   without the reader opening anything. */
.note-kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.note-kicker:empty {
  display: none;
}

.note-cat {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--cat);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.note-part {
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.note-title {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.4;
}

.note-title a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.note-summary {
  max-width: var(--measure-card);
  margin-top: 0.35rem;
  color: var(--text-secondary);
  font-size: var(--text-base);
}

.note-row .chip-row {
  margin-top: 0.75rem;
}

@media (min-width: 768px) {
  .note-row {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 2rem;
  }

  .note-row .exp-when {
    text-align: right;
  }
}

/* A post. Narrower than the page container: running prose needs a short
   measure far more than a list of cards does. */
.note {
  max-width: var(--measure-prose);
}

.note-head .eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
}

.note-head .eyebrow a:hover {
  color: var(--text);
}

.crumb-sep {
  color: var(--border-strong);
}

/* Names the run a note belongs to and where it sits in it, so a chapter can
   be opened cold from a search result and still make sense. */
.note-series {
  margin-top: 0.9rem;
  color: var(--cat);
  font-size: var(--text-md);
  font-weight: 600;
}

.note-series .dot {
  margin: 0 0.45rem;
  color: var(--border-strong);
  font-weight: 400;
}

.note-meta {
  margin-top: 0.75rem;
  color: var(--text-muted);
  font-size: var(--text-md);
}

.note-meta .dot {
  margin: 0 0.5rem;
}

.note-head .chip-row {
  margin-top: 1rem;
}

.note-prose {
  /* Overrides the .prose card measure: this block is the page, not a column
     inside one, so it follows the reading measure instead. */
  max-width: none;
  margin-top: 2.5rem;
}

.note-prose h2 {
  margin: 2.5rem 0 0.75rem;
  color: var(--text);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.35;
}

.note-prose h3 {
  margin: 2rem 0 0.5rem;
  color: var(--text);
  font-size: var(--text-lg);
  font-weight: 600;
}

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

.note-prose a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border-strong);
}

.note-prose a:hover {
  text-decoration-color: var(--text);
}

.note-prose strong {
  font-weight: 600;
  color: var(--text);
}

.note-prose ul,
.note-prose ol {
  margin: 1rem 0;
  padding-left: 1.4rem;
}

.note-prose li + li {
  margin-top: 0.5rem;
}

/* The opening line of a short note, set one step up so the post states its
   result before the reader commits to the body. */
.note-lede {
  color: var(--text);
  font-size: var(--text-xl);
  line-height: 1.6;
}

.note-prose blockquote {
  margin: 1.75rem 0;
  padding: 0.25rem 0 0.25rem 1.25rem;
  border-left: 2px solid var(--border-strong);
  color: var(--text);
  font-size: 1.0625rem;
  line-height: 1.65;
}

.note-prose pre {
  overflow-x: auto;
  margin: 1.5rem 0;
  padding: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-soft);
  font-size: var(--text-md);
  line-height: 1.6;
}

.note-prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Inline code only. The selector excludes code inside pre, which already has
   its own background and would otherwise get a second one. */
.note-prose :not(pre) > code {
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
  background: var(--surface-soft);
  font-size: 0.875em;
}

.note-figure {
  margin: 2rem 0;
}

.note-figure-body {
  padding: 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.note-caption {
  margin-top: 0.6rem;
  color: var(--text-muted);
  font-size: var(--text-md);
}

/* Wide content scrolls inside its own box rather than widening the page. */
.table-scroll {
  overflow-x: auto;
  margin: 1.5rem 0;
}

.note-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-base);
}

.note-table th,
.note-table td {
  padding: 0.6rem 1rem 0.6rem 0;
  text-align: left;
  white-space: nowrap;
}

/* nowrap suits a short numeric table, where wrapping would break the columns
   apart. A reference table whose cells are sentences needs the opposite, or
   it scrolls sideways for the width of its longest paragraph. */
.note-table.wrap th,
.note-table.wrap td {
  min-width: 7rem;
  white-space: normal;
}

/* A long reference note: many wide tables and full job scripts, which the
   42rem reading measure would cram. Prose measure is still applied to the
   paragraphs inside so the running text does not stretch with it. */
.note-wide {
  max-width: none;
}

.note-wide .note-prose > p,
.note-wide .note-prose > ul,
.note-wide .note-prose > ol,
.note-wide .note-prose > blockquote {
  max-width: var(--measure-prose);
}

/* Section jump list for a note long enough to need one. */
.note-toc {
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.note-toc-label {
  margin-bottom: 0.75rem;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.note-toc ol {
  display: grid;
  gap: 0.35rem 2rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 700px) {
  .note-toc ol {
    grid-template-columns: 1fr 1fr;
  }
}

.note-toc a {
  color: var(--text-secondary);
  font-size: var(--text-base);
}

.note-toc a:hover {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.note-toc .toc-num {
  display: inline-block;
  min-width: 2.2rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* An aside that must not be missed: a default that bites, a policy limit.
   Louder than a blockquote, quieter than an error. */
.callout {
  margin: 1.75rem 0;
  padding: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--cat, var(--border-strong));
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-secondary);
  font-size: var(--text-base);
}

.callout strong {
  color: var(--text);
}

.callout p + p {
  margin-top: 0.6rem;
}

/* Anchor targets sit under the fixed header without this. */
.note-prose h2[id] {
  scroll-margin-top: 6rem;
}

.note-table thead th {
  border-bottom: 1px solid var(--border-strong);
  color: var(--text);
  font-weight: 600;
}

.note-table tbody tr + tr td {
  border-top: 1px solid var(--border);
}

.note-refs {
  color: var(--text-secondary);
}

.note-links {
  margin-top: 2rem;
}

/* Step either side within a series. Two columns where both exist, and the
   lone "next" stays on the right when there is no previous. */
.note-pager {
  display: grid;
  gap: 0.75rem;
  margin-top: 3rem;
}

.pager {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 0.15rem 0.6rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.15s;
}

.pager:hover {
  border-color: var(--cat);
}

.pager svg {
  grid-row: span 2;
  width: 16px;
  height: 16px;
  color: var(--cat);
}

.pager.prev svg {
  transform: scaleX(-1);
}

.pager-label {
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.pager-title {
  color: var(--text);
  font-size: var(--text-base);
}

.pager.next {
  grid-template-columns: minmax(0, 1fr) auto;
  text-align: right;
}

.pager.next svg {
  grid-row: span 2;
  grid-column: 2;
}

.pager.next .pager-label,
.pager.next .pager-title {
  grid-column: 1;
}

@media (min-width: 600px) {
  .note-pager {
    grid-template-columns: 1fr 1fr;
  }

  /* A next with no previous sits in the second column rather than sliding
     left, so the arrow stays on the outside edge. */
  .note-pager > .next:only-child {
    grid-column: 2;
  }
}

.note-foot {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
}

/* The .more treatment is scoped to .section-head, so it is restated here
   rather than inherited. Points back to the index, so the arrow is mirrored
   and it slides left on hover instead of right. */
.note-foot .more {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--text-md);
  color: var(--text-muted);
}

.note-foot .more svg {
  width: 15px;
  height: 15px;
  transform: scaleX(-1);
  transition: transform 0.15s;
}

.note-foot .more:hover {
  color: var(--text);
}

.note-foot .more:hover span {
  text-decoration: underline;
}

.note-foot .more:hover svg {
  transform: scaleX(-1) translateX(2px);
}
