/* ---------- Fonts ---------- */
/* Instrument Sans — a variable font served from this site (not Google), as
   two files that cover Regular (400) and Medium (500): latin for English
   text, latin-ext for accented/extended letters (the browser only downloads
   latin-ext if a page actually uses such a character). */
@font-face {
  font-family: 'Instrument Sans';
  src: url('../assets/fonts/Instrument_Sans/InstrumentSans-latin-ext.woff2') format('woff2');
  font-weight: 400 500;
  font-style: normal;
  font-stretch: 100%;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Instrument Sans';
  src: url('../assets/fonts/Instrument_Sans/InstrumentSans-latin.woff2') format('woff2');
  font-weight: 400 500;
  font-style: normal;
  font-stretch: 100%;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Instrument Serif (Regular + Italic), same idea. */
@font-face {
  font-family: 'Instrument Serif';
  src: url('../assets/fonts/Instrument_Serif/InstrumentSerif-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Instrument Serif';
  src: url('../assets/fonts/Instrument_Serif/InstrumentSerif-Italic.ttf') format('truetype');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

/* ---------- Reset & base ---------- */
* { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
}
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: var(--tracking-sans);
  font-size: var(--body-size);
  line-height: var(--body-lh);
}
img { max-width: 100%; }
a { color: inherit; }

/* Text highlight: solid text colour (#362727), with the text itself turning
   white so it stays readable on top. */
::selection { background: var(--text); color: var(--bg); }
::-moz-selection { background: var(--text); color: var(--bg); }

/* ---------- Type scale ----------
   Sizes live in tokens.css. Apply to any element on any page; mix in
   .t-serif (Instrument Serif italic) for the serif runs inside Title/Body,
   and .t-regular for the Regular-weight sans in subtext. */
.t-title   { font-size: var(--title-size);   line-height: var(--title-lh); }
.t-h1      { font-size: var(--h1-size);      line-height: var(--h1-lh); }
.t-body    { font-size: var(--body-size);    line-height: var(--body-lh); }
.t-subtext { font-size: var(--subtext-size); line-height: var(--subtext-lh); color: var(--text-subtle); }
.t-serif   { font-family: var(--font-serif); font-style: italic; font-weight: 400; letter-spacing: normal; }
.t-regular { font-weight: 400; }

.container {
  width: 100%;
  padding-inline: var(--margin);
}

/* ---------- Page grid ----------
   Every page is laid out on this 6-column grid: --gutter between columns,
   --margin on both sides, and no max-width — it always stretches to the
   full browser width, at every breakpoint. Put .grid on a section, then
   say how many columns each child takes and where it starts:

     <section class="grid">
       <p class="span-4 start-3">…</p>   paragraph over columns 3–6
       <figure class="span-6">…</figure> full-width video/image
       <img class="span-2">              two columns (a third of the width)
     </section>

   span-1…6 = how many columns; start-1…6 = which column it starts in
   (omit start-* to let it flow into the next free slot). A "third" of the
   page is span-2, a "half" is span-3, two thirds is span-4, full width is
   span-6. Every child needs a span-*: with none it covers just one column. */
.grid {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--row-gap);
  width: 100%;
  padding-inline: var(--margin);
}
.grid > * { grid-column: var(--start, auto) / span var(--span, 1); }
.span-1 { --span: 1; }
.span-2 { --span: 2; }
.span-3 { --span: 3; }
.span-4 { --span: 4; }
.span-5 { --span: 5; }
.span-6 { --span: 6; }
.start-1 { --start: 1; }
.start-2 { --start: 2; }
.start-3 { --start: 3; }
.start-4 { --start: 4; }
.start-5 { --start: 5; }
.start-6 { --start: 6; }

/* ---------- Media ----------
   Any video, Vimeo embed, gif or photo sits in a .media box, which fills
   the columns it spans and keeps a fixed shape. Pick the shape with a
   ratio-* class (default 16:9):

     <div class="media ratio-16x9 span-6">
       <iframe …Vimeo embed…></iframe>   or   <img …> / <video …>
     </div>

   To swap a placeholder image for a real Vimeo video, delete the <img> and
   paste the embed's <iframe> in its place — its fixed width/height
   attributes are overridden here.

   Match the ratio to the video's real shape. Vimeo's player fits the video
   inside the box without cropping, so a box that's even slightly taller or
   shorter than the video leaves empty bars above/below it (and the gap to
   whatever is above looks bigger than it is). A video's exact size is in
   Vimeo's oEmbed: https://vimeo.com/api/oembed.json?url=<video url> */
.media {
  position: relative;
  aspect-ratio: var(--ratio, 16 / 9);
  overflow: hidden;
  align-self: start;
}
.media > img,
.media > video,
.media > iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}
.ratio-16x9 { --ratio: 16 / 9; }
.ratio-7x3  { --ratio: 7 / 3; }   /* 2.33:1 */
.ratio-wide { --ratio: 426 / 176; } /* 2.42:1 — the true shape of the wide Vimeo films */
.ratio-3x1  { --ratio: 3 / 1; }     /* 3:1 — the SKO 25 hero film */
.ratio-32x17 { --ratio: 32 / 17; }  /* 1.88:1 — 1920×1020 images */
.ratio-1x1  { --ratio: 1 / 1; }
.ratio-3x4  { --ratio: 3 / 4; }
.ratio-9x16 { --ratio: 9 / 16; }
.ratio-4x3  { --ratio: 4 / 3; }

/* ---------- Slideshow ----------
   One photo at a time inside a .media box. Give it a ratio-* and a span-*
   like any other media, and add as many <img> as you like. Everything else
   comes from js/slideshow.js, so this markup is all a page needs:

     <div class="media ratio-16x9 span-6 reveal" data-slideshow>
       <div class="slides">
         <img src="…" alt="" loading="lazy">
         <img src="…" alt="" loading="lazy">
       </div>
     </div>

   What every slideshow does:
   - advances by itself every 5 seconds, looping endlessly in the same
     direction (after the last slide the first slides in from the right);
   - dot indicators along the bottom (no arrow buttons — the cursor does
     that job);
   - click the left half to go back a slide, the right half to go forward
     (the cursor turns into a white ← or → to show which);
   - can't be dragged, swiped or scrolled by hand.
*/
.slides {
  position: absolute;
  inset: 0;
  display: flex;
  /* hidden, not auto: js scrolls the strip, the user can't (no drag, swipe,
     trackpad or wheel scrolling sideways). Vertical page scroll still works. */
  overflow-x: hidden;
  touch-action: pan-y;
  scroll-snap-type: x mandatory;
}
.slides img {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  object-fit: cover;
  scroll-snap-align: start;
  user-select: none;
  -webkit-user-drag: none;
}
/* Click-to-navigate cursors (slideshow and gallery): js sets
   data-side="left|right" on the component from where the mouse is. A plain
   white arrow (single colour, no outline); the hotspot is the middle of
   the arrow. */
[data-slideshow][data-side="left"] .slides,
[data-gallery][data-side="left"] .gallery-track {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M30 20H8M17 11l-9 9 9 9'/%3E%3C/svg%3E") 20 20, w-resize;
}
[data-slideshow][data-side="right"] .slides,
[data-gallery][data-side="right"] .gallery-track {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 20h22M23 11l9 9-9 9'/%3E%3C/svg%3E") 20 20, e-resize;
}

/* Dot indicators (slideshow and gallery) — one per slide/photo, bottom
   centre, current one solid. The button is a 20px hit area around an 8px
   dot. */
.slide-dots {
  position: absolute;
  left: 50%;
  bottom: var(--margin);
  translate: -50% 0;
  display: flex;
}
.slide-dot {
  width: 20px;
  height: 20px;
  padding: 0;
  border: 6px solid transparent;
  border-radius: 50%;
  background: #fff;
  background-clip: padding-box;
  opacity: .5;
  cursor: pointer;
  transition: opacity .2s ease;
}
.slide-dot.is-active { opacity: 1; }

/* ---------- Horizontal gallery ----------
   A row of photos, each exactly 4 grid columns wide, that runs off the
   right edge of the browser. It always spans all 6 columns; the photos
   keep a 16:9 shape unless you add a ratio-* class. On phones each photo
   is nearly full width so the next one peeks in. Markup is all a page
   needs — js/slideshow.js does the rest:

     <div class="gallery span-6 reveal" data-gallery>
       <div class="gallery-track">
         <figure class="gallery-item"><img src="…" alt="" loading="lazy"></figure>
         <figure class="gallery-item"><img src="…" alt="" loading="lazy"></figure>
       </div>
     </div>

   What every gallery does — the same as a slideshow:
   - advances by itself every 5 seconds, looping endlessly in one direction
     (after the last photo the first one slides in from the right);
   - dot indicators along the bottom (no arrow buttons — the cursor does
     that job);
   - click the left half to go back a photo, the right half to go forward
     (the cursor turns into a white ← or → to show which);
   - can't be dragged, swiped or scrolled by hand.
*/
.gallery {
  position: relative;
  margin-right: calc(-1 * var(--margin)); /* bleed to the browser's right edge */
}
.gallery-track {
  display: flex;
  gap: var(--gutter);
  /* hidden, not auto: js scrolls the track, the user can't (see Slideshow). */
  overflow-x: hidden;
  touch-action: pan-y;
  scroll-snap-type: x mandatory;
}
.gallery-item {
  /* 4 columns + the gutters between them, measured against the page
     content width (the track is --margin wider than that). */
  flex: 0 0 calc((2 * (100% - var(--margin)) - var(--gutter)) / 3);
  aspect-ratio: var(--ratio, 16 / 9);
  margin: 0;
  overflow: hidden;
  scroll-snap-align: start;
}
.gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}

/* ---------- Project / inner pages ----------
   The menu is always showing (home fades it
   in on scroll; there is nothing to fade here). Content is built from
   <section class="grid"> blocks: inside a block, rows are separated by just
   the gutter (10px); blocks are separated by --space-l (150px on desktop, shrinking at the
   laptop / tablet / phone breakpoints — see tokens.css). The first
   block under the header gets --space-m (40px) when it starts with an
   intro paragraph (.intro), and the block right after that intro sits
   --space-intro (80px) below it. */
.inner-page .site-nav { opacity: 1; pointer-events: auto; }
.inner-page main { padding-bottom: var(--space-bottom); }
.inner-page .grid { row-gap: var(--gutter); }
.inner-page main > section + section { margin-top: var(--space-l); }
.inner-page .intro { padding-top: var(--space-m); }
.inner-page .intro + section { margin-top: var(--space-intro); }

/* Text blocks — headings and paragraphs sit flush (no browser margins). */
:where(.inner-page) :where(h1, h2, p) {
  margin: 0;
  font-size: inherit;
  line-height: inherit;
  font-weight: inherit;
}

/* A heading or paragraph directly above media: --space-s (15px) from the
   text's baseline (the bottom of the letters) to the media. The grid's row
   gap supplies the gutter; this adjusts for the rest, including pulling
   back by the leading under the last line (--body-baseline-gap) so the gap
   is measured from the text, not from its line box. */
.lead-in { margin-bottom: calc(var(--space-s) - var(--gutter) - var(--body-baseline-gap)); }

/* Bottom-align a text block with the media beside it. */
.align-end { align-self: end; }

/* Several paragraphs in one text block: one blank line (a body line height)
   between them. */
.text-stack > * + * { margin-top: var(--body-lh); }

/* Project facts (Type / For / Animated by …): labels left in Medium,
   values right in Regular, tucked into columns 5–6. */
.subtext {
  grid-column: 5 / span 2;
  justify-self: end;
  width: 100%;
  max-width: 266px;
  margin: 0;
}
.subtext div { display: flex; justify-content: space-between; gap: 20px; }
.subtext dt { font-weight: 500; flex: none; }
.subtext dd { margin: 0; font-weight: 400; text-align: right; }

/* A right-hand column (columns 5–6) that stacks several things — the Info
   page puts its facts and the social icons in one. Same position and width
   as .subtext above. */
.aside {
  grid-column: 5 / span 2;
  justify-self: end;
  width: 100%;
  max-width: 266px;
}

/* Social icons: a right-aligned row of SVG links, at the same 50% strength
   as the subtext; on hover each one disappears, like the other links. */
.social {
  display: flex;
  justify-content: flex-end;
  gap: 32px;
  margin-top: 40px;
}
.social a { display: block; line-height: 0; opacity: .5; transition: opacity .12s ease; }
.social a:hover { opacity: 0; }
.social img { width: 24px; height: 24px; display: block; }

/* Links to pages that aren't built yet carry data-soon="where-it-will-go"
   instead of an href: they look and react to hover like any other link but
   go nowhere. To switch one on, swap data-soon for href. */

/* Underlined inline link (same look as the home title paragraph's). */
.link-underline {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
  text-decoration-color: var(--text);
  transition: color .12s ease;
}
.link-underline:hover { color: transparent; }

/* ---------- Header ----------
   Only the logo + nav bar is sticky, and it never changes size — no more
   animating height. The paragraph lives in normal document flow (see
   .tagline-row) and scrolls with the page like anything else; its fade
   is driven directly by scroll position in home.js rather than a
   discrete collapse toggle. This is what keeps the logo genuinely
   fixed, the rest of the page scrolling in one piece, and the paragraph
   fading smoothly in lockstep with the scroll instead of a separate
   timed animation that can drift out of sync with it.

   The header is transparent on every page — nothing sits behind the logo
   and nav, so content scrolls visibly underneath them. On home the
   paragraph is pulled up to overlap this header's own box (see the
   negative margin on .tagline-row) so the two align on the same top
   edge; a solid background would also abruptly clip that paragraph as it
   slides up and fades.

   Because it overlaps the paragraph, its empty space would otherwise sit
   on top and swallow clicks meant for the paragraph's links ("motion
   graphics", "risograph") even though nothing is visibly there —
   pointer-events follows the box, not the background's transparency.
   pointer-events: none here lets clicks fall through to whatever's
   underneath by default; the logo and nav opt back in individually since
   those are the only parts of this bar actually meant to be clickable. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
  pointer-events: none;
}

.header-bar {
  position: relative;
  height: var(--bar-h);
}

/* Only the mark itself is a link. Stays put and fully visible at all
   times — it never swaps for a smaller mark or fades. */
.brand-icon-link {
  position: absolute;
  top: var(--margin);
  left: var(--margin);
  display: block;
  line-height: 0;
  pointer-events: auto;
}

.brand-icon {
  width: 44px;
  height: auto;
  display: block;
  object-fit: contain;
}

/* The paragraph's row — normal document flow, not part of the sticky
   bar, so it scrolls away while the bar stays put. The negative
   margin-top pulls it back up by the bar's own height so it starts at
   the very top of the page instead of below the bar, and padding-top
   then pushes its actual content down by --margin — landing the
   paragraph's top edge on the exact same line as the logo's. This is
   also what makes the header transparency above necessary: this row now
   visually sits *underneath* the sticky bar's box on load, not after it. */
.tagline-row {
  /* Columns, gutter and side margins come from the shared .grid class
     (the paragraph itself is span-4 start-3 in the HTML). */
  margin-top: calc(-1 * var(--bar-h));
  padding-top: var(--margin);
}
.tagline {
  margin: 0;
  font-size: var(--title-size);
  line-height: var(--title-lh);
  /* This transition value is only ever "live" during the one-time
     load-in below (home.js sets it inline, then clears it once done).
     After that, home.js sets opacity directly on every scroll frame,
     scrubbed 1:1 with scroll position — an active transition here would
     fight that and lag behind it. */
}
.tagline.is-loading {
  opacity: 0;
  transform: translateY(70px);
}
.tagline-bold {
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: var(--tracking-sans);
}
.tagline-italic {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: normal;
}
/* Same "text goes invisible on hover" behavior as .nav-link, but scoped
   to color (not opacity) so the underline/circle — explicitly colored,
   not relying on currentColor — stay visible while the text itself
   disappears. */
.tagline-underline {
  text-decoration: underline;
  text-decoration-style: solid;
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
  text-decoration-color: var(--text);
  transition: color .12s ease;
}
.tagline-underline:hover {
  color: transparent;
}
.tagline-circle {
  position: relative;
  display: inline-block;
  text-decoration: none;
  transition: color .12s ease;
}
.tagline-circle:hover {
  color: transparent;
}
.tagline-circle::after {
  content: "";
  position: absolute;
  left: -6px;
  right: -6px;
  top: -8px;
  bottom: -4px;
  border: 1.5px solid var(--text);
  border-radius: 50% 50% 48% 52% / 55% 48% 52% 45%;
  transform: rotate(-4deg);
  pointer-events: none;
}

/* The one divider. Full document width (a plain block, outside any
   padded/grid container, so nothing insets it). Native position:sticky
   — not JS, not opacity-driven — is what gives the "scrolls 10px below
   the paragraph, then locks 10px below the logo and stays" behavior:
   the browser handles the transition between those two states itself,
   perfectly in sync with scroll, so it can't visibly swap or drift.
   Always full opacity once its load-in (below) finishes; it does not
   fade with the paragraph. */
.page-divider {
  /* Hidden while trying a cleaner header design without the line — the
     rest of the rules are left in place so it's a one-line change to
     bring back. */
  display: none;
  position: sticky;
  /* var(--bar-h) already reaches 10px below the logo's own visual
     bottom edge (10px top + 44px tall = 54px, and --bar-h is 64px) —
     adding another +10px here double-counted that gap, landing the
     line 20px below the logo instead of 10px. It also left a 10px band
     with no opaque backdrop between the header's own background (which
     stops at --bar-h) and the line, so scrolled-up thumbnails were
     visible peeking through that gap. Sticking flush at --bar-h fixes
     both: the header's opaque background and this line's position now
     meet with no gap between them. */
  top: var(--bar-h);
  height: 1.25px;
  margin-top: 10px;
  margin-bottom: 40px;
  background: var(--text);
  z-index: 40;
}
.page-divider.is-loading {
  opacity: 0;
  /* Matches .tagline's own 70px exactly, not just its timing/curve —
     with the same duration and easing but a much smaller distance
     (this used to be 16px), the last few percent of progress covers so
     few pixels here that it reads as "already stopped" well before the
     paragraph, even though the transition math is identical. Same
     distance is what makes them look like one settling motion. */
  transform: translateY(70px);
}

/* The nav becomes visible as soon as the paragraph has fully faded out
   (home.js toggles .is-visible at that point, with a bit of hysteresis
   around the threshold so it can't flicker) — a real CSS transition with
   a 100ms delay, so there's a clean beat before it appears. Pinned to the
   same margin/44px-tall band as the logo so the two line up. */
.site-nav {
  position: absolute;
  top: var(--margin);
  right: var(--margin);
  height: 44px;
  display: flex;
  align-items: center;
  gap: 28px;
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s ease;
}
.site-nav.is-visible {
  opacity: 1;
  pointer-events: auto;
  transition: opacity .3s ease .1s;
}
.nav-link {
  text-decoration: none;
  color: var(--text);
  /* H1 style — same as the thumbnail text. */
  font-size: var(--h1-size);
  line-height: var(--h1-lh);
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: var(--tracking-sans);
  transition: opacity .12s ease;
}
.nav-link:hover {
  opacity: 0;
}

/* ---------- Work grid ---------- */
.work-grid {
  /* Columns, gutter, row gap and side margins come from the shared .grid
     class; each card says span-2 or span-4 in the HTML. */
  padding-top: 180px;
  padding-bottom: var(--margin);
}

.project-card {
  display: block;
  text-decoration: none;
  color: inherit;
  /* Grid items stretch to the tallest card in their row by default, which
     made a shorter card's empty space below its caption part of the link
     (hover + click). Hugging its own content keeps the hit area to the
     image and caption only. */
  align-self: start;
}

.thumb {
  position: relative;
  width: 100%;
  overflow: hidden;
  /* A hairline radius, not a visible design choice — Chrome's rectangular
     overflow-clip on transformed video uses a different (buggy)
     rasterization path than a rounded one, and even 1px is enough to
     route it onto the clean path and get rid of the black seam that shows
     up along the edge during the hover-scale. clip-path is kept too as a
     second, independent way of expressing the same clip. */
  clip-path: inset(0 round 1px);
  border-radius: 1px;
  background: transparent;
  transform: translateZ(0);
  backface-visibility: hidden;
}
.thumb img,
.thumb video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Slightly overscaled so it always overflows .thumb's clip edge by a
     safe margin — belt-and-suspenders alongside the layer isolation
     above, so no subpixel rounding gap is ever geometrically possible. */
  transform: scale(1.02);
}
.thumb--1x1 { aspect-ratio: 1 / 1; }
.thumb--3x4 { aspect-ratio: 3 / 4; }
.thumb--16x9 { aspect-ratio: 16 / 9; }

/* Thumbnail + title scale down together as one rigid unit — the whole
   card, not the two pieces independently (a separately-transformed
   .thumb/.project-meta was tried to work around the black-seam bug above,
   but scaling from two different centers instead of one wasn't wanted).
   Until the card's reveal finishes, .reveal's own (slow) transition is
   what's live on it. reveal.js then adds .reveal-done, which swaps in a
   fast, symmetric transform transition (300ms easeInOutQuad — same as the
   old Webflow site) so hovering in and un-hovering both use that speed.
   The hover *scale* only exists once .reveal-done is there — hovering
   mid-slide would make `scale()` and the entrance's `translateY()` fight
   over the same `transform` property. Clicking is NOT gated, though:
   .reveal keeps pointer-events on throughout, so the card is a working
   link and shows a pointer cursor from the moment it starts appearing. */
a.project-card.reveal-done {
  transition: transform .3s cubic-bezier(.455, .03, .515, .955);
}
a.project-card.reveal-done:hover {
  transform: scale(.9);
  backface-visibility: hidden;
}

.project-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}
.project-client,
.project-title {
  margin: 0;
  font-size: var(--h1-size);
  line-height: var(--h1-lh);
}
.project-client {
  font-family: var(--font-sans);
  font-weight: 500;
  letter-spacing: var(--tracking-sans);
}
.project-title {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: normal;
}

/* ---------- Scroll reveal (site-wide) ----------
   Add class="reveal" to anything — a video, image, paragraph, card — and
   js/reveal.js fades it in and slides it up as it scrolls into view (and
   staggers items that land in the same row). Same durations and curves as
   the home title paragraph's load-in (ENTRANCE_TRANSITION in home.js), so
   everything settles at the same soothing pace. */
.reveal {
  opacity: 0;
  transform: translateY(120px);
  transition: opacity 1s cubic-bezier(.65, 0, .35, 1), transform 1.6s cubic-bezier(.22, .61, .36, 1);
  /* Clickable and pointer-cursored from the start — nothing about the
     reveal blocks interaction, so there's no dead window where an item
     looks done but doesn't respond yet. */
  pointer-events: auto;
}
.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- Responsive ----------
   Tablet (≤900px): the grid stays 6 columns — the same layout as desktop,
   just narrower; the title paragraph gets smaller type (see tokens.css)
   so it fits in its four columns. Phone (≤560px): everything stacks into
   one column, whatever its span-/start- classes say. */
@media (max-width: 560px) {
  .grid { grid-template-columns: 1fr; }
  .grid > * { grid-column: 1 / -1; }
  /* One column puts the paragraph full-width, so it can't share the
     logo's row any more — drop the overlap and let it sit below the bar. */
  .tagline-row { margin-top: 0; padding-top: 0; }
  .site-nav { gap: 16px; }
  /* Project facts drop under the intro, full width. */
  .subtext, .aside { justify-self: stretch; max-width: none; margin-top: calc(var(--space-s) - var(--gutter)); }
  .aside .subtext { margin-top: 0; }
  .social { justify-content: flex-start; }
  .gallery-item { flex-basis: calc((100% - var(--margin)) * .85); }
}
