@font-face {
  font-family: "Schibsted Grotesk";
  src: url("fonts/schibsted-grotesk.woff2") format("woff2");
  font-weight: 400 900;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("fonts/jetbrains-mono.woff2") format("woff2");
  font-weight: 100 800;
  font-display: swap;
}

:root {
  color-scheme: light only;
  --bg: #ffffff;
  --soft: #f4f4f4;
  --ink: #000000;
  --muted: #6b6b6b;
  --line: #e2e2e2;
  --university: #6b4bd6;
  --school: #1f6fd1;
  --personal: #13896a;
  --client: #c7731e;
  /* Darker shades of the same hues for text, so small text meets AA contrast (4.5:1). */
  --personal-text: #117d60;
  --client-text: #a15d18;
  --font: "Schibsted Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* --c: category colour for lines and dots; --c-text: for text. */
[data-category="university"] { --c: var(--university); --c-text: var(--university); }
[data-category="school"] { --c: var(--school); --c-text: var(--school); }
[data-category="personal"] { --c: var(--personal); --c-text: var(--personal-text); }
[data-category="client"] { --c: var(--client); --c-text: var(--client-text); }

/* --n: colour of the next project on the timeline, which the line blends into. */
[data-next="university"] { --n: var(--university); }
[data-next="school"] { --n: var(--school); }
[data-next="personal"] { --n: var(--personal); }
[data-next="client"] { --n: var(--client); }

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

html {
  background: var(--bg);
  scroll-padding-top: env(safe-area-inset-top, 0px);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

a { color: inherit; }

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

.wrap {
  max-width: 920px;
  margin: 0 auto;
  padding: 0 20px;
}

.skip-link {
  position: absolute;
  top: -60px;
  left: 16px;
  padding: 8px 14px;
  background: var(--ink);
  color: var(--bg);
  font-weight: 600;
  z-index: 1;
}
.skip-link:focus { top: 16px; }

/* Safari on iPhone draws the page under the status bar too, and fills that zone with the colour of
   what is fixed to the top of the window, if it is about as wide as the window and more than 10px
   tall. This box is there only to be found: it has the page colour, but its padding leaves nothing
   to paint, so the status bar always has the page colour behind it instead of the page. */
.top-fill {
  position: fixed;
  inset: 0 0 auto;
  z-index: 3;
  height: 12px;
  padding-top: 12px;
  background: var(--bg) content-box;
  pointer-events: none;
}

/* Header */

.site-header { padding: 64px 0 28px; }

h1 {
  margin: 0 0 18px;
  font-size: clamp(44px, 8vw, 84px);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.035em;
}

.bio {
  max-width: 58ch;
  margin: 0 0 22px;
  color: var(--muted);
  font-size: 19px;
}

.contacts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  font-weight: 500;
}
.contacts a { text-decoration-thickness: 1px; text-underline-offset: 4px; }
/* inline-block keeps the underline off the arrow */
.ext { display: inline-block; margin-left: 3px; color: var(--muted); font-size: 0.85em; }

/* Category filter: one at a time */

.lanes {
  display: none;
  flex-wrap: wrap;
  gap: 8px;
  margin: 36px 0 28px;
}
/* Shown only with JavaScript (see the inline script in <head>). */
.js .lanes { display: flex; }

.lane-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px 6px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.lane-btn.all { padding-left: 14px; }
.lane-btn:hover { color: var(--ink); }
.lane-btn[aria-pressed="true"] { border-color: var(--ink); color: var(--ink); }
.lane-btn:disabled { opacity: 0.45; cursor: default; }
.lane-btn:disabled:hover { color: var(--muted); }

.lane-btn .sw {
  width: 12px;
  height: 12px;
  border: 2px solid var(--c);
  border-radius: 50%;
}
.lane-btn[aria-pressed="true"] .sw { background: var(--c); }

.empty { margin: 36px 0; color: var(--muted); }

/* Timeline: one line with a node per project */

.timeline { margin: 0; padding: 0; list-style: none; }

.row {
  display: grid;
  grid-template-columns: 56px 44px minmax(0, 1fr);
  position: relative;
}
.row[hidden] { display: none; }

.year {
  padding-top: 22px;
  color: var(--muted);
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* --x, --y: centre of the node inside the row. */
.graph {
  --x: 14px;
  --y: 34px;
  position: relative;
}

/* The line runs from this project's node to the next one's. It keeps the project's colour for at
   least the first half and blends into the next colour over the last stretch, so an open project
   does not wash it out. */
.line {
  position: absolute;
  top: var(--y);
  bottom: calc(-1 * var(--y));
  left: calc(var(--x) - 1px);
  width: 2px;
  background: linear-gradient(var(--c) max(50%, calc(100% - 120px)), var(--n) calc(100% - 8px));
  background: linear-gradient(in oklab, var(--c) max(50%, calc(100% - 120px)), var(--n) calc(100% - 8px));
}
/* After the last project the line runs down to the end of its card (26px: the card's bottom
   padding), however tall the project is, and fades out over the last stretch. */
.row:not([data-next]) .line {
  bottom: 26px;
  background: linear-gradient(var(--c) max(35%, calc(100% - 120px)), transparent);
}

.node {
  position: absolute;
  top: calc(var(--y) - 8px);
  left: calc(var(--x) - 8px);
  width: 16px;
  height: 16px;
  border: 4px solid var(--c);
  border-radius: 50%;
  background: var(--bg);
}

.card { padding: 14px 0 26px; }

.card-head {
  display: block;
  list-style: none;
  border-radius: 10px;
  cursor: pointer;
}
.card-head::-webkit-details-marker { display: none; }
.card-head:focus-visible { outline-offset: 4px; }

.meta {
  display: block;
  margin: 0 0 2px;
  color: var(--muted);
  font-size: 14px;
}
.meta b { color: var(--c-text); font-weight: 600; }

h2 {
  margin: 0 0 6px;
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.pitch { display: block; max-width: 62ch; }

.badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.badge {
  padding: 2px 9px;
  border: 1px solid var(--line);
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
}

.toggle {
  display: inline-block;
  margin-top: 12px;
  color: var(--c-text);
  font-size: 14px;
  font-weight: 600;
}
.toggle .when-open, .card[open] .toggle .when-closed { display: none; }
.card[open] .toggle .when-open { display: inline; }

/* A compact copy of the head (category, status, title, "Close"), added by main.js, which stays at
   the top of the window while the open project scrolls by. It lies behind the head, which covers
   it together with the gap down to the detail (::before, 30px: the margin of .detail). That gap is
   what lets the head's own "Close" leave the window before the one of the bar comes out, so the
   two are never seen together. */
.card[open] > .card-head { position: relative; z-index: 2; }
.card[open] > .card-head::before {
  content: "";
  position: absolute;
  inset: 0 0 -30px;
  z-index: -1;
  background: var(--bg);
}
/* Like .top-fill, the bar fills the zone under the status bar in Safari on iPhone, where the open
   project would show through above it: for that its box reaches both sides of the window
   (--bar-left, --bar-right: from the card to the sides, up to a 960px window), while only the part
   over the card is painted and takes clicks. */
.row { --bar-left: 120px; --bar-right: 20px; }
.card-bar {
  position: sticky;
  top: 0;
  z-index: 1;
  height: 54px;
  margin: -54px calc(-1 * var(--bar-right)) 0 calc(-1 * var(--bar-left));
  padding: 0 var(--bar-right) 0 var(--bar-left);
  background: var(--bg) content-box;
  pointer-events: none;
}
.card-bar button {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-content: center;
  column-gap: 16px;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  pointer-events: auto;
}
.card-bar button > .meta, .card-bar button > b {
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.card-bar button > b {
  grid-column: 1;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.015em;
}
.bar-close {
  grid-area: 1 / 2 / 3;
  align-self: center;
  color: var(--c-text);
  font-size: 14px;
  font-weight: 600;
}

/* Project detail */

.detail {
  margin-top: 30px;
  padding: 22px 24px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--c);
}

.cover-link { display: block; margin: 0 0 20px; }
.cover {
  display: block;
  width: 100%;
  height: auto;
}

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px 24px;
  margin: 0 0 20px;
}
.facts dt { color: var(--muted); font-size: 13px; }
.facts dd { margin: 0; font-weight: 600; }

.detail h3 {
  margin: 22px 0 6px;
  font-size: 16px;
  font-weight: 700;
}
.detail h4, .detail h5, .detail h6 { margin: 16px 0 4px; font-size: 15px; font-weight: 700; }
.detail p, .detail li { max-width: 66ch; }
.detail ul, .detail ol { margin: 6px 0; padding-left: 20px; }
.detail code { font-family: var(--mono); font-size: 0.875em; }
.detail pre {
  margin: 0;
  padding: 12px;
  overflow-x: auto;
  background: var(--soft);
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.5;
}
.detail pre code { font-size: inherit; }

.detail .stack {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 0;
  list-style: none;
}
.stack li {
  padding: 3px 9px;
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 13px;
}

.note {
  padding: 10px 14px;
  background: var(--soft);
  font-size: 15px;
}

.io {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: 10px;
  align-items: stretch;
  margin-top: 8px;
}
.io figure { margin: 0; min-width: 0; }
.io figcaption { margin-bottom: 4px; color: var(--muted); font-size: 13px; }
.io .arrow { align-self: center; color: var(--c-text); font-size: 22px; }

.shots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
  align-items: start;
  margin-top: 8px;
}
.shot { display: block; }
.shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* Lightbox, built by main.js: the cover and the screenshots of a project as a full-screen carousel.
   The track scrolls sideways with scroll snapping, one image per slide. */

.js .cover-link, .js .shot { cursor: zoom-in; }
html:has(.lightbox[open]) { overflow: hidden; }

.lightbox {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: max(16px, env(safe-area-inset-top, 0px)) max(16px, env(safe-area-inset-right, 0px)) max(16px, env(safe-area-inset-bottom, 0px)) max(16px, env(safe-area-inset-left, 0px));
  border: 0;
  background: var(--ink);
  color: var(--bg);
}
.lightbox[open] {
  display: grid;
  grid-template:
    "count count close" auto
    "prev track next" minmax(0, 1fr)
    / 44px minmax(0, 1fr) 44px;
  gap: 12px 16px;
  align-items: center;
}
.lightbox :focus-visible { outline-color: var(--bg); }

.lightbox button {
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--muted);
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
.lightbox button:hover { border-color: var(--bg); }
.lightbox svg {
  display: block;
  margin: auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.lb-count { grid-area: count; font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
.lb-close { grid-area: close; }
.lb-prev { grid-area: prev; }
.lb-next { grid-area: next; }

.lb-track {
  grid-area: track;
  align-self: stretch;
  display: flex;
  overflow-x: auto;
  overscroll-behavior: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  touch-action: pan-x pinch-zoom;
}
.lb-track::-webkit-scrollbar { display: none; }
.lb-slide {
  flex: 0 0 100%;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 12px;
  place-items: center;
  min-width: 0;
  margin: 0;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}
.lb-slide img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.lb-slide figcaption {
  max-width: 70ch;
  font-size: 15px;
  text-align: center;
}

.links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 22px;
}
.detail .links:first-child, .facts + .links { margin-top: 0; margin-bottom: 20px; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--ink);
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
}
.btn.primary { border-color: var(--ink); background: var(--ink); color: var(--bg); }
.btn[aria-disabled="true"] { opacity: 0.55; pointer-events: none; }

.detail .press { padding: 0; list-style: none; }
.press li { padding: 6px 0; border-bottom: 1px solid var(--line); }
.press li:last-child { border-bottom: 0; }
.press a { font-weight: 600; text-underline-offset: 3px; }
.press small { display: block; color: var(--muted); font-size: 14px; }
.lang { color: var(--muted); font-size: 13px; font-weight: 400; }

/* Lower sections */

section.block { padding: 56px 0 8px; }
section.block h2 { font-size: 32px; letter-spacing: -0.025em; }
.block .lead { margin: 0 0 20px; color: var(--muted); }

.rlist {
  margin: 0;
  padding: 0;
  border-top: 2px solid var(--ink);
  list-style: none;
}
.rlist li {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr);
  gap: 4px 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.rlist .d {
  padding-top: 2px;
  color: var(--muted);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}
.rlist a { font-weight: 600; text-underline-offset: 3px; }
.rlist .p { display: block; color: var(--muted); font-size: 14px; }
.rlist .p b { color: var(--c-text); font-weight: 600; }

.cv h3 { margin: 0; font-size: 18px; font-weight: 700; line-height: 1.3; }
.cv .where { display: block; margin: 2px 0 6px; color: var(--muted); font-size: 15px; }
.cv .where a { font-weight: inherit; text-decoration-thickness: 1px; }
.cv p { max-width: 62ch; margin: 0; font-size: 16px; }
.cv .tag {
  display: inline-block;
  margin-top: 8px;
  padding: 1px 8px;
  border: 1px solid var(--c);
  color: var(--c-text);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
}

.cta {
  margin: 72px 0 64px;
  padding: 56px 0;
  border-top: 2px solid var(--ink);
}
.cta h2 {
  margin: 0 0 16px;
  font-size: clamp(34px, 6vw, 60px);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
}
.cta p {
  max-width: 52ch;
  margin: 0 0 28px;
  color: var(--muted);
  font-size: 19px;
}
.cta .row-btn { display: flex; flex-wrap: wrap; gap: 10px; }
.cta .btn { padding: 12px 20px; font-size: 17px; }

@media (max-width: 640px) {
  body { font-size: 16px; }
  .site-header { padding-top: 40px; }
  /* The year moves above the card, next to the node. */
  .row { grid-template-columns: 32px minmax(0, 1fr); --bar-left: 52px; }
  .row .graph { --x: 8px; --y: 28px; grid-row: 1 / span 2; grid-column: 1; }
  .row .year { grid-row: 1; grid-column: 2; padding-top: 16px; }
  .row .card { grid-row: 2; grid-column: 2; padding-top: 4px; }
  .detail { padding: 18px 16px; }
  .io { grid-template-columns: minmax(0, 1fr); }
  .io .arrow { justify-self: center; transform: rotate(90deg); }
  .rlist li { grid-template-columns: minmax(0, 1fr); }
  /* The images take the full width; the arrows move below them. */
  .lightbox[open] {
    grid-template:
      "count count close" auto
      "track track track" minmax(0, 1fr)
      "prev . next" auto
      / 44px minmax(0, 1fr) 44px;
    gap: 12px;
  }
  .lb-slide figcaption { font-size: 14px; }
}

@media screen and (prefers-reduced-motion: no-preference) {
  .card[open] > .detail { animation: open 0.22s ease-out; }
  @keyframes open {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: none; }
  }

  .lightbox[open] { animation: fade 0.18s ease-out; }
  @keyframes fade {
    from { opacity: 0; }
  }

  /* Scroll animation, switched on by main.js: the line is drawn as the page scrolls (down to
     --drawn, with a soft tip; the rest stays faint under a veil) and each node lights up, with a
     ring spreading out once, when the line reaches it. */
  .anim .line { overflow: hidden; }
  .anim .line::after {
    content: "";
    position: absolute;
    inset: calc(var(--drawn, 0px) - 56px) 0 0;
    background: linear-gradient(transparent, var(--bg) 56px);
    opacity: 0.8;
  }
  .anim .node { transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.3s; }
  .anim .row:not(.reached) .node {
    border-color: var(--line);
    border-color: color-mix(in oklab, var(--c) 20%, var(--bg));
    transform: scale(0.7);
  }
  .anim .reached .node::after {
    content: "";
    position: absolute;
    inset: -4px;
    border: 2px solid var(--c);
    border-radius: 50%;
    opacity: 0;
    animation: ring 0.7s ease-out;
  }
  @keyframes ring {
    from { opacity: 0.6; transform: none; }
    to { opacity: 0; transform: scale(2.6); }
  }
}

/* Print: the page as a CV, without what only works on screen. Colours are kept, so the timeline
   line and the filled buttons are printed too. */
@media print {
  body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .skip-link, .contacts a[target="_blank"], .js .lanes, .toggle, .card-bar, .cta { display: none; }
  .site-header { padding-top: 0; }
  h2, h3 { break-after: avoid; }
  .card-head, .rlist li { break-inside: avoid; }
}
