:root {
  --bg: #F2F6FA;
  --ink: #0B0B0D;
  --ink-70: #3C4249;
  --ink-50: #5E656C;
  --line: rgba(11, 11, 13, 0.16);
  --line-soft: rgba(11, 11, 13, 0.09);
  --mint: #CFEFDB;
  --accent: #EEF23A;
  --squiggle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='9' viewBox='0 0 24 9'%3E%3Cpath d='M0 6 Q 6 1.6 12 6 T 24 6' fill='none' stroke='%23FF6B4A' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E");
  /* Dot field, after uiverse.io/Juanes200122/yellow-zebra-53 (MIT).
         Still far below the original rgba(12,12,12,0.171) at 2px, which reads
         as a pattern competing with the work. At 1.4px it stays paper tooth
         under the grain already on the page.
         The two themes are matched by perceived contrast, not by alpha. The
         dark grid is 0.075 white on the dark ground, which is a CIELAB
         deltaL* of 7.6; reaching the same 7.6 on paper takes 0.092, because
         L* is much more sensitive near black than near white. Copying 0.075
         across would land at 6.1 and read visibly weaker than dark. Move the
         two together, and re-solve rather than assuming they share a number. */
  --dot: rgba(11, 11, 13, 0.092);
  --dot-gap: 30px;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --sans: "Instrument Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color-scheme: light dark;
  --pad: clamp(20px, 5vw, 88px);
  /* Surfaces, edges and photo treatment are tokenised so the dark theme is a
         palette swap rather than a second stylesheet. Light values below are the
         literals this page shipped with; light mode renders unchanged. */
  --surface: rgba(255, 255, 255, 0.42);
  --surface-2: rgba(255, 255, 255, 0.5);
  --surface-hi: rgba(255, 255, 255, 0.85);
  --surface-row: rgba(255, 255, 255, 0.75);
  --surface-out: rgba(255, 255, 255, 0.7);
  --backer: #fff;
  --edge: var(--ink);
  --edge-strong: var(--ink);
  --nav-bg: rgba(242, 246, 250, 0.72);
  --ghost: rgba(11, 11, 13, 0.055);
  --mint-0: rgba(207, 239, 219, 0);
  --blob-a-o: 0.72;
  --blob-b-o: 0.5;
  --blob-c-o: 0.55;
  --blob-contact-o: 0.62;
  --grain-blend: multiply;
  --grain-o: 0.032;
  --photo-b: 1;
  --photo-b-hi: 1;
  --lift-tile: 0 18px 40px -26px rgba(11, 11, 13, 0.5);
  --lift-honour: 0 18px 34px -24px rgba(11, 11, 13, 0.55);
  --lift-chip: 0 10px 20px -12px rgba(11, 11, 13, 0.45);
  --edge-hover: var(--edge);
  --btn-solid-bg: var(--ink);
  --ambient-o: 0.85;
  --tile-glow-o: 0.75;
  --tile-glow-soft-o: 0.3;
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background-color: var(--bg);
  background-image: radial-gradient(var(--dot) 1.4px, transparent 0);
  background-size: var(--dot-gap) var(--dot-gap);
  background-position: -5px -5px;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
}

::selection {
  background: var(--accent);
  color: #0B0B0D;
}

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

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

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

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

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

h1,
h2,
h3,
h4 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.028em;
  text-wrap: balance;
}

/* ---------- reveal ---------- */
[data-anim="on"] [data-reveal] {
  opacity: 0;
  transform: translateY(16px);
}

[data-anim="on"] [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.7s cubic-bezier(0.22, 0.61, 0.36, 1), transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  [data-anim="on"] [data-reveal] {
    opacity: 1;
    transform: none;
  }

  /* No animation means no reason to hold three blurred compositor layers. */
  .blob {
    animation: none !important;
    will-change: auto;
  }
}

/* ---------- shell ---------- */
.wrap {
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  padding-left: var(--pad);
  padding-right: var(--pad);
}

.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 6;
  background-image: var(--grain);
  opacity: var(--grain-o);
  mix-blend-mode: var(--grain-blend);
}

/* ---------- nav ---------- */
.nav {
  position: sticky;
  top: 0;
  z-index: 5;
  backdrop-filter: saturate(1.4) blur(14px);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  background: var(--nav-bg);
  border-bottom: 1px solid var(--line-soft);
}

.nav-in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  height: 62px;
}

.mark {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.mark-badge {
  width: 26px;
  height: 26px;
  border: 1.4px solid var(--edge-strong);
  border-radius: 8px;
  display: grid;
  place-items: center;
  font-size: 11px;
  letter-spacing: -0.06em;
  line-height: 1;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}

.nav-links a {
  position: relative;
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--ink-70);
  padding: 8px 12px;
  border-radius: 8px;
  transition: color 0.2s ease, background 0.2s ease;
}

.nav-links a::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 5px;
  height: 1.4px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.28s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.nav-links a:hover {
  color: var(--ink);
}

.nav-links a:hover::after {
  transform: scaleX(1);
}

.pill-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  font-family: var(--mono);
  font-size: 12.5px;
  font-weight: 500;
  padding: 0 16px;
  border: 1.4px solid var(--edge-strong);
  border-radius: 999px;
  transition: background 0.22s ease, color 0.22s ease, transform 0.22s ease;
}

.pill-cta:hover {
  background: var(--ink);
  color: var(--bg);
  transform: translateY(-1px);
}

.pill-cta svg {
  transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.pill-cta:hover svg {
  transform: translate(3px, -3px);
}

.nav-end {
  display: flex;
  align-items: center;
  gap: 10px;
}

.pill-cta {
  flex: 0 0 auto;
}

/* The toggle only does something with JS, so it stays out of the document
       until the head bootstrap marks the page ready, and no layout shift comes
       with it because that bootstrap runs before first paint. Note the gate is
       the bootstrap, not the click handler, so a page where the head script
       runs and the body script does not would show an inert button. */
.theme-toggle {
  display: none;
}

:root[data-theme-ready] .theme-toggle {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  padding: 0;
  background: none;
  cursor: pointer;
  color: var(--ink);
  border: 1.4px solid var(--edge-strong);
  border-radius: 999px;
  transition: background 0.22s ease, color 0.22s ease, transform 0.22s ease;
}

.theme-toggle:hover {
  background: var(--ink);
  color: var(--bg);
  transform: translateY(-1px);
}

.theme-toggle svg {
  display: block;
}

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

:root:not([data-theme="dark"]) .theme-toggle .icon-sun {
  display: none;
}

/* The clip-path animation below is driven entirely by the Web Animations
       call in the script, so the default cross-fade has to be switched off or
       the two run against each other. */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* Unconditional on purpose. The pseudo tree only exists while a
       transition is running, so gating this on an attribute bought nothing and
       raced with itself: a second click aborted the first, whose rejected
       ready promise then tore the attribute down mid-flight and dropped the
       group back to the UA default duration. */
::view-transition-group(root) {
  animation-duration: 400ms;
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle {
    transition: none;
  }

  .theme-toggle:hover {
    transform: none;
  }
}

/* ---------- hero ---------- */
.hero {
  position: relative;
  overflow: hidden;
  padding: clamp(64px, 12vh, 128px) 0 clamp(56px, 9vh, 104px);
}

.mesh {
  position: absolute;
  inset: -20% -10%;
  z-index: 0;
  pointer-events: none;
}

.blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  will-change: transform;
}

.blob-a {
  width: 52%;
  height: 62%;
  left: -8%;
  top: -6%;
  opacity: var(--blob-a-o);
  animation: drift-a 26s ease-in-out infinite alternate;
}

.blob-b {
  width: 46%;
  height: 58%;
  left: 34%;
  top: 18%;
  opacity: var(--blob-b-o);
  animation: drift-b 32s ease-in-out infinite alternate;
}

.blob-c {
  width: 40%;
  height: 50%;
  right: -6%;
  top: 4%;
  opacity: var(--blob-c-o);
  background: radial-gradient(circle, var(--mint) 0%, var(--mint-0) 70%);
  animation: drift-a 30s ease-in-out infinite alternate-reverse;
}

@keyframes drift-a {
  from {
    transform: translate3d(0, 0, 0) scale(1);
  }

  to {
    transform: translate3d(4%, 3%, 0) scale(1.08);
  }
}

@keyframes drift-b {
  from {
    transform: translate3d(0, 0, 0) scale(1.04);
  }

  to {
    transform: translate3d(-5%, -2%, 0) scale(0.96);
  }
}

.ghost {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  user-select: none;
  left: 50%;
  top: 46%;
  transform: translate3d(0, 0, 0);
  font-family: var(--mono);
  font-weight: 400;
  font-size: clamp(300px, 44vw, 660px);
  line-height: 0.78;
  letter-spacing: -0.16em;
  color: var(--ghost);
  white-space: nowrap;
  margin-left: -0.62em;
  margin-top: -0.5em;
}

.hero-in {
  position: relative;
  z-index: 1;
}

.eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-70);
}

.eyebrow .sep {
  width: 22px;
  height: 1px;
  background: var(--line);
}

h1.display {
  margin: 26px 0 0;
  font-size: clamp(40px, 6.6vw, 88px);
  line-height: 0.98;
  letter-spacing: -0.038em;
  max-width: 17ch;
}

.hero-sub {
  margin-top: 26px;
  max-width: 60ch;
  font-size: clamp(16px, 1.25vw, 19px);
  color: var(--ink-70);
}

.hero-sub strong {
  color: var(--ink);
  font-weight: 600;
}

.hero-sub em {
  font-style: italic;
  color: var(--ink);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 34px;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 0 22px;
  border-radius: 999px;
  border: 1.4px solid var(--edge-strong);
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 500;
  transition: transform 0.22s ease, background 0.22s ease, color 0.22s ease;
}

.btn-solid {
  background: var(--btn-solid-bg);
  border-color: var(--btn-solid-bg);
  color: var(--bg);
}

.btn-solid:hover {
  color: var(--bg);
  transform: translateY(-2px);
}

.btn-ghost:hover {
  background: var(--ink);
  color: var(--bg);
  transform: translateY(-2px);
}

.btn svg {
  transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* Scrambled email label. Monospace plus a ch-based min-width keeps the
       button exactly one width whether the text is scrambled or resolved. */
.scramble {
  font-family: var(--mono);
  font-variant-ligatures: none;
  white-space: pre;
}

.btn:hover svg {
  transform: translate(3px, -3px);
}

.platforms {
  margin-top: 54px;
  padding-top: 20px;
  border-top: 1px solid var(--line-soft);
  display: flex;
  flex-wrap: wrap;
  gap: 8px 22px;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-50);
}

/* ---------- sections ---------- */
section {
  position: relative;
  scroll-margin-top: 80px;
}

.band {
  padding: clamp(72px, 11vh, 132px) 0;
}

.band-line {
  border-top: 1px solid var(--line-soft);
}

/* The heading inside .label. `font: inherit` and `letter-spacing: normal`
       undo the shared heading reset so the label looks exactly as it did
       when it was a <span>. */
.label-title {
  font: inherit;
  letter-spacing: normal;
  margin: 0;
}

.label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 600;
}

.dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--ink);
  flex: 0 0 auto;
}

.label-link {
  text-decoration: none;
  transition: color 0.2s ease;
}

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

.label-note {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-50);
  font-weight: 400;
  letter-spacing: 0.02em;
}

.head-row {
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
}

/* ---------- project ---------- */
.project {
  display: grid;
  grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
  gap: clamp(28px, 4vw, 64px);
  margin-top: 52px;
}

.project + .project {
  margin-top: clamp(72px, 9vw, 120px);
  padding-top: clamp(72px, 9vw, 120px);
  border-top: 1px solid var(--line-soft);
}

.proj-title {
  font-size: clamp(32px, 3.6vw, 50px);
  line-height: 1.02;
  letter-spacing: -0.035em;
}

.proj-kicker {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-50);
  margin-bottom: 14px;
}

.proj-lead {
  margin-top: 20px;
  font-size: 17px;
  color: var(--ink-70);
  max-width: 46ch;
}

.proj-quote {
  margin-top: 16px;
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1.7;
  color: var(--ink);
  max-width: 44ch;
}

.meta {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.meta li {
  display: flex;
  gap: 12px;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-70);
}

.meta .plus {
  color: var(--ink);
  flex: 0 0 auto;
}

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

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.chip {
  display: inline-flex;
  align-items: center;
  border: 1.2px solid var(--edge);
  border-radius: 999px;
  padding: 5px 12px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.03em;
  transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1), box-shadow 0.24s ease, background 0.24s ease, color 0.24s ease;
}

.chip-soft {
  border-color: var(--line);
  color: var(--ink-70);
}

.chips .chip:hover {
  transform: translateY(-2px);
  background: var(--ink);
  color: var(--bg);
  box-shadow: var(--lift-chip);
}

@media (prefers-reduced-motion: reduce) {
  .chip {
    transition: none;
  }

  .chips .chip:hover {
    transform: none;
  }
}

.awards-detail {
  margin-top: 14px;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.85;
  color: var(--ink-70);
  max-width: 52ch;
}

.links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 26px;
}

.link-out {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 0 15px;
  border: 1.2px solid var(--line);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-70);
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}

.link-out:hover {
  border-color: var(--ink);
  color: var(--ink);
  background: var(--surface-out);
  transform: translateY(-1px);
}

.link-out svg {
  transition: transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.link-out:hover svg {
  transform: translate(3px, -3px);
}

.shots {
  display: grid;
  gap: 14px;
  align-content: start;
}

.shots-key {
  grid-template-columns: 1fr;
}

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

.frame {
  overflow: hidden;
  border-radius: 14px;
  border: 1.4px solid var(--edge);
  background: var(--surface-2);
}

.frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.02) brightness(var(--photo-b));
  transition: transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1), filter 0.8s ease;
}

.frame:hover img {
  transform: scale(1.035);
  filter: grayscale(0) contrast(1.02) brightness(var(--photo-b-hi));
}

@media (prefers-reduced-motion: reduce) {
  .frame img {
    transition: none;
  }
}

.frame-key img {
  aspect-ratio: 16 / 10;
}

.frame-sq img {
  aspect-ratio: 3 / 2;
}

.frame-tall img {
  aspect-ratio: 9 / 19;
  object-position: top;
}

/* Ambient video, after ui.unlumen.com/components/video-ambient: a low
       resolution canvas samples the playing frame, then CSS scales and blurs it
       behind the player so the glow is the footage's own colour, live. The
       canvas is 64x36 because it is only ever seen through a 60px blur. */
.ambient {
  position: relative;
  margin-top: 14px;
}

.ambient-glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  transform: scale(1.06) translateZ(0);
  filter: blur(60px) saturate(1.5);
  opacity: var(--ambient-o);
  pointer-events: none;
  border-radius: 16px;
}

.ambient video {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: auto;
  border-radius: 14px;
  background: #0B0B0D;
}

@media (prefers-reduced-motion: reduce) {
  .ambient-glow {
    display: none;
  }
}

/* ---------- tiles ---------- */
.tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 28px);
  margin-top: 44px;
}

.tile {
  display: flex;
  flex-direction: column;
  gap: 0;
  border: 1.4px solid var(--edge);
  border-radius: 16px;
  padding: 12px 12px 18px;
  background: var(--surface);
  transition: transform 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), background 0.28s ease, box-shadow 0.28s ease;
}

.tile:hover {
  transform: translateY(-5px);
  background: var(--surface-hi);
  box-shadow: var(--lift-tile);
  border-color: var(--edge-hover);
}

.tile-img {
  overflow: hidden;
  border-radius: 9px;
  background: var(--backer);
}

.tile-img img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  filter: grayscale(1) contrast(1.02) brightness(var(--photo-b));
  transition: transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1), filter 0.8s ease;
}

.tile:hover .tile-img img {
  transform: scale(1.04);
  filter: grayscale(0) contrast(1.02) brightness(var(--photo-b-hi));
}

@media (prefers-reduced-motion: reduce) {
  .tile-img img {
    transition: none;
  }
}

.tile-body {
  padding: 16px 6px 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.tile-name {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 16px;
  font-weight: 600;
}

.tile-plat {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink-70);
}

.tile-date {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-50);
}

.tile-note {
  margin-top: 8px;
  font-size: 14px;
  color: var(--ink-70);
  line-height: 1.5;
}

/* Tile ambient: the cover art stays the default state, and the clip fades
       in over it while the tile is hovered, lit by the same canvas glow the
       hero players use. Nothing downloads until the pointer arrives. */
.tile {
  position: relative;
}

.tile-glow {
  position: absolute;
  inset: 4px;
  z-index: 0;
  width: calc(100% - 8px);
  height: calc(100% - 8px);
  border-radius: 13px;
  filter: blur(42px) saturate(1.5);
  opacity: 0;
  transition: opacity 0.5s ease;
  pointer-events: none;
}

.tile.playing .tile-glow {
  opacity: var(--tile-glow-o);
}

/* Footage with a bright, saturated palette throws too much colour at
       this size, so those tiles get a dimmer, wider glow. */
.tile.playing .tile-glow-soft {
  opacity: var(--tile-glow-soft-o);
  filter: blur(54px) saturate(1.1);
}

.tile-img,
.tile-body {
  position: relative;
  z-index: 1;
}

.tile-vid {
  position: absolute;
  inset: 0;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.45s ease;
  pointer-events: none;
}

.tile-vid video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.tile.playing .tile-vid {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .tile-vid {
    display: none;
  }
}

.tag {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  border: 1.1px solid var(--edge-strong);
  border-radius: 999px;
  padding: 3px 8px;
  line-height: 1;
}

/* ---------- work history, compact ---------- */
/* Five full-width rows became one card of paired entries: same company
       names and years, about half the height. The metric line appears only
       where a number traces to a source, so three of the five carry none. */
.roles {
  margin-top: 26px;
  border: 1px dashed var(--line);
  border-radius: 16px;
  padding: clamp(20px, 2.6vw, 30px);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(18px, 2.4vw, 28px) clamp(24px, 4vw, 54px);
}

.role-meta {
  display: flex;
  align-items: baseline;
  gap: 12px;
}

.role-co {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-50);
}

.role-yr {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-50);
  margin-left: auto;
}

.role-title {
  margin: 7px 0 0;
  font-size: 15px;
  line-height: 1.46;
  letter-spacing: -0.01em;
}

.role-stat {
  margin: 8px 0 0;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--ink);
}

@media (max-width: 860px) {
  .roles {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- laurels ---------- */
/* One <symbol> mirrored with scaleX, so ten badges cost one path. Strokes
       and fills are currentColor, which is what keeps dark mode free: no new
       token, nothing to add to the four copies of the dark palette. */
.laurels {
  list-style: none;
  margin: 30px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(12px, 1.8vw, 20px);
}

.laurel {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px;
  padding: 10px 0;
  text-align: center;
}

.laurel-sprig {
  width: 14px;
  height: 42px;
  color: var(--ink-50);
  flex: none;
  transition: color 0.24s ease;
}

.laurel-sprig.r {
  transform: scaleX(-1);
}

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

/* Two lines reserved whether the title needs them or not, so every issuer
       line sits on the same baseline across the grid. The clamp is the ceiling:
       a longer award can never push a badge to three lines and break the row. */
/* Two lines reserved so every issuer line shares a baseline, and the title
       centred inside that box so a one-line award does not leave dead space
       above the issuer. The centring needs a real flex parent: -webkit-box-pack
       is ignored by the line-clamp implementation, which is why the inner span
       exists. The clamp on that span is the ceiling, so a longer award can
       never push a badge to three lines. */
.laurel-award {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 2.64em;
  font-size: 12.5px;
  line-height: 1.32;
  letter-spacing: -0.008em;
}

.laurel-award > span {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.laurel-src {
  display: block;
  margin-top: 5px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-50);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 980px) {
  .laurels {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 620px) {
  .laurels {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (prefers-reduced-motion: reduce) {
  .laurel-sprig {
    transition: none;
  }
}

/* ---------- hover preview ---------- */
/* Two references combined: media-between-text from fancycomponents.dev and
       the floating hover preview from pro.reactbits.dev. Inline-between-text
       was built first and rejected on measurement: the phrase wraps across two
       lines and has 15px of clearance above it, so an anchored panel had
       nowhere to go and the inline pill was capped at 88px by the line box. A
       cursor-anchored preview escapes both limits and reflows nothing, so it
       costs no CLS. The accent underline is the affordance; without it nothing
       said there was anything to hover. box-decoration-break keeps it drawn on
       both line fragments. */
/* background-image, not mask-image. A mask clips the whole element, text
       included, which blanked the words entirely. The colour cannot live in the
       data URI because a URI cannot read var(), so the whole URI is the token
       and each theme carries its own stroke. */
.mbt {
  background-image: var(--squiggle);
  background-repeat: repeat-x;
  background-position: 0 0.9em;
  background-size: 0.5em auto;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  animation: squiggle 3.2s linear infinite;
}

/* One tile of travel, so the loop has no seam. Deliberately no hover
       duration change: altering animation-duration mid-flight keeps currentTime
       in ms and re-divides it by the new duration, so progress jumps and it
       reads as the animation resetting under the cursor. */
@keyframes squiggle {
  from {
    background-position: 0 0.9em;
  }

  to {
    background-position: 0.5em 0.9em;
  }
}

.mbt-preview {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 60;
  width: 300px;
  aspect-ratio: 16 / 10;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 26px 60px -22px rgba(11, 11, 13, 0.5);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  transform: rotate(-6deg) scale(0.94) translateY(10px);
  transition: opacity 0.2s ease, transform 0.24s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.mbt-preview.on {
  opacity: 1;
  transform: rotate(-3deg) scale(1) translateY(0);
}

.mbt-preview video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (hover: none) {
  .mbt-preview {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mbt {
    animation: none;
  }

  .mbt-preview {
    display: none;
  }
}

/* ---------- explorations ---------- */
.note-card {
  margin-top: clamp(48px, 6vw, 76px);
  border: 1px dashed var(--line);
  border-radius: 16px;
  padding: clamp(22px, 3vw, 34px);
  display: grid;
  grid-template-columns: minmax(0, 0.4fr) minmax(0, 1fr);
  gap: clamp(18px, 3vw, 42px);
}

.note-card h4 {
  font-size: 22px;
  letter-spacing: -0.025em;
}

/* ---------- about ---------- */
.about {
  display: grid;
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr);
  gap: clamp(28px, 4vw, 68px);
  margin-top: 48px;
  align-items: start;
}

.portrait-wrap {
  position: relative;
}

.portrait {
  width: 100%;
  max-width: 300px;
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  border: 1.4px solid var(--edge-strong);
}

.portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  filter: grayscale(1) brightness(var(--photo-b));
}

.about-body p + p {
  margin-top: 18px;
}

.about-body {
  font-size: 17px;
  color: var(--ink-70);
  max-width: 62ch;
}

.about-body strong {
  color: var(--ink);
  font-weight: 600;
}

.facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px 34px;
  margin-top: 34px;
}

.fact-k {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-50);
}

.fact-v {
  font-family: var(--mono);
  font-size: 13.5px;
  margin-top: 6px;
  line-height: 1.5;
}

.timeline {
  list-style: none;
  margin: 40px 0 0;
  padding: 0;
  border-top: 1px solid var(--line-soft);
}

.timeline li {
  display: grid;
  grid-template-columns: 82px minmax(0, 1fr);
  gap: 18px;
  align-items: baseline;
  padding: 15px 0;
  border-bottom: 1px solid var(--line-soft);
  font-family: var(--mono);
  font-size: 13.5px;
}

.timeline .yr {
  color: var(--ink-50);
  letter-spacing: 0.08em;
}

.earlier {
  margin-top: 26px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-50);
  line-height: 1.8;
}

/* ---------- honours ---------- */
.honours {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(20px, 2.6vw, 34px) clamp(16px, 2vw, 28px);
  margin-top: 52px;
}

.honour {
  position: relative;
  padding-bottom: 20px;
}

.honour-img {
  overflow: hidden;
  border-radius: 13px;
  border: 1.4px solid var(--edge);
  background: var(--backer);
  transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1), box-shadow 0.3s ease;
}

.honour-img img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  filter: grayscale(1) contrast(1.03) brightness(var(--photo-b));
  transition: filter 0.7s ease;
}

.honour:hover .honour-img {
  transform: translateY(-4px);
  box-shadow: var(--lift-honour);
  border-color: var(--edge-hover);
}

.honour:hover .honour-img img {
  filter: grayscale(0.35) contrast(1.03) saturate(1.15) brightness(var(--photo-b-hi));
}

@media (prefers-reduced-motion: reduce) {
  .honour-img img {
    transition: none;
  }
}

.honour-cap {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  background: var(--backer);
  border: 1.2px solid var(--edge-strong);
  border-radius: 999px;
  padding: 6px 16px;
  font-family: var(--mono);
  font-size: 11.5px;
  white-space: nowrap;
}

/* ---------- press ---------- */
.press {
  list-style: none;
  margin: 48px 0 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

.press > li {
  border-bottom: 1px solid var(--line-soft);
}

.press-row {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr) 92px 28px;
  gap: 20px;
  align-items: center;
  padding: 20px 10px;
  min-height: 64px;
  border-radius: 12px;
  transition: background 0.24s ease;
}

a.press-row:hover {
  background: var(--surface-row);
}

.press-src {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-50);
}

.press-title {
  font-size: 17px;
  letter-spacing: -0.012em;
}

.press-yr {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-50);
  text-align: right;
}

.press-arrow {
  display: grid;
  place-items: center;
  color: var(--ink-50);
  transition: transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1), color 0.24s ease;
}

a.press-row:hover .press-arrow {
  transform: translate(4px, -4px);
  color: var(--ink);
}

.covered {
  margin-top: 30px;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px 18px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-70);
}

.covered b {
  color: var(--ink-50);
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-size: 11px;
}

/* ---------- contact ---------- */
.contact {
  position: relative;
  overflow: hidden;
  padding: clamp(84px, 13vh, 150px) 0 0;
}

.contact-in {
  position: relative;
  z-index: 1;
}

.contact h3 {
  font-size: clamp(36px, 5.4vw, 74px);
  line-height: 1.0;
  letter-spacing: -0.04em;
  max-width: 16ch;
}

.contact-links {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 38px;
}

.foot {
  margin-top: clamp(64px, 9vh, 110px);
  padding: 22px 0 30px;
  border-top: 1px solid var(--line-soft);
  display: flex;
  flex-wrap: wrap;
  gap: 10px 24px;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink-50);
  letter-spacing: 0.04em;
}

/* ---------- responsive ---------- */
@media (max-width: 1000px) {
  .project {
    grid-template-columns: minmax(0, 1fr);
  }

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

  .portrait {
    max-width: 190px;
  }

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

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

  .press-row {
    grid-template-columns: 150px minmax(0, 1fr) 76px 24px;
    gap: 14px;
  }
}

@media (max-width: 720px) {
  body {
    font-size: 16px;
  }

  .nav-links {
    display: none;
  }

  .nav-in {
    height: 58px;
  }

  .pill-cta {
    min-height: 44px;
    padding: 0 18px;
  }

  .ghost {
    font-size: 300px;
    top: 52%;
  }

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

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

  .shots-3 {
    display: flex;
    gap: 10px;
    overflow-x: auto;
    padding-bottom: 4px;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
  }

  .shots-3 > .frame {
    flex: 0 0 60%;
    scroll-snap-align: start;
  }

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

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

  .press-row {
    grid-template-columns: minmax(0, 1fr) 26px;
    gap: 6px 14px;
    padding: 18px 8px;
    align-items: start;
  }

  .press-src {
    grid-column: 1;
    order: 1;
  }

  .press-title {
    grid-column: 1;
    order: 3;
  }

  .press-yr {
    grid-column: 1;
    order: 4;
    text-align: left;
  }

  .press-arrow {
    grid-column: 2;
    grid-row: 1 / span 3;
    order: 2;
    align-self: center;
  }

  .timeline li {
    grid-template-columns: 64px minmax(0, 1fr);
    gap: 12px;
    font-size: 13px;
  }

  .btn,
  .link-out {
    min-height: 46px;
  }

  .label-note {
    margin-left: 0;
    width: 100%;
  }
}

/* ---------- dark ----------
       A darkroom, not an inverted sheet of paper.

       The ground keeps the light theme's cool blue cast instead of going
       neutral charcoal, and every foreground is tinted from that same hue
       rather than grayed. The three text tokens land within half a point of
       the light theme's own contrast ratios (ink-70 8.9:1 against 9.4:1,
       ink-50 5.4:1 against 5.4:1), so the hierarchy reads at the same weight
       rather than merely passing.

       Three things cannot be swapped and are rebuilt:
       1. The grain multiplies to nothing on a dark ground, so it screens.
       2. The shadow lift is invisible, so elevation becomes edge-light:
          the surface, the border and a top highlight brighten together.
       3. The blobs and the ambient video glow stop being pigment on paper
          and become light, which is far louder, so their opacity drops.
       Photographs pull back to 86 percent brightness; grayscale stills that
       sit correctly on paper blaze against near-black.

       THE TOKEN LIST BELOW APPEARS TWICE AND THE TWO COPIES MUST MATCH.
       The media query serves visitors who have expressed no preference, and
       covers the no-JS path (the reveal system already degrades to visible).
       The attribute rule serves an explicit choice made with the toggle,
       which is why it has to beat the media query rather than live inside
       it. Keep both in sync when a token changes. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0B0E13;
    --ink: #E9EDF3;
    --squiggle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='9' viewBox='0 0 24 9'%3E%3Cpath d='M0 6 Q 6 1.6 12 6 T 24 6' fill='none' stroke='%23FF8F70' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E");
    --ink-70: #A7B1BF;
    --ink-50: #7E8896;
    --line: rgba(233, 237, 243, 0.20);
    --line-soft: rgba(233, 237, 243, 0.11);
    --mint: #6FE3A8;
    --mint-0: rgba(111, 227, 168, 0);
    --dot: rgba(233, 237, 243, 0.075);
    --ghost: rgba(233, 237, 243, 0.05);
    --surface: rgba(233, 237, 243, 0.035);
    --surface-2: rgba(233, 237, 243, 0.04);
    --surface-hi: rgba(233, 237, 243, 0.075);
    --surface-row: rgba(233, 237, 243, 0.055);
    --surface-out: rgba(233, 237, 243, 0.08);
    --backer: #14181F;
    --edge: rgba(233, 237, 243, 0.20);
    --edge-hover: rgba(233, 237, 243, 0.34);
    --edge-strong: rgba(233, 237, 243, 0.44);
    --btn-solid-bg: var(--accent);
    --nav-bg: rgba(11, 14, 19, 0.72);
    --grain-blend: screen;
    --grain-o: 0.05;
    --photo-b: 0.72;
    --photo-b-hi: 0.95;
    /* A dim yellow is olive: any low-intensity yellow over near-black
           lands around rgb(88,92,32), army green rather than lamplight.
           Lowering opacity cannot fix that because the problem is hue, not
           amount, so the blobs get a higher-lightness, lower-chroma yellow
           for dark and let the CTA carry the saturated brand accent. */
    --glow: #F7F0AE;
    --blob-a-o: 0.34;
    --blob-b-o: 0.22;
    --blob-c-o: 0.26;
    --blob-contact-o: 0.3;
    --ambient-o: 0.3;
    --tile-glow-o: 0.28;
    --tile-glow-soft-o: 0.12;
    --lift-tile: inset 0 1px 0 rgba(233, 237, 243, 0.10);
    --lift-honour: inset 0 1px 0 rgba(233, 237, 243, 0.10);
    --lift-chip: none;
  }
}

:root[data-theme="dark"] {
  --bg: #0B0E13;
  --ink: #E9EDF3;
  --squiggle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='9' viewBox='0 0 24 9'%3E%3Cpath d='M0 6 Q 6 1.6 12 6 T 24 6' fill='none' stroke='%23FF8F70' stroke-width='2.6' stroke-linecap='round'/%3E%3C/svg%3E");
  --ink-70: #A7B1BF;
  --ink-50: #7E8896;
  --line: rgba(233, 237, 243, 0.20);
  --line-soft: rgba(233, 237, 243, 0.11);
  --mint: #6FE3A8;
  --mint-0: rgba(111, 227, 168, 0);
  --dot: rgba(233, 237, 243, 0.075);
  --ghost: rgba(233, 237, 243, 0.05);
  --surface: rgba(233, 237, 243, 0.035);
  --surface-2: rgba(233, 237, 243, 0.04);
  --surface-hi: rgba(233, 237, 243, 0.075);
  --surface-row: rgba(233, 237, 243, 0.055);
  --surface-out: rgba(233, 237, 243, 0.08);
  --backer: #14181F;
  --edge: rgba(233, 237, 243, 0.20);
  --edge-hover: rgba(233, 237, 243, 0.34);
  --edge-strong: rgba(233, 237, 243, 0.44);
  --btn-solid-bg: var(--accent);
  --nav-bg: rgba(11, 14, 19, 0.72);
  --grain-blend: screen;
  --grain-o: 0.05;
  --photo-b: 0.72;
  --photo-b-hi: 0.95;
  /* A dim yellow is olive: any low-intensity yellow over near-black
           lands around rgb(88,92,32), army green rather than lamplight.
           Lowering opacity cannot fix that because the problem is hue, not
           amount, so the blobs get a higher-lightness, lower-chroma yellow
           for dark and let the CTA carry the saturated brand accent. */
  --glow: #F7F0AE;
  --blob-a-o: 0.34;
  --blob-b-o: 0.22;
  --blob-c-o: 0.26;
  --blob-contact-o: 0.3;
  --ambient-o: 0.3;
  --tile-glow-o: 0.28;
  --tile-glow-soft-o: 0.12;
  --lift-tile: inset 0 1px 0 rgba(233, 237, 243, 0.10);
  --lift-honour: inset 0 1px 0 rgba(233, 237, 243, 0.10);
  --lift-chip: none;
}

:root[data-theme="dark"] {
  color-scheme: dark;
}

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

.tile-img img,
.honour-img img {
  height: auto
}
