/* ==========================================================================
   danielgordon10.github.io - design system
   Hand-written CSS. No framework, no build step beyond copying this file.

   Theme switching works by setting data-theme on <html> from a tiny inline
   script in <head>, so there is never a flash of the wrong theme. Every color
   below is a custom property; the dark and light palettes define the same set
   of names, which is what makes the toggle a one-line change.
   ========================================================================== */

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

:root {
  /* type */
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
    Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Newsreader", ui-serif, Georgia, "Times New Roman", serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;

  --step--1: clamp(0.8rem, 0.78rem + 0.1vw, 0.85rem);
  --step-0: clamp(1rem, 0.97rem + 0.15vw, 1.075rem);
  --step-1: clamp(1.2rem, 1.13rem + 0.35vw, 1.4rem);
  --step-2: clamp(1.45rem, 1.3rem + 0.75vw, 1.95rem);
  --step-3: clamp(1.75rem, 1.45rem + 1.5vw, 2.75rem);
  --step-4: clamp(2.1rem, 1.5rem + 3vw, 4.25rem);

  /* spacing / shape */
  --gap: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
  --pad: clamp(1.1rem, 1rem + 0.6vw, 1.75rem);
  --section: clamp(3.5rem, 2.5rem + 5vw, 7rem);
  --measure: 68ch;
  --width: 74rem;
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 22px;
  --header-h: 62px;
  /* Generous on small screens. At 390px the timeline gutter plus the card's own
     padding were eating enough of the width that a card filled nearly the whole
     viewport; the wider floor gives the card somewhere to sit. Named so
     full-bleed children can cancel it and line their rules up with .wrap's. */
  --gutter: clamp(1.5rem, 0.75rem + 3vw, 2.5rem);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 0.28s;
}

/* dark is the default; the inline head script may flip this before paint */
:root,
[data-theme="dark"] {
  color-scheme: dark;

  --bg: #0a0c12;
  --bg-2: #0e111a;
  --surface: #12151f;
  --surface-2: #171b27;
  --surface-hover: #1c2130;
  --line: #ffffff14;
  --line-strong: #ffffff26;

  --text: #eef1f7;
  --text-2: #b3bacb;
  --text-3: #7d8598;

  --accent: #93a1ff;
  --accent-strong: #a9b3ff;
  --accent-ink: #0a0c12;
  --accent-soft: #93a1ff1f;
  --accent-line: #93a1ff45;

  --warm: #ffb782;
  --good: #5fd6a0;
  --shadow-1: 0 1px 2px #0006;
  --shadow-2: 0 18px 40px -22px #000000cc;
  --shadow-3: 0 40px 80px -40px #000000e6;
  /* The tint exists to keep the intro text legible over the photo, not to
     gray the photo out. It was at 90% by 62% of the hero's height, which
     buried the image; it now ramps in later and tops out much lower. */
  --hero-tint: linear-gradient(180deg, #0a0c1200 0%, #0a0c1299 46%, #0a0c1266 78%, var(--bg) 100%);
  --scrim: #0a0c12cc;
  --grain-opacity: 0.16;
}

[data-theme="light"] {
  color-scheme: light;

  --bg: #fbfbfd;
  --bg-2: #f4f5f9;
  --surface: #ffffff;
  --surface-2: #f7f8fb;
  --surface-hover: #f0f2f7;
  --line: #0b102018;
  --line-strong: #0b10202e;

  --text: #131722;
  --text-2: #4a5162;
  /* 4.7:1 on the lightest surface. The muted grays carry a lot of small text
     - eyebrows, card authors, captions, footer - so this cannot go any lighter
     without dropping below the 4.5:1 minimum. */
  --text-3: #666e80;

  --accent: #4a56d2;
  --accent-strong: #3a45bd;
  --accent-ink: #ffffff;
  --accent-soft: #4a56d214;
  --accent-line: #4a56d23d;

  --warm: #a94e0b;
  --good: #0f7a52;
  --shadow-1: 0 1px 2px #0b102012;
  --shadow-2: 0 18px 40px -24px #0b10202e;
  --shadow-3: 0 40px 80px -44px #0b102038;
  --hero-tint: linear-gradient(180deg, #fbfbfd00 0%, #fbfbfd8c 46%, #fbfbfd5c 78%, var(--bg) 100%);
  --scrim: #fbfbfdcc;
  --grain-opacity: 0.05;
}

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

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

* {
  margin: 0;
}

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

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

body {
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.65;
  font-feature-settings: "cv02", "cv03", "cv04", "cv11";
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
}

img,
svg,
video,
iframe {
  display: block;
  max-width: 100%;
}

img {
  height: auto;
  background: var(--surface-2);
}

a {
  color: var(--accent);
  text-decoration-color: var(--accent-line);
  text-underline-offset: 0.18em;
  transition: color var(--dur) var(--ease);
}

a:hover {
  color: var(--accent-strong);
}

button,
input {
  font: inherit;
  color: inherit;
}

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

::selection {
  background: var(--accent-soft);
  color: var(--text);
}

h1,
h2,
h3,
h4 {
  line-height: 1.12;
  font-weight: 600;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

h1,
h2 {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -0.025em;
}

p {
  text-wrap: pretty;
}

code,
pre,
kbd {
  font-family: var(--font-mono);
  font-size: 0.875em;
}

hr {
  border: 0;
  border-top: 1px solid var(--line);
}

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

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

.wrap--narrow {
  max-width: 52rem;
}

main {
  flex: 1 0 auto;
}

section {
  padding-block: var(--section);
}

section + section {
  padding-top: 0;
}

/* The gap between Featured and Experience on the home page. The generic
   --section padding put 112px below the last featured row, and since both
   section heads are sticky the effect was worse than the space suggested:
   there were 180px of scroll where neither head was docked under the bar, so
   the two titles never appeared to meet. Almost all of it was this bottom
   padding, so removing it lets the incoming head reach the bar while the
   outgoing one is still on its way out. */
#selected {
  padding-top: 10px;
  /* 16px, not 0. The last featured row was losing its bottom border: `.rows`
     has an 8px bottom margin, and the Experience section's sticky head has a
     -11.2px margin-top that lets it reach the header bar. Because this section
     had zero bottom padding, those two collapsed together to -3.2px, pulling
     `#experience` up so it covered the last 3px of the final row - its border
     included. A non-zero padding stops the collapse, which leaves a net
     16 + 8 - 11.2 = ~13px between the row and the next section.
     Kept small on purpose: the reason the 112px of padding was removed in the
     first place is the sticky handover, and 16px barely registers against that. */
  padding-bottom: 16px;
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem var(--gap);
  margin-bottom: clamp(1.5rem, 1rem + 1.5vw, 2.5rem);
}

/* A section head that parks under the header while its section scrolls by.

   Full-bleed by negative margin, so rows pass behind the whole width of the
   head rather than showing through beside the title. Positive padding keeps the
   text on the content gutter while the painted box covers the whole viewport.

   No rule underneath: a full-width hairline here is the same line that was just
   taken off /work/, and the edge between this fill and the glass header is a
   change of material, which reads as an edge on its own. */
.section-head--sticky {
  position: sticky;
  top: calc(var(--header-h) - 1px);
  z-index: 30;
  /* Fully opaque, no backdrop-filter. This was the header's glass at one point,
     reading three ramped custom properties off :root so the two bars would be
     the same material - and that is exactly what made them look wrong together.
     Blurred and adjacent, the header and the head met in a visible line, which
     read as a seam between them. The head now paints `var(--bg)` flat, the same
     color as the page behind it, so the only edge at the boundary is the
     header's own hairline. Opaque also means no reliance on JS: with no
     backdrop-filter there is nothing for a no-JS visit to lose, where the glass
     version needed a fallback fill on the ramp's ceiling just to stay legible
     over passing rows. */
  background: var(--bg);
  /* The top margin stays at 0.7rem while the top padding grows past it, and the
     difference is the point: an exactly matching negative margin would swallow
     every added pixel and leave the title sitting where it was, growing only
     the painted box, which is invisible until something scrolls under it.
     Holding the margin lets the 20px read as space above the title while the
     fill still starts above the section, so rows pass behind solid fill rather
     than through the gap.

     The bottom is back to near its original 1rem. The 20px added there made the
     fill so tall that it parked the sticky head for most of the section's
     scroll, which is why the hand-off to Experience felt late. Top asymmetric
     with bottom on purpose: the space above the title is what the eye reads as
     section separation, and the space below just delays the first row. */
  margin: -0.7rem calc(-1 * var(--gutter)) 0;
  padding: calc(0.7rem + 20px) var(--gutter) 0.85rem;
}

/* Later in the document wins a z-index tie, so the incoming head already paints
   over the outgoing one during the handover. The lists stay below the heads so
   rows pass behind the fill rather than in front of it. */
.section-head--sticky ~ .rows,
.section-head--sticky ~ .roles {
  position: relative;
  z-index: 1;
}

.eyebrow {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 0.6rem;
}

.section-title {
  font-size: var(--step-3);
}

.section-note {
  color: var(--text-2);
  max-width: 34ch;
  font-size: var(--step--1);
}

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

.prose > * + * {
  margin-top: 1.05em;
}

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

.prose em {
  color: var(--text);
  font-family: var(--font-display);
  font-size: 1.05em;
}

.prose a {
  color: var(--accent);
}

.prose ul,
.prose ol {
  padding-left: 1.3em;
}

.prose li + li {
  margin-top: 0.4em;
}

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

.skip-link {
  position: absolute;
  left: 50%;
  top: 0;
  translate: -50% -120%;
  z-index: 200;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 0.6rem 1.1rem;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  font-weight: 600;
  text-decoration: none;
  transition: translate var(--dur) var(--ease);
}

.skip-link:focus {
  translate: -50% 0;
  color: var(--accent-ink);
}

/* ---------------------------------------------------------------- grain --- */
/* A tiny inline SVG noise. Adds a little depth so large flat areas do not
   band, and is cached with the document. */

.grain::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
  /* Faded out over the bottom quarter, and this is not cosmetic - it was the
     cause of a visible line where the hero meets Featured in dark mode. The
     hero is `.grain`, and `inset: 0` stopped this overlay dead at the hero's
     bottom edge. Overlay blending is not neutral on every backdrop: on the
     near-black dark `--bg` it lifts the pixels, so the last row inside the hero
     painted lighter than the page background it was supposed to blend into,
     then the overlay vanished and the background took over at full strength.
     Measured across that boundary: delta 8 per channel without the mask,
     3 with it. On the near-white light `--bg` the same blend is close to a
     no-op, which is why this only ever showed up in dark mode and looked
     like a problem with the hero image or the tint gradient. Neither of those
     was at fault: with the grain removed the boundary measured clean.
     75% keeps full grain over the name and intro, which is where it does its
     job, and only eases it out where the section change has to read as one
     continuous surface. */
  -webkit-mask-image: linear-gradient(180deg, #000 0 75%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0 75%, transparent 100%);
}

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

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  /* All three of these ramp together, and all three have to. The blur was
     constant 18px once, which made the bar look opaque even at 0% fill because
     a hard blur is a visibly different material from the sharp image beside it.
     Fixing only the blur left `saturate(160%)` applied at the top of the page,
     where it tinted the hero image 74% more saturated than the strip below -
     the same edge, just from a different property. --header-sat starts at 100%,
     which is a no-op, so at scrollY 0 the image runs under the bar untouched.

     The veil tops out at 78%, not 100%, so the bar never becomes a slab. */
  background: color-mix(in oklab, var(--bg) var(--header-veil, 0%), transparent);
  backdrop-filter: saturate(var(--header-sat, 100%)) blur(var(--header-blur, 0px));
  -webkit-backdrop-filter: saturate(var(--header-sat, 100%))
    blur(var(--header-blur, 0px));
  /* No border-bottom at all: the stuck hairline is gone, see below. No
     transition either - the veil is already a continuous function of scroll,
     and transitioning it would lag the scroll it is supposed to be tracking. */
}

/* No border-bottom on the stuck header, and no rule to replace it with. The
   hairline used to ride the same ramp as the fill so it would grow in rather
   than pop on at scrollY > 0, and with the fill now reaching a full 100% it was
   the last hard edge anywhere in the header: a 1px line across the viewport
   directly under the bar, separating it from the content it was sitting on top
   of. It read as a seam for the same reason the blurred bars did - two
   materials meeting where the eye expects one continuous surface.

   The fill now does all the separating. At scrollY > 0 the bar is fully opaque
   `var(--bg)`, and everything it can sit over - the hero, the section heads, the
   /work/ filter bar - is that same color, so there is nothing for a rule to be
   telling the eye. Keeping the border property around would only leave a
   transparent one that paints nothing. */

.site-header .wrap {
  display: flex;
  /* baseline, not center. Centring lined the boxes up to within half a pixel,
     but the name is 17px and the nav links are 14px, so their baselines sat ~2px
     apart and the words read as crooked rather than aligned. Baselines are what
     has to match. The icon buttons opt out below - they are SVG, so they have no
     baseline to speak of and would be aligned by their bottom margin edge. */
  align-items: baseline;
  gap: var(--gap);
}

.brand {
  display: inline-flex;
  /* center, so the mark and the name sit at the same vertical center at any
     size. Baseline used to tie the two together and it worked only because the
     30px mark and the 17px name happened to land center-on-center. Growing the
     mark to 34px to match the icon buttons breaks that coincidence - under
     baseline alignment the mark is hung off the name's baseline, so a taller
     square pushes its own center up and the name ends up sitting low inside it.
     The mark opts out of the baseline below, so this change does not disturb the
     name's baseline relationship with the nav links - see the comments there. */
  align-items: center;
  gap: 0.6rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--text);
  text-decoration: none;
}

.brand:hover {
  color: var(--text);
}

.brand__mark {
  display: grid;
  place-items: center;
  /* 38px, not 30px. Two reasons, both measured.

     The buttons either side are 34px, so a 30px mark left them hanging 3px
     below it and the logo read as pinned to the top of the bar. Growing the mark
     fixes that without shrinking the 34px tap targets.

     And it has to clear the nav links' 36px boxes to land exactly. The header
     `.wrap` is baseline-aligned, so the line box is as tall as its tallest
     item: at 34px the nav links still set the height and the buttons centering
     in that box left the mark 0.6px high. At 38px the mark is the tallest item,
     so everything - mark, name, both buttons - centers on one line. */
  width: 38px;
  height: 38px;
  /* Opts out of the brand's baseline calculation (see .brand above), so the
     brand's baseline - the one the nav links are measured against - comes from
     the name and nothing else. Without this the grid mark becomes the reference
     and the name drifts off the nav. */
  align-self: center;
  border-radius: 11px;
  background: linear-gradient(145deg, var(--accent), var(--warm));
  color: var(--accent-ink);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.03em;
  flex: none;
}

/* Beside the name, not opposite it. Reading order is now "who this is, where
   you can go" with the space left over on the right, which is what the eye
   expects from a wordmark. It also puts Home one click from every page: the
   name itself is the link, so a separate Home item would be redundant. */
.nav {
  display: flex;
  align-items: baseline;
  gap: 0.15rem;
  /* 1.2px down, to put the nav back on the name's baseline. Growing the mark to
     38px made it the tallest item in the header's line box, which moved the
     brand's baseline up with it and left the nav 1.2px high of the name - a
     regression against the value this site tuned to (+1.00px). Shifting the nav
     down by the same amount restores it exactly and leaves the mark and name
     perfectly centered, which are the two things that matter more. */
  position: relative;
  top: 1.2px;
}

/* The slack goes between the nav and the controls, so the nav stays beside the
   name on the left and the theme and menu buttons stay pinned to the right. */
.nav {
  margin-right: auto;
}

.nav__link {
  display: inline-block;
  padding: 0.4rem 0.75rem;
  border-radius: 999px;
  font-size: var(--step--1);
  font-weight: 500;
  /* --text, not --text-2. --text-2 is a body-copy tone; the nav has to stay
     legible against the hero image showing through the glass at the top of the
     page, and on --text-2 it measured 2.25:1 light / 1.72:1 dark. Scoped here so
     the rest of the page keeps the secondary tone for body text. */
  color: var(--text);
  text-decoration: none;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.nav__link:hover {
  color: var(--text);
  background: var(--surface-hover);
}

/* The active page. This cannot be a --surface-2 fill: the header is glass at
   55% opacity, so over a plain page background --surface-2 resolves to within a
   couple of points of the bar itself and the pill vanishes. An accent fill
   survives both themes and matches the active chip on the work index. */
.nav__link[aria-current="page"] {
  color: var(--accent-strong);
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}

.nav__link[aria-current="page"]:hover {
  color: var(--accent-strong);
  background: color-mix(in oklab, var(--accent) 16%, transparent);
}

.nav__link--external::after {
  content: "\2197";
  font-size: 0.85em;
  margin-left: 0.15em;
  opacity: 0.6;
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  /* Opt out of the header's baseline alignment. The bar now aligns items by
     their first baseline; a fixed-size square button has no text baseline, so
     Chrome would align it by its bottom margin edge and drop the icon below the
     words. Re-centering it against the tallest text in the row is correct here. */
  align-self: center;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text-2);
  cursor: pointer;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

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

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

.theme-toggle .icon-moon,
[data-theme="dark"] .theme-toggle .icon-sun {
  display: none;
}

[data-theme="dark"] .theme-toggle .icon-moon {
  display: block;
}

.nav-toggle {
  display: none;
}

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

.hero {
  position: relative;
  isolation: isolate;
  /* Pulled up under the sticky header so the background image runs behind the
     bar instead of starting at a hard line beneath it. Without this the hero's
     box began below the header, leaving the top 62px showing bare --bg - which
     is what made the glass bar read as a solid slab over a different color. The
     padding adds the same height back, so the name, intro and contact card sit
     exactly where they did before. */
  margin-top: calc(-1 * var(--header-h));
  padding-block: calc(var(--header-h) + clamp(3.5rem, 2rem + 8vw, 8rem))
    clamp(3rem, 2rem + 5vw, 6rem);
  overflow: hidden;
}

.hero__bg {
  position: absolute;
  inset: 0;
  z-index: -2;
  /* The image is applied inline by the template; see home.html. */
  background-size: cover;
  background-position: center 42%;
  /* Less desaturation and more opacity than before: the tint gradient above is
     now doing the legibility work, so the image itself can stay visible. */
  filter: saturate(0.9);
  opacity: 0.72;
}

.hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--hero-tint);
}

/* a soft accent bloom behind the headline */
.hero__glow {
  position: absolute;
  z-index: -1;
  top: -28%;
  left: 50%;
  translate: -50% 0;
  width: min(1100px, 140vw);
  aspect-ratio: 1.6;
  background: radial-gradient(
    ellipse at center,
    color-mix(in oklab, var(--accent) 26%, transparent),
    transparent 68%
  );
  filter: blur(46px);
  pointer-events: none;
}

.hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21rem;
  align-items: center;
  gap: clamp(1.5rem, 1rem + 3vw, 4rem);
}

/* The hero's h1. Carries "About Me" rather than the name - the name lives in
   the header brand and the footer now. Everything else here is unchanged from
   when this styled the name, so the heading keeps the exact size, weight,
   leading, tracking and inherited font it always had; only the text changed. */
.hero__name {
  font-size: var(--step-4);
  font-weight: 500;
  line-height: 1.02;
  letter-spacing: -0.035em;
  margin-bottom: 0.5rem;
}

.hero__intro {
  max-width: 54ch;
  color: var(--text-2);
  font-size: var(--step-0);
}

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

/* Light theme only: the hero's description sits on the photograph, not on a flat
   surface, so contrast has to be measured against the image. Sampled across the
   intro's box at 1440px, over every pixel it covers:

     --text-2 (#4a5162)  mean 4.63  worst 2.44   <- was unreadable in patches
     #1e2230             mean 9.23  worst 4.85   <- clears AA everywhere

   Dark is left alone: --text-2 there measures mean 6.64, and the user asked
   about light mode specifically. */
[data-theme="light"] .hero__intro {
  color: #1e2230;
}

/* The two links in that paragraph were the worst of it - the accent blue is
   light enough that over the brighter parts of the photo it fell to 1.81:1,
   far below anything readable. Darkened to the point where the worst pixel in
   the block clears AA (4.71), while staying visibly blue so it still reads as a
   link rather than as more body text. Scoped to the hero for the same reason:
   the accent works fine on the flat surfaces everywhere else. */
[data-theme="light"] .hero__intro a {
  color: #191e58;
  text-decoration-color: #191e5870;
}

[data-theme="light"] .hero__intro a:hover {
  color: #10143f;
}

/* The hero no longer needs a row of buttons - the contact card carries every
   link - but the 404 page reuses the name for its pair of recovery links. */
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

@media (max-width: 720px) {
  .hero__inner {
    /* One column, so the hero's children stack in DOM order. */
    grid-template-columns: 1fr;
  }
  /* No `order: -1` on the contact card any more. It used to be lifted above the
     name here, which put a 21rem portrait and a column of buttons before
     anything said who this page belonged to — on a phone that is the first
     thing anyone sees, and it read as a contact sheet rather than as a person.
     The name and intro are already first in the markup, so with the card left
     at its default order they now lead and the card follows the description.
     The card carries the portrait, so it is still the first *image* on the
     page, just no longer the first block. */
}

/* --------------------------------------------------------------- button --- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.6rem 1.1rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-size: var(--step--1);
  font-weight: 550;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
    color var(--dur) var(--ease), translate var(--dur) var(--ease);
}

.btn svg {
  width: 15px;
  height: 15px;
  flex: none;
  opacity: 0.75;
}

.btn:hover {
  background: var(--surface-hover);
  border-color: var(--accent-line);
  color: var(--text);
  translate: 0 -1px;
}

.btn--primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-ink);
  font-weight: 600;
}

.btn--primary svg {
  opacity: 0.85;
}

.btn--primary:hover {
  background: var(--accent-strong);
  color: var(--accent-ink);
}

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

.btn--sm {
  padding: 0.4rem 0.75rem;
  font-size: 0.8rem;
}

.btn--block {
  width: 100%;
  justify-content: center;
}

/* ---------------------------------------------------------------- chips --- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  /* Rows need more air than columns. Only this list wraps: the gap between the
     "Tags" heading and the first row is the panel title's 0.9rem margin (15.6px
     measured), but when the chips wrap - which is what a squeezed item page
     does - the 0.4rem row gap left them crowded, the pill borders nearly
     touching. 0.75rem puts a real gap between rows without loosening the
     label-to-chips relationship any further. Card chips keep the tighter
     `.chips--tight` gap, which is declared after this and resets row-gap. */
  row-gap: 0.75rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.24rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 0.76rem;
  font-weight: 500;
  line-height: 1.4;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}

.chip:hover {
  background: var(--surface-hover);
  color: var(--text);
  border-color: var(--line-strong);
}

.chip--link {
  cursor: pointer;
}

.chip[aria-pressed="true"],
.chip.is-active {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent);
}

.chip__count {
  font-variant-numeric: tabular-nums;
  font-size: 0.72rem;
  color: var(--text-3);
}

.chip[aria-pressed="true"] .chip__count {
  color: inherit;
  opacity: 0.75;
}

.chip--static {
  background: transparent;
  cursor: default;
}

.chip--static:hover {
  background: transparent;
  color: var(--text-2);
  border-color: var(--line);
}

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

/* The work index and the home page's "Selected work" list use the same entry,
   styled as a card either way. They used to differ - the home list carried a
   spine and dots - but two treatments of one component meant the same item
   looked different depending on where it appeared. One card, used twice.

   Stacked rather than a card grid: the index is a filterable list of 18 entries
   that should be scannable down one column, and the featured set is four items,
   which in a three-column grid left the last one stranded on its own row.

   thumb | title + detail | date. The date is pinned right and centered against
   the entry so the eye can run down one edge and read the chronology. */
/* The results list sits under the sticky filter bar, which is already the
   boundary above it. The generic `section` padding was adding a further 7rem
   of daylight between the chips and the first row, so the control and the
   thing it controls were on opposite ends of a screen. */
.work-results {
  padding-top: 0;
}

.rows {
  display: grid;
  /* Space around each card, not a hairline between them: at 0.15rem the
     entries read as one striped block rather than separate items. */
  gap: clamp(0.75rem, 0.5rem + 1vw, 1.1rem);
  /* Room for the shadows to breathe without them clipping at the section edge. */
  margin-block: 0.5rem;
}

.row {
  position: relative;
  /* Contains the stretched title overlay's negative z-index. Without a stacking
     context here, `z-index: -1` on that overlay resolves against the root and
     puts it behind this card's own background, where it stops being clickable
     and stops being visible. `isolation` is the cheapest way to establish one -
     it creates the context without changing layout or paint order of siblings. */
  isolation: isolate;
  display: grid;
  /* Twice the old clamp (88/124px): the thumbnail was reading as a
     postage stamp next to the title rather than as the work itself. */
  /* The date track sizes itself to the widest date on the page. A fixed 8rem
     was narrower than a two-ended range ("Jun 2020 - Mar 2024" is ~134px here),
     so nowrap text overflowed its own cell and consumed the right padding -
     which is why the ranged entries sat closer to the border than the single
     ones. The 8rem floor keeps a lone "Mar 2020" from collapsing the column. */
  grid-template-columns: clamp(224px, 24vw, 360px) minmax(0, 1fr) minmax(8rem, max-content);
  gap: clamp(1rem, 0.5rem + 2vw, 2rem);
  /* stretch, not center: the date is one line either way, and centering the
     content cell left the thumbnail stranded at the top of a taller row. */
  align-items: stretch;
  padding: 1.15rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-1);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease), translate var(--dur) var(--ease);
}

.row:hover {
  border-color: var(--accent-line);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  translate: 0 -2px;
}

.row[hidden] {
  display: none;
}

.row__title a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  /* Behind the row's own content, not over it. The overlay exists so the empty
     parts of the card are clickable, but painted on top it also covered every
     word in the row - and a drag that starts on an empty absolutely-positioned
     box begins a click, not a selection, so none of the text could be
     highlighted or copied.
     `z-index: -1` puts it above `.row`'s background and below all of its
     content, which is exactly the split wanted: the background, the thumbnail
     and the gaps around the text navigate; the text itself is hit as text.
     The title is a real anchor, so it stays clickable as well.
     Needs `.row { isolation: isolate }` above - a negative z-index escapes to
     the nearest ancestor stacking context otherwise, which here is the root, so
     it would land behind `.row`'s own background and become invisible. */
  z-index: -1;
}

.row__thumb {
  position: relative;
  /* The thumbnail is content, so it paints above the stretched title overlay -
     which means it also swallows clicks on the image, the one part of a row that
     most obviously should open the item. Nothing inside is interactive, so the
     pointer is let through to the overlay underneath. */
  pointer-events: none;
  /* Landscape, but never taller than its cell. Stretching alone let a row full
     of chips drive the image to 433px tall on a 390px screen - a portrait-shaped
     column rather than a thumbnail. 4:3 is the widest of the two box ratios that
     keeps the image visibly wider than it is tall once cropped, and
     max-height: 100% means it still grows to fill a cell that is shorter than
     that, so no dead space reappears in the compact rows. */
  aspect-ratio: 4 / 3;
  align-self: center;
  max-height: 100%;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface-2);
}

.row__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.row__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: #0003;
  color: #fff;
}

.row__play--bare {
  background: none;
}

.row__play svg {
  width: 34px;
  height: 34px;
  filter: drop-shadow(0 1px 3px #0008);
}

.row__main {
  min-width: 0;
}

.row__title {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: -0.02em;
  margin: 0;
}

.row__title a {
  color: var(--text);
  text-decoration: none;
}

.row:hover .row__title a {
  color: var(--accent);
}

.row__sub,
.row__venue {
  font-size: var(--step--1);
  color: var(--text-3);
}

.row__venue {
  font-style: italic;
}

.row__abstract {
  margin: 0.5rem 0 0;
  font-size: 0.92rem;
  color: var(--text-2);
  max-width: 62ch;
}

.row__main .chips {
  margin-top: 0.6rem;
}

/* The stretched title anchor spans the whole card, so the chips are lifted above
   it to stay independently clickable. Now that it is painted behind the row's
   content rather than over it, they would win the hit test anyway - kept
   explicit so that adding anything above them later cannot quietly put a chip
   under a dead overlay. */
.row__main .chips a {
  position: relative;
  z-index: 2;
}

.row__when {
  font-size: 0.8rem;
  color: var(--text-3);
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  /* Inset from the card's right edge so the date does not sit flush against
     the border, where it read as a printer's mark. */
  padding-right: 1.5rem;
  /* One line of text, centered on the row. The row stretches its cells so the
     thumbnail can fill them, which would otherwise pin the date to the top. */
  align-self: center;
}

/* 840px, not the 640px this used to be: the two-column row needs a thumbnail
   track (clamp(224px, 24vw, 360px)) plus a text track plus a date track plus
   two gaps, and below ~840px that leaves the text column too narrow for titles
   to sit on one line. Phones were the only case that mattered before, because
   nothing else was ever narrow enough to hit 640px in practice — a 768px
   tablet or a small laptop window was getting the cramped three-column desktop
   row instead. The `.role` breakpoint below stays at 640px: a role entry has
   no thumbnail, so it has room to keep two columns well below this. */
@media (max-width: 840px) {
  .row {
    /* One column. The image goes above the title at full width rather than
       sitting in a 46% column beside it: stacked narrow, a side-by-side
       thumbnail left the text about 170px wide, which is narrow enough that
       titles broke over three lines and the image read as a thumbnail of a
       caption rather than as the work. Stacked, both blocks get the full
       measure.
       The markup order is already thumb | main | when, so no order property is
       needed - the two-column layout was the only reason they interleaved. */
    grid-template-columns: minmax(0, 1fr);
    /* Tight, because every gap here falls between two lines of the same entry -
       title, authors, venue, date. At 0.7rem each, four stacked items spent more
       vertical space on leading than on text, and the entry read as four
       disconnected lines rather than one block. The space that earns its keep is
       above the title (see .row__thumb and .row__title below) and below it
       before the tags (.row__main .chips), so both are added back explicitly
       rather than by inflating the one gap that affects everything. */
    row-gap: 0.15rem;
    padding: 0.95rem 1rem 1.1rem;
  }
.row__thumb {
    /* 16:9 rather than the desktop's 4:3. Full width, a 4:3 box came out 268px
       tall on a 390px screen and pushed the title below the fold; 16:9 keeps it
       to about 200px and still reads as a wide shot of the project. */
    aspect-ratio: 16 / 9;
    /* Own row now, so it is no longer competing with the text for height. */
    align-self: stretch;
    max-height: none;
    /* Lowest of the ordered items, so the image stays above the title. Every
       sibling in the row track is sorted by `order` together once .row__main
       goes display:contents, and the default is 0 - which would have landed the
       title above this. */
    order: -3;
    /* The one gap that should be generous: this is the break between the image
       and the text it belongs to, and at the tightened 0.3rem row-gap above the
       title butted straight up against the bottom of the photo. Pushed out on
       the thumb rather than the title so it also separates the image from the
       card's own top padding. */
    margin-bottom: 0.85rem;
  }
  .row__main {
    min-width: 0;
  }
  .row__abstract {
    display: none;
  }
  .row__when {
    /* Left-aligned under the title rather than right-aligned across the full
       width, which put a lone "Mar 2020" at the far edge with a long empty run
       leading back to it. */
    text-align: left;
    padding-right: 0;
    font-size: 0.9rem;
    /* The one gap below the text that stays generous. The date is the last line
       of prose in the entry and the chips below it are the first interactive
       thing, so this is the break between description and controls - the same
       role the break above the chips plays on the desktop row, and the reason
       the row still has exactly one clear division in it. Everything above the
       date is packed tight. */
    margin-top: 0.45rem;
    /* Space *below* the date, which is the only generous gap left in the row.
       Sits on the date rather than on the chips above it so it reads as "end of
       the entry" instead of "start of the tags". */
    margin-bottom: 0.75rem;
    /* Between the venue line and the chips, so the tags finish the row. Placed
       here rather than left at the end because the chips are nested inside
       .row__main, which sits above this cell in the markup; `order` cannot move
       a grid item into the middle of a sibling's children. */
    order: 1;
  }
  .row__main {
    /* Its children become items of the .row grid so .row__when can slot between
       the venue and the chips. The box itself is not needed on one column -
       .row__main only existed to hold the text cell, and it carries no
       background, border or padding. Descendant selectors such as
       `.row__main .chips` still match, since the DOM is unchanged. */
    display: contents;
  }
  .row__title {
    order: -2;
    /* One step up from desktop: the title is what the row is, and at the
       desktop's 1.2rem floor it was the smallest thing on the screen. */
    font-size: var(--step-2);
    /* Extra space above the title, on top of the thumb's margin-bottom. This is
       the second half of the same break the image wants, and it is what makes
       the title read as the head of the entry rather than as another line in a
       stack - the thumb's margin is about the image, this is about the text
       starting. Together they are the room the tightened row-gap gave up. */
    margin-top: 0.5rem;
    /* Air under the title too, but only just. The authors and venue lines below
       it are set small and muted, and with literally nothing between them and
       the largest text on the card the hierarchy the font-size jump creates
       disappears. This is the floor - enough to read as a heading, not enough
       to start a new block. */
    margin-bottom: 0.15rem;
  }
  .row__sub,
  .row__venue {
    order: -1;
    /* Off the 0.8rem floor, which is below comfortable reading size on a
       phone. */
    font-size: var(--step-0);
    /* Tighter than the 0.3rem row-gap between the two of them. Authors and venue
       are the same piece of information about the same thing - who and where it
       appeared - so they read as one two-line block, not as two items. */
    line-height: 1.45;
  }
  .row__venue {
    /* Pulled up into the authors line, so the pair reads as one two-line block.
       Same information about the same thing - who wrote it and where it
       appeared - so they should not be spaced like separate items. */
    margin-top: -0.1rem;
  }
  .row__main .chips {
    /* `order: 2` and the default 0 above puts title/sub/venue first, then the
       date, then the tags. 0 would place the chips between the sub and the
       venue instead, since order sorts every item in the track together. */
    order: 2;
    /* Just the row-gap plus the date's own margin above it - the tags hang off
       the bottom of the entry rather than starting a third block. */
    margin-top: 0.15rem;
  }
  .row__main .chips .chip {
    /* 0.76rem is a tag in a dense desktop card. Full-size in the stacked row. */
    font-size: 0.84rem;
    padding: 0.3rem 0.7rem;
  }
  .row__main .chips .chip__count {
    font-size: 0.8rem;
  }
}

/* ---------------------------------------------------------------- cards --- */

.grid {
  display: grid;
  gap: clamp(0.9rem, 0.7rem + 0.8vw, 1.4rem);
}

.grid--2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
}

.grid--3 {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
}

/* --- work card --- */

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
  isolation: isolate;
  transition: border-color var(--dur) var(--ease), translate var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.card:hover {
  border-color: var(--line-strong);
  translate: 0 -3px;
  box-shadow: var(--shadow-2);
}

.card__media {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--surface-2);
  overflow: hidden;
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: scale 0.5s var(--ease);
}

.card:hover .card__media img {
  scale: 1.035;
}

.card__media--video {
  display: grid;
  place-items: center;
  background: linear-gradient(150deg, var(--surface-2), var(--bg-2));
}

.card__media--video .play {
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--surface) 82%, transparent);
  border: 1px solid var(--line-strong);
  color: var(--text);
  backdrop-filter: blur(6px);
  transition: scale var(--dur) var(--ease), background var(--dur) var(--ease);
}

.card:hover .card__media--video .play {
  scale: 1.08;
  background: var(--accent);
  color: var(--accent-ink);
  border-color: transparent;
}

.card__media--video .play svg {
  width: 20px;
  height: 20px;
  margin-left: 2px;
}

.card__media--video .glyph {
  position: absolute;
  right: 0.9rem;
  bottom: 0.9rem;
  width: 30px;
  height: 30px;
  color: var(--text-3);
  opacity: 0.5;
}

.card__body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: var(--pad);
  flex: 1;
}

.card__meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: var(--step--1);
  color: var(--text-3);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.16rem 0.5rem;
  border-radius: 6px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.5;
}

.badge--type {
  background: var(--surface-2);
  color: var(--text-2);
  border: 1px solid var(--line);
}

.badge--award {
  background: color-mix(in oklab, var(--warm) 16%, transparent);
  color: var(--warm);
}

.card__title {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.22;
}

.card__title a {
  color: var(--text);
  text-decoration: none;
}

/* Whole card is clickable, but only the title is a real link, so keyboard
   users get one stop per card and the accessible name is just the title. */
.card__title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.card__authors {
  color: var(--text-3);
  font-size: 0.85rem;
  line-height: 1.5;
}

/* Your own name in an author list is the one name worth finding, so it is
   weight rather than color alone - on a row the author line is already
   secondary text and a shade shift alone disappears. */
.card__authors .me,
.row__sub .me {
  color: var(--text-2);
  font-weight: 600;
}

.card__abstract {
  color: var(--text-2);
  font-size: 0.9rem;
  line-height: 1.6;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card__tags {
  margin-top: auto;
  padding-top: 0.35rem;
  position: relative;
  z-index: 2;
}

.card__foot {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem var(--pad);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  font-size: 0.8rem;
  color: var(--text-3);
}

.card--flat {
  background: var(--surface-2);
}

/* --- role card --- */

.role {
  position: relative;
  /* As on `.row` - keeps the org link's stretched overlay behind this entry's
     text so it can be selected. */
  isolation: isolate;
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  gap: 0.35rem 1.5rem;
  padding: var(--pad) 0;
  border-top: 1px solid var(--line);
}

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

.role__when {
  position: relative;
  grid-row: span 3;
  align-self: center;
  font-size: var(--step--1);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- roles on a timeline: home page "Where I've worked" ----------------- */

/* A spine down a left gutter with a dot per entry. Both the dot and the date
   are centered in the row rather than sitting at its top edge: the date is what
   the dot marks, so the two have to be level with each other, and a date
   floating in the middle of a three-line entry looks accidental. */
.roles--timeline {
  --tl-gutter: 1.7rem;
  position: relative;
  padding-left: var(--tl-gutter);
}

.roles--timeline::before {
  content: "";
  position: absolute;
  left: calc(var(--tl-gutter) / 2 - 0.5px);
  top: var(--pad);
  bottom: var(--pad);
  width: 1px;
  background: var(--line);
  pointer-events: none;
}

/* The horizontal rules are dropped here: a rule across every entry plus a rule
   down the side is one divider too many. Vertical rhythm comes from the gap. */
.roles--timeline .role {
  border-top: 0;
  padding-block: calc(var(--pad) * 0.8);
  align-items: center;
}

.roles--timeline .role:last-child {
  border-bottom: 0;
}

/* The whole entry is the hit target, not just the org name. Same stretched
   ::after technique as a work row: one anchor, so there is one tab stop and no
   nested-interactive-elements problem from wrapping the grid in an <a>. */
.role__body {
  position: static;
}

.role__org a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 6px;
  /* Same treatment as the work row's overlay, for the same reason - see
     `.row__title a::after`. Without it none of the text in a timeline entry can
     be selected. */
  z-index: -1;
}

.role:hover .role__org a {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Focus lands on the title, not on a box around the row. */
.role__org a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* A degree sits on the same spine, but there is nothing to click into, so its
   dot is a ring in the same gray as the roles rather than the accent-colored
   one that means "this leads somewhere". */
.role__node--degree {
  border-color: var(--line-strong, var(--line));
}

.role__node {
  display: none;
}

.roles--timeline .role__node {
  display: block;
  position: absolute;
  /* The date cell starts one gutter in from the wrapper, and the spine runs
     down the middle of that gutter, so half a gutter to the date's left is
     exactly the spine - at any --tl-gutter, in either layout. */
  left: calc(-1 * var(--tl-gutter) / 2);
  /* Centred on the date's own line box, which is what puts it level with the
     date wherever that date sits in the entry. */
  top: 50%;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  /* Hollow, and the same on every entry. A --bg center inside an --accent ring
     reads as a dot on the spine; filling the current role's dot instead made
     the two markers look like different kinds of thing. That role already
     carries a "Current" badge, so the dot has nothing left to disambiguate. */
  background: var(--bg);
  border: 2px solid var(--accent);
  transform: translate(-50%, -50%);
  z-index: 1;
  transition: border-color var(--dur) var(--ease), scale var(--dur) var(--ease);
}

.roles--timeline .role:hover .role__node {
  border-color: var(--accent-strong);
  scale: 1.25;
}

.role__org {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: -0.02em;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* The badge inherited the 0.5rem flex gap, which is right for spacing between
   two peers but too tight here: the org name and the badge sat close enough to
   read as "AppleCurrent". The extra inset separates them without changing the
   gap used anywhere else in the heading. */
.role__org .badge {
  margin-left: 0.35rem;
}

.role__title {
  color: var(--text-2);
  font-size: 0.95rem;
}

.role__detail {
  color: var(--text-2);
  font-size: 0.92rem;
  line-height: 1.65;
  max-width: 62ch;
}

.role__detail p + p {
  margin-top: 0.7em;
}

.role--current .role__org {
  color: var(--text);
}

@media (max-width: 640px) {
  .role {
    grid-template-columns: 1fr;
    gap: 0.4rem;
  }
  .role__when {
    grid-row: auto;
    padding-top: 0;
  }
}

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

/* /about/ has no rule under the page head. It spans the full width, so it sat
   above the prose and the contact card alike and read as a stray line rather
   than as a divider between anything. The extra padding below replaces the
   space the border used to imply. The rule stays on the other page heads. */
.page-about .page-head {
  padding-bottom: clamp(2rem, 1.5rem + 2vw, 3.5rem);
}

.page-head {
  padding-block: clamp(2.5rem, 2rem + 3vw, 4.5rem) clamp(0.75rem, 0.5rem + 1vw, 1.25rem);
  background: radial-gradient(
    ellipse 80% 100% at 50% 0%,
    var(--accent-soft),
    transparent 70%
  );
}

.page-head__title {
  font-size: var(--step-4);
  font-weight: 500;
  letter-spacing: -0.03em;
}

.page-head__lede {
  margin-top: 0.8rem;
  color: var(--text-2);
  max-width: 58ch;
  font-size: var(--step-0);
}

.item-head {
  padding-block: clamp(2rem, 1.5rem + 2.5vw, 3.5rem) 0;
}

.item-title {
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: -0.03em;
  margin-block: 0.7rem 1rem;
}

.item-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem 0.7rem;
  color: var(--text-3);
  font-size: var(--step--1);
}

.item-authors {
  margin-top: 1.3rem;
  color: var(--text-2);
  font-size: 0.98rem;
  line-height: 1.7;
  max-width: var(--measure);
}

.item-authors .me {
  color: var(--text);
  font-weight: 600;
}

.star {
  color: var(--accent);
  font-weight: 700;
  font-size: 0.8em;
  vertical-align: super;
  line-height: 0;
}

.item-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  margin-top: 1.7rem;
}

.media {
  margin-top: 2.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

.media__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #000;
}

.media__frame iframe,
.media__frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}

/* Image-only items render in `.panel--media` in the sidebar, so nothing uses
   `.media--contain` or a `.media` caption any more. Both existed only to
   letterbox a non-16:9 image into the video's frame and to draw a rule above
   the caption. Videos keep `.media__frame` at its own true 16:9. */

/* lazy facade: the YouTube iframe is only inserted once the poster is clicked,
   so no third-party cookies or requests happen until then */
.media__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  padding: 0;
  /* Fallback artwork for any video with no vendored still: abstract shapes
     rather than a gray box. When static/images/video/<id>.jpg exists the
     template sets background-image inline to use the real frame instead. */
  background:
    radial-gradient(120% 110% at 28% 18%, #6366f160 0%, transparent 58%),
    radial-gradient(95% 95% at 78% 82%, #f0abfc3d 0%, transparent 62%),
    linear-gradient(158deg, #131727 0%, #0a0c12 100%);
  cursor: pointer;
  display: grid;
  place-items: center;
}

.media__poster::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    #0a0c1222,
    #0a0c1299 55%,
    #0a0c12d9
  );
}

.media__poster .play {
  position: relative;
  display: grid;
  place-items: center;
  width: 68px;
  height: 68px;
  border-radius: 50%;
  background: #ffffff1a;
  border: 1px solid #ffffff40;
  backdrop-filter: blur(8px);
  color: #fff;
  transition: scale var(--dur) var(--ease), background var(--dur) var(--ease);
}

.media__poster:hover .play {
  scale: 1.07;
  background: var(--accent);
  border-color: transparent;
}

.media__poster .play svg {
  width: 24px;
  height: 24px;
  margin-left: 3px;
}

.media__poster .label {
  position: absolute;
  bottom: 0.9rem;
  left: 1rem;
  color: #fff;
  font-size: 0.8rem;
  font-weight: 500;
  opacity: 0.9;
}

.media__frame iframe {
  border-radius: inherit;
}

.badge svg {
  width: 1.05em;
  height: 1.05em;
  flex: none;
}

.btn__note {
  font-size: 0.78em;
  font-weight: 400;
  opacity: 0.65;
  padding-left: 0.15em;
}

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 0.9rem;
  margin-top: 2.2rem;
}

.gallery figure {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
}

.gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.gallery figcaption {
  padding: 0.6rem 0.8rem;
  color: var(--text-3);
  font-size: 0.8rem;
  border-top: 1px solid var(--line);
}

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

/* Content column plus a narrower sticky sidebar. minmax(0, ...) on the first
   track stops a long word or a wide <pre> in the prose from forcing the grid
   wider than the page. */
.about-grid {
  display: grid;
  /* 21rem matches the hero's card track, so the contact card renders
     pixel-identical on both pages - including the email fitting on one
     line, which it did not at 20rem. */
  grid-template-columns: minmax(0, 1fr) 21rem;
  gap: clamp(2rem, 1rem + 4vw, 4.5rem);
}

/* Same two-track shape as .about-grid, and deliberately sharing its 21rem rail
   so the panels in an item's sidebar and the contact card on /about/ are the
   same width on the same screen. The prose track is minmax(0, 1fr) so a wide
   <pre> - the BibTeX block, mostly - cannot push the grid wider than the page. */
.item-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21rem;
  gap: clamp(2rem, 1rem + 4vw, 4.5rem);
  align-items: start;
}

@media (max-width: 860px) {
  .about-grid,
  .item-layout {
    grid-template-columns: 1fr;
  }
}

/* --- sidebar --- */

.aside {
  position: sticky;
  top: calc(var(--header-h) + 1.5rem);
  display: grid;
  gap: 1.1rem;
  /* Without this the single-row grid stretches the contact card to the full
     height of the (much taller) content column, leaving a long empty box under
     the links. Panels keep their natural height and the sticky offset still
     works. */
  align-content: start;
}

@media (max-width: 860px) {
  .aside {
    position: static;
  }
}

/* On an item page the sidebar is a set of panels hanging off the bottom of the
   header, and the header ends with the row of buttons (Paper / Code / Semantic
   Scholar). The aside had no top margin, so the first panel's top border landed
   on exactly the same line as the bottom of that button row - on a wide screen
   the Semantic Scholar button ended level with the Tags panel and the two looked
   joined. The offset matches `.prose.mt-lg`, which is what the article column
   uses, so the sidebar's first panel now starts level with the abstract's first
   line instead of floating above it. Scoped to `.item-layout`: on /about/ the
   contact card and the bio are meant to start together. */
@media (min-width: 861px) {
  .item-layout > .aside {
    margin-top: clamp(2rem, 1.5rem + 2vw, 3.5rem);
  }
}

/* An image-only item's figure, in its own panel above the tags. Modelled on
   `.contact-card`: the panel's own padding is what insets the picture, so the
   rounded corners have card surface to sit against rather than being clipped by
   the panel's own edge. */
.panel--media {
  padding: var(--pad);
}

.panel__media-frame {
  border-radius: calc(var(--radius) - 4px);
  overflow: hidden;
  background: var(--surface-2);
}

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

/* Space above the caption instead of a 1px rule under the image. The rule ran
   the full width of the panel and read as a border splitting the card in two,
   where the gap reads as "this line explains the picture above it". */
.panel__media-cap {
  margin: 0.7rem 0 0;
  color: var(--text-3);
  font-size: 0.82rem;
  line-height: 1.5;
}

.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: var(--pad);
}

.panel__title {
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 0.9rem;
}

.bibtex {
  position: relative;
}

.bibtex pre {
  margin: 0;
  padding: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: 0.75rem;
  line-height: 1.65;
  overflow-x: auto;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 22rem;
}

.copy-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.7rem;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-2);
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--dur) var(--ease);
}

.copy-btn:hover {
  color: var(--text);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

.copy-btn svg {
  width: 13px;
  height: 13px;
}

.copy-btn[data-copied="true"] {
  color: var(--good);
  border-color: color-mix(in oklab, var(--good) 40%, transparent);
  background: color-mix(in oklab, var(--good) 12%, transparent);
}

/* ------------------------------------------------------------- filtering --- */

/* Full-bleed bar. The rule under the filters sat at the content gutter while
   the rule under "Work" above it ran edge to edge, so the two horizontals that
   bracket the same heading were different lengths and read as a mistake. The
   negative margin plus matching padding puts the sticky bar and its rule across
   the whole viewport while the chips still line up with the rows below. */
.filters {
  position: sticky;
  top: var(--header-h);
  z-index: 40;
  padding-block: 1rem;
  padding-inline: var(--gutter);
  margin-inline: calc(-1 * var(--gutter));
  margin-bottom: 0.35rem;
  /* Opaque, like the section heads. This was 86% plus blur(14px) saturate(180%),
     which put a second blurred bar directly under the header's - and the same
     seam showed up here, at the one place on the site where the two are always
     touching. */
  background: var(--bg);
}

.filters__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.7rem;
}

/* One line: the topic chips on the left, Clear hard against the trailing edge.
   `flex: 1` on the group makes it take whatever the button does not, so the
   button is pushed right rather than sitting after the last chip. */
.filters__bar {
  display: flex;
  flex-wrap: wrap;
  /* flex-start, not center: the chips wrap onto several rows, and `center`
     pushed Clear to the middle of the block instead of in line with the first
     row of tags. */
  align-items: flex-start;
  gap: 0.7rem 1.1rem;
}

/* Collapses to nothing until a filter is active, at which point site.ts unhides
   the button. margin-left: auto does the pushing, so it only takes effect once
   there is a button to push. */
.filter-status {
  display: flex;
  align-items: center;
  /* Nudged down past the label block so Clear lands on the chips' line, which
     is the line it belongs to. */
  margin: 1.64rem 0 0 auto;
}

/* The label is a block on its own line rather than a fixed-width column beside
   the chips. As a 4.5rem column it shared a flex row with the chips, so any
   chip that did not fit wrapped to the next line and landed underneath the
   label, indented to nothing - it looked like it belonged to the label. */
.filter-group {
  display: block;
  padding-block: 0.4rem;
  /* basis 0, not auto: with `auto` the group's basis was the full width of its
     unwrapped chips, which does not fit beside Clear, so the whole group moved
     to its own line and Clear was pushed below it. A zero basis lets the group
     shrink to whatever Clear leaves and wrap its chips inside that. */
  flex: 1 1 0;
  min-width: 0;
}

.filter-group__label {
  display: block;
  margin-bottom: 0.5rem;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* The chips wrap among themselves, inside the group, clear of the label. */
.filter-group__chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

/* --- empty state ------------------------------------------------------- */

/* Hidden until site.ts sets data-shown="true" on [data-empty]. */
.empty {
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding-block: clamp(3rem, 2rem + 4vw, 5rem);
  text-align: center;
  color: var(--text-2);
}

.empty[data-shown="true"] {
  display: flex;
}

.empty svg {
  width: 26px;
  height: 26px;
  color: var(--text-3);
}

.empty p {
  margin: 0;
}

/* --- chips inside a row ------------------------------------------------- */

.chips--tight {
  gap: 0.3rem;
}

.chips--tight .chip {
  padding: 0.2rem 0.5rem;
  font-size: 0.74rem;
}

.contact-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.15rem;
}

.contact-list a,
.contact-list button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  /* Tighter than a normal row: the email is 28 unbroken characters and was
     wrapping to a second line inside the 20rem card. */
  padding: 0.55rem 0.45rem;
  margin-inline: -0.45rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-2);
  font-size: 0.88rem;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}

.contact-list a:hover,
.contact-list button:hover {
  background: var(--surface-2);
  color: var(--text);
}

.contact-list svg {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--text-3);
}

.contact-list .handle {
  margin-left: auto;
  font-size: 0.78rem;
  color: var(--text-3);
  /* The handle sits after the label in a flex row, so it takes whatever width
     is left over. `overflow: hidden` alone then clipped handles like
     "danielgordon10" mid-word without ever showing the ellipsis. `0` lets it
     shrink to nothing; `auto` sizes it to the content and only truncates when
     the content genuinely cannot fit. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

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

/* The address is a long unbroken string. Without this it sets the card's
   intrinsic width and, in the narrow single-column hero layout, spills out of
   the panel instead of wrapping inside it. */
.contact-list > li > a > span:first-of-type,
.contact-list > li > button > span:first-of-type {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Confirmation bubble for the email copy button. site.ts sets
   data-copied for ~1.8s; the bubble is a ::after so it needs no extra markup
   and is removed by deleting the attribute. */
.contact-list button[data-copied] {
  position: relative;
  color: var(--text);
}

.contact-list button[data-copied]::after {
  content: "Copied";
  position: absolute;
  bottom: calc(100% + 6px);
  right: 0.6rem;
  z-index: 2;
  padding: 0.22rem 0.5rem;
  border-radius: var(--radius-sm);
  /* Inverted against the page, so it reads at a glance against both themes. */
  background: var(--text);
  color: var(--bg);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  box-shadow: var(--shadow-2);
  pointer-events: none;
  animation: bubble 170ms var(--ease);
}

@keyframes bubble {
  from {
    opacity: 0;
    translate: 0 4px;
  }
}

/* The source JPEG has a colored band baked into its outer ~6%: the edge
   pixels average (88,136,130), a teal, against a neutral (85,85,83)
   interior. It settles to neutral around 24-30px into the 512px file. Left
   alone, that band sits under the rounded corners and reads as a hard frame
   part-way round the image, which is what looked wrong.

   Cropped in CSS rather than in the file, because the photo is also used at
   other sizes and re-editing it means regenerating every rendition in the
   srcset. The frame clips; the photo is scaled up so the band falls outside
   the clip. 1.12 removes 6% from each side, which covers the 30px band with
   room to spare. `object-fit: cover` on a square source and a square frame
   means there is nothing to lose at the edges anyway. */
.contact-card__frame {
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--surface-2);
  /* the gap to the contact list below, which the photo used to carry */
  margin-bottom: var(--pad);
}

.contact-card__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
  scale: 1.12;
  /* Rounded again. Square against the rounded card read as a pasted-in
     screenshot, and a full circle read as a logo badge. The frame clips on
     its own radius, so this only has to hide the corners pre-scale. */
  border-radius: var(--radius);
  background: var(--surface-2);
}

/* The hero card sits opposite the intro text, the /about/ one in a sidebar.
   Same element and same styles either way; only the track it sits in differs.
   The hero track is a fixed width rather than `auto` because the portrait's
   intrinsic 512px width made an auto track resolve to 563px, which left the
   336px card stranded against the left of its own track with a band of dead
   space to its right. */
.contact-card {
  width: 100%;
  min-width: 0;
  /* See-through, like the header: 90% was still effectively a solid panel, so
     the page behind it barely showed. At 72% it reads as glass. The border and
     shadow are what separate it from the page, not the fill. */
  background: color-mix(in oklab, var(--surface) 72%, transparent);
  backdrop-filter: saturate(140%) blur(14px);
  -webkit-backdrop-filter: saturate(140%) blur(14px);
}


.fact {
  display: grid;
  gap: 0.1rem;
}

.fact__k {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.fact__v {
  font-size: 0.95rem;
}

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

.site-footer {
  /* A closing credit line, not a section: --section here left ~110px of
     padding and margin above two lines of text. */
  margin-top: clamp(1.5rem, 1rem + 2vw, 2.5rem);
  border-top: 1px solid var(--line);
  background: var(--bg-2);
  padding-block: clamp(1rem, 0.8rem + 0.8vw, 1.5rem);
  font-size: 0.85rem;
  color: var(--text-3);
}

.site-footer .wrap {
  display: flex;
  justify-content: center;
  text-align: center;
}

.site-footer__line {
  margin: 0;
}

.site-footer a {
  color: var(--text-2);
  text-decoration: none;
}

.site-footer a:hover {
  color: var(--accent);
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 1.1rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.footer-links a {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.footer-links svg {
  width: 14px;
  height: 14px;
  opacity: 0.7;
}

/* ---------------------------------------------------------- tap targets --- */
/* WCAG 2.2 asks for 24px minimum. `min-height` is ignored on inline boxes, so
   each target is first made a flex/inline-flex box. Written late so it can
   tighten up the defaults above without those rules having to know. */

.footer-links a,
.contact-list a,
.contact-list button,
.card__foot a,
.card__title a,
.row__title a,
.chip,
.role__org a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

/* On a touch screen the primary navigation and the standalone link lists get
   full-size targets. Dense repeated controls (the tag chips) stay compact. */
@media (pointer: coarse) {
  .nav__link,
  .brand,
  .footer-links a,
  .contact-list a,
  .contact-list button {
    min-height: 44px;
  }
}

/* ------------------------------------------------------------------ 404 --- */

.notfound {
  display: grid;
  place-items: center;
  text-align: center;
  gap: 1rem;
  min-height: 62vh;
  padding-block: 4rem;
}

.notfound__code {
  font-family: var(--font-display);
  font-size: clamp(5rem, 16vw, 11rem);
  line-height: 0.9;
  font-weight: 400;
  letter-spacing: -0.04em;
  background: linear-gradient(160deg, var(--accent), var(--warm));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ------------------------------------------------------------ utilities --- */

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

.center {
  text-align: center;
}

.stack-sm > * + * {
  margin-top: 0.5rem;
}

.stack > * + * {
  margin-top: 1rem;
}

.row-between {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem var(--gap);
}

.mt-lg {
  margin-top: clamp(2rem, 1.5rem + 2vw, 3.5rem);
}

.noscript-note {
  display: none;
  padding: 0.7rem 1rem;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--text-2);
  font-size: 0.85rem;
  margin-bottom: 1.2rem;
}

.no-js .noscript-note {
  display: block;
}

/* ------------------------------------------------------------- entrance --- */
/* Cards fade up as they scroll into view. The hidden state is applied by
   site.ts via `.is-armed`, and only to elements that are actually below the
   fold: if the script never runs, or fails, or the observer never fires, the
   content is simply already visible rather than permanently invisible. */

[data-reveal] {
  transition: opacity 0.6s var(--ease), translate 0.6s var(--ease);
}

[data-reveal].is-armed {
  opacity: 0;
  translate: 0 14px;
}

[data-reveal].is-armed.is-revealed {
  opacity: 1;
  translate: 0 0;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    transition: none;
  }
  [data-reveal].is-armed {
    opacity: 1;
    translate: none;
  }
}

/* --------------------------------------------------------------- mobile --- */

@media (max-width: 760px) {
  .nav {
    position: fixed;
    inset: var(--header-h) 0 auto;
    flex-direction: column;
    align-items: stretch;
    gap: 0.2rem;
    padding: 0.9rem clamp(1.15rem, 0.6rem + 2.5vw, 2.5rem) 1.4rem;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    translate: 0 -8px;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur) var(--ease), translate var(--dur) var(--ease),
      visibility var(--dur);
  }

  .nav[data-open="true"] {
    translate: 0 0;
    opacity: 1;
    visibility: visible;
  }

  .nav__link {
    padding: 0.65rem 0.75rem;
    border-radius: var(--radius-sm);
    font-size: var(--step-0);
  }

  .nav-toggle {
    display: grid;
  }
}

@media (min-width: 761px) {
  /* The toggle is available at every width - the choice is part of the
     request, not a mobile-only convenience. */
  .nav-toggle {
    display: none;
  }
  .site-header__inner {
    gap: var(--space-6);
  }
}

@media print {
  .site-header,
  .site-footer,
  .filters,
  .item-links,
  .copy-btn,
  .hero__bg {
    display: none !important;
  }
  body {
    background: #fff;
    color: #000;
  }
}


/* --- item page additions ------------------------------------------------- */

/* The type badge is a link back to a filtered index, so it needs the same
   affordance as a chip. Not .chip itself - it carries the badge shape. */
a.badge--type:hover {
  border-color: var(--accent);
  color: var(--accent);
  text-decoration: none;
}

/* A course's slides: thirteen destinations of equal weight, so a list
   with rules between them rather than another row of buttons. */
/* A block grouping inside the item body: the abstract, then a media figure,
   then whatever else an item carries. Enough to separate two blocks, not the
   page-level breathing room `section` gets. */
.item-section {
  margin-top: 2.25rem;
}

.notes-list {
  margin: 0.9rem 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line);
}

.notes-list li {
  border-bottom: 1px solid var(--line);
}

.notes-list a {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  /* 0.15rem inset the text and the arrow both sat almost on the row edges: the
     title hard against the rule above it and the arrow hard against the page
     margin, which made the row read as two loose ends rather than one link.
     0.75rem matches the inset other rows in this page already use. */
  padding: 0.6rem 0.75rem;
  color: var(--text);
  text-decoration: none;
}

.notes-list a::after {
  content: "\2197";
  margin-left: auto;
  color: var(--text-3);
  font-size: 0.85em;
}

.notes-list a:hover {
  color: var(--accent);
  background: var(--surface-2, transparent);
}
