:root {
  /* Fluid scale: floor for phones, vw in the middle, ceiling for wide screens. */
  --gutter: clamp(1rem, 4vw, 3rem);
  --size-title: clamp(1.75rem, 6.25vw, 4rem);
  --size-body: clamp(0.875rem, 0.65rem + 0.45vw, 1rem);
}

/* Otherwise iOS enlarges the text by its own measure when a phone is turned
   on its side, and the fluid scale above stops being the one in charge. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
}

.michroma-regular {
  font-family: "Michroma", sans-serif;
  font-weight: 400;
  font-style: normal;
}

.title {
  display: block;
  font-family: "Michroma", sans-serif;
  font-weight: 400;
  font-style: normal;
  font-size: var(--size-title);
  line-height: 1.1;
  margin: var(--gutter);
  /* It closes the open section, so it is a button; it should still read as the
     plain name of the site. */
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.menu {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1rem, 3vw, 2.5rem);
  font-family: "Roboto Mono", monospace;
  font-weight: 400;
  font-style: normal;
  font-size: var(--size-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: var(--gutter);
}

.menu-item {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  text-decoration: none;
  background: none;
  border: 0;
  /* A fingertip-sized target around small type, given back by the margin so
     nothing moves. Sideways it stays under half the gap, so neighbours never
     overlap. */
  padding: 1em 0.5em;
  margin: -1em -0.5em;
  cursor: pointer;
}

.menu-item.is-active {
  text-decoration: underline;
  text-underline-offset: 0.35em;
}

/* Sections are toggled through the hidden attribute; this beats the display
   rules the individual sections set for themselves. */
.section[hidden] {
  display: none;
}

.section a {
  color: inherit;
  text-underline-offset: 0.3em;
}

.bio-container {
  font-family: "Roboto Mono", monospace;
  font-weight: 400;
  font-style: normal;
  font-size: var(--size-body);
  line-height: 1.6;
  max-width: 60ch;
  margin: var(--gutter);
}

.projects-container {
  margin: calc(var(--gutter) * 2) var(--gutter);
}

/* The list is what the Projects section is. The carousel is layered over it at
   runtime and only if it can run, so these rules are also the fallback. */
.projects-list {
  display: flex;
  flex-direction: column;
  gap: clamp(3rem, 8vw, 6rem);
}

.project {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--gutter);
  align-items: start;
}

.project-image {
  display: block;
  width: 100%;
  height: auto;
  max-height: 32rem;
  object-fit: contain;
  object-position: left top;
}

.project-image--empty {
  aspect-ratio: 3 / 2;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid currentColor;
  font-family: "Roboto Mono", monospace;
  font-size: var(--size-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.4;
}

.project-text {
  max-width: 60ch;
}

.project-title {
  font-family: "Michroma", sans-serif;
  font-weight: 400;
  font-size: clamp(1.125rem, 0.9rem + 1vw, 1.75rem);
  line-height: 1.2;
  margin: 0 0 0.75em;
}

.project-meta {
  font-family: "Roboto Mono", monospace;
  font-size: var(--size-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 0 0 1.5em;
  opacity: 0.6;
}

.project-meta:has(+ .project-credits) {
  margin-bottom: 0.75em;
}

.project-credits {
  font-family: "Roboto Mono", monospace;
  font-size: var(--size-body);
  line-height: 1.6;
  margin: 1.5em 0 1.5em;
  opacity: 0.6;
}

.project-description {
  font-family: "Roboto Mono", monospace;
  font-size: var(--size-body);
  line-height: 1.6;
  margin: 0;
}

.project-description + .project-description {
  margin-top: 1em;
}

.project-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5em 1.5em;
  font-family: "Roboto Mono", monospace;
  font-size: var(--size-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  margin: 1.5em 0 0;
  color: rgb(57 122 255);
}

.project-links-label {
  opacity: 0.6;
}

/* --- the projects carousel ------------------------------------------------
   Everything below waits on .carousel-ready, which js/carousel/index.js adds
   only once a WebGL context exists and the first image is on the atlas. Until
   then — and again if the context is ever lost — none of it applies and the
   section is the plain list above. */

.goo-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.carousel-strip {
  display: none;
  /* Out past the gutter: the cards run off both edges, which is what says
     there are more of them than the ones on screen. */
  margin: 0 calc(var(--gutter) * -1) calc(var(--gutter) * 1.5);
  /* Width, not height. The cards are sized by whichever axis is tighter, and on
     a tall narrow window that is the width — so a height in vh alone reserves a
     band of empty canvas above and below them. */
  height: clamp(18rem, 44vw, 34rem);
  /* Press and hold is a gesture here — it softens the goo — so it must not
     also select the words nearby, raise the iOS callout, or flash the whole
     canvas grey the way a tapped clickable element does on iOS. */
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.carousel-ready .carousel-strip {
  display: block;
}

.carousel-strip canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* Horizontal swipes drive the carousel, vertical ones still scroll the page
     and two fingers still zoom it. Upstream can take the whole gesture because
     nothing else on its page scrolls; this strip sits in a page that does. */
  touch-action: pan-y pinch-zoom;
}

.carousel-strip:focus-visible {
  outline: 2px solid rgb(57 122 255);
  outline-offset: 4px;
}

/* Only the centred project's words are shown, stacked on the rest so the
   panel does not resize as they cross over. Not on a phone; see below. */
.carousel-ready .projects-list {
  display: grid;
  gap: 0;
}

.carousel-ready .project {
  grid-area: 1 / 1;
  grid-template-columns: minmax(0, 1fr);
  opacity: 0;
  /* visibility, not opacity alone: a transparent link is still tabbable, and
     ten invisible projects' worth of links in the tab order is a trap. */
  visibility: hidden;
  transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}

.carousel-ready .project.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.45s ease;
}

/* The pictures are in the atlas now. */
.carousel-ready .project-image {
  display: none;
}

/* The title is drawn by the morph below, but the heading itself stays in the
   accessibility tree — the morph is aria-hidden, and losing the <h2> would
   leave the active project as a run of unlabelled paragraphs. Only the active
   article is visible, so only its heading is exposed. */
.carousel-ready .project-title {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Two stacked copies of the title, blurred past each other through the
   alpha threshold in the page's <defs>. Grid rather than absolute positioning
   so the box still sizes itself to whichever title is longer. */
.carousel-title {
  display: none;
  font-family: "Michroma", sans-serif;
  font-weight: 400;
  font-size: clamp(1.125rem, 0.9rem + 1vw, 1.75rem);
  line-height: 1.2;
  margin: 0 0 0.75em;
}

.carousel-ready .carousel-title {
  display: block;
}

.carousel-title-goo {
  display: grid;
  will-change: filter;
}

.carousel-title-layer {
  grid-area: 1 / 1;
}

/* The morph is decoration; this is what is actually announced. */
.carousel-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.calendar-container {
  font-family: "Roboto Mono", monospace;
  font-size: var(--size-body);
  line-height: 1.6;
  margin: calc(var(--gutter) * 2) var(--gutter);
}

.calendar-heading {
  font-family: "Michroma", sans-serif;
  font-weight: 400;
  font-size: clamp(1.125rem, 0.9rem + 1vw, 1.75rem);
  line-height: 1.2;
  margin: 0 0 1.5em;
}

.calendar-heading + * {
  margin-top: 0;
}

.calendar-heading:not(:first-child) {
  margin-top: 3em;
}

.calendar-list {
  display: grid;
  grid-template-columns: 8em minmax(0, 1fr);
  gap: 0.25em 2em;
  margin: 0;
}

.calendar-list dt {
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.6;
}

/* Years group several entries: only the first row of a group sits next to its
   year, the rest continue in the second column. */
.calendar-list dd {
  grid-column: 2;
  margin: 0;
}

.calendar-list dt + dd {
  grid-row: auto;
}

.calendar-list dt:not(:first-of-type) {
  margin-top: 1.5em;
}

.calendar-list dt:not(:first-of-type) + dd {
  margin-top: 1.5em;
}

.portfolio-download {
  margin: 3em 0 0;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.bio-text {
  margin: 0;
}

.link-list {
  display: flex;
  flex-direction: column;
  gap: 0.75em;
  list-style: none;
  padding: 0;
  margin: 3em 0 0;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgb(57 122 255);
}

@media (max-width: 40rem) {
  .project {
    grid-template-columns: minmax(0, 1fr);
  }

  /* A fixed height rather than a share of the width. minScale holds every
     phone to the same card, so the strip that gives that card its reference
     share of the height (anchorFill in params.js) is the same on all of them. */
  .carousel-strip {
    height: 22.5rem;
  }

  /* Stacked, the panel is as tall as the longest project, and on a phone that
     is screens of blank page under every shorter one. Here only the active
     project takes up room; the one fading out is lifted out of flow, over the
     top of it, so the two still cross. Hidden or not, what is out of flow
     still stretches the page, so the list clips it — top and bottom only, and
     with a little room below for the last link's focus ring. */
  .carousel-ready .projects-list {
    position: relative;
    overflow-y: clip;
    padding-bottom: 0.5em;
  }

  .carousel-ready .project:not(.is-active) {
    position: absolute;
    inset: 0 0 auto;
  }

  .calendar-list {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.25em;
  }

  .calendar-list dd {
    grid-column: 1;
  }

  /* In one column a year's first entry sits under it rather than beside it,
     so the year's space above is already between them. */
  .calendar-list dt:not(:first-of-type) + dd {
    margin-top: 0;
  }
}

/* A phone on its side. Whatever height the strip was given, it may not fill
   the screen, and the cards shrink with it (anchorFill in params.js). */
@media (max-height: 30rem) {
  .carousel-strip {
    max-height: 75svh;
  }
}

/* The carousel does not build itself at all under this, so this only catches
   the panel's own crossfade. */
@media (prefers-reduced-motion: reduce) {
  .carousel-ready .project {
    transition: none;
  }
}
