
/* ---- Persistent header ---------------------------------------------------
   The big hero name and this small one are two elements that cross-fade, not
   one element being morphed. Morphing a 170px display face into a 17px link
   across a position change is fragile; a cross-fade reads identically and
   cannot break. src/animations/scroll.js drives the reveal. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: 0.85rem clamp(1rem, 4vw, 2.5rem);
  align-items: flex-end;
  background: color-mix(in oklab, var(--bg) 82%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  /* Always present, as in the reference: a persistent credit top-left and nav
     top-right. Only the backdrop firms up once you leave the photograph. */
  background: transparent;
  backdrop-filter: none;
  transition: background-color 0.4s ease, backdrop-filter 0.4s ease,
              border-color 0.4s ease;
}
.site-header.is-visible {
  background: color-mix(in oklab, var(--bg) 82%, transparent);
  backdrop-filter: blur(12px);
  border-bottom-color: var(--border);
}
/* Over the photograph the header text must be light regardless of palette. */
.site-header:not(.is-visible) .site-header__name { color: #ffffff; }
.site-header:not(.is-visible) .site-nav__link { color: rgba(255, 255, 255, 0.82); }
.site-header:not(.is-visible) .site-nav__link:hover { color: #ffffff; }
.site-header__name {
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 500;
  color: var(--text);
  text-decoration: none;
  letter-spacing: -0.01em;
  white-space: nowrap;
}
.site-header__name:hover { color: var(--accent); }

/* Sits on the location's line rather than the very top edge, so the nav and
   "LONDON, UK" read as a single row beneath the name. */
.site-nav { display: flex; gap: clamp(0.75rem, 2vw, 1.75rem); padding-top: 2.1rem; }
.site-nav__link {
  position: relative;
  font-size: var(--step--1);
  color: var(--text-muted);
  text-decoration: none;
  padding-block: 0.25rem;
  transition: color 0.25s ease;
}
.site-nav__link::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; background: var(--accent);
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.site-nav__link:hover { color: var(--text); }
.site-nav__link:hover::after { transform: scaleX(1); }

@media (max-width: 640px) {
  .site-nav { gap: 0.75rem; }
  .site-nav__link { font-size: 0.75rem; }
}
/* The hero breaks out of main's max-width + padding so the photograph fills the
   viewport edge to edge. Its CONTENTS are re-constrained by .hero__inner.
   Do NOT add overflow clipping here — that clips the break-out straight back to
   the container and the photo silently stops reaching the edges. */
.hero {
  position: relative;
  width: 100vw;
  margin-left: 50%;
  transform: translateX(-50%);
  min-height: 100vh;
  display: flex;
  isolation: isolate;
  /* Safe to clip HERE (and only here): .hero is itself exactly 100vw, so this
     trims the parallax-scaled media back to the viewport instead of clipping
     the break-out. Putting overflow on an ancestor, or on .hero while the media
     was the thing breaking out, is what silently killed full-bleed before. */
  overflow: hidden;
}

/* The photograph is the background. A scrim sits over it so overlaid text is
   legible; it is deliberately heaviest bottom-left where the name and CTAs sit
   and lightest over the centre-right, where his face is. */
/* .hero is already full-viewport-width, so the media simply fills it. */
.hero__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-size: cover;
  background-position: 50% 56%;
}

.hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, rgba(8, 10, 14, 0.82) 0%, rgba(8, 10, 14, 0.34) 32%, rgba(8, 10, 14, 0.04) 58%, rgba(8, 10, 14, 0.30) 88%, rgba(8, 10, 14, 0.52) 100%),
    linear-gradient(to right, rgba(8, 10, 14, 0.42) 0%, rgba(8, 10, 14, 0.08) 45%, rgba(8, 10, 14, 0.00) 72%, rgba(8, 10, 14, 0.14) 100%);
}

/* Sits over the hero photograph, so its colours are fixed light values rather
   than palette tokens — the surface/text tokens would disappear against it. */
.hero__chip {
  display: inline-block;
  margin: 0;
  padding: 0.4rem 0.85rem;
  border: 1px solid rgba(255, 255, 255, 0.38);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  backdrop-filter: blur(6px);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.94);
}

.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-xs); }

.btn {
  display: inline-flex;
  align-items: center;
  padding: 0.7rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.95rem;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.18s ease, background-color 0.18s ease;
}
.btn:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
/* GSAP drives `transform` directly on magnetic buttons (src/animations/cursor.js),
   writing inline styles ~60x/s. A CSS transition on the same property fights it:
   the effect reads damped, and the inline transform GSAP leaves behind on
   pointerleave permanently defeats .btn:hover's translateY. Let GSAP own
   `transform` exclusively here; keep the rest of the hover feedback on CSS. */
.btn[data-magnetic] {
  transition: box-shadow 0.18s ease, background-color 0.18s ease;
}
.btn--primary {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}
.btn--primary:hover { background: var(--accent); border-color: var(--accent); color: var(--on-highlight); }

/* The hero CTAs sit on the photograph, so they take fixed light values rather
   than surface tokens, which would vanish against it. */
.hero__actions .btn {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.40);
  color: #ffffff;
  backdrop-filter: blur(8px);
}
.hero__actions .btn:hover { background: rgba(255, 255, 255, 0.22); }
.hero__actions .btn--primary {
  background: #ffffff; border-color: #ffffff; color: #14161b;
}
.hero__actions .btn--primary:hover { background: var(--accent); border-color: var(--accent); color: var(--on-highlight); }



.about__lede { font-size: var(--step-2); max-width: none; color: var(--text); }
.about__para { margin-top: var(--space-md); max-width: none; color: var(--text); }
.about__track { margin-top: var(--space-lg); }

.milestones {
  list-style: none; padding: 0;
  margin-top: var(--space-md);
  display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md);
}

.milestone {
  position: relative;
  padding-top: var(--space-md);
  border-top: 2px solid var(--border);
}
.milestone::before {
  content: ''; position: absolute; top: -7px; left: 0;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--border);
}
.milestone[data-state='done'] { border-top-color: var(--accent); }
.milestone[data-state='done']::before { background: var(--accent); }
.milestone[data-state='next'] { border-top-color: var(--highlight); }
.milestone[data-state='next']::before { background: var(--highlight); }

.milestone__label { display: block; font-weight: 600; }
.milestone__detail { display: block; color: var(--text-muted); font-size: 0.9rem; }

@media (max-width: 760px) { .milestones { grid-template-columns: 1fr; } }

.eyebrow--spaced { margin-top: var(--space-lg); }
.roles { margin-top: var(--space-md); }

.role {
  position: relative;
  border-top: 1px solid var(--border);
  padding: var(--space-md);
  margin-inline: calc(-1 * var(--space-md));
  transition: background-color 0.35s ease;
}
.role:hover, .role:focus-within {
  background: color-mix(in oklab, var(--accent) 8%, transparent);
}
.role summary {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.15rem var(--space-md);
  cursor: pointer;
  list-style: none;
}
.role summary::-webkit-details-marker { display: none; }
.role__title { font-family: var(--font-display); font-size: var(--step-1); }
.role__org { color: var(--text-muted); }
.role__period {
  grid-column: 2; grid-row: 1 / span 2;
  font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted);
  text-align: right; white-space: nowrap;
}
.role__headline { grid-column: 1; color: var(--text); margin-top: var(--space-xs); }
.role summary::after {
  content: 'Show detail'; grid-column: 1;
  font-family: var(--font-mono); font-size: 0.72rem;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent);
  margin-top: var(--space-xs);
}
.role[open] summary::after { content: 'Hide detail'; }
.role__details { margin-top: var(--space-md); padding-left: 1.1rem; color: var(--text-muted); }
.role__details li { margin-bottom: var(--space-xs); }

.education__row {
  display: flex; justify-content: space-between; gap: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  margin-inline: calc(-1 * var(--space-md));
  border-top: 1px solid var(--border);
  transition: background-color 0.35s ease;
}
.education__row:hover { background: color-mix(in oklab, var(--accent) 8%, transparent); }
.education__institution { display: block; font-weight: 600; }
.education__qualification { display: block; }
.education__detail { display: block; color: var(--text-muted); font-size: 0.9rem; }
.education__period { font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); white-space: nowrap; }

.skills { list-style: none; padding: 0; margin-top: var(--space-md); display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.skill {
  padding: 0.3rem 0.75rem;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-alt);
  font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted);
}

@media (max-width: 640px) {
  .role summary { grid-template-columns: 1fr; }
  .role__period { grid-column: 1; grid-row: auto; text-align: left; }
}

.projects__grid {
  margin-top: var(--space-md);
  display: grid; grid-template-columns: repeat(2, 1fr); column-gap: var(--space-xl); row-gap: 0;
}

.project {
  display: flex; flex-direction: column;
  position: relative;
  padding: var(--space-lg) var(--space-md);
  border-top: 1px solid var(--border);
  transition: background-color 0.35s ease, padding-left 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
/* The highlight IS the affordance now that the boxes are gone: the row lifts
   out of the page on hover rather than a card lifting off a surface. */
.project::before {
  content: '';
  position: absolute; left: 0; top: -1px; bottom: -1px;
  width: 2px; background: var(--accent);
  transform: scaleY(0); transform-origin: 50% 0;
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.project:hover, .project:focus-within {
  background: color-mix(in oklab, var(--accent) 9%, transparent);
  padding-left: calc(var(--space-md) + 0.75rem);
}
.project:hover::before, .project:focus-within::before { transform: scaleY(1); }

.project--flagship {
  grid-column: 1 / -1;
  border-top-color: var(--accent);
}
.project--flagship h3 { font-size: var(--step-2); }

.project__header { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-sm); }
.project__year { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); }
.project__summary { margin-top: var(--space-sm); }
.project__detail { margin-top: var(--space-xs); color: var(--text-muted); font-size: 0.92rem; }

.project__tags { list-style: none; padding: 0; margin-top: var(--space-md); display: flex; flex-wrap: wrap; gap: 0.4rem; }
.project__tag {
  padding: 0.22rem 0.6rem; border-radius: 999px;
  background: var(--surface-alt); border: 1px solid var(--border);
  font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted);
}
.project__link { margin-top: var(--space-md); font-weight: 600; text-decoration: none; }
.project__link:hover { text-decoration: underline; }

@media (max-width: 760px) { .projects__grid { grid-template-columns: 1fr; } }

.bento {
  margin-top: var(--space-md);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md);
}

.tile {
  display: flex; flex-direction: column;
  padding: var(--space-md);
  border-top: 1px solid var(--border);
  transition: background-color 0.35s ease;
  transition: transform 0.25s cubic-bezier(0.34, 1.4, 0.64, 1), box-shadow 0.25s ease;
}
.tile:hover { transform: translateY(-4px); box-shadow: var(--shadow-lift); }
.tile[data-size='tall'] { grid-row: span 2; }
.tile[data-size='wide'] { grid-column: span 2; }

.tile__title { font-size: var(--step-1); }
.tile__blurb { margin-top: var(--space-xs); color: var(--text-muted); }
.tile__count { margin-top: var(--space-sm); font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); }
.tile__action {
  margin-top: auto; padding-top: var(--space-md);
  font-weight: 600; text-decoration: none;
}
.tile__action:hover { text-decoration: underline; }

.map { width: 100%; height: auto; margin-top: var(--space-sm); }
.map path {
  fill: var(--map-land);
  stroke: var(--map-stroke);
  stroke-width: 0.5;
  transition: fill 0.3s ease;
}
.map path[data-visited='true'] { fill: var(--text); }
.map path[data-visited='true']:hover { fill: #ffffff; }
.map__marker { fill: var(--text); stroke: var(--bg); stroke-width: 1; }

@media (max-width: 860px) {
  .bento { grid-template-columns: 1fr 1fr; }
  .tile[data-size='wide'] { grid-column: span 2; }
  .tile[data-size='tall'] { grid-row: auto; }
}
@media (max-width: 560px) {
  .bento { grid-template-columns: 1fr; }
  .tile[data-size='wide'] { grid-column: span 1; }
}

.contact { text-align: center; padding-bottom: var(--space-xl); }
.contact__lede { margin: var(--space-md) auto 0; max-width: 52ch; color: var(--text-muted); }
.contact__actions { margin-top: var(--space-md); display: flex; justify-content: center; flex-wrap: wrap; gap: var(--space-xs); }
.contact__foot {
  margin-top: var(--space-xl);
  font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-muted);
}
.btn.is-copied { background: var(--highlight); border-color: var(--highlight); color: var(--on-highlight); }

.edu__list { display: grid; gap: var(--space-md); margin-top: var(--space-lg); }
.edu__row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--space-md);
  padding: var(--space-md);
  margin-inline: calc(-1 * var(--space-md));
  border-bottom: 1px solid var(--border);
  transition: background-color 0.35s ease, padding-left 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.edu__row:hover {
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  padding-left: calc(var(--space-md) + 0.5rem);
}
.edu__main { display: grid; gap: 0.2rem; }
.edu__institution { font-family: var(--font-display); font-size: var(--step-1); }
.edu__qualification { color: var(--text); }
.edu__detail { color: var(--text-muted); font-size: var(--step--1); }
.edu__period { color: var(--text-muted); font-size: var(--step--1); white-space: nowrap; }
@media (max-width: 640px) {
  .edu__row { flex-direction: column; gap: var(--space-xs); }
}

.media__cols {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--space-xl); margin-top: var(--space-lg);
}
.media__list { list-style: none; padding: 0; margin: var(--space-sm) 0 0; display: grid; gap: var(--space-sm); }
.media__item {
  display: grid; gap: 0.15rem;
  padding: var(--space-xs) var(--space-md);
  margin-inline: calc(-1 * var(--space-md));
  transition: background-color 0.3s ease, padding-left 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.media__item:hover {
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  padding-left: calc(var(--space-md) + 0.5rem);
}
.media__title { font-family: var(--font-display); font-size: var(--step-0); }
.media__author { color: var(--text-muted); font-size: var(--step--1); }
@media (max-width: 720px) {
  .media__cols { grid-template-columns: 1fr; gap: var(--space-lg); }
}

/* Hero — Snellenberg-style showpiece: flat background, a huge name as the
   dominant element over a full-bleed photograph, everything else (location,
   chip, CTAs) small and pinned to the corners. `.hero` itself spans the whole
   viewport; this re-constrains the CONTENT back to the page's reading width so
   the text lines up with the sections below it. */
.hero__inner {
  width: 100%;
  max-width: none;
  margin-inline: 0;
  /* Matches .hero__name's left offset so the chip and CTAs line up with it. */
  padding-inline: clamp(1rem, 4vw, 2.5rem);
  padding-block: 2.95rem var(--space-xl);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-lg);
  box-sizing: border-box;
}

.hero__top { display: flex; justify-content: space-between; align-items: flex-start; }
.hero__location {
  margin: 0;
  color: rgba(255, 255, 255, 0.96);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.75);
}

.hero__headline {
  flex: 1;
  display: flex;
  align-items: flex-end;
  min-width: 0;
}

/* One name, fixed to the viewport so it can travel into the header as you
   scroll. transform-origin is top-left so the scale-down converges on the
   header slot rather than drifting off it. */
.hero__name {
  position: fixed;
  left: clamp(1rem, 4vw, 2.5rem);
  bottom: clamp(8rem, 17vh, 12rem);
  margin: 0;
  z-index: 101;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(2.5rem, 9vw, 8rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: #ffffff;
  white-space: nowrap;
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.35);
  transform-origin: left top;
  will-change: transform;
  pointer-events: none;
  transition: color 0.3s ease, text-shadow 0.3s ease;
}

/* Once the name lands on the light page it must stop being white, or it simply
   disappears. Keyed off <body> because the name sits outside .site-header. */
.past-hero .hero__name {
  color: var(--text);
  text-shadow: none;
}
.hero__spacer { flex: 1; }

/* The landing target. Its ghost text is invisible but reserves exactly the
   right box, so the flying name has something real to converge on and the nav
   never shifts when it arrives. */
.site-header__slot {
  display: inline-block;
  text-decoration: none;
  pointer-events: auto;
}
.site-header__ghost {
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 500;
  letter-spacing: -0.01em;
  white-space: nowrap;
  visibility: hidden;
}


.hero__bottom {
  display: flex; flex-wrap: wrap;
  justify-content: space-between; align-items: flex-end;
  gap: var(--space-md);
}

@media (max-width: 860px) {
  .hero__inner { min-height: auto; }
  .hero__headline { flex-direction: column; align-items: flex-start; gap: var(--space-md); }
  .hero__name {
  color: #ffffff;
  text-shadow: 0 2px 24px rgba(0,0,0,0.35); font-size: clamp(var(--step-5), 16vw, 6rem); }
  }


/* ---- Interests bento -------------------------------------------------------
   Explicit placement so the grid resolves into a full rectangle with no empty
   cell: three small tiles across the top, the map and podcasts side by side
   beneath, and books running the full width underneath them. */
.bento {
  grid-template-columns: repeat(3, 1fr);
  grid-auto-flow: row dense;
}
.tile[data-id='travel']   { grid-column: span 2; grid-row: span 2; }
.tile[data-id='podcasts'] { grid-column: span 1; grid-row: span 2; }
.tile[data-id='books']    { grid-column: 1 / -1; }

.tile__media { list-style: none; padding: 0; margin: var(--space-sm) 0 0; display: grid; gap: var(--space-xs); }
.tile[data-id='books'] .tile__media {
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  column-gap: var(--space-lg);
}
.tile__media li { display: grid; gap: 0.05rem; }
.tile__media-title { font-size: var(--step-0); color: var(--text); }
.tile__media-author { font-size: var(--step--1); color: var(--text-muted); }

@media (max-width: 860px) {
  .tile[data-id='travel'],
  .tile[data-id='podcasts'],
  .tile[data-id='books'] { grid-column: 1 / -1; grid-row: auto; }
}

/* Same size and weight as the lede, and butted directly against it: they read
   as one thought, not two stacked statements. */
.contact__hello {
  margin-top: 0;
  font-size: inherit;
}

/* ---- Linked project cards --------------------------------------------------
   The whole card is clickable via a ::after overlay stretched from the title's
   anchor. This keeps exactly one interactive element per card — nesting a card
   link around inner links would produce invalid markup and unusable tab order. */
.project--linked { cursor: pointer; }
.project__title-link {
  color: inherit;
  text-decoration: none;
}
.project__title-link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}
.project--linked:hover .project__title-link { color: var(--accent); }
.project__title-link:focus-visible { outline: none; }
.project--linked:focus-within {
  background: color-mix(in oklab, var(--accent) 9%, transparent);
}

.project__cta {
  margin-top: var(--space-md);
  font-weight: 600;
  color: var(--accent);
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.project--linked:hover .project__cta { transform: translateX(4px); }

/* ---- Mobile corrections ----------------------------------------------------
   Placed last on purpose: several base rules above were appended after the
   original breakpoints and were therefore winning at every width. Measured on a
   390px viewport, where the page had 84px of horizontal overflow and genuinely
   scrolled sideways. */
@media (max-width: 620px) {
  /* The hero name was 406px wide in a 390px viewport.

     `bottom` is raised off the 17vh default too: on a 664px-tall phone that
     resolved to 128px, which put the name straight on top of the chip (they
     overlapped by 26px). The chip/CTA block is anchored to the bottom by a
     fixed padding, so a fixed rem offset clears it at any phone height. */
  .hero__name { font-size: clamp(1.9rem, 11vw, 3rem); bottom: 12rem; }

  /* Five nav links do not fit; shrink and let them wrap rather than overflow.
     The nav's top padding is NOT zeroed: that is what dropped it off
     "LONDON, UK"'s line on mobile while desktop stayed correct. */
  .site-header { gap: var(--space-xs); }
  .site-nav { flex-wrap: wrap; justify-content: flex-end; gap: 0.6rem; }
  .site-nav__link { font-size: 0.72rem; }
  .report-header { gap: var(--space-xs); }

  /* The flight scales by the ghost's HEIGHT, so shrinking the ghost shrinks the
     landed name. At the default size the name landed 130px wide, which does not
     fit beside 235px of nav links in a 390px viewport. scroll.js reserves the
     landed width exactly; this makes that width small enough to fit. */
  .site-header__ghost { font-size: 0.85rem; line-height: 1; }

  /* The bento's 3-column rule was declared after the old breakpoint and beat it. */
  .bento { grid-template-columns: 1fr; }
  .tile,
  .tile[data-id='travel'],
  .tile[data-id='podcasts'],
  .tile[data-id='books'] { grid-column: 1 / -1; grid-row: auto; }
  .tile[data-id='books'] .tile__media { grid-template-columns: 1fr; }

  /* Projects stack; the flagship no longer needs a full-width span of its own. */
  .projects__grid { grid-template-columns: 1fr; column-gap: 0; }

  .hero__inner { padding-inline: 1rem; }
  .hero__actions { gap: var(--space-xs); }
  .hero__chip { font-size: 0.7rem; }
}

/* Nothing may produce horizontal scroll. `clip` (unlike `hidden`) does not
   create a scroll container, so it cannot break position:sticky elsewhere. */
body { overflow-x: clip; }

/* ---- Travel map modal ------------------------------------------------------ */
.tile[data-id='travel'] { position: relative; }
/* Invisible overlay over the map: makes the whole thing clickable while keeping
   one real button for keyboard and screen-reader users. */
.tile__map-open {
  position: absolute; inset: 0;
  border: 0; background: transparent; cursor: zoom-in;
}
.tile__map-open:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

.map-modal {
  border: 0; padding: 0;
  width: min(880px, 92vw); max-width: none; max-height: 88vh;
  /* The reset's `* { margin: 0 }` beats the UA stylesheet's `dialog { margin:
     auto }`, which is what pinned this to the top-left corner instead of
     centring it. Restore the centring explicitly. */
  margin: auto;
  background: transparent; color: var(--text);

  /* Zoom-out resting state. `overlay` and `display` need allow-discrete so the
     CLOSE direction animates too — without them the dialog is yanked out of
     the top layer on frame one and only the open animation is ever seen. */
  opacity: 0;
  transform: scale(0.94);
  transition:
    opacity 240ms ease,
    transform 240ms cubic-bezier(0.22, 1, 0.36, 1),
    overlay 240ms allow-discrete,
    display 240ms allow-discrete;
}
.map-modal[open] { opacity: 1; transform: scale(1); }
/* The entry animation needs a from-state that exists before the element is
   first rendered; that is exactly what @starting-style supplies. Browsers
   without it simply show the dialog instantly, which is a fine fallback. */
@starting-style {
  .map-modal[open] { opacity: 0; transform: scale(0.94); }
}

.map-modal::backdrop {
  background: rgba(8, 10, 14, 0.62); backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity 240ms ease, overlay 240ms allow-discrete, display 240ms allow-discrete;
}
.map-modal[open]::backdrop { opacity: 1; }
@starting-style {
  .map-modal[open]::backdrop { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .map-modal, .map-modal[open] { transform: none; }
  .map-modal {
    transition: opacity 1ms, overlay 1ms allow-discrete, display 1ms allow-discrete;
  }
}

.map-modal__inner {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
  padding: var(--space-md);
  max-height: 88vh; overflow-y: auto;
}
.map-modal__head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-md); }
.map-modal__head h3 { font-size: var(--step-1); }
.map-modal__close {
  border: 1px solid var(--border); background: var(--surface-alt); color: var(--text);
  border-radius: 999px; width: 2rem; height: 2rem; cursor: pointer; line-height: 1;
}
.map-modal__close:hover { background: var(--accent); color: var(--on-highlight); border-color: var(--accent); }

.map-modal__body {
  display: grid; grid-template-columns: minmax(11rem, 15rem) 1fr;
  gap: var(--space-md); margin-top: var(--space-md); align-items: start;
}
.map-modal__list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.1rem; }
.map-modal__list li {
  position: relative; padding: 0.3rem 0.5rem; border-radius: 6px;
  font-size: var(--step--1); cursor: default;
  transition: background-color 0.2s ease, padding-left 0.2s ease;
}
.map-modal__list li.is-active {
  background: color-mix(in oklab, var(--accent) 14%, transparent);
  padding-left: 0.9rem;
}
/* The "line to its location": a rule that extends from the name toward the map. */
.map-modal__list li.is-active::after {
  content: ''; position: absolute; right: -0.6rem; top: 50%;
  width: 0.6rem; height: 1px; background: var(--accent);
}
.map-modal__svg { width: 100%; height: auto; }
.map-modal__svg [data-visited='true'] { transition: fill 0.2s ease; }
.map-modal__svg [data-visited='true'].is-active { fill: var(--accent); }

@media (max-width: 620px) {
  .map-modal__body { grid-template-columns: 1fr; }
}
