/* ===== base.css ===== */
/*
  Shared design language for every CMS theme. Loaded before the theme's styles.css.

  - Two typefaces: --font-text (reading + display) and --font-mono (metadata). Both come from
    site.css, which the build writes from the theme options.
  - One ink, one paper, one muted ink, one rule colour. --accent defaults to ink.
  - 8px spacing unit; 12-column grid; 16:9 media frames.
  - Site width: --site-max (from site.css, via the built-in "Site width" option) caps every .wrap
    and centres it. <html> carries width-site (header and footer share the cap), width-content
    (header .nav and footer .foot span the window) or width-full (no cap). Display type scales
    with the capped width (--vw), not the window, so headlines stop growing on big monitors.
  - Landing banner: shared/banner.html + banner.js, styled below; themes place the caption.
  - Colour mode: follows the visitor's system unless <html data-mode="light|dark"> forces one.
  - Flourishes are opt-in via f-* classes on <html> (flourish.js).
*/

/* ---------- tokens ---------- */
:root {
  --paper: #f7f6f3;
  --ink: #141414;
  --mute: #6f6e6a;
  --rule: #dcdbd6;
  --wash: #ecebe6;
  --accent: var(--ink);

  --u: 8px;
  --s1: calc(var(--u) * 1);  --s2: calc(var(--u) * 2);  --s3: calc(var(--u) * 3);  --s4: calc(var(--u) * 4);
  --s5: calc(var(--u) * 6);  --s6: calc(var(--u) * 8);  --s7: calc(var(--u) * 12); --s8: calc(var(--u) * 20);

  --gutter: clamp(20px, 4vw, 56px);
  --gap: var(--s3);
  --measure: 62ch;

  --site-max: 100vw;                                   /* site.css overrides, e.g. 1440px */
  --vw: min(1vw, calc(var(--site-max) / 100));         /* 1% of the window or of the site width, whichever is less */

  --t-xs: 12px;  --lh-xs: 16px;
  --t-s: 14px;   --lh-s: 20px;
  --t-m: 17px;   --lh-m: 24px;
  --t-l: clamp(20px, calc(1.6 * var(--vw)), 24px);   --lh-l: 1.3;
  --t-xl: clamp(28px, calc(3 * var(--vw)), 40px);    --lh-xl: 1.15;
  --t-2xl: clamp(40px, calc(6 * var(--vw)), 80px);   --lh-2xl: 1.0;
  --t-3xl: clamp(56px, calc(10 * var(--vw)), 144px); --lh-3xl: 0.92;

  --track-display: -0.02em;
  --track-mono: 0.02em;
  --dur: 240ms;
  --ease: cubic-bezier(.2,.6,.2,1);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-mode="light"]) { --paper: #121212; --ink: #ecebe6; --mute: #8f8e89; --rule: #2a2a2a; --wash: #1c1c1c; }
}
:root[data-mode="dark"] { --paper: #121212; --ink: #ecebe6; --mute: #8f8e89; --rule: #2a2a2a; --wash: #1c1c1c; }
:root[data-mode="light"] { --paper: #f7f6f3; --ink: #141414; --mute: #6f6e6a; --rule: #dcdbd6; --wash: #ecebe6; }

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--font-text); font-size: var(--t-m); line-height: var(--lh-m); font-weight: 400; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote, pre { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 0.15em; text-decoration-thickness: 1px; }
img, video, svg { display: block; max-width: 100%; height: auto; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
::selection { background: var(--ink); color: var(--paper); }

/* ---------- type ---------- */
.mono { font-family: var(--font-mono); letter-spacing: var(--track-mono); font-weight: 400; }
.t-xs { font-size: var(--t-xs); line-height: var(--lh-xs); }
.t-s  { font-size: var(--t-s);  line-height: var(--lh-s); }
.t-m  { font-size: var(--t-m);  line-height: var(--lh-m); }
.t-l  { font-size: var(--t-l);  line-height: var(--lh-l); letter-spacing: -0.01em; }
.t-xl { font-size: var(--t-xl); line-height: var(--lh-xl); letter-spacing: var(--track-display); font-weight: 500; }
.t-2xl{ font-size: var(--t-2xl);line-height: var(--lh-2xl);letter-spacing: var(--track-display); font-weight: 500; }
.t-3xl{ font-size: var(--t-3xl);line-height: var(--lh-3xl);letter-spacing: -0.03em; font-weight: 500; }
.mute { color: var(--mute); }
.caps { text-transform: uppercase; letter-spacing: 0.08em; }
.measure { max-width: var(--measure); }
.tnum { font-variant-numeric: tabular-nums; }
.meta { font-family: var(--font-mono); font-size: var(--t-s); line-height: var(--lh-s); letter-spacing: var(--track-mono); color: var(--mute); }
.meta b { font-weight: 400; color: var(--ink); }
.tag { font-family: var(--font-mono); font-size: var(--t-xs); letter-spacing: var(--track-mono); color: var(--mute); }

/* ---------- prose (rendered Markdown) ---------- */
.prose > * + * { margin-top: var(--s2); }
.prose p, .prose li, .prose blockquote { max-width: var(--measure); }
.prose h2 { font-size: var(--t-l); line-height: var(--lh-l); font-weight: 500; margin-top: var(--s5); }
.prose h3 { font-size: var(--t-m); font-weight: 500; margin-top: var(--s4); }
.prose a { text-decoration: underline; text-underline-offset: 0.15em; text-decoration-color: var(--mute); }
.prose ul { list-style: disc; padding-left: 1.2em; }
.prose ol { list-style: decimal; padding-left: 1.4em; }
.prose li + li { margin-top: 4px; }
.prose blockquote { border-left: 2px solid var(--rule); padding-left: var(--s2); color: var(--mute); }
.prose pre { padding: var(--s2); background: var(--wash); overflow: auto; font-family: var(--font-mono); font-size: var(--t-s); }
.prose code { font-family: var(--font-mono); font-size: 0.9em; }
.prose table { border-collapse: collapse; font-size: var(--t-s); }
.prose th, .prose td { border-bottom: 1px solid var(--rule); padding: var(--s1) var(--s2) var(--s1) 0; text-align: left; }
.prose hr { border: 0; border-top: 1px solid var(--rule); margin: var(--s4) 0; }
.prose .footnotes { margin-top: var(--s5); padding-top: var(--s2); border-top: 1px solid var(--rule); font-size: var(--t-s); color: var(--mute); }
.prose .footnotes ol { list-style: decimal; padding-left: 1.4em; }
.prose .media { margin-top: var(--s4); margin-bottom: var(--s4); }
.prose .media + .media { margin-top: var(--s2); }
.prose .media-image img, .prose .media-image video { width: 100%; }
.prose .size-inline { max-width: var(--measure); }

/* Full-bleed images run to the edges of the site (the window, or the capped width when the site
   is narrower than the window). A theme whose prose column does not start at the page gutter sets
   --bleed-left (distance from the site's left edge to the column) and, if the bleed should stop
   short of the right edge, --bleed-width. */
.prose { --bleed-width: min(100vw, var(--site-max)); --bleed-left: calc((100vw - var(--bleed-width)) / 2 + var(--gutter)); }
.prose .size-full { width: var(--bleed-width); max-width: none; margin-left: calc(-1 * var(--bleed-left)); }
.prose .size-full figcaption { padding-left: var(--bleed-left); }
html, body { overflow-x: clip; }

/* Sticky footer: on a short page (e.g. no banner) the body still fills the window and main
   takes the spare height, so the footer sits at the bottom instead of floating mid-page. On a
   long page nothing changes. Children get width:100% because auto side margins (.wrap) would
   otherwise shrink-wrap inside a flex column. */
body { display: flex; flex-direction: column; min-height: 100vh; min-height: 100svh; }
body > * { width: 100%; }
body > main { flex: 1 0 auto; }

/* Grid columns that hold the body must be allowed to shrink below their images' natural width. */
.project-body > *, .post-body, .prose { min-width: 0; }

/* blocks: text, image beside text, galleries, quotes, spacers */
.prose .block-text > * + * { margin-top: var(--s2); }
.prose .block-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); align-items: center; margin: var(--s4) 0; }
.prose .block-row.split-60 { grid-template-columns: 3fr 2fr; }
.prose .block-row.image-right.split-60 { grid-template-columns: 2fr 3fr; }
.prose .block-row.image-right > .media { order: 2; }
.prose .block-row > .media { margin: 0; max-width: none; }
.prose .block-row .block-text p { max-width: none; }
.media-gallery.cols-2 { grid-template-columns: 1fr 1fr; }
.media-gallery.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.media-gallery.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.media-gallery .gallery-item img { height: 100%; }   /* keep the ratio from width/height, so a row has its height before the pictures load */
.prose .block-quote { border: 0; padding: 0; margin: var(--s5) 0; color: var(--ink); max-width: var(--measure); }
.prose .block-quote p { font-size: var(--t-l); line-height: var(--lh-l); letter-spacing: -0.01em; }
.prose .block-quote cite { display: block; margin-top: var(--s1); font-family: var(--font-mono); font-size: var(--t-s); font-style: normal; color: var(--mute); letter-spacing: var(--track-mono); }
.prose .block-spacer.size-s { height: var(--s3); }
.prose .block-spacer.size-m { height: var(--s5); }
.prose .block-spacer.size-l { height: var(--s7); }
/* Galleries that crop every item to one shape (gallery block `ratio`), with their own gap. */
.media-gallery[style*="--gap"] { gap: var(--gap); }
.media-gallery.cropped .gallery-item { aspect-ratio: var(--ratio); overflow: hidden; }
.media-gallery.cropped .gallery-item img { height: 100%; aspect-ratio: auto; }

/* Layout block: a freeform grid, the way Squarespace's layout engine draws it. 24 columns; each
   row is 2.15% of the grid's width (at least), so a layout keeps its proportions at any width.
   Items are placed by grid lines (--a). Images and loops fill their cell, cropped. A layout
   section can carry its own padding (--pt/--pb, vmax) and minimum height (--mh, vh). On phones
   items use their phone placement (--ma, 8 columns, 24px rows) when the layout has one, and
   otherwise stack in order at their natural shape. */
.block-layout { container-type: inline-size; display: flex; flex-direction: column; justify-content: center;
  padding: var(--pt, 0) 0 var(--pb, 0); min-height: var(--mh, 0); }
.block-layout.valign-start { justify-content: flex-start; }
.block-layout.valign-end { justify-content: flex-end; }
.prose > .block-layout, .prose > * + .block-layout, .prose > .block-layout + * { margin-top: 0; }
.layout-grid { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); gap: var(--gap, 11px);
  grid-template-rows: repeat(var(--rows), minmax(2.15cqw, auto)); }
.layout-item { grid-area: var(--a); position: relative; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
.layout-item.valign-start { justify-content: flex-start; }
.layout-item.valign-end { justify-content: flex-end; }
.layout-item.kind-image, .layout-item.kind-video { overflow: hidden; }
.layout-item.kind-video:has(.layout-natural) { overflow: visible; }
.layout-fill { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; color: transparent; }
.layout-natural { width: 100%; height: auto; }
.prose .layout-item > .media, .layout-item > .media { margin: 0; max-width: none; width: 100%; }
.prose .layout-text p, .prose .layout-text li { max-width: none; }
.layout-text .align-center { text-align: center; }
.layout-text .align-right { text-align: right; }
@media (max-width: 767px) {
  .layout-grid { grid-template-columns: repeat(8, minmax(0, 1fr)); grid-template-rows: repeat(var(--m-rows), minmax(24px, auto)); }
  .layout-item { grid-area: var(--ma); }
  .block-layout:not(.has-phone) .layout-grid { display: flex; flex-direction: column; }
  .block-layout:not(.has-phone) .layout-item.kind-image, .block-layout:not(.has-phone) .layout-item.kind-video { aspect-ratio: 16 / 9; }
}

@media (max-width: 760px) {
  .prose .block-row, .prose .block-row.split-60, .prose .block-row.image-right.split-60 { grid-template-columns: 1fr; }
  .prose .block-row.image-right > .media { order: 0; }
  .media-gallery.cols-3 { grid-template-columns: 1fr 1fr; }
}

/* ---------- media ---------- */
.frame { position: relative; aspect-ratio: 16 / 9; background: var(--wash); overflow: hidden; }
.frame.square { aspect-ratio: 1; }
.frame.portrait { aspect-ratio: 4 / 5; }
.frame > video, .frame > img { width: 100%; height: 100%; object-fit: cover; }
figcaption { font-family: var(--font-mono); font-size: var(--t-xs); line-height: var(--lh-xs); letter-spacing: var(--track-mono); color: var(--mute); margin-top: var(--s1); }
.media-gallery { display: grid; gap: var(--gap); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.media-gallery figcaption { grid-column: 1 / -1; }
.media-gallery img { width: 100%; height: 100%; object-fit: cover; background: var(--wash); }
.embed { position: relative; background: var(--wash); overflow: hidden; display: grid; place-items: center; }
.embed > img, .embed > video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.embed-play { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: var(--s1); padding: var(--s1) var(--s2); background: var(--paper); color: var(--ink); font-family: var(--font-mono); font-size: var(--t-s); letter-spacing: var(--track-mono); text-decoration: none; }
.embed-play:hover { text-decoration: none; background: var(--ink); color: var(--paper); }
.embed-icon::before { content: ""; display: block; width: 0; height: 0; border-style: solid; border-width: 6px 0 6px 10px; border-color: transparent transparent transparent currentColor; }
.embed.is-playing .embed-play, .embed.is-playing > img, .embed.is-playing > video { display: none; }
.project-card { display: block; max-width: 420px; }
.project-card img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; margin-bottom: var(--s1); }
.project-card-title { display: block; }
.project-card-meta { display: block; font-family: var(--font-mono); font-size: var(--t-s); color: var(--mute); }
.broken-link { color: var(--mute); text-decoration: line-through; }

/* ---------- layout + components ---------- */
.wrap { padding-left: var(--gutter); padding-right: var(--gutter); max-width: var(--site-max); margin-left: auto; margin-right: auto; }
html.width-content .nav, html.width-content .foot { max-width: none; }
.grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--gap); row-gap: var(--gap); }
.rule { border-top: 1px solid var(--rule); }
.stack-s > * + * { margin-top: var(--s1); }
.stack   > * + * { margin-top: var(--s3); }
.stack-l > * + * { margin-top: var(--s6); }
.section { padding-top: var(--s6); padding-bottom: var(--s6); }
.nav a[aria-current="page"] { text-decoration: underline; text-underline-offset: 0.15em; }
dl.pairs { display: grid; grid-template-columns: max-content 1fr; column-gap: var(--s3); row-gap: var(--s1); }
dl.pairs dt { font-family: var(--font-mono); font-size: var(--t-s); color: var(--mute); letter-spacing: var(--track-mono); padding-top: 2px; }
.list-rule > * { border-top: 1px solid var(--rule); }
.list-rule > :last-child { border-bottom: 1px solid var(--rule); }
.filters { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s2); }
.filters button { font-family: var(--font-mono); font-size: var(--t-s); color: var(--mute); letter-spacing: var(--track-mono); }
.filters button[aria-pressed="true"] { color: var(--ink); text-decoration: underline; text-underline-offset: 0.15em; }
.pager { display: flex; justify-content: space-between; gap: var(--s3); }
.social { display: flex; flex-wrap: wrap; gap: var(--s2); }
footer { flex-wrap: wrap; }
.tags { display: flex; flex-wrap: wrap; gap: var(--s1); }
.tags li { font-family: var(--font-mono); font-size: var(--t-xs); letter-spacing: var(--track-mono); color: var(--mute); }
.tags li::before { content: "#"; }

/* ---------- landing banner (shared/banner.html + banner.js) ----------
   .banner.fit-contained sits inside the site width like a .wrap; .fit-full runs edge to edge.
   .banner-stage is the banner's footprint: an aspect ratio (--banner-ar, width / height, default
   16:9) or, with .height-screen, the first screen minus --banner-offset (a theme with a static
   header sets that to its height).

   .banner-frame is the media's box, centred in the stage. Its size comes from .size-*:
     --bf  the media's share of the stage on a wide window. Large, medium and small step down by
           one ratio, 0.85 (0.85, 0.85² ≈ 0.72, 0.85³ ≈ 0.61, the last close to the golden section).
     --bm  how fast it shrinks between a 480px-wide stage (media at full width) and a 1280px one
           (media at --bf): the straight line between those two points. Below 480px the media is
           full width; above 1280px it is exactly --bf. No breakpoints, so resizing is smooth.
   In a ratio banner the frame keeps the banner's shape at every width. In a screen-high banner at
   less than full size, the frame is 16:9 (the video's own shape) and fits inside the same scaled
   width and a height scaled by the same rule (full at 360px tall, --bf at 900px).
   Slides stack; only .is-active shows, so without JavaScript the first slide is a plain still or
   loop. Captions mirror the slides one to one. */
.banner { position: relative; }
.banner.fit-contained { padding-left: var(--gutter); padding-right: var(--gutter); max-width: var(--site-max); margin-left: auto; margin-right: auto; }
.banner-stage { position: relative; container-type: size; aspect-ratio: var(--banner-ar, 1.7778); display: grid; place-items: center; }
.banner.height-screen .banner-stage { aspect-ratio: auto; height: calc(100svh - var(--banner-offset, 0px)); min-height: 360px; }
.banner { --bf: 1; --bm: 1; --bmh: 1; }
.banner.size-large  { --bf: 0.85; --bm: 0.76;  --bmh: 0.75; }
.banner.size-medium { --bf: 0.72; --bm: 0.552; --bmh: 0.533; }
.banner.size-small  { --bf: 0.61; --bm: 0.37;  --bmh: 0.35; }
.banner-frame {
  position: relative;
  width: clamp(calc(var(--bf) * 100cqw), calc(480px + var(--bm) * (100cqw - 480px)), 100cqw);
  aspect-ratio: var(--banner-ar, 1.7778);
}
.banner.height-screen .banner-frame { aspect-ratio: auto; width: 100cqw; height: 100cqh; }
/* Screen-high and not full: the media keeps the video's own 16:9 and fits inside the scaled space,
   so its shape never changes with the window; the screen around it is page. */
.banner.height-screen:not(.size-full) .banner-frame {
  aspect-ratio: 16 / 9; height: auto;
  width: min(clamp(calc(var(--bf) * 100cqw), calc(480px + var(--bm) * (100cqw - 480px)), 100cqw),
             calc(clamp(calc(var(--bf) * 100cqh), calc(360px + var(--bmh) * (100cqh - 360px)), 100cqh) * 16 / 9));
}
.banner-slides { position: absolute; inset: 0; background: var(--wash); overflow: hidden; }
.banner-slide { position: absolute; inset: 0; display: block; opacity: 0; transition: opacity 900ms var(--ease); }
.banner-slide.is-active { opacity: 1; }
.banner-slide:hover { text-decoration: none; }
.banner-slide > video, .banner-slide > img { width: 100%; height: 100%; object-fit: cover; }
.banner-caption { display: none; }
.banner-caption.is-active { display: block; }
.banner-caption .title { color: var(--ink); }
@media (prefers-reduced-motion: reduce) { .banner-slide { transition: none; } }

/* ---------- motion ---------- */
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }

/* ---------- flourishes (opt in via html.f-*) ---------- */
html.f-reveal [data-reveal] { opacity: 0; transform: translateY(12px); transition: opacity 600ms var(--ease), transform 600ms var(--ease); }
html.f-reveal [data-reveal].is-in { opacity: 1; transform: none; }
html.f-hover-preview .hover-preview { position: fixed; z-index: 10; width: 320px; pointer-events: none; opacity: 0; transition: opacity var(--dur) var(--ease); }
html.f-hover-preview .hover-preview.is-on { opacity: 1; }
html:not(.f-hover-preview) .hover-preview { display: none; }
html.f-marquee .marquee { overflow: hidden; white-space: nowrap; }
html.f-marquee .marquee > span { display: inline-block; padding-right: var(--s6); animation: marquee 28s linear infinite; }
html:not(.f-marquee) .marquee > span + span { display: none; }
@keyframes marquee { to { transform: translateX(-100%); } }
html:not(.f-clock) [data-clock] { display: none; }
html.f-hover-play [data-hover-play] video { pointer-events: none; }

/* ---------- print ---------- */
@media print { .nav, .hover-preview, [data-clock], .embed-play { display: none !important; } }


/* ===== fonts.css ===== */
/* Fonts: Nitti Grotesk (text) and Nitti (code only). Files in /assets/fonts.
   The page width (--site-max) comes from the theme settings, appended at the end of site.css. */
@font-face { font-family: "Nitti Grotesk"; font-weight: 400; font-style: normal; font-display: swap; src: url("/assets/fonts/Nitti_Grotesk.otf") format("opentype"); }
@font-face { font-family: "Nitti Grotesk"; font-weight: 400; font-style: italic; font-display: swap; src: url("/assets/fonts/Nitti_Grotesk_Italic.otf") format("opentype"); }
@font-face { font-family: "Nitti Grotesk"; font-weight: 500; font-style: normal; font-display: swap; src: url("/assets/fonts/Nitti_Grotesk_Medium.otf") format("opentype"); }
@font-face { font-family: "Nitti Grotesk"; font-weight: 500; font-style: italic; font-display: swap; src: url("/assets/fonts/Nitti_Grotesk_Medium_Italic.otf") format("opentype"); }
@font-face { font-family: "Nitti"; font-weight: 400; font-style: normal; font-display: swap; src: url("/assets/fonts/Nitti-Normal.otf") format("opentype"); }
:root { --font-text: "Nitti Grotesk", "Helvetica Neue", Arial, sans-serif; --font-mono: "Nitti", "SF Mono", Menlo, monospace; }


/* ===== studio.css ===== */
/* Studio — the design system for jamestupper.com. Loads after the shared base.css.
   The style guide (guide.py; built to <site>/_source/guide/index.html) shows every rule below in use.

   Principles
     1. Content is ink; information about content is grey.
     2. One face, one weight: Nitti Grotesk Regular. Hierarchy comes from size and space, never bold.
        Medium (500) only for <strong> and h4.
     3. Everything sits on an 8px grid. Vertical spacing inside articles is measured in lines of
        body text (--line), so text, headings and pictures keep one rhythm.
     4. One reading measure (about 68 characters). Pictures may be wider than the text; text never is.
     5. White space separates; rules (lines) are rare.
   Sources: Bringhurst, The Elements of Typographic Style; Butterick, Practical Typography;
   A List Apart (web typography, vertical rhythm); Typewolf; Utopia (fluid type); WCAG 2.2. */

/* =========================================================================================
   1. Tokens
   ========================================================================================= */
:root {
  /* Colour */
  --paper: #ffffff;
  --ink: #24292e;          /* 15.0:1 on paper */
  --mute: #6a737d;         /* 4.7:1 — AA for text of any size */
  --rule: #e1e4e8;         /* hairlines, decorative only */
  --underline: #c4c9cf;    /* link underlines at rest */
  --wash: #f2f3f5;         /* behind pictures while they load */

  /* Space: an 8px grid */
  --s-4: 4px;  --s-8: 8px;  --s-16: 16px;  --s-24: 24px;  --s-32: 32px;
  --s-48: 48px;  --s-64: 64px;  --s-96: 96px;  --s-128: 128px;

  /* Layout */
  --gutter: 48px;                         /* page side margin (24px on phones) */
  --col-gap: 24px;                        /* between grid columns and paired pictures */
  --row-gap: 48px;                        /* between rows of the project grid */

  /* Type: six sizes, fluid between a 400px and a 1280px window */
  --fs-small:   clamp(15px, calc(14.55px + 0.114vw), 16px);    /* captions, meta, labels: 16/24 */
  --fs-ui:      clamp(16px, calc(15.09px + 0.227vw), 18px);    /* nav, footer: 18/24 */
  --fs-body:    clamp(18px, calc(17.09px + 0.227vw), 20px);    /* reading: 20/32 */
  --fs-lead:    clamp(21px, calc(19.64px + 0.341vw), 24px);    /* ledes, h3, the name: 24/36 */
  --fs-h2:      clamp(26px, calc(23.27px + 0.682vw), 32px);    /* section heads, pull quotes: 32/40 */
  --fs-display: clamp(36px, calc(28.73px + 1.818vw), 52px);    /* the opening sentence, titles */

  /* Rhythm: one line of body text, and the reading measure */
  --line: calc(var(--fs-body) * 1.5);      /* 30px on desktop, 27px on phones (was 1.6: tightened for Nitti's large x-height) */
  --measure: calc(var(--fs-body) * 34);    /* 680px: about 68 characters of Nitti Grotesk */

  --ease-out: cubic-bezier(.25, 1, .5, 1);
}
:root[data-mode="light"] { --paper: #ffffff; --ink: #24292e; --mute: #6a737d; --rule: #e1e4e8; --underline: #c4c9cf; --wash: #f2f3f5; }
@media (prefers-color-scheme: dark) {
  :root:not([data-mode="light"]) { --paper: #0d0e10; --ink: #e6e9ed; --mute: #8b939d; --rule: #24272b; --underline: #4a5058; --wash: #17191c; }
}
:root[data-mode="dark"] { --paper: #0d0e10; --ink: #e6e9ed; --mute: #8b939d; --rule: #24272b; --underline: #4a5058; --wash: #17191c; }
@media (max-width: 600px) { :root { --gutter: 24px; --col-gap: 16px; --row-gap: 40px; } }

/* =========================================================================================
   2. Base type
   ========================================================================================= */
/* Keep the scrollbar's space on every page, so short pages (Info) don't shift sideways against long ones. */
html { scrollbar-gutter: stable; }
body { background: var(--paper); color: var(--ink); font-family: var(--font-text); font-size: var(--fs-body);
  font-weight: 400; line-height: 1.5; letter-spacing: 0; -webkit-font-smoothing: antialiased;
  font-kerning: normal; font-variant-ligatures: common-ligatures; font-variant-numeric: proportional-nums; }
/* Also matched as .prose hN: base.css sets smaller, Medium headings there. */
h1, h2, h3, h4, .prose :is(h1, h2, h3, h4) { font-weight: 400; margin: 0; text-wrap: balance; }
h1, .prose h1 { font-size: var(--fs-display); line-height: 1.1; letter-spacing: -0.02em; }
h2, .prose h2 { font-size: var(--fs-h2); line-height: calc(var(--line) * 1.25); letter-spacing: -0.015em; }   /* 32/37.5 */
h3, .prose h3 { font-size: var(--fs-lead); line-height: var(--line); letter-spacing: -0.005em; }               /* 24/30 */
h4, .prose h4 { font-size: var(--fs-body); line-height: var(--line); font-weight: 500; }                        /* 20/30 */
p { margin: 0; text-wrap: pretty; hanging-punctuation: first; }
strong, b { font-weight: 500; }
small, .size-small { font-size: var(--fs-small); line-height: 1.5; }
.size-large, .lede { font-size: var(--fs-lead); line-height: 1.5; letter-spacing: -0.005em; }
sup, sub { font-size: 0.7em; line-height: 0; }
.nums { font-variant-numeric: tabular-nums; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* Information about content: small, grey. */
.meta, .tile-meta, .item-kicker, .details dt, .pager-label, figcaption {
  font-family: var(--font-text); font-size: var(--fs-small); line-height: 1.5; letter-spacing: 0.01em; color: var(--mute); }

/* Links */
a { color: inherit; text-decoration: none; transition: color 0.3s ease, text-decoration-color 0.3s ease; }
.prose a, .not-found a, .lede a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.22em;
  text-decoration-color: var(--underline); }
.prose a:hover, .not-found a:hover, .lede a:hover { text-decoration-color: currentColor; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
::selection { background: var(--ink); color: var(--paper); }

.skip { position: absolute; left: var(--gutter); top: 8px; z-index: 20; padding: 8px 16px; background: var(--ink); color: var(--paper); font-size: var(--fs-ui); }
.skip:not(:focus) { width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* =========================================================================================
   3. Header and footer
   ========================================================================================= */
.nav { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px 40px;
  padding-top: var(--s-32); padding-bottom: var(--s-32); }
.brand { font-size: var(--fs-lead); line-height: 1.5; letter-spacing: 0.02em; }
.nav-links { display: flex; gap: 40px; }
.nav-links a { font-size: var(--fs-ui); line-height: 1.5; letter-spacing: 0.02em; color: var(--mute); padding: 4px 0; }
.nav-links a:hover, .nav-links a[aria-current="page"] { color: var(--ink); }
.nav a[aria-current="page"] { text-decoration: none; }

.foot { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 8px 40px;
  padding-top: var(--s-48); padding-bottom: var(--s-48); font-size: var(--fs-ui); line-height: 1.5; color: var(--mute); }
.foot-links { display: flex; flex-wrap: wrap; gap: 8px 40px; }
.foot-links a { padding: 4px 0; }
.foot-links a:hover { color: var(--ink); }

/* =========================================================================================
   4. Home and indexes: the opening sentence and the project grid
   ========================================================================================= */
.index { padding-bottom: var(--s-96); }
.intro { font-size: var(--fs-display); line-height: 1.1; letter-spacing: -0.02em; max-width: calc(50% - 12px);
  margin: var(--s-64) 0 80px; text-wrap: balance; }   /* 80 = 10 × 8 */
.index > h1.visually-hidden + .tiles { margin-top: var(--s-32); }

/* Gaps come from the "Space between columns/rows" options (inline on the list). */
.tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(0px, var(--col-gap), 120px); row-gap: clamp(0px, var(--row-gap), 200px); }
.tiles.cols-1 { grid-template-columns: minmax(0, 1fr); }
.tiles.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tiles.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.tile { display: block; }
.tile-image { aspect-ratio: 16 / 9; overflow: hidden; background: var(--wash); }
.tile-image img { width: 100%; height: 100%; object-fit: cover; display: block; color: transparent; }
.tile-caption { display: block; margin: 0; padding-top: var(--s-8); }
/* Title and grey line read as one unit: tight line heights (about 10px from the title's letters to the grey line). */
.tile-title { display: block; font-size: var(--fs-body); line-height: 1.25; letter-spacing: 0.01em; }
.tile-caption .tile-meta { display: block; line-height: 1.3; }
@media (max-width: 900px) {
  .tiles, .tiles:is(.cols-3, .cols-4) { grid-template-columns: 1fr; row-gap: 40px; }
  .intro { max-width: none; }
}

/* Hover zoom: the live site's values (5% over 1s, soft ease-out). It stays on under "reduce motion"
   (base.css turns every other transition off there): it is small and only runs under the pointer. */
.f-hover-zoom .tile-image img { transition: transform 1s var(--ease-out) !important; will-change: transform; }
.f-hover-zoom .tile:hover .tile-image img { transform: scale(1.05); }

/* =========================================================================================
   5. Articles: projects, posts and pages
   -----------------------------------------------------------------------------------------
   The body is a grid with two named column ranges:
     text  — the reading measure (680px)
     wide  — the full content width (pictures, galleries, films)
   Layout "left" (default) puts the text column on the left edge of the page grid, in line with
   the home page's opening sentence, and keeps the space to its right for captions. Layout
   "center" centres the text column (a classic essay).
   ========================================================================================= */
.article { padding-top: var(--s-64); padding-bottom: var(--s-96); }
.article-head, .item-body {
  display: grid; column-gap: var(--col-gap);
  grid-template-columns: [wide-start text-start] minmax(0, var(--measure)) [text-end] minmax(0, 1fr) [wide-end]; }
.layout-center .article-head, .layout-center .item-body {
  grid-template-columns: [wide-start] minmax(0, 1fr) [text-start] minmax(0, var(--measure)) [text-end] minmax(0, 1fr) [wide-end]; }
.article-head > *, .item-body > * { grid-column: text; min-width: 0; margin: 0; }
/* Phones: one column. (The empty margin track would otherwise still take a column gap.) */
@media (max-width: 700px) {
  .article-head, .item-body, .layout-center .article-head, .layout-center .item-body {
    grid-template-columns: [wide-start text-start] minmax(0, 1fr) [text-end wide-end]; }
}

/* Header: kicker, title, lede, facts. Spacing in 8px steps. */
.article-head { margin-bottom: var(--s-96); }
.item-kicker { margin-bottom: var(--s-16); }
.article-head h1 { max-width: 18ch; }
.layout-center .article-head { text-align: center; }
.layout-center .article-head h1 { margin-inline: auto; }
.article-head .lede { margin-top: var(--s-24); }
.article-head .details { margin-top: var(--s-48); }

/* Facts (client, role, year): label over value, in a row. */
.details { display: flex; flex-wrap: wrap; gap: var(--s-16) var(--s-48); margin: 0; }
.layout-center .details { justify-content: center; }
.details > div { min-width: 0; }
.details dd { margin: 0; font-size: var(--fs-small); line-height: 1.5; letter-spacing: 0.01em; }
.item-body + .details { margin-top: var(--s-96); }
.layout-center .item-body + .details { justify-content: center; }

/* ----- Vertical rhythm. One line = --line (30px on desktop). -----
   Each heading's space above + its line + space below adds up to whole lines, so the text after a
   one-line heading lands back on the grid:
     h2   2 lines + 1.25 + 0.75  = 4 lines        h3   1.5 + 1 + 0.5 = 3 lines       h4   1 + 1 + 0.5 = 2.5
   paragraph → paragraph  1 line       quote ↔ text  2 lines         picture ↔ picture  --col-gap
   text ↔ picture  3 lines from the picture's edge, on both sides. A caption (8px + one 24px line)
   reads as part of the white space, so after a captioned picture the text starts 2 lines below the
   caption: 8 + 24 + 60 = 92px from the edge, matching the 90 above it. */
/* The rules below choose each block's space above (--sp); one rule applies it, times the block's own
   "Space above" choice (--k: 0.5 less, 1 normal, 1.5 more; set by data-space, see blocks.css). */
.item-body > * + * { --sp: var(--line); }
.block-text > * + * { margin-top: var(--line); }
.item-body > :is(.block-quote, .project-card),
.item-body > :is(.block-quote, .project-card) + * { --sp: calc(var(--line) * 2); }
.item-body > :is(figure, .block-row, .block-layout, .media-gallery),
.item-body > :is(figure, .block-row, .block-layout, .media-gallery) + * { --sp: calc(var(--line) * 3); }
.item-body > :is(figure, .media-gallery):has(> figcaption) + * { --sp: calc(var(--line) * 2); }
.item-body > .media-gallery.has-captions + * { --sp: calc(var(--line) * 2); }      /* captions under each picture: the same */
/* A caption closes its picture: whatever follows (another picture too) starts 2 lines below it. Pictures without
   captions stay a column gap apart, the same as side by side. (Outranks the picture-to-picture rule below.) */
.item-body > :is(figure, .media-gallery):is(:has(> figcaption), .has-captions) + :is(figure, .block-layout, .media-gallery) { --sp: calc(var(--line) * 2); }
.item-body > :is(figure, .block-layout, .media-gallery) + :is(figure, .block-layout, .media-gallery) { --sp: var(--col-gap); }
.item-body > :first-child { --sp: 0px; }
/* Spacing is set above each block only. base.css gives pictures, rows and quotes a bottom margin
   too, which would add to the space before the next block (92px instead of 60 after a picture). */
.item-body > :is(.media, .block-row, .block-quote, .block-layout, .media-gallery, figure) { margin-bottom: 0; }
.item-body > h2 { --sp: calc(var(--line) * 2); }
.block-text > h2, .block-text > h2:not(:first-child) { margin-top: calc(var(--line) * 2); }
.item-body > h3 { --sp: calc(var(--line) * 1.5); }
.block-text > h3 { margin-top: calc(var(--line) * 1.5); }
.item-body > h4 { --sp: var(--line); }
.block-text > h4 { margin-top: var(--line); }
.item-body > :is(h3, h4) + * { --sp: calc(var(--line) / 2); }
.block-text > :is(h3, h4) + * { margin-top: calc(var(--line) / 2); }
.item-body > h2 + * { --sp: calc(var(--line) * 0.75); }
.block-text > h2 + * { margin-top: calc(var(--line) * 0.75); }
.block-text > :first-child { margin-top: 0; }
.item-body.item-body.item-body > * { margin-top: calc(var(--sp, 0px) * var(--k, 1)); }   /* tripled class: outranks base.css's .prose rules */

/* Text inside the body never grows past the measure; pictures may. */
.prose p, .prose li, .prose blockquote { max-width: var(--measure); }
.item-body ul, .item-body ol { padding-left: 1.25em; }
.item-body ul { list-style: disc; }
.item-body ol { list-style: decimal; }
.item-body li + li { margin-top: calc(var(--line) / 2); }   /* half a line: items separate, lines stay on the half-line grid */
.item-body li::marker { color: var(--mute); }
.item-body hr { border: 0; border-top: 1px solid var(--rule); margin: calc(var(--line) * 2) 0; }
.item-body code { font-family: var(--font-mono); font-size: 0.85em; }
.item-body pre { font-family: var(--font-mono); font-size: var(--fs-small); line-height: 1.5; padding: var(--s-16) var(--s-24); background: var(--wash); overflow: auto; }
.item-body table { border-collapse: collapse; font-size: var(--fs-small); line-height: 1.5; font-variant-numeric: tabular-nums; }
.item-body th, .item-body td { text-align: left; padding: var(--s-8) var(--s-24) var(--s-8) 0; border-bottom: 1px solid var(--rule); }
.item-body th { font-weight: 400; color: var(--mute); }
.item-body .footnotes { margin-top: calc(var(--line) * 2); padding-top: var(--line); border-top: 1px solid var(--rule); font-size: var(--fs-small); line-height: 1.5; color: var(--mute); }

/* Quotes. Inline (from Markdown): grey rule, body size. Pull quote (quote block): large, no rule,
   the opening quote mark hangs into the margin so the text keeps its edge. */
.item-body > blockquote:not(.block-quote), .block-text > blockquote { margin-left: 0; padding-left: var(--s-24); border-left: 2px solid var(--rule); color: var(--mute); }
.item-body .block-quote { border: 0; padding: 0; max-width: var(--measure); }
.item-body .block-quote p { font-size: var(--fs-h2); line-height: calc(var(--line) * 1.25); letter-spacing: -0.015em; text-indent: -0.4em; color: var(--ink); max-width: none; }
.item-body .block-quote p::before { content: "\201C"; }     /* curly quotes, the opening one hanging */
.item-body .block-quote p::after { content: "\201D"; }
.item-body .block-quote cite { display: block; margin-top: var(--s-16); font-family: var(--font-text); font-style: normal; font-size: var(--fs-small); line-height: 1.5; letter-spacing: 0.01em; color: var(--mute); }

/* ----- Pictures -----
   inline  the text column       wide  the content width       full  the window, edge to edge
   Captions sit under the picture, left-aligned, grey, never wider than the measure. */
.item-body > figure { margin-left: 0; margin-right: 0; }
.item-body > :is(.media.size-wide, .media-embed, .media-video, .media-gallery, .media-mosaic, .block-row, .block-layout, .project-card) { grid-column: wide; }
/* Full: the window's width, edge to edge, whatever the site width (the base sizes it to the site
   width, which leaves a gap on wide windows). The wide range is centred, so 50% − 50vw reaches the
   window's left edge; the caption steps back onto the grid. */
.item-body > .media.size-full { grid-column: wide; width: 100vw; max-width: none; margin-left: calc(50% - 50vw); }
.item-body .media img, .item-body .media video { display: block; width: 100%; height: auto; background: var(--wash); }
.item-body figcaption { margin-top: var(--s-8); max-width: var(--measure); text-align: left; font-style: normal; }
.item-body > .size-full figcaption { padding-left: calc(50vw - 50%); padding-left: max(var(--gutter), calc((100vw - min(100vw, var(--site-max))) / 2 + var(--gutter))); }

/* Left layout, wide windows: an inline picture's caption moves into the margin, level with the
   picture's foot (a side caption, as in print). */
@media (min-width: 1100px) {
  .layout-left .item-body > .media.size-inline { grid-column: wide; display: grid; grid-template-columns: subgrid; align-items: end; }
  .layout-left .item-body > .media.size-inline > :not(figcaption) { grid-column: text; }
  .layout-left .item-body > .media.size-inline > figcaption { grid-column: text-end / wide-end; margin-top: 0; max-width: 24ch; }
}

/* Pairs and galleries: equal columns at the column gap; the caption spans them. */
.item-body .media-gallery { display: grid; gap: var(--col-gap); grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr)); }
.item-body .media-gallery.cols-2 { --cols: 2; } .item-body .media-gallery.cols-3 { --cols: 3; } .item-body .media-gallery.cols-4 { --cols: 4; }
.item-body .media-gallery > figcaption { grid-column: 1 / -1; margin-top: calc(var(--s-8) - var(--col-gap)); }
.item-body .gallery-item { min-width: 0; }
.item-body .gallery-item :is(img, video) { width: 100%; height: 100%; object-fit: cover; }
/* Phones (as mosaics and the article grid): rows of pictures stack, one picture per line, full width. */
@media (max-width: 700px) { .item-body .media-gallery.media-gallery { --cols: 1; } }
/* One caption per picture: each item is a figure spanning two rows of the gallery's grid (subgrid), pictures in
   the first and captions in the second, so the pictures stay level whatever the captions' lengths. Captions sit
   8px under their picture; a further row of pictures starts the column gap below the longest caption. */
.item-body .media-gallery.has-captions { row-gap: 0; }
.item-body .has-captions > .gallery-item { display: grid; grid-row: span 2; grid-template-rows: subgrid; margin: 0; }
.item-body .has-captions > .gallery-item > figcaption { margin-top: var(--s-8); }
/* Each picture sits in a plain box (.gallery-pic), as in uncaptioned rows: the box takes the column's width, so the
   row is as tall as the tallest picture at that width, and a shorter one fills the box and crops to match.
   (A bare image here would size the row from the image file's own pixel height.) */
.item-body .has-captions .gallery-pic { min-width: 0; }
.item-body .has-captions.cols-2 > .gallery-item:nth-last-child(n+3),
.item-body .has-captions.cols-3 > .gallery-item:nth-last-child(n+4),
.item-body .has-captions.cols-4 > .gallery-item:nth-last-child(n+5) { padding-bottom: var(--col-gap); }
@media (max-width: 700px) { .item-body .has-captions > .gallery-item:not(:last-child) { padding-bottom: var(--col-gap); } }

/* Mosaic: pictures that sat in overlapping rows (a tall one beside a stack) keep that arrangement.
   Columns share the width by their stacks' width-to-height ratio (--a, set by the build) so their heights
   match; any last few pixels are cropped. On phones every picture stacks at its own shape. */
.item-body .media-mosaic { display: flex; flex-wrap: wrap; gap: var(--col-gap); align-items: stretch; }
/* A mosaic's caption goes on its own line under the columns, 8px below them (as under any picture). */
.item-body .media-mosaic > figcaption { flex: 0 0 100%; max-width: none; padding-right: max(0px, calc(100% - var(--measure)));
  margin-top: calc(var(--s-8) - var(--col-gap)); }      /* full-width line (so it wraps under the columns); text stays within the measure */
/* ×1000: flex shares that add up to less than 1 only hand out that fraction of the width (tall pictures give small
   shares), so scale them up; the proportions stay the same. */
.item-body .mosaic-col { flex: calc(var(--a, 1) * 1000) 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--col-gap); }
.item-body .mosaic-col :is(img, video) { flex: 1 1 auto; min-height: 0; width: 100%; height: auto; object-fit: cover; display: block; }
@media (max-width: 700px) { .item-body .media-mosaic { flex-direction: column; } .item-body .mosaic-col :is(img, video) { flex: none; } }

/* Picture beside text */
.item-body .block-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--col-gap); align-items: center; margin-bottom: 0; }
.item-body .block-row.split-60 { grid-template-columns: 3fr 2fr; }
.item-body .block-row.image-right.split-60 { grid-template-columns: 2fr 3fr; }
.item-body .block-row > .media { margin: 0; }
@media (max-width: 700px) { .item-body .block-row, .item-body .block-row.split-60, .item-body .block-row.image-right.split-60 { grid-template-columns: 1fr; } }

/* Imported layout blocks (from Squarespace) keep their own grid; the system spaces them. */
.item-body > .block-layout { padding-top: 0; padding-bottom: 0; min-height: 0; }

/* Spacers: whole lines */
.item-body .block-spacer.size-s { height: var(--line); }
.item-body .block-spacer.size-m { height: calc(var(--line) * 2); }
.item-body .block-spacer.size-l { height: calc(var(--line) * 4); }

/* Films: click to play (nothing loads from Vimeo until then) */
/* The frame keeps the film's own shape; the poster always fills it (cropping a sliver if its shape differs). */
.media-embed .embed > img.embed-poster { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }   /* outranks .item-body .media img */
.embed-play { padding: 0; width: 64px; height: 64px; justify-content: center; border-radius: 50%; background: rgba(36, 41, 46, .72); color: #fff; }
.embed-play:hover { background: #24292e; color: #fff; }
.embed-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.embed-icon::before { border-width: 9px 0 9px 15px; margin-left: 4px; }

/* Posts read at body size with a looser line for long text. */
.post .item-body .block-text { line-height: 1.5; }

/* ----- Previous / next ----- */
.pager { display: grid; grid-template-columns: 1fr 1fr; gap: var(--col-gap); margin-top: var(--s-96);
  padding-top: var(--s-32); border-top: 1px solid var(--rule); }
.pager a { display: flex; flex-direction: column; gap: var(--s-4); }
.pager-next { grid-column: 2; text-align: right; align-items: flex-end; }
.pager-title { font-size: var(--fs-body); line-height: 1.4; }
.pager-prev .pager-label::before { content: "\2190\00a0"; }
.pager-next .pager-label::after { content: "\00a0\2192"; }
.pager a:hover .pager-title { color: var(--mute); }

/* Pages (Info and the like) */
.page { padding-top: var(--s-64); padding-bottom: var(--s-96); }
.page .item-body h1 { font-size: var(--fs-display); line-height: 1.1; letter-spacing: -0.02em; }
.not-found { padding-top: var(--s-64); padding-bottom: var(--s-128); }
.not-found .lede { margin-top: var(--s-24); max-width: var(--measure); }

/* =========================================================================================
   6. Print — the article as a document: one column, black on white, links spelled out.
   ========================================================================================= */
@media print {
  @page { size: A4; margin: 20mm 22mm 24mm; }
  :root { --paper: #fff; --ink: #000; --mute: #555; --rule: #bbb; --wash: #fff;
    --fs-small: 8.5pt; --fs-ui: 9pt; --fs-body: 10.5pt; --fs-lead: 13pt; --fs-h2: 16pt; --fs-display: 26pt;
    --line: 15pt; --measure: 100%; --col-gap: 4mm; }
  body { padding: 0; min-height: 0; display: block; }
  .nav-links, .foot-links, .pager, .skip, .embed-play { display: none !important; }
  .nav { display: flex !important; padding: 0 0 6mm; }     /* base.css hides the header in print; keep the name */
  .item-body .block-quote p { text-indent: 0; }             /* no hanging mark in print: the margin may clip it */
  .foot { padding: 8mm 0 0; }
  .wrap { padding: 0; max-width: none; }
  .article { padding: 0; }
  .article-head, .item-body, .layout-center .article-head, .layout-center .item-body { display: block; text-align: left; }
  .article-head { margin-bottom: 8mm; }
  .item-body > * + * { margin-top: var(--line); }
  h1, h2, h3, h4 { break-after: avoid; }
  p, li { orphans: 3; widows: 3; }
  figure, .media-gallery, .block-row, .block-quote, .tile { break-inside: avoid; }
  .item-body img, .tile-image img { max-height: 110mm; width: auto; max-width: 100%; }
  .layout-left .item-body > .media.size-inline { display: block; }
  .item-body figcaption { padding-left: 0; }
  .prose a[href^="http"]::after { content: " (" attr(href) ")"; font-size: var(--fs-small); color: var(--mute); word-break: break-all; }
  .tiles, .tiles:is(.cols-3, .cols-4) { grid-template-columns: 1fr 1fr; gap: 6mm; }
  .intro { max-width: none; margin: 0 0 10mm; }
}


/* ===== about.css ===== */
/* About page (layout "Anchored"): the portrait fills the left grid column; the bio sits at the top of
   the right column and the facts, as hairline rows, end on the photo's bottom edge. */
.about { padding-top: var(--s-64); padding-bottom: var(--s-96); display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--col-gap); align-items: stretch;
  align-content: start; }   /* rows are as tall as their content: in a tall window <main> grows, and without this the grid would stretch its rows (and the photo) to fill it */
/* The photo is a 4:5 portrait, and grows taller only when the text column is taller (narrow windows),
   cropping instead of leaving the rows hanging. Its bottom edge is always the rows' last hairline.
   The ::before sets the 4:5 minimum (padding 125% of the width); the image fills the box. */
.about-photo { position: relative; margin: 0; align-self: stretch; }
.about-photo::before { content: ""; display: block; padding-top: 125%; }
.about-photo img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 35%; background: var(--wash); }
.about-text { display: flex; flex-direction: column; justify-content: space-between; gap: var(--s-48); }
/* The bio opens the page as a statement (as the home page's sentence does): h2 size, tight tracking, no label. */
.about-statement { font-size: var(--fs-h2); line-height: 1.25; letter-spacing: -0.015em; text-wrap: pretty; }
.about-reach { margin-top: var(--s-32); font-size: var(--fs-lead); line-height: 1.5; color: var(--mute); }
.about a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.22em; text-decoration-color: var(--underline); }
.about a:hover { text-decoration-color: currentColor; }
.about-reach a { color: var(--ink); }
/* Rows: a grey label in a narrow column, the items flowing at body size, a hairline between rows. */
.about-rows { margin: 0; border-top: 1px solid var(--rule); }
.about-rows > div { display: grid; grid-template-columns: 9em minmax(0, 1fr); column-gap: var(--col-gap); padding: calc(var(--line) / 2) 0; border-bottom: 1px solid var(--rule); }
.about-rows dt { font-size: var(--fs-small); line-height: var(--line); letter-spacing: 0.01em; color: var(--mute); }
.about-rows dd { margin: 0; font-size: var(--fs-body); line-height: var(--line); text-wrap: pretty; }
@media (max-width: 800px) { .about { grid-template-columns: 1fr; row-gap: var(--s-48); } .about-rows > div { grid-template-columns: 7em minmax(0, 1fr); } }


/* ===== blocks.css ===== */
/* Pages as a stack of blocks (Home, Work, Play, Info and any new page). Loads after studio.css.

   Space is set ABOVE each block only, and depends on which two blocks meet, so moving a block never
   doubles a gap. All values are on the 8px grid:
     first block under the header   64   (a grid that opens a page: 32, a banner: 32)
     after the intro sentence       80   (10 × 8, as before)
     after a banner                 64
     anything else                  96
   Each block's "Space above" choice multiplies that: less 0.5, normal 1, more 1.5. */

@property --k { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --sp { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --above { syntax: "<length>"; inherits: false; initial-value: 96px; }

[data-space="less"] { --k: 0.5; }
[data-space="more"] { --k: 1.5; }

.blocks { padding-bottom: var(--s-96); }
.blocks > * { --above: var(--s-96); margin-top: calc(var(--above, 96px) * var(--k, 1)); margin-bottom: 0; }
.blocks > :first-child { --above: var(--s-64); }
.blocks > .visually-hidden { margin: 0; }                         /* a page's hidden h1 takes no space */
.blocks > .visually-hidden + * { --above: var(--s-64); }          /* ... so the block after it counts as first */
.blocks > .visually-hidden + .block-grid,
.blocks > :first-child:is(.block-grid) { --above: var(--s-32); }
.blocks > .visually-hidden + .block-banner,
.blocks > :first-child:is(.block-banner) { --above: var(--s-32); }
.blocks > .intro + * { --above: 80px; }
.blocks > .block-banner + * { --above: var(--s-64); }

/* The intro sentence: display size (the home page's opening line) or large (h2 size). */
.blocks > .intro { max-width: calc(50% - 12px); }
.intro.intro-large { font-size: var(--fs-h2); line-height: 1.25; letter-spacing: -0.015em; }
@media (max-width: 900px) { .blocks > .intro { max-width: none; } }

/* Grid of projects or posts: tile shape and the "see all" link. Columns: .tiles.cols-2 / .cols-3 (studio.css). */
.tiles.shape-4x3 .tile-image { aspect-ratio: 4 / 3; }
.tiles.shape-1x1 .tile-image { aspect-ratio: 1 / 1; }
.grid-more { margin-top: var(--s-48); font-size: var(--fs-ui); line-height: 1.5; }
.grid-more a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.22em; text-decoration-color: var(--underline); }
.grid-more a:hover { text-decoration-color: currentColor; }

/* Banner: a picture or looping video across the top, at the page width or edge to edge. */
.block-banner { position: relative; }
.block-banner .banner-media { overflow: hidden; background: var(--wash); }
.block-banner .banner-media :is(img, video) { display: block; width: 100%; height: 100%; object-fit: cover; }
.block-banner.banner-full { width: 100vw; max-width: none; margin-left: calc(50% - 50vw); }
.block-banner .media-embed { margin: 0; }
.block-banner .embed :is(img, video) { width: 100%; height: 100%; object-fit: cover; }

/* Text, pictures and films on a page use the article column (centred, as projects and posts). */
.blocks > .layout-center > .item-body { display: grid; }
.blocks > .block-about { padding-top: 0; padding-bottom: 0; }

/* Project facts: a partner's name links to their website, underlined as text links are elsewhere. */
.details dd a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.22em; text-decoration-color: var(--underline); }
.details dd a:hover { text-decoration-color: currentColor; }

/* Preview only (the builder adds .is-preview): the selected block. Never in the built site. */
.is-preview [data-block].is-selected { outline: 2px solid #2563eb; outline-offset: 8px; }


/* ===== settings (site.json → theme settings) ===== */
:root { --site-max: 1440px; --gutter: 48px; --col-gap: 24px; --row-gap: 48px; }
@media (max-width: 600px) { :root { --gutter: 24px; --col-gap: 16px; --row-gap: 40px; } }
