/* Source Sans 3 (variable, latin), self-hosted from @fontsource-variable/source-sans-3 (OFL). */
@font-face { font-family: "Source Sans 3"; font-style: normal; font-weight: 200 900; font-display: swap; src: url(/assets/fonts/source-sans-3/source-sans-3-latin-wght-normal.woff2) format("woff2"); }
@font-face { font-family: "Source Sans 3"; font-style: italic; font-weight: 200 900; font-display: swap; src: url(/assets/fonts/source-sans-3/source-sans-3-latin-wght-italic.woff2) format("woff2"); }

/* ============================================================================
   luding.phd — overrides on top of Dashmix (Bootstrap 5). Uses Bootstrap CSS
   custom properties (--bs-*) where possible so it tracks the active theme.
   ============================================================================ */

:root {
  --nav-h: 64px;
  --maxw: 1080px;          /* "1080p" content width with comfortable side margins */
  --gutter: 1.5rem;        /* shared left/right margin for nav, content, and footer */
  --toc-w: 250px;
  --page-h1-h: 4.5rem;     /* vertical space the full-width page H1 occupies (offsets the fixed TOC) */

  /* Brand primary — the ONE knob. Change this and the derived colors below follow.
     (xplay recolors the primary *classes* to terracotta but never sets the root
     --bs-primary token, so we set it here.) */
  --bs-primary: #c74433;
  --bs-primary-rgb: 199, 68, 51;   /* keep in sync with --bs-primary (Bootstrap's rgba() utilities) */
  --accent: var(--bs-primary);

  /* Derived from --bs-primary via color-mix so they track it automatically. */
  --primary-dark: color-mix(in srgb, var(--bs-primary) 75%, black);    /* ≈ #963327 (xplay .bg-primary-dark) */
  --primary-wash: color-mix(in srgb, var(--bs-primary) 12%, white);    /* light warm tint, ≈ #f4eae7 */

  /* Text links: Bootstrap colors <a> from --bs-link-color-rgb (the theme compiles this to a fixed
     terracotta triplet). Point it at the primary so links track the brand color — and recolor live
     when the editor updates --bs-primary-rgb. */
  --bs-link-color-rgb: var(--bs-primary-rgb);
  --bs-link-hover-color-rgb: var(--bs-primary-rgb);

  /* Site typeface: Source Sans 3 (self-hosted, see @font-face above), overriding the theme's default. */
  --bs-font-sans-serif: "Source Sans 3", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --bs-body-font-family: var(--bs-font-sans-serif);

  /* Bump the base body text size (Bootstrap default is 1rem). */
  --bs-body-font-size: 1.1rem;
}

html { scroll-behavior: smooth; }
body { font-family: var(--bs-font-sans-serif); }

/* Sticky-footer scaffold: footer sits at the bottom even on short pages. */
body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
main { flex: 1 0 auto; }

/* Text links track the brand color. The xplay theme hardcodes `a { color:#c74433 }` (and loads
   before this file), so the --bs-link-color-rgb override above can't win — we restate `a` here so
   links follow --bs-primary and recolor live in the editor. Nav/TOC set their own (more specific)
   colors and are unaffected; inverted bands keep white links (rule later in this file wins). */
a { color: var(--bs-primary); }
a:hover { color: var(--primary-dark); }

/* ===== Top nav: three zones via grid so the name stays truly centered ===== */
.site-nav {
  background: var(--bs-body-bg, #fff);
  border-bottom: 1px solid var(--bs-border-color, #e3e4e8);
}
.site-nav-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  height: var(--nav-h);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: end;                 /* sit content toward the bottom of the bar */
  padding: 0 var(--gutter) 0.4rem;  /* small bottom gap */
}
.nav-left {
  justify-self: start;
  display: flex;
  align-items: center;
  gap: 1.1rem;
}
.nav-cv {
  position: relative;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--bs-body-color);
  font-size: 0.95rem;
}
.nav-me {
  position: relative;
  text-decoration: none;
  color: var(--bs-body-color);
  font-size: 0.95rem;
}
.nav-cv:hover,
.nav-me:hover { color: var(--accent); }

/* Optional FontAwesome icon on a nav link, rendered before the label. */
.nav-icon { margin-right: 0.4rem; }

/* Hover underline: a centered dot that grows into a 2px rule across the link.
   Animated with transform: scaleX (GPU-composited) rather than width to avoid
   layout thrash; rounded ends keep the small/early state reading as a dot. */
.nav-cv::after,
.nav-me::after,
.nav-right a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  border-radius: 1px;
  background: var(--accent);
  transform: scaleX(0);       /* hidden; grows from the center on hover */
  transform-origin: center;
  opacity: 0;
  transition: transform 0.28s ease, opacity 0.12s ease;
}
.nav-cv:hover::after,
.nav-me:hover::after,
.nav-right a:hover::after {
  transform: scaleX(1);
  opacity: 1;
}
/* Active page link (CV / Me): accent color + heavier weight. Deliberately
   distinct from the hover underline so the two states never read the same. */
.nav-cv.is-active,
.nav-me.is-active { color: var(--accent); font-weight: 700; }
.nav-name {
  justify-self: center;
  font-size: 2.1rem;
  font-weight: 700;
  line-height: 1;
  text-decoration: none;
  color: var(--accent);
  white-space: nowrap;
}
.nav-right {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 1.4rem;
}
.nav-right a {
  position: relative;
  text-decoration: none;
  color: var(--bs-body-color);
  font-size: 0.95rem;
}
.nav-right a:hover { color: var(--accent); }

/* ===== Full-width section bands + a fixed TOC =====
   Each <section> is a full-width band; its padding centers content to --maxw and
   clears the fixed TOC on the left, so band backgrounds run full-width (including
   behind the TOC). The TOC is pinned and never scrolls. */
.cv { position: relative; }

/* Simple centered container (used by 404.html). */
.cv-container { max-width: var(--maxw); margin: 0 auto; padding: 2.25rem var(--gutter); }

/* Floating TOC card, pinned at the left (default) or right edge of the centered column. Its top
   clears the full-width page H1 above it (--page-h1-h). Alignment is set per page via has-toc-* on
   <main> (NavItem.TocAlignment). */
.cv-toc {
  position: fixed;
  top: calc(var(--nav-h) + 1px + var(--page-h1-h));
  left: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  width: var(--toc-w);
  max-height: calc(100vh - var(--nav-h) - var(--page-h1-h) - 1rem);
  overflow: auto;
  background: var(--bs-body-bg, #fff);
  border-radius: 0.65rem;
  padding: 1rem;
  z-index: 5;
}
.has-toc-right .cv-toc {
  left: auto;
  right: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
}
/* Rectangular profile photo, ~ the combined height of the menu items below it. */
.toc-photo {
  width: 100%;
  height: 230px;
  margin-bottom: 1rem;
  border-radius: 0.4rem;
  object-fit: cover;          /* fill the frame without distortion */
  background: #dfe3e8;        /* shows only if the image fails to load */
}
.toc-list { display: flex; flex-direction: column; }
/* The auto (section-derived) and manual ("TOC link" items) halves are wrappers only — display:contents
   keeps their links as direct flex children of .toc-list, so every .toc-list a rule still applies. */
.toc-auto, .toc-manual { display: contents; }
/* Rule between the generated entries and the manually placed ones. Hidden by the server (and by
   site.js) when either half is empty, so it never leads or trails the list. */
.toc-sep {
  margin: 0.5rem 0.6rem;
  border: 0;
  border-top: 1px solid var(--bs-border-color, #dfe4f1);
  opacity: 1;
}
.toc-list a {
  position: relative;
  padding: 0.6rem 0.6rem;
  border-radius: 0.35rem;
  text-decoration: none;
  color: var(--bs-body-color);
  font-weight: 500;
  transition: color 0.2s ease, padding-left 0.22s ease;
}
/* Dot lives on every link but is hidden until active; it sits where the text
   normally begins (left: 0.6rem) so it pops in just left of the slid text. */
.toc-list a::before {
  content: "";
  position: absolute;
  left: 0.6rem;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -3.5px;
  border-radius: 1px;                       /* near-sharp corners for a diamond */
  background: var(--accent);
  opacity: 0;
  transform: scale(0.3) rotate(45deg);      /* rotate the square into a diamond */
  transition: opacity 0.18s ease, transform 0.22s ease;
}
.toc-list a:hover {
  background: var(--bs-tertiary-bg, #f1f3f5);
  color: var(--accent);
}
/* Scroll-spy: the section the reader is currently "on" (see js/site.js).
   Text slides right to open a gap, and the accent dot fades/scales in. */
.toc-list a.active {
  color: var(--accent);
  font-weight: 600;
  padding-left: 1.55rem;
}
.toc-list a.active::before {
  opacity: 1;
  transform: scale(1) rotate(45deg);
}

/* Generic prosites admin shortcut: a small pill pinned to the very top-right corner, shown only
   to a logged-in user (rendered by _SiteScripts on every layout). Themed via --bs-primary. */
.ps-admin-btn {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 1500;                 /* above the sticky nav; below the edit-mode bar (2000) */
  padding: 0.3rem 0.7rem;
  background: var(--bs-primary);
  color: #fff;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  border-bottom-left-radius: 0.4rem;
  opacity: 0.85;
  transition: opacity 0.15s ease;
}
.ps-admin-btn:hover { opacity: 1; color: #fff; }

/* Generic contact form (site.js builds it when a profile has a hosted form endpoint): a native
   <dialog>, themed via --bs-primary, so every layout gets it. */
.ps-contact {
  width: min(32rem, calc(100vw - 2rem));
  padding: 0;
  border: 0;
  border-radius: 0.75rem;
  color: #1d1530;
  background: #fff;
  box-shadow: 0 24px 60px rgba(20, 10, 30, 0.35);
}
.ps-contact::backdrop { background: rgba(20, 10, 30, 0.55); }
.ps-contact-form { display: grid; gap: 0.85rem; padding: 1.5rem; }
.ps-contact-form h2 { margin: 0 0 0.25rem; font-size: 1.35rem; color: var(--bs-primary); }
.ps-contact-form label { display: grid; gap: 0.3rem; font-size: 0.85rem; font-weight: 600; }
.ps-contact-form input, .ps-contact-form textarea {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid #d6d0de;
  border-radius: 0.45rem;
  font: inherit;
  font-weight: 400;
  color: inherit;
  background: #fff;
}
.ps-contact-form textarea { resize: vertical; min-height: 8rem; }
.ps-contact-form input:focus, .ps-contact-form textarea:focus {
  outline: 2px solid color-mix(in srgb, var(--bs-primary) 55%, transparent);
  outline-offset: 1px;
  border-color: var(--bs-primary);
}
.ps-contact-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }   /* spam honeypot */
.ps-contact-status { margin: 0; min-height: 1.2em; font-size: 0.85rem; }
.ps-contact-status[data-kind="ok"] { color: #1a7f37; }
.ps-contact-status[data-kind="error"] { color: #cf222e; }
.ps-contact-actions { display: flex; justify-content: flex-end; gap: 0.6rem; }
.ps-contact-actions button {
  padding: 0.5rem 1.1rem;
  border-radius: 999px;
  border: 1px solid var(--bs-primary);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
/* The inline Contact form block: same form on the page, no card chrome. */
.ps-contact-block { margin: 1.25rem 0; }
.ps-contact-inline { max-width: 40rem; padding: 0; }
.ps-contact-inline fieldset { display: grid; gap: 0.85rem; margin: 0; padding: 0; border: 0; min-width: 0; }
.ps-contact-inline .ps-contact-actions { justify-content: flex-start; }
.ps-contact-note { margin: 0; padding: 0.75rem 1rem; border: 1px dashed #d6d0de; border-radius: 0.5rem; color: #6a6478; font-size: 0.9rem; }
.ps-contact-cancel { background: transparent; color: var(--bs-primary); }
.ps-contact-send { background: var(--bs-primary); color: #fff; }
.ps-contact-send:disabled { opacity: 0.6; cursor: progress; }

/* Full-width page H1, within the left/right margins (NOT cleared by the TOC — it sits above it).
   Style: large bold title with a short accent underline (accent-underline, left-aligned). */
.page-heading {
  padding: 1.75rem max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter))) 0;
}
.page-heading h1 {
  margin: 0;
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.2;
}
.page-heading .page-heading-text {
  display: inline-block;
  padding-bottom: 0.35rem;
  border-bottom: 3px solid var(--accent);
}

/* Bands: full-width; padding centers content to --maxw with gutter margins. TOC clearance is added
   only when the page actually shows a floating TOC, on whichever side it's aligned (has-toc-*). */
.band {
  padding-block: 2.5rem;
  padding-left: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  padding-right: max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  scroll-margin-top: calc(var(--nav-h) + 1rem);
}
/* Clear the fixed TOC on its side (incl. 861–1080px where the column offset goes negative). */
.has-toc-left .band {
  padding-left: calc(max(var(--gutter), (100% - var(--maxw)) / 2 + var(--gutter)) + var(--toc-w) + 2.5rem);
}
.has-toc-right .band {
  padding-right: calc(max(var(--gutter), (100% - var(--maxw)) / 2 + var(--gutter)) + var(--toc-w) + 2.5rem);
}
.band-wash   { background: var(--primary-wash); }   /* warm terracotta wash, derived */
.band-invert { background: var(--primary-dark); color: #fdf6f5; }
.band-invert h2 { color: #fff; border-bottom-color: rgba(255, 255, 255, 0.55); }
/* On the dark (primary-dark) band, links are a light tint of the primary so they stay on-brand and
   readable, yet distinct from the near-white body text. */
.band-invert a { color: color-mix(in srgb, var(--bs-primary) 50%, white); }
.band-invert a:hover { color: #fff; }
.band-invert .muted { color: rgba(255, 255, 255, 0.8); }

/* Plain text links (prose/body) are underlined for clarity; styled .source-link chips are excluded
   (they keep their icon + hover underline). Applies in normal and inverted bands. */
.band a:not(.source-link) { text-decoration: underline; }

/* Stacked tagline parts (title / specialty / institution) read as one tight headline block. */
.cv-headline { line-height: 1.3; margin-bottom: 0.75rem; }

.cv-lead { margin: 0; }

.band h2 {
  display: inline-block;
  font-size: 1.5rem;
  margin: 0 0 1rem;
  padding-bottom: 0.35rem;
  border-bottom: 2px solid var(--accent);
}

/* Sub-heading (SubHeadingItem): a lighter <h3> to break up content within a section — no underline,
   no band; top margin separates it from the block above. */
.sub-heading {
  font-size: 1.15rem;
  font-weight: 600;
  margin: 1.5rem 0 0.6rem;
  color: var(--bs-body-color);
}
.sub-heading:first-child { margin-top: 0; }
.band-invert .sub-heading { color: #fff; }

.muted { color: var(--bs-secondary-color, #6b6b70); font-size: 0.9rem; }
.source-link { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: 600; text-decoration: none; }
.source-link:hover { text-decoration: underline; }
.source-link i { font-size: 0.8em; }

/* Research: Google Scholar stat cards */
.cv-stats { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin: 0 0 0.75rem; }
/* Small spinner shown beside the stats while a background Google Scholar refresh is in flight. */
.cv-stats-spin {
  width: 1.1rem;
  height: 1.1rem;
  flex: 0 0 auto;
  border: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: cv-stats-spin 0.8s linear infinite;
}
@keyframes cv-stats-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .cv-stats-spin { animation-duration: 2s; } }
/* Editor-only "Refresh from Scholar" button. */
.cv-stats-refresh {
  flex: 0 0 auto;
  font: inherit;
  font-size: 0.78rem;
  cursor: pointer;
  padding: 0.35rem 0.7rem;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  background: var(--bs-body-bg, #fff);
  color: var(--accent);
  border-radius: 0.5rem;
}
.cv-stats-refresh:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.cv-stats.is-refreshing .cv-stats-refresh { opacity: 0.5; pointer-events: none; }
.stat {
  flex: 1 1 0;
  min-width: 110px;
  background: var(--bs-tertiary-bg, #f1f3f5);
  border-radius: 0.6rem;
  padding: 0.9rem 1rem;
  text-align: center;
}
.stat-num { display: block; font-size: 1.7rem; font-weight: 700; color: var(--accent); line-height: 1.1; }
.stat-label { font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--bs-secondary-color, #6b6b70); }

/* "Text" block: markdown paragraphs and/or lists. Trim outer margins so it sits flush in a band. */
.prose-text { margin: 0; }
.prose-text > :first-child { margin-top: 0; }
.prose-text > :last-child { margin-bottom: 0; }
.prose-text ul, .prose-text ol { margin: 0.5rem 0; padding-left: 1.2rem; }
.prose-text li { padding: 0.2rem 0; }

/* A "Text" block with an image beside it. The figure is always FIRST in the DOM (so the mobile
   stack puts the image on top and screen readers hit it first); prose-img-right flips the row. */
.prose-block { display: flex; gap: 1rem; align-items: flex-start; }
.prose-block.prose-img-left { flex-direction: row; }
.prose-block.prose-img-right { flex-direction: row-reverse; }
.prose-block .prose-text { flex: 1 1 auto; min-width: 0; }
/* --img-w is set inline from the item's ImageWidthPercent (the gap share already subtracted), so the
   width is free-form rather than a fixed set of classes. Using a custom property rather than a bare
   inline width keeps the mobile override below from needing !important. */
.prose-figure { flex: 0 0 auto; margin: 0; min-width: 0; max-width: 100%; width: var(--img-w, calc(50% - 0.5rem)); }

/* Consecutive Image items are grouped into one .image-row (PageViewModel.GroupBlocks) so they sit
   side by side at their widths, wrap when the row is full, and align as a GROUP — a floated figure
   could never be centred. A lone image is simply a one-item row. */
.image-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  /* The gutter is per-figure padding rather than `gap`, so percentage widths still add up to exactly
     100% and a 4×25% row can't spill onto a second line; the negative inline margin pulls the outer
     edges back flush with the surrounding text. */
  margin-inline: -0.375rem;
}
.image-row-left { justify-content: flex-start; }
.image-row-center { justify-content: center; }
.image-row-right { justify-content: flex-end; }

/* Standalone image item: full width by default, or a percentage set inline via --img-w. */
.content-figure {
  flex: 0 0 auto;
  width: var(--img-w, 100%);
  max-width: 100%;
  box-sizing: border-box;
  padding-inline: 0.375rem;
}
.content-figure-empty { padding-block: 1rem; border: 1px dashed var(--bs-border-color, #dfe4f1); border-radius: 0.4rem; }

/* Sizing comes from Dashmix's .img-fluid (max-width:100%; height:auto) applied in the partial, so an
   image can never force its column — or the page — wider than the space available. Add .img-fluid-100
   (Dashmix) alongside it if a given image should stretch to fill rather than cap at natural size. */
.prose-figure img, .content-figure img {
  display: block;
  border-radius: 0.4rem;
}
/* Text items sit flush in a band, but an image needs breathing room from its neighbours — without
   pushing the band's own padding around, hence the first/last trim (same idea as .prose-text above).
   The outer spacing belongs to the ROW, not the figure: inside a row every figure is a first-child,
   so a margin there would be trimmed away and the images would collide with the text above. */
.prose-block, .image-row { margin-block: 1.25rem; }
.prose-block:first-child, .image-row:first-child { margin-top: 0; }
.prose-block:last-child, .image-row:last-child { margin-bottom: 0; }

.prose-figure figcaption, .content-figure figcaption {
  margin-top: 0.4rem;
  font-size: 0.85rem;
  line-height: 1.35;
  color: var(--bs-secondary-color, #6b6b70);
}

/* Phones: the split collapses to one column, image above the text. An image row keeps its layout but
   no figure drops below ~half the line, so a 3- or 4-up set degrades to 2-up instead of postage stamps. */
@media (max-width: 640px) {
  .prose-block { flex-direction: column; }
  .prose-block.prose-img-left, .prose-block.prose-img-right { flex-direction: column; }
  .prose-figure { width: 100%; }
  .content-figure { width: max(var(--img-w, 100%), 45%); }
}

/* Education + Work History: vertical timeline */
.timeline {
  position: relative;
  list-style: none;
  margin: 0;
  padding-left: 1.5rem;
  border-left: 2px solid var(--bs-border-color, #e3e4e8);
}
/* Dashmix ships its own .timeline component whose ::before draws a 0.25rem bar at
   left:1.375rem (a second, thicker line beside the content). Class-name collision —
   kill that pseudo-element so only our border-left spine + dots remain. */
.timeline::before { content: none; }
.timeline-item { position: relative; padding: 0 0 1.5rem 0.4rem; }
.timeline-item:last-child { padding-bottom: 0; }
.timeline-item::before {
  content: "";
  position: absolute;
  left: calc(-1.5rem - 7px);
  top: 0.3rem;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--bs-body-bg, #fff);
}
.tl-when { font-size: 0.8rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent); }
.tl-title { font-weight: 600; margin: 0.1rem 0 0; }
.tl-org { color: var(--bs-secondary-color, #6b6b70); }
/* Per-role responsibilities/accomplishments under a timeline entry. */
.tl-points {
  margin: 0.5rem 0 0;
  color: var(--bs-secondary-color, #6b6b70);
}
.tl-points ul { margin: 0; padding-left: 1.1rem; }
.tl-points li { margin-bottom: 0.25rem; }
.tl-points li:last-child { margin-bottom: 0; }
/* Optional institution logo beside a timeline entry: logo left, text right, the logo
   sized to roughly span the title + org lines. */
.tl-main { display: flex; gap: 0.85rem; align-items: flex-start; }
.tl-text { min-width: 0; flex: 1 1 auto; }
.tl-logo {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  object-fit: contain;
  border-radius: 0.3rem;
  margin-top: 0.15rem;
}

/* Grants: timeline dot + a subtle full-width card. The year sits top-left inside
   the card (aligned with the dot), the role top-right; the amount anchors a
   footer row. Role vs amount are told apart by type, not color. */
.grant-list .timeline-item::before { top: 1.1rem; }   /* re-center dot on the in-card year */
.grant-card {
  padding: 0.85rem 1rem;
  background: var(--bs-tertiary-bg, #f8f9fa);
  border-radius: 0.5rem;
}
.grant-head {
  display: flex;
  justify-content: space-between;        /* year left, role hard right */
  align-items: baseline;
  gap: 1rem;
}
.grant-role {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;            /* label voice */
  color: var(--bs-secondary-color, #6b6b70);
}
.grant-title { margin: 0.35rem 0 0; font-weight: 700; font-size: 1.1rem; color: var(--bs-body-color); }
.grant-sponsor { margin: 0.15rem 0 0; color: var(--bs-secondary-color, #6b6b70); font-size: 0.95rem; }
.grant-desc { margin: 0.55rem 0 0; font-size: 0.98rem; line-height: 1.55; }
.grant-meta {
  margin-top: 0.85rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--bs-border-color, #dfe4f1);
  text-align: right;
}
.grant-amount {
  font-weight: 700;
  font-size: 1.15rem;
  font-variant-numeric: tabular-nums;   /* figure voice */
  color: var(--bs-body-color);
}

/* Publications: title + venue + citation badge (no table) */
.pub-list { list-style: none; margin: 0; padding: 0; }
.pub {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--bs-border-color, #e3e4e8);
}
.pub:last-child { border-bottom: 0; }
.pub-body { flex: 1 1 auto; min-width: 0; }
.pub-title { font-weight: 600; margin: 0 0 0.15rem; }
.pub-meta { font-size: 0.9rem; color: var(--bs-secondary-color, #6b6b70); margin: 0; }
.pub-author-self { font-weight: 700; color: var(--bs-body-color); }
.pub-cite {
  flex: 0 0 auto;
  min-width: 64px;
  text-align: center;
  background: var(--bs-tertiary-bg, #f1f3f5);
  border-radius: 0.5rem;
  padding: 0.35rem 0.5rem;
}
.pub-cite .n { display: block; font-weight: 700; color: var(--accent); line-height: 1.1; }
.pub-cite .l { font-size: 0.65rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--bs-secondary-color, #6b6b70); }

/* Teaching: course chips */
.courses { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0 0; padding: 0; list-style: none; }
.course-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  background: var(--bs-tertiary-bg, #f1f3f5);
  border-radius: 2rem;
  padding: 0.4rem 0.9rem;
  font-size: 0.9rem;
}
.course-chip code { color: var(--accent); font-weight: 700; }
/* Breathing room for a note that follows the course chips (e.g. the reviewer line). */
.courses + .muted { margin-top: 1rem; }

/* ===== Footer ===== */
.site-footer {
  flex-shrink: 0;
  background: var(--bs-body-bg, #fff);
  border-top: 1px solid var(--bs-border-color, #e3e4e8);
}
.site-footer-inner {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 1.25rem var(--gutter);
  color: var(--bs-secondary-color, #6b6b70);
  font-size: 0.9rem;
  text-align: center;
}

/* ===== About ("Me") page ===== */
/* Full-width like .cv (so the page transition treats both the same), with the
   readable content centered to --maxw by an inner wrapper. */
.about-inner { max-width: var(--maxw); margin: 0 auto; padding: 3rem var(--gutter); }
.about-head { display: flex; align-items: center; gap: 1.75rem; margin-bottom: 1.75rem; }
.about-photo {
  flex: 0 0 auto;
  width: 150px;
  height: 150px;
  border-radius: 50%;
  object-fit: cover;
  background: #dfe3e8;
}
.about-name { margin: 0; font-size: 2.1rem; font-weight: 700; color: var(--accent); }
.about-tagline { margin: 0.3rem 0 0; font-weight: 600; color: var(--bs-secondary-color, #6b6b70); }
.about-page p + p { margin-top: 1rem; }

/* ===== Page-swap transition (CV <-> Me) via the View Transitions API =====
   A cross-fade: the outgoing page fades out while the incoming page fades in on
   top of it (it paints above, so it "bleeds" over the old one) with a small
   upward settle. No slide and no clipping, so the full-width bands fade at their
   natural full width. Nav + footer stay put. Unsupported browsers swap instantly. */
.cv,
.about-page { view-transition-name: page; }

@keyframes page-fade-out { to   { opacity: 0; } }
@keyframes page-fade-in  { from { opacity: 0; transform: translateY(10px); } }

/* Don't morph the group between the two pages' differing heights; let each
   snapshot sit at its natural size and just cross-fade. */
::view-transition-group(page) { animation: none; overflow: visible; }
::view-transition-old(page) { animation: page-fade-out 0.3s ease both; }
::view-transition-new(page) { animation: page-fade-in 0.42s cubic-bezier(0.2, 0, 0, 1) both; }

/* Nav + footer (the root snapshot) stay put. */
::view-transition-group(root) { animation: none; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(page), ::view-transition-new(page) { animation: none; }
}

/* ===== Responsive: un-pin the TOC to the top; bands drop the TOC clearance ===== */
@media (max-width: 860px) {
  .cv-toc {
    position: static;
    width: auto;
    max-height: none;
    overflow: visible;
    margin: 2.25rem var(--gutter) 0;
    text-align: center;             /* center the stacked photo + links on mobile */
  }
  .toc-photo { height: 200px; max-width: 240px; margin-left: auto; margin-right: auto; }
  .toc-list { align-items: center; }   /* center each link box in the column */
  /* Drop the TOC clearance on mobile (the has-toc-* rules out-specify a bare .band, so neutralize them too). */
  .band,
  .has-toc-left .band,
  .has-toc-right .band {
    padding-left: var(--gutter);
    padding-right: var(--gutter);
  }
  /* Mobile: give the name its own centered row above the menu items (left + right beneath it). */
  .site-nav-inner {
    height: auto;
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "name name"
      "left right";
    align-items: center;
    row-gap: 0.5rem;
    padding-top: 0.6rem;
  }
  .nav-name { grid-area: name; justify-self: center; font-size: 1.6rem; }
  .nav-left { grid-area: left; justify-self: start; gap: 0.8rem; }
  .nav-right { grid-area: right; justify-self: end; gap: 1rem; }
}
