@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&family=IBM+Plex+Mono:wght@400;500&family=Manrope:wght@400;500;600&display=swap');

:root {
  color-scheme: dark;
  --ink: #f2f1ec;
  --muted: #9c9b94;
  --line: #343431;
  --paper: #181817;
  --surface: #222220;
  --acid: #dfff9a;
  --gutter: clamp(20px, 3vw, 48px);
  /* Global media corner rounding; the admin overrides this on <html>. */
  --media-radius: 14px;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }
html.book-turning { overflow-anchor: none; scroll-behavior: auto; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: Manrope, sans-serif;
  font-size: 16px;
  line-height: 1.45;
}

[data-selection-disabled='true'] { -webkit-user-select: none; cursor: default; user-select: none; -webkit-user-drag: none; }
body.portfolio-editor-canvas [data-selection-disabled='true'] { -webkit-user-select: text !important; cursor: text !important; user-select: text !important; }

a { color: inherit; }
img, video { display: block; max-width: 100%; }

.dot-grid-host { isolation: isolate; }
.dot-grid__canvas { height: 100%; inset: 0; pointer-events: none; position: absolute; width: 100%; z-index: 0; }
.dot-grid-host > :not(.dot-grid__canvas) { position: relative; z-index: 1; }

.portfolio { min-height: 100vh; }

/*
 * No dividing rules anywhere on the page.
 *
 * Every block used to close with a 1px --line hairline. Over a continuous dot
 * field each one reads as a seam -- the background stops being one surface and
 * becomes a stack of panels. Spacing separates the sections; a stroke is not
 * needed to say where one ends.
 */
.site-header {
  align-items: center;
  display: flex;
  justify-content: space-between;
  min-height: 72px;
  padding: 0 var(--gutter);
}

.wordmark {
  font-family: "Archivo Black", sans-serif;
  font-size: 15px;
  text-decoration: none;
}

.site-header nav { display: flex; gap: clamp(18px, 3vw, 42px); }
.site-header nav a, .site-footer a {
  font-family: "IBM Plex Mono", monospace;
  font-size: 12px;
  text-decoration: none;
}
.site-header nav a:hover, .site-footer a:hover { color: var(--acid); }

.intro {
  align-items: end;
  /* No hairline under the hero: the dot field runs straight through into the
     block below, and a 1px --line rule across it reads as a seam. */
  display: grid;
  gap: 40px;
  grid-template-columns: minmax(0, 2.2fr) minmax(240px, 0.8fr);
  min-height: min(68vh, 760px);
  padding: clamp(80px, 12vw, 180px) var(--gutter) 48px;
}

.intro h1 {
  font-family: "Archivo Black", sans-serif;
  font-size: clamp(58px, 11vw, 168px);
  letter-spacing: -0.065em;
  line-height: 0.82;
  margin: 0;
  text-transform: uppercase;
}

.intro p {
  color: var(--muted);
  font-size: clamp(16px, 1.5vw, 22px);
  margin: 0;
  max-width: 34ch;
}

.project-grid {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.project-stream { background: #111; display: grid; gap: 10px; overflow: hidden; padding: 10px; }
.project-stream.is-loading { min-height: clamp(372px, 46vw, 670px); }
.project-stream__placeholder { align-items: center; color: #aaa; display: flex; grid-column: 1 / -1; justify-content: center; }
.project-stream__lane { min-width: 0; overflow: hidden; }
.project-stream__lane--right { transform: scaleX(-1); }
.project-stream__track { display: flex; gap: 10px; width: max-content; will-change: transform; }
.project-stream__tile { border-radius: var(--media-radius); display: block; flex: 0 0 clamp(220px, 27vw, 430px); height: clamp(180px, 23vw, 330px); isolation: isolate; overflow: hidden; position: relative; }
.project-stream__lane--right .project-stream__tile { transform: scaleX(-1); }
.project-stream__media { border-radius: inherit; display: block; height: 100%; object-fit: cover; position: relative; width: 100%; z-index: 1; }
.project-stream__media { border-radius: inherit; }
.project-stream__track > img.project-stream__tile { display: block; object-fit: cover; width: auto; }
.project-stream__backdrop { background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(24px) saturate(1.02) brightness(0.9); inset: -28px; opacity: 0.94; pointer-events: none; position: absolute; transform: scale(1.08); z-index: 0; }
.project-stream__backdrop::after { background: linear-gradient(135deg, rgb(10 14 20 / 0.12), rgb(10 14 20 / 0.32)); content: ''; inset: 0; position: absolute; }
.project-stream__video-play { align-items: center; background: rgb(16 16 15 / 0.72); border-radius: 50%; color: #fff; display: flex; font-size: 24px; height: 64px; justify-content: center; left: 50%; padding-left: 4px; pointer-events: none; position: absolute; top: 50%; transform: translate3d(-50%, -50%, 0); width: 64px; will-change: transform; z-index: 2; }
.project-stream[data-stream-fit="contain"] .project-stream__media { object-fit: contain; }
.project-stream__tile--wide { flex-basis: clamp(360px, 48vw, 760px); }
.project-stream__tile--tall { flex-basis: clamp(140px, 17vw, 260px); }

/* Grid heading.  A real <h2>, not generated content: the dot-grid effect
   builds its mask by walking text nodes, and ::before content has none, so a
   CSS-only title was treated as background and got dotted over. */
.portfolio-project-grid__title {
  color: var(--ink);
  font-size: clamp(20px, 2.4vw, 36px);
  font-weight: 500;
  grid-column: 1 / -1;
  letter-spacing: -0.01em;
  margin: 0;
  /* Equal space above and below, controlled from the grid panel. */
  padding: var(--project-grid-title-space, 32px) var(--gutter);
  text-align: center;
}

.portfolio-project-grid {
  --project-grid-columns: 4;
  --project-grid-gap: 1px;
  --project-grid-width: 100%;
  --project-grid-title-space: 32px;
  display: grid;
  gap: var(--project-grid-gap);
  grid-template-columns: repeat(var(--project-grid-columns), minmax(0, 1fr));
}

/* Width is opt-in: a grid with no data-full-width attribute keeps the original
   full-bleed layout, so existing pages are untouched.  Unticking "Full width"
   in the editor writes "false" and constrains it like a smart gallery. */
.portfolio-project-grid[data-full-width="false"] {
  margin-inline: auto;
  width: min(var(--project-grid-width), calc(100% - (var(--gutter) * 2)));
}

.portfolio-editor-canvas .portfolio-project-grid {
  min-height: 180px;
  outline: 1px dashed #77776f;
  outline-offset: -1px;
}

/* Editor-only hints: never persisted or shown on the published site. */
.portfolio-editor-canvas .portfolio-project-card__title:empty::before {
  color: rgb(255 255 255 / 0.86);
  content: 'Project title';
}

.portfolio-editor-canvas .portfolio-project-card__meta:empty::before {
  color: rgb(255 255 255 / 0.68);
  content: 'Details';
}

.portfolio-project-card {
  border-radius: var(--media-radius);
  /* The water-hover effect is a WebGL <canvas>, which Chrome composites on its
     own layer -- and a composited child is NOT reliably clipped by an
     ancestor's overflow:hidden + border-radius, so the effect painted square
     corners over the rounded card.  clip-path clips composited layers too. */
  clip-path: inset(0 round var(--media-radius));
  aspect-ratio: 1;
  background: var(--surface);
  color: #fff;
  display: block;
  min-width: 0;
  overflow: hidden;
  position: relative;
  text-decoration: none;
}

/* Catch-all: anything dropped inside a card at runtime -- the WebGL hover
   canvas, the overlay, an effect added later -- is full-bleed and must follow
   the card's corners.  Composited layers ignore the parent's clip, so each
   child carries its own. */
.portfolio-project-card > * {
  clip-path: inset(0 round var(--media-radius));
}

.portfolio-project-card__image,
.portfolio-project-card > .placeholder-media {
  border-radius: inherit;
  height: 100%;
  inset: 0;
  object-fit: cover;
  position: absolute;
  width: 100%;
  z-index: 0;
}

.portfolio-water-hover-canvas {
  border-radius: inherit;
  /* Clipping the card is not enough: this canvas is composited on its own
     layer, so it has to carry the rounded clip itself. */
  clip-path: inset(0 round var(--media-radius));
  height: 100% !important;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  position: absolute;
  transition: opacity 80ms linear;
  width: 100% !important;
  z-index: 1;
}

.portfolio-project-card__overlay {
  border-radius: inherit;
  align-items: center;
  background: rgba(8, 8, 8, 0.62);
  display: flex;
  flex-direction: column;
  inset: 0;
  isolation: isolate;
  justify-content: center;
  opacity: 0;
  padding: clamp(18px, 3vw, 40px);
  position: absolute;
  text-align: center;
  transition: opacity 220ms ease;
  z-index: 2;
}

.portfolio-project-card:hover .portfolio-project-card__overlay,
.portfolio-project-card:focus-visible .portfolio-project-card__overlay { opacity: 1; }

.portfolio-project-card:focus-visible { outline: 2px solid var(--acid); outline-offset: -2px; }
.portfolio-project-card__title { font-size: clamp(18px, 2vw, 30px); font-weight: 500; line-height: 1.08; margin: 0; position: relative; z-index: 1; }
.portfolio-project-card__meta { font-family: "IBM Plex Mono", monospace; font-size: 10px; letter-spacing: 0.08em; margin: 8px 0 0; position: relative; text-transform: uppercase; z-index: 1; }

.project { background: var(--paper); min-width: 0; }
.project--wide { grid-column: 1 / -1; }

.project-media {
  aspect-ratio: 4 / 3;
  background: var(--surface);
  overflow: hidden;
}

.project--wide .project-media { aspect-ratio: 16 / 7; }
.project-media img, .project-media video { height: 100%; object-fit: cover; width: 100%; }
.placeholder-media {
  background-color: #2b2b28;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 42px 42px;
}

.project-meta {
  align-items: baseline;
  display: flex;
  justify-content: space-between;
  padding: 18px var(--gutter) 34px;
}

.project-meta h2 { font-size: 20px; font-weight: 600; margin: 0; }
.project-meta p {
  color: var(--muted);
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  margin: 0;
}

.content-section {
  padding: clamp(64px, 9vw, 140px) var(--gutter);
}
.content-section h2 { font-size: clamp(36px, 6vw, 92px); line-height: 0.95; margin: 0; }
.content-section p { color: var(--muted); font-size: 18px; max-width: 60ch; }
.text-split-section {
  display: grid;
  gap: clamp(28px, 7vw, 120px);
  grid-template-columns: minmax(0, 0.9fr) minmax(280px, 1.1fr);
  padding: clamp(64px, 9vw, 140px) var(--gutter);
}
.text-split-section h2, .text-statement h2 { font-size: clamp(38px, 6vw, 96px); line-height: 0.95; margin: 0; }
.text-split-section p, .text-statement p { color: var(--muted); font-size: clamp(17px, 1.5vw, 22px); line-height: 1.45; margin: 0; max-width: 42ch; }
.text-statement { padding: clamp(64px, 9vw, 140px) var(--gutter); }
.text-statement p { margin-top: 22px; }
.text-caption { color: var(--muted); font-family: "IBM Plex Mono", monospace; font-size: 13px; padding: 22px var(--gutter); }
.full-media img, .full-media video { max-height: 92vh; object-fit: cover; width: 100%; }

.smart-gallery {
  --gallery-columns: 2;
  --gallery-gap: 16px;
  --gallery-width: 100%;
  margin: var(--gallery-gap) auto;
  width: min(var(--gallery-width), calc(100% - (var(--gutter) * 2)));
}

.smart-gallery[data-full-width="true"] { margin-inline: 0; width: 100%; }

.image-slider { overflow: visible; position: relative; touch-action: pan-y; user-select: none; }
.image-slider img { -webkit-user-drag: none; }
.image-slider .gallery-item { display: none; margin: 0; }
.image-slider .gallery-item:first-child { display: block; }
.image-slider.is-ready .gallery-item { display: none; }
.image-slider.is-ready .gallery-item.is-active { display: block; }
.image-slider .gallery-item img { height: auto; max-height: 80vh; object-fit: contain; }
.image-slider__hit-area { background: transparent; border: 0; bottom: 0; cursor: pointer; padding: 0; position: absolute; top: 0; width: 22%; z-index: 2; }
.image-slider.has-video .image-slider__hit-area { bottom: 64px; }
.image-slider__hit-area--previous { left: 0; }
.image-slider__hit-area--next { right: 0; }
.image-slider__dots { align-items: center; display: flex; gap: 8px; justify-content: center; padding-top: 14px; }
.image-slider__dots button { background: #c9c9c4; border: 0; border-radius: 99px; cursor: pointer; height: 7px; padding: 0; transition: transform 160ms ease, background-color 160ms ease; width: 7px; }
.image-slider__dots button.is-active { background: #171716; transform: scale(1.3); }

:is(.book-flip, .book[data-book-mode="flip"]) {
  aspect-ratio: var(--book-flip-ratio, 1.414);
  overflow-anchor: none;
  overflow: visible;
  perspective: 1800px;
  position: relative;
  touch-action: pan-y;
  user-select: none;
}
:is(.book-flip, .book[data-book-mode="flip"]) img { -webkit-user-drag: none; }
:is(.book-flip, .book[data-book-mode="flip"]) .gallery-item { backface-visibility: hidden; display: none; height: 100%; margin: 0; transform-origin: left center; will-change: transform; }
:is(.book-flip, .book[data-book-mode="flip"]) .gallery-item:first-child { display: block; }
:is(.book-flip, .book[data-book-mode="flip"]).is-ready .gallery-item { display: none; }
:is(.book-flip, .book[data-book-mode="flip"]).is-ready .gallery-item.is-active { display: block; }
:is(.book-flip, .book[data-book-mode="flip"]).is-turning .gallery-item.is-leaving {
  animation: book-flip-forward var(--book-flip-duration, 560ms) ease-in-out both;
  display: block;
  inset: 0;
  position: absolute;
  transform-origin: left center;
  z-index: 2;
}
:is(.book-flip, .book[data-book-mode="flip"]).is-turning.is-turning-back .gallery-item.is-leaving {
  animation-name: book-flip-backward;
  transform-origin: right center;
}
:is(.book-flip, .book[data-book-mode="flip"])[data-flip-speed="fast"] { --book-flip-duration: 360ms; }
:is(.book-flip, .book[data-book-mode="flip"])[data-flip-speed="slow"] { --book-flip-duration: 820ms; }
:is(.book-flip, .book[data-book-mode="flip"]) .gallery-item :is(img, video) { height: 100%; max-height: none; object-fit: contain; width: 100%; }
/* Same reach for the single-page book. */
:is(.book-flip, .book[data-book-mode="flip"]) .image-slider__hit-area {
  --book-reach: min(400px, max(0px, (100vw - 20px - 100%) / 2));
  background: transparent;
  border: 0;
  bottom: 0;
  cursor: pointer;
  padding: 0;
  position: absolute;
  top: 0;
  width: calc(24% + var(--book-reach));
  z-index: 3;
}
:is(.book-flip, .book[data-book-mode="flip"]) .image-slider__hit-area--previous { left: calc(-1 * var(--book-reach)); }
:is(.book-flip, .book[data-book-mode="flip"]) .image-slider__hit-area--next { right: calc(-1 * var(--book-reach)); }

/* A page mid-turn is rotated in 3D; perspective magnifies it far past the
   book's own box, and Firefox folds that into the document's scrollable area,
   so the scrollbar thumb jumped on every flip. The turn happens in this layer
   instead: position:fixed boxes are excluded from the document's scrollable
   overflow, so the animation paints unclipped and the page height never moves.
   The layer is pinned over the book's box with its perspective, so the
   animation is geometrically identical to running it in place. */
.book-flying-layer {
  perspective: 1800px;
  pointer-events: none;
  position: fixed;
  z-index: 40;
}
.book-flying-layer > .gallery-item {
  backface-visibility: hidden;
  display: block;
  inset: 0;
  margin: 0;
  position: absolute;
  transform-origin: left center;
  will-change: transform;
}
.book-flying-layer > .gallery-item.is-leaving {
  animation: book-flip-forward var(--book-flip-duration, 560ms) ease-in-out both;
}
.book-flying-layer.is-turning-back > .gallery-item.is-leaving {
  animation-name: book-flip-backward;
  transform-origin: right center;
}
.book-flying-layer > .gallery-item :is(img, video) { height: 100%; max-height: none; object-fit: contain; width: 100%; }

@keyframes book-flip-forward {
  from { opacity: 1; transform: rotateY(0deg); }
  to { opacity: 0; transform: rotateY(-180deg); }
}

@keyframes book-flip-backward {
  from { opacity: 1; transform: rotateY(0deg); }
  to { opacity: 0; transform: rotateY(180deg); }
}

@media (prefers-reduced-motion: reduce) {
  :is(.book-flip, .book[data-book-mode="flip"]).is-turning .gallery-item.is-leaving { animation: none; }
}

:is(.book-preview, .book[data-book-mode="preview"]) {
  overflow-anchor: none;
  overflow: visible;
  position: relative;
  touch-action: pan-y;
  user-select: none;
}
.book-preview > .gallery-item, .book[data-book-mode="preview"] > .gallery-item { display: inline-block; margin: 0; vertical-align: top; width: 50%; }
.book-preview > .gallery-item img, .book-preview > .gallery-item video, .book[data-book-mode="preview"] > .gallery-item img, .book[data-book-mode="preview"] > .gallery-item video { display: block; height: auto; max-height: none; object-fit: contain; width: 100%; }
.book-preview.is-ready > .gallery-item, .book[data-book-mode="preview"].is-ready > .gallery-item { display: none; }
.book-preview__stage {
  aspect-ratio: var(--book-preview-ratio, 2);
  background: #e7e4dd;
  box-shadow: 0 12px 24px rgb(0 0 0 / 0.18);
  overflow: visible;
  perspective: 1800px;
  position: relative;
  width: 100%;
}
.book-preview__spread { display: grid; grid-template-columns: 1fr 1fr; height: 100%; inset: 0; position: absolute; }
.book-preview__stage:is(.is-single-page, .is-turning-to-single) { background: transparent; box-shadow: none; }
.book-preview__spread.is-single-page { bottom: 0; grid-template-columns: 1fr; left: 0; right: auto; top: 0; width: 50%; }
.book-preview__spread.is-single-page .book-preview__page { box-shadow: none; }
.book-preview__page {
  background: #f6f4ee;
  min-width: 0;
  overflow: hidden;
  position: relative;
  /* Corners change when a spread becomes a single sheet and back again. Easing
     the change stops the last page snapping between square and round. */
  transition: border-radius 220ms ease;
}
.book-preview__page--left { box-shadow: none; }
.book-preview__page--right { box-shadow: none; }
.book-preview__page.is-blank { background: #f1eee6; }
/*
 * The gutter shadow.
 *
 * Two sheets meeting at a fold are never a hard line in a real book -- the
 * paper curves into the spine and darkens, and the turning page's edge has
 * somewhere to disappear into. Dial --book-gutter-shadow to 0 to remove it.
 *
 * It is painted twice: once in the book, and once in the layer the turning
 * page flies in. The page is composited above everything in the book, so a
 * gutter that lived only in the book was covered by it for the whole turn --
 * which is why it looked like it faded out as a page came across.
 */
.book-preview__stage::after,
.book-flying-layer.has-gutter::after {
  /* Falls off like light does: a narrow dark core at the fold, then a long
     faint tail. Even steps read as a painted band rather than a shadow. */
  background: linear-gradient(to right,
    rgb(0 0 0 / 0) 0%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.02)) 18%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.07)) 30%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.18)) 39%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.45)) 45%,
    rgb(0 0 0 / var(--book-gutter-shadow, 0.24)) 50%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.45)) 55%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.18)) 61%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.07)) 70%,
    rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 0.02)) 82%,
    rgb(0 0 0 / 0) 100%);
  bottom: 0;
  content: '';
  left: calc(50% - 96px);
  pointer-events: none;
  position: absolute;
  top: 0;
  width: 192px;
}
.book-preview__stage::after { z-index: 2; }
/* Above the turning sheet, which is z-index 3 inside this layer. */
.book-flying-layer.has-gutter::after { z-index: 4; }
/* Exactly one gutter at a time. While a page is turning the layer draws it, so
   the book's own must step aside -- two of them stacked read as a darker fold
   that lightens again the moment the turn ends. */
.book-preview__stage:is(.is-single-page, .is-turning, .is-turning-to-single)::after { display: none; }
/* Spine setting: none, shadow (the gradient above, the default), or stroke --
   a single drawn line instead of a fade. */
:is(.book-preview__stage, .book-flying-layer)[data-book-spine="none"]::after { display: none; }
:is(.book-preview__stage, .book-flying-layer)[data-book-spine="stroke"]::after {
  background: rgb(0 0 0 / calc(var(--book-gutter-shadow, 0.24) * 1.6));
  left: calc(50% - 1px);
  width: 2px;
}
.book-preview__page.is-transparent { background: transparent; }
/*
 * cover, not contain.
 *
 * The stage's aspect ratio is taken from the media, so a page fits its image
 * exactly and cover crops nothing.  What contain did was letterbox the
 * sub-pixel remainder, and the page's cream background showed through it -- a
 * white hairline down the gutter that appeared and moved as the turning leaf's
 * box rounded differently from the page beneath it.  With cover there is no
 * remainder for the background to show through.
 */
.book-preview__page > img, .book-preview__page > video { display: block; height: 100%; object-fit: cover; width: 100%; }
.book-preview__leaf {
  bottom: 0;
  display: block;
  height: 100%;
  position: absolute;
  top: 0;
  transform-style: preserve-3d;
  width: 50%;
  will-change: transform;
  z-index: 3;
}
.book-preview__leaf-face { backface-visibility: hidden; bottom: 0; height: 100%; inset: 0; position: absolute; top: 0; width: 100%; }
/*
 * No translateZ on the faces.
 *
 * They used to be pushed 1px toward the camera, which under perspective
 * magnifies them slightly -- and as the leaf rotates, that magnification
 * changes, so the hinge edge drifts across a pixel boundary and back. That is
 * the stroke at the spine that moves and flickers through the whole turn.
 * Measured over a turn: with the 1px offset the seam sat at -1, 0, 0, 0, -1...;
 * without it, -1 on every frame. backface-visibility keeps the two faces from
 * fighting, so the offset was buying nothing.
 */
.book-preview__leaf-face--front { transform: rotateY(0deg); }
.book-preview__leaf-face--back { transform: rotateY(180deg); }
/*
 * The turn animation reads its length from the block's Flip speed setting,
 * which the runtime writes onto the stage.  The 900ms fallback is what the
 * two-page book always used.
 */
.book-preview__leaf--forward {
  animation: book-preview-turn-forward var(--book-preview-turn, 900ms) cubic-bezier(.645, .045, .355, 1) both;
  left: 50%;
  transform-origin: left center;
}
.book-preview__leaf--backward {
  animation: book-preview-turn-backward var(--book-preview-turn, 900ms) cubic-bezier(.645, .045, .355, 1) both;
  left: 0;
  transform-origin: right center;
}
:is(.book-flip, .book[data-book-mode="flip"]) > .image-slider__dots:not(.book__dots),
:is(.book-preview, .book[data-book-mode="preview"]) > .image-slider__dots:not(.book__dots) { display: none; }
:is(.book-flip, .book-preview, .book) > .book__dots:not([data-book-runtime-dots]),
:is(.book-flip, .book-preview, .book)[data-slider-dots="false"] .image-slider__dots { display: none !important; }
/* A Book has exactly one navigation row. The runtime appends its row last;
   stale editor/runtime rows must never become a second visible row. */
:is(.book-flip, .book-preview, .book) > .image-slider__dots:not([data-book-runtime-dots]),
:is(.book-flip, .book-preview, .book) > .image-slider__dots[data-book-runtime-dots]:not(:last-of-type) { display: none !important; }
/*
 * Tap targets that reach past the book.
 *
 * An 18% strip inside each edge was not obvious enough -- readers could not
 * tell where to tap. These now claim the empty margin beside the book as well,
 * up to 400px, or the whole margin when it is narrower. --book-reach is that
 * limit; the 20px keeps them clear of a scrollbar so the page never gains a
 * sideways scroll.
 */
.book-preview__hit-area {
  --book-reach: min(400px, max(0px, (100vw - 20px - 100%) / 2));
  background: transparent;
  border: 0;
  bottom: 32px;
  cursor: pointer;
  padding: 0;
  position: absolute;
  top: 0;
  width: calc(18% + var(--book-reach));
  z-index: 4;
}
.book-preview__hit-area--previous { left: calc(-1 * var(--book-reach)); }
.book-preview__hit-area--next { right: calc(-1 * var(--book-reach)); }
:is(.book-preview, .book[data-book-mode="preview"]) img { -webkit-user-drag: none; cursor: default; }

/* Two-page book: only the outer edges round, so the halves still meet flush at
   the spine. A turning leaf rounds the edge away from its hinge -- the hinge is
   the spine for the whole turn. Its back face carries rotateY(180deg), which
   mirrors it, so that face's corners are named the other way round -- get this
   wrong and the outer edge goes square while the spine rounds. A solo last page
   is a single sheet and rounds all the way round. */
.book-preview__stage { border-radius: var(--media-radius); }
.book-preview__page--left { border-radius: var(--media-radius) 0 0 var(--media-radius); }
.book-preview__page--right { border-radius: 0 var(--media-radius) var(--media-radius) 0; }
.book-preview__spread.is-single-page .book-preview__page { border-radius: var(--media-radius); }
/*
 * Turning onto the solo last page, the page left behind ends up a single sheet
 * and needs its outer corners rounded -- but not before the turning page has
 * passed the middle.  Rounding it the moment the turn starts pulls the corners
 * in while the spread is still a spread, which reads as the gutter breaking.
 * The runtime adds this class at the halfway point.
 */
.book-preview__stage.is-past-middle .book-preview__page--left { border-radius: var(--media-radius); }
.book-preview__leaf.is-to-single .book-preview__leaf-face--back,
.book-preview__leaf.is-from-single .book-preview__leaf-face--front {
  border-radius: var(--media-radius);
}
/*
 * Both faces reach one pixel past the hinge.
 *
 * The gap is at the hinge, and BOTH faces need to cover it -- the back face
 * too, which is the one showing for the second half of the turn.  Its own 180
 * degree rotation mirrors its picture, but its box still starts at the leaf's
 * hinge edge, so it overhangs on the same side as the front face.  Getting
 * that backwards is why the stroke survived the first attempt: it was fixed
 * for the first half of the turn and left open for the second.
 */
.book-preview__leaf--forward .book-preview__leaf-face {
  left: -1px;
  right: 0;
  width: auto;
}
.book-preview__leaf--backward .book-preview__leaf-face {
  left: 0;
  right: -1px;
  width: auto;
}
.book-preview__leaf--forward .book-preview__leaf-face--front,
.book-preview__leaf--backward .book-preview__leaf-face--back {
  border-radius: 0 var(--media-radius) var(--media-radius) 0;
}
.book-preview__leaf--forward .book-preview__leaf-face--back,
.book-preview__leaf--backward .book-preview__leaf-face--front {
  border-radius: var(--media-radius) 0 0 var(--media-radius);
}

/*
 * The page is hidden while it is edge-on.
 *
 * Around the halfway point the sheet is nearly perpendicular to the screen, so
 * it projects to a column a pixel or two wide -- its own picture squeezed into
 * a hairline at the gutter, which appears and flickers as it passes through.
 * A page seen exactly edge-on has nothing to show anyway, so it is taken out
 * for the few hundredths of the turn where that is true.
 */
@keyframes book-preview-turn-forward {
  from { transform: rotateY(0deg); visibility: visible; }
  47% { visibility: visible; }
  47.01% { visibility: hidden; }
  53% { visibility: hidden; }
  53.01% { visibility: visible; }
  to { transform: rotateY(-180deg); visibility: visible; }
}

@keyframes book-preview-turn-backward {
  from { transform: rotateY(0deg); visibility: visible; }
  47% { visibility: visible; }
  47.01% { visibility: hidden; }
  53% { visibility: hidden; }
  53.01% { visibility: visible; }
  to { transform: rotateY(180deg); visibility: visible; }
}

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

.smart-gallery[data-layout="grid"] {
  display: grid;
  gap: var(--gallery-gap);
  grid-template-columns: repeat(var(--gallery-columns), minmax(0, 1fr));
}

.smart-gallery[data-layout="grid"] .gallery-item { aspect-ratio: 4 / 3; }

.smart-gallery[data-layout="masonry"] {
  column-count: var(--gallery-columns);
  column-gap: var(--gallery-gap);
}

/* Asymmetric / bento box: tiles are absolutely positioned by
   bento-gallery.js, which sizes each one to its image's true aspect ratio --
   nothing is cropped.  The rules below are the shell plus a sane pre-script
   fallback for when the layout has not run yet. */
.smart-gallery[data-layout="bento"] {
  --gallery-row-height: 230px;
  --gallery-row-variety: 0.55;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gallery-gap);
  position: relative;
}

.smart-gallery[data-layout="bento"] .gallery-item {
  flex: 1 1 220px;
  height: var(--gallery-row-height);
}

.smart-gallery[data-layout="bento"].is-bento { display: block; }

.smart-gallery[data-layout="bento"].is-bento .gallery-item {
  flex: none;
  left: 0;
  position: absolute;
  top: 0;
}

.smart-gallery[data-layout="bento"] .gallery-item img,
.smart-gallery[data-layout="bento"] .gallery-item video {
  height: 100%;
  object-fit: cover;
  width: 100%;
}

/* The two-page book stays square: rounding the pages would break the spread
   where the two halves meet. The one-page book is a single sheet, so it
   follows --media-radius like every other medium -- in the flying layer too,
   which is where a page lives while it turns. */
.book[data-book-mode="preview"] .gallery-item, .book-preview .gallery-item,
.book[data-book-mode="preview"] .gallery-item :is(img, video),
.book-preview .gallery-item :is(img, video) {
  border-radius: 0;
}
:is(.book-flip, .book[data-book-mode="flip"]) .gallery-item :is(img, video),
.book-flying-layer > .gallery-item :is(img, video) {
  border-radius: inherit;
}

.gallery-item {
  border-radius: var(--media-radius);
  background: var(--surface);
  margin: 0;
  min-width: 0;
  overflow: hidden;
}
.gallery-video { position: relative; }
.gallery-video__play {
  align-items: center;
  background: rgb(16 16 15 / 0.72);
  border: 0;
  border-radius: 50%;
  color: #fff;
  cursor: pointer;
  display: flex;
  font-size: 28px;
  height: 74px;
  justify-content: center;
  left: 50%;
  padding-left: 5px;
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 74px;
}

.smart-gallery[data-layout="masonry"] .gallery-item {
  break-inside: avoid;
  margin-bottom: var(--gallery-gap);
}

.gallery-item img, .gallery-item video {
  border-radius: inherit;
  display: block;
  height: 100%;
  object-fit: cover;
  width: 100%;
}

.smart-gallery[data-layout="masonry"] .gallery-item img, .smart-gallery[data-layout="masonry"] .gallery-item video { height: auto; }

/* Packed by bento-gallery.js into the shortest column each time, because
   column-count fills one column at a time and leaves the last one short.
   Heights still come from each image's own ratio, so object-fit has nothing to
   crop -- it is there to absorb the sub-pixel rounding. */
.smart-gallery[data-layout="masonry"].is-packed {
  column-count: initial;
  display: block;
  position: relative;
}
.smart-gallery[data-layout="masonry"].is-packed .gallery-item {
  left: 0;
  margin: 0;
  position: absolute;
  top: 0;
}
.smart-gallery[data-layout="masonry"].is-packed .gallery-item img,
.smart-gallery[data-layout="masonry"].is-packed .gallery-item video {
  height: 100%;
  object-fit: cover;
  width: 100%;
}

.gallery-placeholder {
  align-items: center;
  aspect-ratio: 4 / 3;
  background-color: #2b2b28;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 42px 42px;
  color: var(--muted);
  display: flex;
  font-family: "IBM Plex Mono", monospace;
  justify-content: center;
}

.site-footer {
  align-items: end;
  display: grid;
  gap: 40px;
  grid-template-columns: 2fr 1fr;
  min-height: 40vh;
  padding: 64px var(--gutter) 32px;
}
.site-footer p { font-size: clamp(28px, 4vw, 64px); line-height: 1.05; margin: 0; max-width: 22ch; }

.smart-gallery img { cursor: zoom-in; }
.portfolio-lightbox {
  align-items: center;
  background: rgb(12 12 11 / 0.9);
  display: flex;
  inset: 0;
  justify-content: center;
  padding: clamp(20px, 5vw, 64px);
  position: fixed;
  z-index: 1000;
}
.portfolio-lightbox[hidden] { display: none; }
.portfolio-lightbox__image { max-height: 100%; max-width: 100%; object-fit: contain; }

/*
 * The small-screen safety nets below hold ONLY until you tune a block yourself.
 *
 * They exist because a hero or a grid carries the pixel values it was given on
 * desktop, which run off the side of a phone -- and they need !important,
 * because the editor writes id rules that would otherwise win. The cost was
 * that nothing could be adjusted on a phone at all: the handle wrote a height
 * and `height: auto !important` ate it. The editor now marks a block
 * data-mobile-tuned the first time you change it on a phone or tablet, and the
 * net lets go of that block only.
 */
@media (max-width: 1100px) {
  .portfolio-project-grid:not([data-mobile-tuned]) { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}

@media (max-width: 760px) {
  .site-header { align-items: flex-start; padding-block: 20px; }
  .site-header nav { gap: 10px; flex-direction: column; text-align: right; }
  .intro { grid-template-columns: 1fr; min-height: 70vh; }
  .text-split-section { grid-template-columns: 1fr; }
  .project-grid:not([data-mobile-tuned]), .portfolio-project-grid:not([data-mobile-tuned]) { grid-template-columns: 1fr !important; }
  .project-stream { gap: 6px; padding: 6px; }
  .project-stream__track { gap: 6px; }
  .project--wide { grid-column: auto; }
  .project--wide .project-media { aspect-ratio: 4 / 3; }
  .project-meta { align-items: flex-start; flex-direction: column; gap: 6px; }
  .smart-gallery:not([data-mobile-tuned]) { --gallery-columns: 2 !important; }
  .site-footer { grid-template-columns: 1fr; }
}

/* No pointer to hover with, so the name is shown permanently -- but as a
   caption, not the hover state. Holding the hover overlay open kept its flat
   62% black veil across the whole card, which read as something covering the
   work rather than labelling it. A gradient carries the text instead and
   leaves the image alone. */
@media (hover: none) {
  .portfolio-project-card__overlay {
    align-items: flex-start;
    background: linear-gradient(to top, rgb(8 8 8 / 0.78) 0%, rgb(8 8 8 / 0.38) 26%, rgb(8 8 8 / 0) 56%);
    justify-content: flex-end;
    opacity: 1;
    padding: clamp(14px, 4.5vw, 22px);
    text-align: left;
  }

  .portfolio-project-card__title { font-size: clamp(17px, 4.6vw, 24px); }
}

@media (max-width: 480px) {
  .smart-gallery:not([data-mobile-tuned]) { --gallery-columns: 1 !important; }
}

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

/*
 * Click hint.
 *
 * A book looks like a picture until you try it, so a chevron sits at the edge
 * and nudges every few seconds. It goes for good on the first turn -- once the
 * reader knows, it is noise -- and never appears if the reader has asked for
 * reduced motion. On a touch screen it is three times the size and sits mostly
 * outside the book, where a thumb is not covering it.
 */
.book-hint {
  align-items: center;
  color: #f6f4ee;
  display: flex;
  filter: drop-shadow(0 2px 5px rgb(0 0 0 / 0.5));
  justify-content: center;
  pointer-events: none;
  position: absolute;
  right: clamp(6px, 2%, 26px);
  top: 50%;
  translate: 0 -50%;
  z-index: 5;
}
.book-hint svg {
  animation: book-hint-nudge 3.6s ease-in-out infinite;
  display: block;
  height: 52px;
  width: 52px;
}
@keyframes book-hint-nudge {
  0%, 62%, 100% { opacity: 0.55; transform: translateX(0); }
  70% { opacity: 1; transform: translateX(9px); }
  78% { opacity: 0.8; transform: translateX(0); }
  86% { opacity: 1; transform: translateX(9px); }
  94% { opacity: 0.55; transform: translateX(0); }
}
@media (pointer: coarse), (max-width: 900px) {
  /* The hint sits outside the book, in the margin, so it adds a few pixels to
     the page's width. Nothing on this site scrolls sideways, so clip that
     rather than let the page rubber-band. */
  body { overflow-x: clip; }
  /* Beside the book, never on it: anchored to the book's right edge and pushed
     clear of it, into the margin. */
  .book-hint {
    left: 100%;
    right: auto;
    translate: 0 -50%;
  }
  .book-hint svg { height: 140px; width: 140px; }
}
@media (max-width: 620px) {
  /* A phone has less margin to give it, so it is smaller -- still entirely
     outside the book, and still inside the screen. */
  .book-hint svg { height: 88px; width: 88px; }
}
@media (prefers-reduced-motion: reduce) { .book-hint { display: none; } }

/*
 * The hero on small screens.
 *
 * A hero headline set in the editor carries the sizes it was given there --
 * pixel font size, pixel width, sometimes absolute positioning. Those are
 * desktop numbers, so on a tablet or a phone the words run off the side. Below
 * 900px the block ignores them and sizes itself to the screen, centred, since
 * the headline is usually all there is on it. !important because the editor
 * writes its values as inline id rules, which otherwise win.
 */
@media (max-width: 900px) {
  .intro:not([data-mobile-tuned]) {
    align-items: center !important;
    display: flex !important;
    height: auto !important;
    justify-content: center !important;
    min-height: 62vh !important;
    padding: clamp(72px, 16vw, 130px) clamp(18px, 6vw, 48px) !important;
    text-align: center !important;
  }
  .intro:not([data-mobile-tuned]) h1 {
    display: block !important;
    font-size: clamp(34px, 12.5vw, 96px) !important;
    letter-spacing: -0.04em !important;
    line-height: 1.02 !important;
    margin: 0 !important;
    max-width: 100% !important;
    padding: 0 !important;
    position: static !important;
    text-align: center !important;
    width: 100% !important;
  }
  .intro:not([data-mobile-tuned]) p { margin-inline: auto !important; text-align: center !important; }
}

/*
 * Media boxes are see-through, so the dot grid runs behind them.
 *
 * Every tile, card and stream band used to paint --surface, which is why the
 * dots stopped dead at each block edge and a section read as a stack of grey
 * rectangles. A photo that covers its box hides that fill anyway -- it only
 * ever showed while the image was still loading.
 */
:is(.gallery-item, .portfolio-project-card, .project-media, .project-stream) { background: transparent; }
/*
 * ...except where the fill is doing real work.
 *
 * PNG/SVG/GIF/WEBP can carry alpha, and a logo sitting straight on the dots is
 * unreadable. Matched on the authored src, not on what the browser loaded, so
 * a webp derivative of a JPEG stays see-through.
 */
:is(.gallery-item, .portfolio-project-card, .project-media):has(:is(img, video)[src$=".png" i]),
:is(.gallery-item, .portfolio-project-card, .project-media):has(:is(img, video)[src$=".svg" i]),
:is(.gallery-item, .portfolio-project-card, .project-media):has(:is(img, video)[src$=".gif" i]),
:is(.gallery-item, .portfolio-project-card, .project-media):has(:is(img, video)[src$=".webp" i]) {
  background: var(--surface);
}
/* Contain-fitted media letterboxes, and a book is a sheet of paper. */
:is(.image-slider, .book-flip, .book-preview, .book) .gallery-item { background: var(--surface); }

/*
 * Crop frame.
 *
 * An <img> cannot clip its own picture, so zooming one means giving it a box
 * that does the clipping. The frame carries no size of its own -- it wraps to
 * the picture, so every width, height and per-device rule already on the image
 * keeps working -- and the zoom is a transform on the picture inside it.
 */
.media-crop { border-radius: var(--media-radius); display: block; overflow: hidden; }
/* The frame is the box, the picture fills it. Sizing the picture instead left
   the frame at full column width, so a zoomed picture was clipped against the
   wrong rectangle and the resize handles sat outside the visible edge. */
.media-crop > img, .media-crop > video { border-radius: 0; display: block; height: 100%; width: 100%; }

/* ============================================================ About page ==
 * Built from the CV: the same information architecture -- identity, about,
 * experience with three roles, tools, strengths, studies, contact -- carried
 * over as sections with the site's own type and spacing rather than as one
 * paragraph. Classes are prefixed .about- so nothing else can inherit them.
 */
/*
 * No vertical padding in the stylesheet.
 *
 * Every section's top and bottom space is set as a component style instead, so
 * the editor's Style panel shows the actual number and you can change it there.
 * A gap you cannot find in the settings is a gap you cannot fix.
 */
.about-section { padding: 0 var(--gutter); }
/* One column of content, centred, so a capped line length leaves its slack in
   the margins instead of a hole beside the text. */
.about-section, .about-hero { margin-inline: auto; max-width: 1320px; width: 100%; }


/* Section marker: a line icon and a mono label, the way the CV heads its
   columns. The icon inherits colour, so it works on any background. */
/* Section labels carry the page's structure, so they have to be readable at a
   glance -- 12px muted mono was a footnote. */
.about-label {
  align-items: center;
  color: rgb(242 241 236 / 0.82);
  display: flex;
  font-family: "IBM Plex Mono", monospace;
  font-size: 17px;
  gap: 12px;
  letter-spacing: 0.1em;
  margin: 0 0 clamp(20px, 2.4vw, 34px);
  text-transform: uppercase;
}
.about-label svg { flex: none; height: 22px; width: 22px; }

.about-hero {
  /* end, not centre: the portrait is the taller column, and centring the text
     against it dropped ~34px of slack under the last line -- space that showed
     up as a gap with no setting behind it. Aligned to the baseline of the
     block, everything below the hero is padding you can see and edit. */
  align-items: end;
  display: grid;
  gap: clamp(28px, 5vw, 72px);
  grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.62fr);
  padding: 0 var(--gutter);
}
.about-hero h1 {
  font-family: "Archivo Black", sans-serif;
  font-size: clamp(46px, 8.4vw, 132px);
  letter-spacing: -0.055em;
  line-height: 0.84;
  margin: 0;
  text-transform: uppercase;
}
.about-hero__role {
  color: rgb(242 241 236 / 0.7);
  font-family: "IBM Plex Mono", monospace;
  font-size: clamp(13px, 1.2vw, 16px);
  letter-spacing: 0.18em;
  margin: 22px 0 0;
  text-transform: uppercase;
}
.about-hero__portrait {
  aspect-ratio: 3 / 4;
  border-radius: var(--media-radius);
  object-fit: cover;
  width: 100%;
}

/* Body copy is capped by MEASURE, not by the column it sits in: bullets were
   running 120 characters across a 923px line at 16px, which is roughly twice a
   comfortable reading width. Everything below is capped near 65ch and lifted a
   step in size, and long copy uses a brighter grey than the labels. */
.about-lead { font-size: clamp(20px, 1.9vw, 28px); line-height: 1.34; margin: 22px 0 0; max-width: 26ch; }
/* The bio reads as one piece of writing, not three orphaned fragments spread
   over two sections -- so it sits under the name as a single block. */
.about-bio { display: grid; gap: 14px; margin-top: 26px; max-width: 62ch; }
.about-bio p { color: rgb(242 241 236 / 0.82); font-size: clamp(17px, 1.35vw, 19px); line-height: 1.55; margin: 0; }
.about-bio p:first-child { color: var(--ink); font-size: clamp(19px, 1.6vw, 22px); line-height: 1.45; }
.about-columns {
  display: grid;
  gap: clamp(22px, 2.6vw, 48px);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  margin-top: clamp(20px, 2.2vw, 32px);
}
.about-columns p { color: rgb(242 241 236 / 0.76); font-size: 17px; line-height: 1.6; margin: 0; max-width: 58ch; }

/* The numbers the CV buries inside bullets, given their own weight. */
.about-stats {
  display: grid;
  gap: 1px;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.about-stat { padding: clamp(20px, 2.4vw, 34px) 0; }
.about-stat__value {
  display: block;
  font-family: "Archivo Black", sans-serif;
  font-size: clamp(26px, 3.2vw, 46px);
  letter-spacing: -0.03em;
  line-height: 1;
}
.about-stat__label { color: rgb(242 241 236 / 0.7); display: block; font-size: 15px; line-height: 1.5; margin-top: 12px; max-width: 26ch; }

/* One role: the mark and the dates on the left, what was actually done on the
   right -- the CV's own two-column reading order. */
.about-role {
  border-top: 1px solid rgb(255 255 255 / 0.09);
  display: grid;
  gap: clamp(20px, 3vw, 56px);
  grid-template-columns: minmax(200px, 0.42fr) minmax(0, 1fr);
  padding: clamp(28px, 3.4vw, 52px) 0;
}
.about-role:last-of-type { border-bottom: 1px solid rgb(255 255 255 / 0.09); }
.about-role__chip {
  align-items: center;
  background: #fff;
  border-radius: 12px;
  display: flex;
  height: 66px;
  justify-content: center;
  margin-bottom: 18px;
  padding: 12px 16px;
  width: min(210px, 100%);
}
.about-role__chip img { max-height: 100%; object-fit: contain; width: auto; }
.about-role__title { font-size: 20px; font-weight: 600; margin: 0; }
.about-role__dates { color: var(--muted); font-family: "IBM Plex Mono", monospace; font-size: 12px; letter-spacing: 0.08em; margin: 8px 0 0; }
.about-role ul { list-style: none; margin: 0; padding: 0; }
.about-role li { color: rgb(242 241 236 / 0.76); font-size: 17px; line-height: 1.55; max-width: 64ch; padding-left: 24px; position: relative; }
.about-role li + li { margin-top: 14px; }
.about-role li::before { color: var(--acid); content: '—'; left: 0; position: absolute; }
.about-role li strong { color: var(--ink); font-weight: 600; }

.about-tools { display: grid; gap: 14px clamp(16px, 2vw, 32px); grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.about-tool { align-items: center; display: flex; font-size: 16px; gap: 13px; }
.about-tool img { border-radius: 8px; flex: none; height: 34px; width: 34px; }
.about-subhead { color: rgb(242 241 236 / 0.66); font-family: "IBM Plex Mono", monospace; font-size: 14px; letter-spacing: 0.12em; margin: 0 0 18px; text-transform: uppercase; }
.about-tools + .about-subhead { margin-top: clamp(30px, 3.4vw, 48px); }

.about-list { list-style: none; margin: 0; padding: 0; }
.about-list li { border-top: 1px solid rgb(255 255 255 / 0.09); display: flex; font-size: 17px; justify-content: space-between; padding: 15px 0; }
.about-list li span + span { color: var(--muted); font-family: "IBM Plex Mono", monospace; font-size: 13px; }

.about-study { display: grid; gap: clamp(16px, 3vw, 56px); grid-template-columns: minmax(200px, 0.42fr) minmax(0, 1fr); }
.about-study__degree { font-size: 19px; font-weight: 600; margin: 0; }
.about-study__school { font-size: clamp(20px, 2.2vw, 30px); margin: 0; }
.about-study__faculty { color: rgb(242 241 236 / 0.7); font-size: 16px; margin: 10px 0 0; }

.about-contact { display: grid; gap: 14px; margin-top: clamp(24px, 3vw, 40px); }
.about-contact a { align-items: center; color: var(--ink); display: flex; font-size: clamp(16px, 1.6vw, 21px); gap: 14px; text-decoration: none; width: fit-content; }
.about-contact a:hover { color: var(--acid); }
.about-contact svg { flex: none; height: 20px; opacity: 0.7; width: 20px; }

@media (max-width: 900px) {
  .about-hero { grid-template-columns: minmax(0, 1fr); }
  /* No cap on the portrait here. A stylesheet max-width is invisible to the
     editor: the panel said 513px, the picture drew at 320px, and no handle
     could move it -- the cap won every time. The size lives in the editor. */
  .about-role, .about-study { grid-template-columns: minmax(0, 1fr); }
}
