/* ============================================
   FONTS
   ============================================ */
@font-face {
  font-family: "DGMFette";
  src: url("fonts/DGMFette.woff2") format("woff2"),
       url("fonts/DGMFette.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "VVDS Fifties";
  src: url("fonts/VVDS-Fifties-Med-Bold.woff2") format("woff2"),
       url("fonts/VVDS-Fifties-Med-Bold.otf") format("opentype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Courier Prime";
  src: url("fonts/courier_prime_regular.woff2") format("woff2"),
       url("fonts/courier_prime_regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Courier Prime";
  src: url("fonts/courier_prime_bold.woff2") format("woff2"),
       url("fonts/courier_prime_bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ============================================
   DESIGN TOKENS
   ============================================ */
:root {
  --paper: #ffffff;
  --ink: #101112;
  --ink-soft: #55585c;
  --line: #e4e4e0;
  --accent: #1e3fff;

  /* site background + header/footer band */
  --site-bg: #cbe8b6;
  --band-bg: #6b8d53;

  /* project card colors */
  --c-blue: #1e3fff;
  --c-yellow: #ffd400;
  --c-pink: #ff3d8a;
  --c-green: #17c97b;
  --c-black: #101112;

  --font-display: "DGMFette", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-body: "Courier Prime", "Courier New", Courier, monospace;

  --max-width: 1200px;
  --gutter: clamp(20px, 5vw, 64px);
}

/* ============================================
   RESET & BASE
   ============================================ */
* {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  /* always reserve space for the vertical scrollbar, whether or not the
     page is currently tall enough to need it. Without this, any small
     height change (like the footer's input/link swap) can tip the page
     just over or under the viewport height, causing the scrollbar to
     appear/disappear — which changes the available viewport width and
     visibly shifts anything sized with vw units, including the crate. */
  overflow-y: scroll;
}

body {
  margin: 0;
  background-color: var(--site-bg);
  background-image: url("textures/texture-bg.png");
  background-repeat: repeat;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  /* clip, where supported: stops sideways scroll the same way, but unlike
     hidden it doesn't turn the body into a scroll container, which was
     silently stopping every position: sticky on the site (the header band,
     for one, never actually came back on scroll-up) */
  overflow-x: clip;
}

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

a {
  color: inherit;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  margin: 0;
  line-height: 1.05;
}

p {
  margin: 0;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

button {
  font-family: inherit;
}

/* visible keyboard focus everywhere */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ============================================
   SCROLL REVEAL
   The .reveal-hidden class is only ever added by JS (see script.js), so if
   JS fails or doesn't run, elements simply stay in their normal visible state.
   ============================================ */
.reveal-hidden {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.7s cubic-bezier(0.22, 0.8, 0.24, 1),
              transform 0.7s cubic-bezier(0.22, 0.8, 0.24, 1);
}

.reveal-hidden.is-visible {
  opacity: 1;
  transform: none;
}

/* ============================================
   LAYOUT SHELL
   ============================================ */
.wrap {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* ============================================
   NAV
   ============================================ */
.site-header-band,
.site-footer-band {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  background-color: var(--band-bg);
  background-image: url("textures/texture-header.png");
  background-repeat: repeat;
}

.site-header-band {
  position: sticky;
  top: 0;
  z-index: 50;
  /* opacity, not transform:translateY — a sliding reveal moves the band's
     bottom edge into the viewport before its top edge, which means
     whatever sits lower in the stack (Copywriter) becomes visible before
     whatever sits higher (Jake Gunther) during the ~0.35s transition. An
     earlier version used translateY and could be caught mid-slide in
     exactly that half-revealed state. Opacity fades the entire band,
     including everything inside it, in and out together — there's no
     way for only part of it to be visible partway through. */
  transition: opacity 0.3s ease;
}

.site-header-band.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* About page: while the record is lifted, the header fades upward out of
   its way (body.record-lifted, set by the About record block in script.js) */
.site-header-band { transition: opacity 0.3s ease, transform 0.35s ease; }
body.record-lifted .site-header-band {
  opacity: 0;
  transform: translateY(-40%);
  pointer-events: none;
}

/* phones: the header just scrolls away with the page (it's ~225px tall
   there, too big to keep sliding back in) — until the mobile pass */
@media (max-width: 700px) {
  .site-header-band { position: relative; }
}

.site-nav {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  row-gap: 14px;
  /* the brand block (name + title stacked) is inherently much taller than
     the footer's single line of text, so matching padding alone (as tried
     before) can't equalize total height — this uses minimal padding on the
     header and much more on the footer to close that gap. See
     design-guidelines.md for the reasoning if this needs revisiting. */
  padding: 10px var(--gutter);
  max-width: var(--max-width);
  margin: 0 auto;
}

.site-nav__brand {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  line-height: 1.15;
}

/* Shifts the brand left to align the "J" with the crate's first (cream)
   record, which sits at 2.687vw since the crate is full-bleed. The nav
   itself is constrained to max-width 1200px + centered + padded, a totally
   different coordinate system — this calc() bridges the two. Only applied
   above 1280px, since that's where --gutter is reliably maxed at 64px and
   the math holds; below that, the gutter itself shrinks (clamp) and this
   formula would no longer be accurate, so the brand just uses its normal
   position instead of an incorrect shift. */
@media (min-width: 1280px) {
  .site-nav__brand {
    margin-left: calc(-47.313vw + 536px);
  }
}

/* the JG logo: only shown in the phone header of project pages (MOBILE PASS) */
.site-nav__jg { display: none; }

.site-nav__name {
  font-family: var(--font-display);
  font-size: clamp(34px, 6vw, 60px);
  font-weight: 400;
}

.site-nav__title {
  font-family: var(--font-body);
  font-size: clamp(18px, 3vw, 36px);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
  margin-top: 6px;
}

/* Header JG: once you've scrolled, the name folds into the JG logo (while
   the header is hidden), and unfolds back at the top. See script.js. The
   folded pieces only hide (opacity), so the header never changes size. */
.site-nav__brand { position: relative; }
.site-nav__brand .jg-word { white-space: nowrap; }
.site-nav__brand .jg-part { display: inline-block; white-space: pre; }
.site-nav__brand .jg-baseline { display: inline-block; width: 0; height: 0; }
.site-nav__brand.jg-drawn .jg-J,
.site-nav__brand.jg-drawn .jg-G { color: transparent; }  /* drawn on the canvas instead */
/* "ake" / "unther" wait until the G has slid past "ake" (~0.33s into its
   0.6s move), then slide out from behind the J and G */
.site-nav__brand .jg-fill {
  transition: opacity 0.35s ease 0.33s, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.33s;
}
.site-nav__brand .site-nav__title { transition: opacity 0.35s ease 0.2s; }
.site-nav__brand.jg-folded .jg-fill { opacity: 0; transform: translateX(-0.12em); }
.site-nav__brand.jg-folded .site-nav__title { opacity: 0; }
.jg-canvas { position: absolute; pointer-events: none; }

.site-nav__links {
  display: flex;
  gap: 32px;          /* was set inline in the header (which always won); phones scale it down below */
}

/* thin vertical divider between nav items — visually separates them as
   two distinct things, not just spaced-apart text. :not(:first-child)
   rather than targeting the second item specifically, so this still
   works correctly if a third nav item is ever added. */
.site-nav__links li:not(:first-child) {
  border-left: 1px solid var(--ink-soft);
  padding-left: 16px;
}

/* mirrors .site-nav__brand's left-shift calc (see that rule for the full
   derivation) so the links sit the same distance from the right edge as
   the brand does from the left — same technique, opposite direction.
   Only applied above 1280px for the same reason the brand's shift is:
   below that, --gutter itself starts shrinking (it's a clamp()), so a
   fixed-viewport-width formula stops being accurate. */
@media (min-width: 1280px) {
  .site-nav__links {
    margin-right: calc(-47.313vw + 536px);
  }
}

.site-nav__links a {
  font-family: var(--font-body);
  font-weight: 400;
  text-decoration: none;
  font-size: 27px;
  position: relative;
  display: inline-block;
  color: var(--ink);
  padding-bottom: 4px;
  transition: font-weight 0.2s ease;
}

.site-nav__links a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: 0;
  height: 2px;
  background: currentColor;
  transition: right 0.3s ease;
}

.site-nav__links a:hover {
  font-weight: 800;
}

.site-nav__links a:hover::after {
  right: 0;
}

/* ============================================
   HOMEPAGE HERO
   ============================================ */
.hero {
  padding: clamp(60px, 12vh, 140px) 0 clamp(40px, 8vh, 80px);
}

.hero h1 {
  font-size: clamp(40px, 7vw, 84px);
  max-width: 14ch;
}

.hero p {
  margin-top: 24px;
  max-width: 46ch;
  font-size: 18px;
  color: var(--ink-soft);
}

/* ============================================
   CRATE SECTION — full-bleed section built from real, unedited photos.
   backdrop_full.png (the "point A" rest-state crate) is ALWAYS visible and
   NEVER changes — this guarantees the wood and the 4 untouched records can
   never shift, drift, or double-expose, no matter what the middle record
   does. middle_hover_tight.png is a small, tightly-cropped piece of the
   "point B" photo, covering only the middle record + its vinyl reveal —
   confined to that small area, on purpose, so nothing outside it can be
   affected. ONLY the middle slot has any hover behavior at all (see the
   crate JS in script.js) — the other 4 are dim-and-navigate hit-zones only.
   ============================================ */
.crate-outer {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  margin-top: clamp(24px, 5vh, 70px);
  margin-bottom: clamp(60px, 10vh, 110px);
}

.crate-box {
  position: relative;
  width: 100%;
  aspect-ratio: 1749 / 887;
}

/* the backdrop never changes — always visible, full opacity, no exceptions.
   This guarantees the wood and the 4 untouched records can never shift or
   drift even slightly, since they're always the exact same static pixels. */
.crate-backdrop {
  display: block;
  width: 100%;
  height: auto;
  position: relative;
  z-index: 1;
}

/* a slot's resting cover art, replacing that slot's plain record in the
   backdrop photo. Sized to the printed record's face (not the hover
   overlay's box) and clipped at the front wall via overflow, so the bottom
   of the square art tucks behind the wood like the other records. Paints
   above the backdrop (later in the DOM, same z-index) and below the hover
   overlays (z-index 2). */
.crate-record-rest {
  position: absolute;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.crate-record-rest img {
  display: block;
  width: 100%;
  height: auto;
}

/* each slot's own clean record cutout, positioned to land bottom-flush
   against that slot's real sleeve position (zero gap — see
   design-guidelines.md for why this is deliberate, not a shortcut).
   Individually faded in via JS adding .is-visible to the matching one. */
/* each slot's overlay is now a small container holding two independently
   animated layers, split from the same clean photo: the sleeve (on top)
   slides up into place with a little spring/overshoot, and the vinyl
   (behind it) slides out from underneath to the right, slightly after.
   The container itself has no transition — it's the two children's own
   transform + opacity that create the entrance, so the motion reads as
   "the record moves" rather than "a box fades in". */
.crate-record-overlay {
  position: absolute;
  z-index: 2;
  pointer-events: none;
  /* clips anything that extends past this container's bounds — this is
     what makes the resting translateY(22px) offset on the sleeve/vinyl
     actually hide their bottom portion, the same way the crate's front
     wall physically hides the bottom of the other records in the
     backdrop photo itself. Without this, the offset just shifts the
     image down into empty space rather than clipping it. */
  overflow: hidden;
}

/* every vinyl overlay sits above every sleeve overlay (z-index 3 vs 2).
   A vinyl never overlaps its OWN sleeve (its track is clipped to start at
   the sleeve's edge), so this keeps each sliding disc in front of the
   neighboring record no matter which order records were hovered/left. */
.crate-record-overlay--vinyl {
  z-index: 3;
}

/* ---- record hover motion ----
   Two layers of motion, each on its own element so nothing competes:
   - .crate-record-lift  moves the WHOLE record (sleeve + vinyl) up out of
     the crate, so the vinyl always travels with its sleeve.
   - .crate-record-vinyl slides sideways inside .crate-record-vinyl-track,
     a clip box that starts exactly at the sleeve's right edge. At rest the
     vinyl sits entirely left of that edge (hidden behind the sleeve), so
     it genuinely emerges from behind it — no opacity fade on the disc.
   Only transform/opacity animate, on layers promoted up front
   (will-change) so Safari doesn't create/destroy layers mid-hover, which
   is what causes a first-frame hitch and an end-of-animation snap.
   Enter and exit use different timing (transition values on the
   destination state): on enter the record lifts, the vinyl follows ~70ms
   later; on exit the vinyl slides home first, then the record settles.
   No overshoot curves — they read as springy/mechanical at this size. */
.crate-record-lift {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(22px) rotate(-2deg);
  will-change: transform, opacity;
  /* exit */
  transition: transform 480ms cubic-bezier(0.45, 0, 0.2, 1) 90ms,
              opacity 240ms ease 280ms;
}

/* a record whose cover is already showing at rest (paired with a
   .crate-record-rest layer): visible from the start, sitting exactly over
   the resting cover, lifted by --rest-drop (the share of the square sleeve
   hidden behind the wall, set per slot inline) so it ends bottom-flush.
   Its overlay is sized to the resting face (see index.html), so it only
   ever moves straight up — no scaling. */
.crate-record-lift--at-rest {
  opacity: 1;
  transform: translateY(var(--rest-drop, 7.31%));
}

.crate-record-overlay.is-visible .crate-record-lift {
  opacity: 1;
  transform: translateY(0) rotate(0deg);
  /* enter */
  transition: transform 560ms cubic-bezier(0.22, 0.8, 0.24, 1) 0ms,
              opacity 180ms ease-out 0ms;
}

.crate-record-sleeve {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  display: block;
  z-index: 2;
}

.crate-record-vinyl-track {
  position: absolute;
  top: 0;
  height: 100%;
  z-index: 1;
  overflow: hidden;
}

.crate-record-vinyl {
  display: block;
  width: 100%;
  height: 100%;
  transform: translateX(-100%);
  will-change: transform;
  /* exit: slides straight back in, easing into the sleeve */
  transition: transform 400ms cubic-bezier(0.5, 0, 0.25, 1) 0ms;
}

.crate-record-overlay.is-visible .crate-record-vinyl {
  transform: translateX(0);
  /* enter: gentle pickup, long soft settle */
  transition: transform 720ms cubic-bezier(0.33, 0, 0.12, 1) 70ms;
}

/* Every hit-zone is positioned independently (not flexbox), so none of them
   can shift based on another being added/removed/resized. Every one is
   tight to its own record's real visible area — hovering the wood or blank
   space around a record doesn't trigger anything, only the record itself
   does — and every one has the same hover effect now. */
.crate-hit {
  position: absolute;
  display: block;
  text-decoration: none;
  z-index: 5;
}

/* ============================================
   FOOTER
   ============================================ */
.site-footer {
  padding: 52px var(--gutter);
  max-width: var(--max-width);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 16px 40px;
  align-items: baseline;
  justify-content: space-between;
}

/* Site-wide footer (started as homepage-only, now on every page by direct
   request — the class name is a holdover): text and icons at opposite ends
   of the container, level with each other. */
.site-footer--home {
  /* text at the start (left) and icons at the end (right) of this
     already-symmetric, centered, equally-padded container — this gives
     the text the same distance from the screen's left edge as the icons
     have from the screen's right edge, automatically, since the
     container itself is symmetric. An earlier version grouped both
     together at the right via flex-end, which didn't have this property. */
  justify-content: space-between;
  align-items: center;
  /* overrides .site-footer's 1200px max-width, which was the actual
     reason the text and icons felt cramped together — the homepage
     footer band is already full-bleed (100vw), so this lets both sides
     spread toward the real screen edges (inset only by --gutter) instead
     of being pulled inward to a 1200px centered column. */
  max-width: none;
}

/* Matches .site-nav__links a's font/underline mechanics — same family of
   interactive text link, different element since this lives in the
   footer. Prefixed with .site-footer for extra specificity: written as
   plain .site-footer__prompt, this rule was silently losing to the older,
   more specific .site-footer a / .site-footer a:hover rules below (meant
   for the plain email/Instagram links on every other page) — which is
   why earlier attempts to change this text's size and hover color had no
   visible effect despite looking correct in the code. */
.site-footer .site-footer__prompt {
  font-family: var(--font-body);
  font-weight: 400;
  text-decoration: none;
  font-size: 45px;
  position: relative;
  display: inline-block;
  color: var(--ink);
  padding-bottom: 4px;
  transition: font-weight 0.2s ease;
}

.site-footer .site-footer__prompt::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: 0;
  height: 2px;
  background: currentColor;
  transition: right 0.3s ease;
}

.site-footer .site-footer__prompt:hover {
  font-weight: 600;
  color: var(--ink);
  transform: none;
}

.site-footer .site-footer__prompt:hover::after {
  right: 0;
}

.site-footer__icons {
  display: flex;
  align-items: center;
  gap: 20px;
}

.site-footer__icons img {
  height: 52px;
  width: auto;
  display: block;
}

.site-footer__icons a {
  display: flex;
  align-items: center;
  opacity: 0.85;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.site-footer .site-footer__icons a:hover {
  opacity: 1;
  transform: scale(1.08);
}

.site-footer a {
  text-decoration: none;
  font-size: 15px;
  display: inline-block;
  transition: color 0.2s ease, transform 0.2s ease;
}

.site-footer a:hover {
  color: var(--accent);
  transform: translateY(-2px);
}

/* ============================================
   PROJECT PAGE
   ============================================ */
.project-header {
  padding: clamp(36px, 7.5vh, 75px) 0 40px;
  /* no bottom border — the thin light line under the description was
     removed by request (Sept 2026); spacing stays the same */
  margin-bottom: clamp(40px, 8vh, 80px);
}

.project-header__back {
  display: inline-block;
  font-size: 14px;
  text-decoration: none;
  color: var(--ink-soft);
  margin-bottom: 28px;
}

.project-header__back:hover {
  color: var(--ink);
}

.project-header h1 {
  font-family: "VVDS Fifties", var(--font-display);
  font-size: 140px; /* starting point — the actual size is set by JS
                        (see script.js, "project title size"): the same
                        size on every project page, the one at which
                        "Diet Coke Cherry" fills the width. This value
                        only matters as a fallback if JS fails to run. */
  line-height: 1.2; /* wider than the global h1/h2/h3/h4 value (1.05) —
                        that tighter value works fine at this page's
                        previous, much smaller H1 size, but clips
                        descenders (a "y" losing its tail) once JS scales
                        titles up to several hundred px. Specific to this
                        selector only, not changed globally. */
  text-align: center; /* titles are one size now, so shorter ones don't fill
                          the width — centered, they line up with the
                          centered tagline and summary below */
  white-space: nowrap;
  overflow: hidden; /* safety net matching the no-JS fallback above —
                        without this, a title that doesn't get resized
                        would overflow past the page edge instead of
                        just being visually too small */
  /* full-bleed — breaks out of .wrap's normal centered/padded width to
     span the full viewport, same technique as .project-media,
     .crate-outer, .site-header-band elsewhere on the site. Deliberate:
     the title is meant to stretch the width of the page itself, not
     stay constrained to the same column the back-link/tagline/summary
     text sit in — those stay put, only the h1 breaks out. A small
     horizontal padding keeps the text from touching the literal screen
     edge, since 100vw with no padding would put the first/last letter
     flush against it. */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 0 24px;
  box-sizing: border-box;
}

/* Subheading under the project title. Full-bleed like the h1 above and
   centered on the page, with the summary below centered to match — the
   title spans edge to edge, so the tagline + summary sit centered under
   it as one symmetric block. */
.project-header__tagline {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(28px, 4.4vw, 72px);
  line-height: 1.1;
  letter-spacing: 0.04em;
  text-align: center;
  margin-top: 0;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 0 24px;
  box-sizing: border-box;
}

.project-header__summary {
  margin-top: 20px;
  margin-left: auto;
  margin-right: auto;
  font-size: clamp(20px, 2.5vw, 28px);
  line-height: 1.5;
  max-width: 62ch;
  text-align: center;
  color: var(--ink-soft);
}

.project-body {
  padding-bottom: clamp(60px, 10vh, 120px);
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 8vh, 72px);
}

.project-body p {
  max-width: 62ch;
  font-size: 18px;
  line-height: 1.65;
}

/* full-bleed (the base; since Sept 25 images, sets and landscape videos
   are framed in the content column from 701px up, see "FRAMED MEDIA"):
   breaks out of .wrap's normal centered/padded width to span
   the full viewport edge-to-edge, same technique used for
   .site-header-band and .crate-outer elsewhere on the site. Only width
   is set to 100vw — height is always auto, based on the media's own real
   aspect ratio, so nothing is stretched or distorted. This means a tall
   (portrait) image or video can end up taller than the viewport itself;
   that's expected, not a bug — the lightbox (see below) is what actually
   gives a fit-to-screen view. */
.project-media {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}

.project-media img {
  cursor: pointer;
}

/* for a set of images meant to be seen together as one execution (e.g. a
   billboard series) — a row on desktop, stacking on narrower screens,
   rather than forcing multiple related images into separate full-width
   .project-media blocks meant for distinct standalone pieces */
.project-media-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}

.project-media-grid img {
  width: 100%;
  height: auto;
  display: block;
  cursor: pointer;
}

.project-video {
  width: 100%;
  height: auto;
  display: block;
  cursor: pointer;
}

/* phone/portrait device-mockup video — constrained to a small centered
   width so it's fully viewable without scrolling, unlike every other
   media type on this page which is full-bleed. This is a deliberate
   exception: full-bleed made this specific video enormously tall (a
   portrait clip's height scales up right along with its width), and
   that wasn't wanted here. Not clickable/lightbox-enabled either,
   unlike other media — it's already fully visible, so there's nothing
   for a lightbox to add (see script.js, the lightbox trigger selector
   explicitly excludes .project-video--phone).

   ⚠️ THE CORNER FIX WENT THROUGH THREE ATTEMPTS. Worth understanding all
   three before touching this again:

   1. border-radius + overflow:hidden directly on the <video> tag. This
      was the original approach, and turned out to be a known-unreliable
      technique in some browsers — video decoding can use its own
      compositing layer that doesn't always respect CSS clipping
      correctly, which is very likely why a persistent white border was
      showing despite the video's own source frames (checked thoroughly:
      every corner, multiple frames across the full 15s duration) being
      confirmed clean.

   2. Moving the same border-radius + overflow:hidden + background-color
      onto this wrapping div (.project-video-frame--phone) instead of
      the <video> element — a plain div clips far more reliably. This
      was a real improvement but, per direct user feedback, was not
      sufficient by itself.

   3. THE ACTUAL FIX: the source video file itself was re-encoded with a
      green overlay masking the four corners OUTSIDE the phone's rounded
      shape — see project-videos/jackson-hole-04.mp4 and
      project-images/jackson-hole-04-poster.jpg, both regenerated from
      this process. This was done with a PRECISE SPATIAL MASK (a
      transparent rounded-rectangle cutout over a solid-green image,
      composited over every frame via ffmpeg's overlay filter) — NOT a
      naive "replace all white pixels with green," which would have
      destroyed the phone's own legitimate white UI content (status bar,
      app background, text). The mask's corner curve doesn't perfectly
      match the real photographed phone's curve at every one of the 4
      corners (real photography has slight per-corner variation a single
      uniform radius can't perfectly capture) — this wrapping div's CSS
      border-radius + overflow:hidden (still in place, see below) is
      what catches that last bit of imprecision with a hard, exact clip.
      **Both layers are needed together now — the video-level fix
      handles the color, the CSS-level clip handles any remaining
      geometric imprecision.** Removing either one on its own is likely
      to bring back a visible seam. */
.project-video-frame--phone {
  max-width: 380px;
  margin: 0 auto;
  aspect-ratio: 856 / 1840;
  border-radius: 32px;
  overflow: hidden;
  background-color: var(--site-bg);
}

/* Spotify ad card video — same "constrained width, fully viewable
   without scrolling" treatment as the phone video above, but simpler:
   this source is just a plain rounded-rectangle ad unit with no device
   bezel/hardware buttons, so there's no corner-masking complexity to
   worry about (see the phone video's documentation above and in
   design-guidelines.md for how much more involved that one was). This
   video has real audio meant to be heard, so it's deliberately NOT
   autoplaying — see the HTML for native `controls` instead of
   muted/loop/autoplay, since browsers block audio autoplay without a
   direct user gesture (the same reason the turntable widget requires an
   explicit click). Also excluded from the lightbox, same as the phone
   video — it already has its own native play controls and doesn't need
   a second "click to view" layer on top. */
.project-video-frame--card {
  max-width: 380px;
  margin: 0 auto;
  aspect-ratio: 476 / 966;
  border-radius: 24px;
  overflow: hidden;
}

.project-video-frame--card video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: default;
}

.project-video-frame--phone .project-video--phone {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: default;
}

/* Phone videos whose audio is the point (the Jackson Hole Spotify ad):
   they autoplay muted on scroll like every other project video, and this
   pill, fixed under the phone (it deliberately doesn't float over it),
   turns sound on (restarting the ad so it's heard from the top). Clicking the video itself does the same, hence the
   pointer. No native controls at all — so no fullscreen, picture-in-
   picture or cast buttons to hide. Behaviour lives in script.js. Styled
   from the site's existing pieces: Courier Prime Bold caps like the
   project tagline, an ink outline pill (the 100px pill radius). */
.project-video-frame--phone .project-video--sound {
  cursor: pointer;
}

.video-sound-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 22px auto 0;
  padding: 10px 22px 10px 18px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 18px;
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
  background: transparent;
  border: 2px solid var(--ink);
  border-radius: 100px;
  cursor: pointer;
  transition: background-color 0.2s ease, color 0.2s ease;
}

.video-sound-toggle__icon {
  width: 22px;
  height: 22px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.video-sound-toggle__icon--on,
.video-sound-toggle[aria-pressed="true"] .video-sound-toggle__icon--muted {
  display: none;
}

.video-sound-toggle[aria-pressed="true"] .video-sound-toggle__icon--on {
  display: block;
}

/* sound playing = filled pill, so the current state reads at a glance */
.video-sound-toggle[aria-pressed="true"] {
  background: var(--ink);
  color: var(--site-bg);
}

@media (hover: hover) and (pointer: fine) {
  .video-sound-toggle:hover {
    background: var(--ink);
    color: var(--site-bg);
  }
}

/* Phone screenshots — a row of stills from a phone-mockup video (Jackson
   Hole: the three Instagram posts + the profile grid), so visitors can
   read each post's copy at their own pace. Transparent-background WebPs,
   so they sit straight on the page texture here and on the lightbox's
   off-white when clicked (they're .project-media imgs, so the existing
   lightbox picks them up with no extra code). Four across on desktop,
   2 x 2 on narrow screens. */
.project-stills {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 240px));
  justify-content: center;
  gap: 40px;
  padding: 0 24px;
  box-sizing: border-box;
}

.project-stills img {
  width: 100%;
  height: auto;
  display: block;
}

@media (max-width: 860px) {
  .project-stills {
    grid-template-columns: repeat(2, minmax(0, 200px));
    gap: 24px 16px;
    padding: 0 20px;
  }
}

/* ============================================
   INSTAGRAM MOCKUP (shared component)
   Markup: _includes/instagram/post.njk. Spec + how to add a post:
   tools/instagram/README.md. Visual reference: tools/instagram/reference.webp
   (a 920 x 2000 iPhone screenshot). Every size below is in that
   screenshot's own pixels: --r = 1/920 of the screen width, so the whole
   interface scales as one unit at any display size. Don't restyle this per
   post; posts only supply content.
   ============================================ */
.ig-phone {
  position: relative;
  max-width: 380px;                 /* same size as the Jackson Hole phone video */
  margin: 0 auto;
  aspect-ratio: 856 / 1840;         /* the phone frame image */
}
.ig-frame { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; user-select: none; }
.ig-screen {
  position: absolute;               /* the frame's screen opening */
  left: calc(39 / 856 * 100%);
  top: calc(38 / 1840 * 100%);
  width: calc(776 / 856 * 100%);
  height: calc(1764 / 1840 * 100%);
  overflow: hidden;
  background: #fff;
  container-type: inline-size;
  --r: calc(100cqw / 920);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: #0c1014;
  -webkit-font-smoothing: antialiased;
  text-align: left;
}
.ig-screen svg { fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; }

/* status-bar time: drawn here (not baked into the frame) so every post can
   show its own time; sized and placed to match the frame's original digits */
.ig-clock {
  position: absolute; z-index: 2;
  left: calc(99.4 * var(--r)); top: calc(44.6 * var(--r));
  font-size: calc(37.4 * var(--r)); line-height: calc(37.4 * var(--r));
  font-weight: 650; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  color: #000;
}

/* top navigation: floating circular back button, "Posts" + handle */
.ig-nav { position: relative; height: calc(268 * var(--r)); }
.ig-nav__back {
  position: absolute; left: calc(37 * var(--r)); top: calc(144 * var(--r));
  width: calc(100 * var(--r)); height: calc(100 * var(--r));
  border-radius: 50%; background: #fff;
  box-shadow: 0 calc(3 * var(--r)) calc(26 * var(--r)) rgba(0, 0, 0, 0.075);
}
.ig-screen .ig-nav__back svg { display: block; width: 100%; height: 100%; stroke-width: 4.5; }
.ig-nav__title, .ig-nav__handle { position: absolute; left: 0; right: 0; text-align: center; white-space: nowrap; }
.ig-nav__title { top: calc(152.8 * var(--r)); font-size: calc(33 * var(--r)); line-height: calc(39 * var(--r)); font-weight: 700; }
.ig-nav__handle { top: calc(197 * var(--r)); font-size: calc(22 * var(--r)); line-height: calc(26 * var(--r)); }

/* account row: avatar, bold handle, badge, two-line menu */
.ig-acct { display: flex; align-items: center; height: calc(121 * var(--r)); padding: 0 calc(29 * var(--r)) 0 calc(24 * var(--r)); }
.ig-acct__avatar { width: calc(74 * var(--r)); height: calc(74 * var(--r)); border-radius: 50%; border: 1px solid #efefef; box-sizing: border-box; flex: none; display: block; object-fit: cover; }
.ig-acct__text { display: flex; flex-direction: column; margin-left: calc(23 * var(--r)); }
.ig-acct__line { display: inline-flex; align-items: center; gap: calc(14 * var(--r)); }
.ig-acct__handle { font-size: calc(27 * var(--r)); line-height: calc(32 * var(--r)); font-weight: 600; }
.ig-screen .ig-acct__badge { width: calc(26 * var(--r)); height: calc(26 * var(--r)); stroke: none; flex: none; position: relative; top: calc(-2 * var(--r)); }
.ig-acct__sub { font-size: calc(22 * var(--r)); line-height: calc(28 * var(--r)); }
.ig-screen .ig-acct__menu { width: calc(45 * var(--r)); height: calc(23 * var(--r)); margin-left: auto; stroke-width: 4; flex: none; position: relative; top: calc(-2 * var(--r)); }

/* media: edge to edge, at the post's own aspect ratio */
.ig-media { position: relative; width: 100%; }
.ig-track {
  display: flex; width: 100%; height: 100%;
  overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none;
  overscroll-behavior-x: contain; outline: none;
}
.ig-track::-webkit-scrollbar { display: none; }
.ig-slide {
  flex: 0 0 100%; width: 100%; height: 100%; object-fit: cover; display: block;
  scroll-snap-align: start; scroll-snap-stop: always;
  user-select: none; -webkit-user-drag: none;
}
.ig-arrow {
  position: absolute; top: 50%;
  width: calc(64 * var(--r)); height: calc(64 * var(--r)); margin-top: calc(-32 * var(--r)); padding: 0;
  border: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.88); color: #262626;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.18);
  display: grid; place-items: center; cursor: pointer;
  opacity: 0; transition: opacity 0.2s ease;
}
.ig-arrow[hidden] { display: none; }
.ig-screen .ig-arrow svg { width: calc(38 * var(--r)); height: calc(38 * var(--r)); stroke-width: 2.4; }
.ig-arrow--prev { left: calc(21 * var(--r)); }
.ig-arrow--next { right: calc(21 * var(--r)); }
.ig-arrow:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) { .ig-media:hover .ig-arrow { opacity: 1; } }
@media (hover: none) { .ig-arrow { display: none; } }   /* touch: swipe, like the app */
/* first view of a carousel: the "next" arrow shows (on touch too), with a
   small nudge, until the first slide change (.ig-hint, set by script.js) */
.ig-hint .ig-arrow--next:not([hidden]) { display: grid; opacity: 1; animation: ig-nudge 1.4s ease-in-out 0.4s 2; }
@keyframes ig-nudge { 0%, 100% { transform: none; } 30% { transform: translateX(calc(10 * var(--r))); } 55% { transform: none; } }
@media (prefers-reduced-motion: reduce) { .ig-hint .ig-arrow--next { animation: none; } }

/* optional ad call-to-action bar (sponsored posts) */
.ig-cta {
  display: flex; align-items: center; justify-content: space-between;
  height: calc(88 * var(--r)); padding: 0 calc(29 * var(--r));
  border-bottom: 1px solid #efefef;
  font-size: calc(27 * var(--r)); font-weight: 600;
}
.ig-screen .ig-cta svg { width: calc(38 * var(--r)); height: calc(38 * var(--r)); stroke-width: 2.1; }

/* carousel dots: centred under the media; 5 visible at most, the ones
   toward an overflow end shrink (full 14, then 9, then 5), active is blue */
.ig-dots { position: relative; height: calc(69 * var(--r)); overflow: hidden; }
.ig-dots--single { height: calc(18 * var(--r)); }
.ig-dots__strip {
  position: absolute; top: 50%; left: 50%; display: flex;
  --shift: 0;
  transform: translate(calc((var(--shift) * -23 - var(--vis) * 11.5) * var(--r)), -50%);
}
.ig-dots__strip span { flex: none; width: calc(23 * var(--r)); height: calc(14 * var(--r)); position: relative; }
.ig-dots__strip span::before {
  content: ""; position: absolute; left: calc(4.5 * var(--r)); top: 0;
  width: calc(14 * var(--r)); height: calc(14 * var(--r)); border-radius: 50%;
  background: #dce0e4; transform: scale(var(--s, 1));
}
.ig-dots__strip span.is-active::before { background: #5267fd; }
.ig-dots.is-ready .ig-dots__strip,
.ig-dots.is-ready .ig-dots__strip span::before { transition: transform 0.25s ease, background-color 0.25s ease; }

/* engagement row: each icon centred in an 83px slot, count right after,
   17.5px to the next slot; bookmark at the far right */
.ig-eng { display: flex; align-items: center; height: calc(55 * var(--r)); padding: 0 calc(14 * var(--r)) 0 calc(13 * var(--r)); gap: calc(17.5 * var(--r)); }
.ig-eng__item { display: inline-flex; align-items: center; }
.ig-screen .ig-eng__icon,
.ig-screen .ig-eng__save { width: calc(55.2 * var(--r)); height: calc(55.2 * var(--r)); padding: 0 calc(13.9 * var(--r)); box-sizing: content-box; flex: none; }
.ig-eng__count { font-size: calc(28 * var(--r)); font-weight: 600; letter-spacing: 0.01em; }
.ig-screen .ig-eng__save { margin-left: auto; }

/* caption (handle bold, text inline, never truncated) and timestamp */
.ig-screen .ig-caption { max-width: none; margin: calc(26 * var(--r)) 0 0; padding: 0 calc(29 * var(--r)); font-size: calc(27 * var(--r)); line-height: calc(32 * var(--r)); }
.ig-caption { white-space: pre-line; }   /* keeps line breaks Jake writes into a caption */
.ig-caption b { font-weight: 600; }
.ig-screen .ig-time { max-width: none; margin: calc(14 * var(--r)) 0 0; padding: 0 calc(29 * var(--r)); font-size: calc(22.5 * var(--r)); line-height: calc(30 * var(--r)); color: #696d70; }

/* a glimpse of the next post: its account row, then its media (Instagram's
   loading grey unless the post supplies next_media) */
.ig-next { margin-top: calc(42.5 * var(--r)); }
.ig-next__media { height: 100cqw; background: #efefef center / cover no-repeat; }
/* the end of the post above, when a phone is shown mid-scroll (post.prev):
   the bottom of its media, then its engagement row, caption and time */
.ig-prev { margin-bottom: calc(42.5 * var(--r)); }
.ig-prev__media { background: #efefef center bottom / 100% auto no-repeat; }

/* profile-grid screen (_includes/instagram/profile.njk): the account's
   profile page in the same frame, units and style as the "Posts" view.
   The same floating back button, the handle as the title, a matching round
   "..." button, then the header (avatar, name, stats, bio, link, buttons,
   tabs) and the photo grid. Header sizes were measured on the Visit Jackson
   Hole profile screen of the original mockup and converted to --r. */
.ig-nav--profile .ig-nav__title--solo { top: calc(174.5 * var(--r)); display: flex; justify-content: center; align-items: center; gap: calc(12 * var(--r)); }
.ig-screen .ig-nav__badge { width: calc(28 * var(--r)); height: calc(28 * var(--r)); stroke: none; flex: none; }
.ig-nav__more {
  position: absolute; right: calc(37 * var(--r)); top: calc(144 * var(--r));
  width: calc(100 * var(--r)); height: calc(100 * var(--r));
  border-radius: 50%; background: #fff;
  box-shadow: 0 calc(3 * var(--r)) calc(26 * var(--r)) rgba(0, 0, 0, 0.075);
}
.ig-screen .ig-nav__more svg { display: block; width: 100%; height: 100%; fill: currentColor; stroke: none; }
.ig-prof { position: relative; height: calc(560.7 * var(--r)); }
.ig-prof > * { position: absolute; margin: 0; max-width: none; white-space: nowrap; }
.ig-prof__avatar {
  left: calc(37.3 * var(--r)); top: calc(23.7 * var(--r));
  width: calc(196.8 * var(--r)); height: calc(196.8 * var(--r));
  border-radius: 50%; border: 1px solid #efefef; box-sizing: border-box; display: block; object-fit: cover;
}
.ig-screen .ig-prof__name { left: calc(295.2 * var(--r)); top: calc(42.7 * var(--r)); font-size: calc(28.6 * var(--r)); line-height: calc(34 * var(--r)); }
.ig-screen .ig-prof__stat { top: calc(111 * var(--r)); }
.ig-prof__stat b, .ig-prof__stat span { display: block; }
.ig-prof__stat b { font-size: calc(35.3 * var(--r)); line-height: calc(42 * var(--r)); font-weight: 600; }
.ig-prof__stat span { margin-top: calc(2.2 * var(--r)); font-size: calc(28.8 * var(--r)); line-height: calc(34 * var(--r)); }
.ig-screen .ig-prof__stat--1 { left: calc(296.4 * var(--r)); }
.ig-screen .ig-prof__stat--2 { left: calc(495.6 * var(--r)); }
.ig-screen .ig-prof__stat--3 { left: calc(710.2 * var(--r)); }
.ig-screen .ig-prof__bio { left: calc(35.6 * var(--r)); top: calc(243 * var(--r)); font-size: calc(33.6 * var(--r)); line-height: calc(40 * var(--r)); }
.ig-screen .ig-prof__link { left: calc(30.4 * var(--r)); top: calc(305.4 * var(--r)); display: flex; align-items: center; gap: calc(13.9 * var(--r)); color: #0a4bc0; font-size: calc(32 * var(--r)); line-height: calc(39 * var(--r)); }
.ig-screen .ig-prof__link svg { width: calc(49.5 * var(--r)); height: calc(49.5 * var(--r)); stroke-width: 2; flex: none; }
.ig-prof__btn {
  top: calc(369.8 * var(--r)); height: calc(74.7 * var(--r));
  display: flex; align-items: center; justify-content: center;
  padding-top: calc(2.4 * var(--r)); box-sizing: border-box;
  border-radius: calc(14 * var(--r)); background: #eceef0;
  font-size: calc(28 * var(--r)); font-weight: 500;
}
.ig-prof__btn--follow { left: calc(28.5 * var(--r)); width: calc(397.1 * var(--r)); background: #0369ff; color: #fff; }
.ig-prof__btn--message { left: calc(435.1 * var(--r)); width: calc(364 * var(--r)); }
.ig-prof__btn--add { left: calc(810.9 * var(--r)); width: calc(81.8 * var(--r)); }
.ig-screen .ig-prof__btn--add svg { width: calc(44 * var(--r)); height: calc(44 * var(--r)); stroke-width: 2.1; }
.ig-prof__tabs { left: 0; right: 0; top: calc(461 * var(--r)); height: calc(99.7 * var(--r)); display: flex; padding-left: calc(4.8 * var(--r)); }
.ig-prof__tab { position: relative; flex: 1 1 0; display: flex; justify-content: center; align-items: center; height: calc(99.8 * var(--r)); padding-bottom: calc(0 * var(--r)); box-sizing: border-box; color: #737373; }
.ig-prof__tab.is-active { color: #0c1014; }
.ig-prof__tab.is-active::after { content: ""; position: absolute; left: 50%; bottom: calc(1.1 * var(--r)); width: calc(118 * var(--r)); height: calc(3.6 * var(--r)); margin-left: calc(-59 * var(--r)); background: #0c1014; }
.ig-screen .ig-prof__tab svg { width: calc(57 * var(--r)); height: calc(57 * var(--r)); stroke-width: 1.75; margin-top: calc(-0.2 * var(--r)); }
.ig-screen .ig-prof__tab.is-active svg { width: calc(49.8 * var(--r)); height: calc(58.1 * var(--r)); }
.ig-screen .ig-prof__tab.is-active svg { fill: currentColor; stroke: none; }
.ig-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: calc(2.4 * var(--r)); background: #fff; }
.ig-grid__tile { display: block; width: 100%; height: auto; object-fit: cover; background: #efefef; user-select: none; -webkit-user-drag: none; }

.placeholder-block {
  width: 100%;
  aspect-ratio: 16 / 10;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 15px;
  color: rgba(255,255,255,0.85);
  border-radius: 4px;
}

/* Shaped placeholders — stand-ins that take up exactly the space the real
   media will, so a page's layout doesn't move when the real image/video is
   swapped in. They mirror the Jackson Hole media blocks: "image" and
   "video" are full-bleed at the real asset's aspect ratio (set inline from
   the CMS, defaulting to Jackson Hole's 1312×1199 mural and 16:9 video),
   "set" is a stacked image set in .project-media-grid, and "phone" uses the
   phone-mockup frame (380px wide, 856:1840, 32px corners). Square edges on
   the full-bleed ones, like the real media. */
.placeholder-block--media {
  border-radius: 0;
}

.project-video-frame--phone.placeholder-block--phone {
  aspect-ratio: 856 / 1840;
  border-radius: 32px;
}

/* Credits: one quiet line at the end of each project, above Previous /
   Next ("credits" in the project's front matter: role, name, optional url).
   Roles in the label face (VVDS Fifties, small caps), names in the body
   face, all in the soft grey; wraps to one credit per line on phones. */
.project-credits {
  margin: 0 auto clamp(8px, 2vh, 24px);
  display: flex; flex-wrap: wrap; justify-content: center;
  column-gap: 28px; row-gap: 8px;
  color: var(--ink-soft);
  font-size: 15px; line-height: 1.5;
  text-align: center;
}
.project-credits__item { display: flex; align-items: baseline; gap: 9px; }
.project-credits__item + .project-credits__item::before {
  content: "·"; margin-right: 19px; color: var(--ink-soft);
}
.project-credits dt {
  font-family: "VVDS Fifties", var(--font-body);
  font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase;
}
.project-credits dd { margin: 0; }
.project-credits a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; text-decoration-color: rgba(85, 88, 92, 0.45); }
@media (hover: hover) { .project-credits a:hover { color: var(--ink); text-decoration-color: currentColor; } }
@media (max-width: 700px) {
  .project-credits { flex-direction: column; align-items: center; }
  .project-credits__item + .project-credits__item::before { content: none; }
}

/* Previous / Back to top + Back to the hits / Next at the bottom of every
   project page.
   Typography only (VVDS Fifties caps, same face as the project titles):
   brand names, no "Previous"/"Next" words, no boxes or pills. Hover (real
   mouse only, per the site's hover/touch rule) grows a black underline
   under the name toward the arrow's direction and nudges the arrow the
   same way; Back to top's underline grows from its centre. Keyboard focus
   shows the same underline. The big bottom padding is deliberate: it's
   what separates this from the footer band so they read as two sections. */
.project-nav {
  /* no top border — the thin light line above these links was removed by
     request (Sept 2026), same as the one under the project description */
  padding: 40px 0 clamp(104px, 16vh, 168px);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "prev top next";
  align-items: center;
  column-gap: 32px;
  font-family: "VVDS Fifties", var(--font-display);
  font-weight: 700;
  font-size: clamp(22px, 2.1vw, 28px);
  line-height: 1.15;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.project-nav a {
  color: var(--ink);
  text-decoration: none;
}

.project-nav__link,
.project-nav__top,
.project-nav__home {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35em;
  padding: 10px 0; /* taller tap target without any visible box */
}

.project-nav__link--prev { grid-area: prev; justify-self: start; }
.project-nav__center     { grid-area: top;  justify-self: center; }

/* Back to top + Back to the hits: two separate links stacked in the middle
   column, each with its own tap area and underline, split by a short thin
   rule so they never read as one phrase. The stack is centred vertically
   against the previous/next links (the grid's align-items: center). */
.project-nav__center {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.project-nav__divider {
  display: block;
  width: 28px;
  height: 1px;
  margin: 6px 0;
  background: var(--ink-soft);
  opacity: 0.45;
}
.project-nav__link--next { grid-area: next; justify-self: end; text-align: right; }

.project-nav__label {
  position: relative;
}

.project-nav__label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.14em;
  height: 2px;
  background: var(--ink);
  transform: scaleX(0);
  transition: transform 0.4s cubic-bezier(0.22, 0.8, 0.24, 1);
}

.project-nav__link--prev .project-nav__label::after { transform-origin: right; }
.project-nav__link--next .project-nav__label::after { transform-origin: left; }
.project-nav__top .project-nav__label::after,
.project-nav__home .project-nav__label::after       { transform-origin: center; }

/* The arrows are drawn, not typed: VVDS Fifties' own arrow glyphs are
   small arrows inside black badges, which don't read as part of the name.
   These are sized to the face's cap height (0.7em) and sit on the baseline,
   with the shaft at its horizontal stroke weight and flat ends like its
   terminals, so they read as the same type. */
.project-nav__arrow {
  display: inline-block;
  transition: transform 0.4s cubic-bezier(0.22, 0.8, 0.24, 1);
}

.project-nav__arrow svg {
  display: inline-block;
  width: 0.868em;  /* 124:100 viewBox at 0.7em tall */
  height: 0.7em;
  overflow: visible;
  fill: none;
  stroke: currentColor;
  stroke-width: 22.5; /* ~0.155em — the face's horizontal stroke weight */
  stroke-linecap: butt;
  stroke-linejoin: miter;
  stroke-miterlimit: 10;
}

@media (hover: hover) and (pointer: fine) {
  .project-nav a:hover .project-nav__label::after { transform: scaleX(1); }
  .project-nav__link--prev:hover .project-nav__arrow { transform: translateX(-4px); }
  .project-nav__link--next:hover .project-nav__arrow { transform: translateX(4px); }
}

.project-nav a:focus-visible .project-nav__label::after { transform: scaleX(1); }

/* Narrow screens: previous + next share the first row (direction stays
   obvious: left and right), the Back to top / Back to the hits stack gets
   its own centred row below. Long
   brand names wrap inside their half instead of shrinking the type. */
@media (max-width: 860px) {
  .project-nav {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "prev next"
      "top  top";
    column-gap: 24px;
    row-gap: 20px;
    align-items: start;
    font-size: 22px;
    padding: 32px 0 104px;
  }

  .project-nav__link,
  .project-nav__top,
  .project-nav__home {
    min-height: 48px;
    padding: 12px 0;
  }

  .project-nav__center { margin-top: 4px; }
  .project-nav__divider { margin: 2px 0; }

  .project-nav__label {
    text-wrap: balance;
  }
}

/* ============================================
   ABOUT PAGE
   ============================================ */
.about-layout {
  padding: clamp(48px, 10vh, 100px) 0 clamp(60px, 10vh, 120px);
  display: grid;
  grid-template-columns: 1.06fr 1.29fr;
  gap: clamp(48px, 8vw, 100px);
  align-items: start;
}

.about-photo {
  border-radius: 4px;
  overflow: hidden;
  /* position:relative (not sticky) — this element scrolls normally with
     the page now, but still needs SOME positioning context, because
     .photo-reveal__overlay inside it uses position:absolute + inset:0 to
     size itself to match this container exactly. Without any positioned
     ancestor here, that overlay has nothing to anchor to and escapes to
     the nearest ancestor that IS positioned (or the page's root, if none
     exists) — stretching to fill THAT instead, which is what happened
     when sticky was removed without replacing it with anything. */
  position: relative;
}

.about-photo img {
  display: block;
  width: 100%;
  height: auto;
}

/* baby photo overlay — fades in on hover, right on top of the headshot in
   the same frame. Same fade technique as the word-trigger popups
   elsewhere on this page (asymmetric visibility delay: instant to show,
   delayed to hide, so it doesn't just vanish — see the word-trigger
   section of design-guidelines.md for why that specific approach is
   needed instead of a plain opacity transition). */
.photo-reveal {
  cursor: pointer;
}

.photo-reveal__overlay {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s ease, visibility 0s 0.35s;
}

@media (hover: hover) and (pointer: fine) {
  .photo-reveal:hover .photo-reveal__overlay,
  .photo-reveal:focus-visible .photo-reveal__overlay {
    opacity: 1;
    visibility: visible;
    transition-delay: 0s;
  }
}

.photo-reveal.is-active .photo-reveal__overlay {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.about-content p {
  font-size: 20px;
  line-height: 1.65;
  max-width: 58ch;
  margin-bottom: 20px;
  color: var(--ink);
}

@media (max-width: 760px) {
  .about-layout {
    grid-template-columns: 1fr;
  }
  .about-photo {
    max-width: 280px;
  }
}

/* ============================================
   ABOUT: LINER NOTES RECORD
   One record in its own crate (about-photos/crate-single.webp, cut from
   Jake's reference, front board shortened), showing its back: the liner
   notes, live text on Jake's distressed paper (about-photos/sleeve-paper.webp).
   Geometry from the crate image (1030 x 875): the sleeve is the 810px
   square at x 108, y 87; the crate's front edge is at y 698, so the bottom
   24.57% of the sleeve (fine print + logo) hides until the record lifts.
   Everything on the sleeve is in cqw (1/100 of the sleeve's width), so it
   scales like print. Layout by the sleeve's
   size (data-size on .liner, set in script.js): full >= 760px (laptops and
   desktops, bio 11px+), mid (tablets: bio below the crate) >= 420px, small (phones:
   tracklist below too). Markup: about.njk.
   ============================================ */
.liner {
  /* crate width: as big as the page allows (legible liner notes), leaving
     room on the left for the vinyl to slide out */
  --cw: min(1180px, 100vw - 2 * clamp(16px, 3vw, 48px));
  padding: 12px 0 clamp(60px, 10vh, 120px);
  width: 100vw; margin-left: calc(50% - 50vw);   /* full width, so the vinyl is only ever clipped at the screen's edge */
  overflow-x: clip;
}
.liner__crate {
  position: relative;
  width: var(--cw);
  margin: clamp(16px, 4vh, 48px) 0 0 50%;
  transform: translateX(-50%);
  aspect-ratio: 1030 / 875;
}
@media (min-width: 1100px) {                /* wide screens: leave room on the left for the vinyl */
  .liner { --cw: min(1180px, 100vw - 2 * clamp(16px, 3vw, 48px) - 120px); }
  .liner__crate { transform: translateX(calc(-50% + 40px)); }
}
.liner__crate-img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.liner__slot {
  position: absolute;
  left: 10.485%; width: 78.641%;
  top: 0; height: 79.771%;                   /* down to the crate's front edge (698 of 875) */
  clip-path: inset(-100vh -100vw 0 -100vw);  /* hides only what's below that edge */
}
.liner__record {
  position: absolute;
  left: 0; top: 12.464%;                     /* the sleeve's top (87 of 698); its bottom 24.57% hides behind the crate at rest */
  width: 100%; aspect-ratio: 1;
  container-type: inline-size;               /* cqw = 1/100 of the sleeve's full width (the sleeve itself has padding) */
  transition: transform 520ms cubic-bezier(0.45, 0, 0.2, 1) 80ms;
  outline: none;
}
.liner__record.is-open,
.liner__record:focus-within { transform: translateY(-24.57%); transition: transform 560ms cubic-bezier(0.22, 0.8, 0.24, 1); }
@media (hover: hover) and (pointer: fine) {
  .liner__record:hover { transform: translateY(-24.57%); transition: transform 560ms cubic-bezier(0.22, 0.8, 0.24, 1); }
}
.liner__vinyl-track { position: absolute; top: 0; right: 100%; width: 31.05%; height: 100%; overflow: hidden; }
.liner__vinyl {
  display: block; width: 100%; height: 100%;
  transform: translateX(100%) scaleX(-1);    /* the homepage's vinyl sliver, mirrored, starting behind the sleeve */
  transition: transform 400ms cubic-bezier(0.5, 0, 0.25, 1);
}
.liner__record.is-open .liner__vinyl,
.liner__record:focus-within .liner__vinyl { transform: scaleX(-1); transition: transform 720ms cubic-bezier(0.33, 0, 0.12, 1) 90ms; }
@media (hover: hover) and (pointer: fine) {
  .liner__record:hover .liner__vinyl { transform: scaleX(-1); transition: transform 720ms cubic-bezier(0.33, 0, 0.12, 1) 90ms; }
}

/* the sleeve: Jake's own distressing (his finished artwork with its printing
   removed, about-photos/sleeve-paper.webp) under the live text, links and
   photo. The printed parts multiply into the paper, so stains darken the
   ink a touch the way real print does, without fading it */
.liner__sleeve {
  position: absolute; inset: 0;
  /* the paper colour sits under the texture so the name's white "cut" lines
     (which multiply into the paper) have something to blend with before the
     texture loads; without it the name shows as a white box for a moment.
     The radius matches the paper's own rounded corners (about 20 of 1254px) */
  background: url("about-photos/sleeve-paper.webp") center / 100% 100% no-repeat, #e8d8c1;
  border-radius: 1.6cqw;
  filter: drop-shadow(0 0.4cqw 1.2cqw rgba(0, 0, 0, 0.22));
  color: #26211c;
  padding: 6.4cqw 7cqw 0;
  box-sizing: border-box;
  font-family: var(--font-body);
}
.liner__sleeve .bio-link, .liner__sleeve .liner__name { color: #221d18; }
.liner__sleeve p { max-width: none; margin: 0; font-size: inherit; line-height: inherit; color: inherit; }
.liner__head { display: flex; justify-content: space-between; align-items: flex-end; align-items: last baseline; gap: 2.4cqw; }
.liner__title { margin: 0; line-height: 1.05; font-weight: 400; letter-spacing: 0; text-transform: none; display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 1.4cqw; row-gap: 0.6cqw; }
/* the name, Bazook-style: Poppins Black with the letters tucked into each
   other; each letter's white outline cuts a thin line into the one behind.
   The whole name multiplies into the paper (white = invisible), so the
   lines show the paper and there's no halo around the outside */
.liner__name {
  font-family: "Poppins", var(--font-display); font-weight: 900; font-size: 6.6cqw; line-height: 1;
  display: inline-flex; align-items: baseline;
  background: #fff; mix-blend-mode: multiply; isolation: isolate; padding: 0 0.06em;
}
.liner__letters { display: inline-flex; align-items: baseline; }
.liner__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.liner__letters > span { position: relative; margin-left: -0.1em; text-shadow: 0.034em 0.000em 0 #fff, 0.031em 0.013em 0 #fff, 0.024em 0.024em 0 #fff, 0.013em 0.031em 0 #fff, 0.000em 0.034em 0 #fff, -0.013em 0.031em 0 #fff, -0.024em 0.024em 0 #fff, -0.031em 0.013em 0 #fff, -0.034em 0.000em 0 #fff, -0.031em -0.013em 0 #fff, -0.024em -0.024em 0 #fff, -0.013em -0.031em 0 #fff, -0.000em -0.034em 0 #fff, 0.013em -0.031em 0 #fff, 0.024em -0.024em 0 #fff, 0.031em -0.013em 0 #fff; }
.liner__letters > span:first-child { margin-left: 0; }
.liner__letters > .liner__sp { width: 0.4em; text-shadow: none; }
.liner__album { font-family: "Unbounded", var(--font-body); font-weight: 400; font-size: 2.9cqw; letter-spacing: -0.02em; }
.liner__sleeve .liner__cat { font-size: 1.3cqw; line-height: 1.35; text-align: right; color: #5b554c; }
/* the email sits where the catalog number was, on the "/ Copywriter" line */
.liner__cat a { color: inherit; text-decoration: none; white-space: nowrap; }
@media (hover: hover) and (pointer: fine) { .liner__cat a:hover { text-decoration: underline; text-underline-offset: 0.3cqw; } }
.liner__cols { display: grid; grid-template-columns: 22fr 52fr 24fr; gap: 2.6cqw; margin-top: 4.4cqw; align-items: start; }
.liner__sleeve .liner__side, .liner__sleeve .liner__label { font-size: 1.35cqw; letter-spacing: 0.08em; text-transform: uppercase; color: #5b554c; margin-bottom: 1.3cqw; }
.liner__tracks ol { list-style: none; margin: 0 0 3cqw; padding: 0; }
.liner__tracks li { margin: 0 0 1.35cqw; font-size: 1.85cqw; line-height: 1.25; white-space: nowrap; }   /* every track name on one line */
.liner__tracks a { color: inherit; text-decoration: none; display: inline-block; }
.liner__tracks small { display: block; font-size: 1.2cqw; letter-spacing: 0.05em; color: #665f55; margin: 0.3cqw 0 0 3cqw; white-space: nowrap; }
.liner__no { display: inline-block; width: 3cqw; }
@media (hover: hover) and (pointer: fine) { .liner__tracks a:hover { text-decoration: underline; text-underline-offset: 0.4cqw; } }
.liner__bio { font-size: 1.46cqw; line-height: 1.4; }
.liner__bio p + p { margin-top: 0.7em; }
.liner__bio .bio-link, .liner__notes .bio-link { font-weight: 700; text-underline-offset: 0.2em; }
.liner__bio .word-trigger__popup { width: 26cqw; }
.liner__photo { margin: 0; }
.liner__photo-frame { position: relative; overflow: hidden; filter: grayscale(1) contrast(1.05); mix-blend-mode: multiply; }
.liner__photo-frame img { display: block; width: 100%; height: auto; aspect-ratio: 1; object-fit: cover; }
.liner__foot {
  position: absolute; left: 7cqw; right: 7cqw; bottom: 5.5cqw;   /* in the band hidden behind the crate at rest */
  display: flex; justify-content: space-between; align-items: flex-end;
}
.liner__sleeve .liner__fine { font-size: 1.15cqw; line-height: 1.4; color: #665f55; }
.liner__logo { width: 8.5cqw; height: auto; display: block; opacity: 0.9; mix-blend-mode: multiply; }
.liner__notes, .liner__notes-tracks { display: none; }
.liner__notes p { font-size: 18px; line-height: 1.65; max-width: 62ch; margin: 0 auto 18px; color: var(--ink); }
.liner__notes .liner__label { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 12px; }

/* mid: the bio moves below the crate; the sleeve keeps the tracklist and a big photo */
.liner[data-size="mid"] .liner__bio, .liner[data-size="small"] .liner__bio { display: none; }
.liner[data-size="mid"] .liner__notes, .liner[data-size="small"] .liner__notes {
  display: block; margin: clamp(28px, 6vw, 48px) auto 0; box-sizing: border-box;
  max-width: calc(62ch + 2 * clamp(16px, 3vw, 48px)); padding: 0 clamp(16px, 3vw, 48px);
}
.liner[data-size="mid"] .liner__cols { grid-template-columns: 1.15fr 1fr; gap: 5cqw; margin-top: 4cqw; }
.liner[data-size="mid"] .liner__name { font-size: 8.6cqw; }
.liner[data-size="mid"] .liner__album { font-size: 4.2cqw; }
.liner[data-size="mid"] .liner__cat { font-size: 2.4cqw; }
.liner[data-size="mid"] .liner__side { font-size: 2cqw; margin-bottom: 1.1cqw; }
.liner[data-size="mid"] .liner__tracks ol { margin-bottom: 2cqw; }
.liner[data-size="mid"] .liner__tracks li { font-size: 2.55cqw; margin-bottom: 1.2cqw; }
.liner[data-size="mid"] .liner__tracks small { font-size: 1.75cqw; margin-left: 4.2cqw; }
.liner[data-size="mid"] .liner__no { width: 4.2cqw; }
.liner[data-size="mid"] .liner__fine { font-size: 1.8cqw; }
.liner[data-size="mid"] .liner__logo { width: 8.5cqw; }

/* small (phones): the sleeve keeps the title, photo and logo; the tracklist joins the bio below */
.liner[data-size="small"] .liner__cols .liner__tracks { display: none; }
.liner[data-size="small"] .liner__cols { grid-template-columns: 1fr; justify-items: center; margin-top: 4cqw; }
.liner[data-size="small"] .liner__photo { width: 40%; }
.liner[data-size="small"] .liner__name { font-size: 10.4cqw; }
.liner[data-size="small"] .liner__album { font-size: 5.2cqw; }
.liner[data-size="small"] .liner__cat { font-size: 3.2cqw; }
.liner[data-size="small"] .liner__fine { font-size: 2.6cqw; }
.liner[data-size="small"] .liner__logo { width: 11cqw; }
.liner[data-size="small"] .liner__notes-tracks { display: block; margin-bottom: 28px; }
.liner__notes-photo { display: none; }
/* phones only: the sleeve (and its email) is hidden there, so the email goes under the bio */
.liner .liner__notes .liner__notes-email { display: none; }
/* phones: no record, just the About page as it was before it: the headshot
   (tap for the baby photo) over the bio, full width */
@media (max-width: 700px) {
  .liner { padding-top: 32px; }
  .liner__crate { display: none; }
  .liner .liner__notes { display: block; margin: 0 auto; padding: 0 clamp(16px, 3vw, 48px); max-width: none; }
  .liner[data-size] .liner__notes-tracks, .liner[data-size] .liner__notes-label, .liner .liner__notes-tracks, .liner .liner__notes-label { display: none; }
  .liner__notes-photo { display: block; position: relative; overflow: hidden; border-radius: 4px; margin-bottom: 36px; }
  .liner__notes-photo img { display: block; width: 100%; height: auto; }
  .liner .liner__notes p { font-size: 20px; line-height: 1.65; max-width: 58ch; margin: 0 0 20px; }
  .liner .liner__notes .liner__notes-email { display: block; margin-top: 32px; }
}
.liner__notes-tracks .liner__side { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); margin: 0 0 8px; }
.liner__notes-tracks ol { list-style: none; margin: 0 0 18px; padding: 0; }
.liner__notes-tracks li { font-size: 18px; line-height: 1.35; margin: 0 0 10px; }
.liner__notes-tracks a { color: inherit; text-decoration: none; }
.liner__notes-tracks small { display: block; font-size: 12px; letter-spacing: 0.06em; color: var(--ink-soft); margin: 2px 0 0 32px; }
.liner__notes-tracks .liner__no { width: 32px; }

/* ============================================
   TURNTABLE WIDGET — fixed bottom-right, present on every page.
   Click (or Enter/Space) toggles play. The record itself is drawn with
   CSS (grooves, label, spindle) rather than a cropped photo, since it
   needs to read correctly from every angle while continuously spinning.
   ============================================ */
.turntable {
  position: fixed;
  /* top/left are set by script.js (not bottom/right) — this is required
     for the drag-and-drop and footer-avoidance logic to both work without
     fighting each other. Default position (bottom-right, matching the
     original look) is computed in JS on load if no saved drag position
     exists. Don't add bottom/right here; it'll conflict. */
  z-index: 200;
  width: 104px;
  height: 104px;
  cursor: grab;
  transition: transform 0.2s ease;
}

.turntable.is-dragging {
  cursor: grabbing;
  transition: none;
}

.turntable:hover {
  transform: scale(1.05);
}

.turntable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 50%;
}

.turntable__base {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #2a2420, #14100e 75%);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.4), inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}

.turntable__record {
  position: absolute;
  inset: 7%;
  border-radius: 50%;
  background:
    repeating-radial-gradient(
      circle at 50% 50%,
      #1a1a1a 0%, #1a1a1a 3%,
      #0c0c0c 3%, #0c0c0c 6%
    ),
    radial-gradient(circle at 50% 50%, #262220 0%, #0a0a0a 75%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05), 0 4px 10px rgba(0, 0, 0, 0.5);
  animation: turntable-spin 2.6s linear infinite;
  animation-play-state: paused;
}

.turntable.is-playing .turntable__record {
  animation-play-state: running;
}

.turntable__label {
  position: absolute;
  inset: 32%;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #d9a34a, #b87f2a);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
}

.turntable__spindle {
  position: absolute;
  inset: 47%;
  border-radius: 50%;
  background: #0a0a0a;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
}

.turntable__tonearm {
  position: absolute;
  top: 6%;
  right: 4%;
  width: 4px;
  height: 58%;
  background: linear-gradient(180deg, #cfcfcf, #8f8f8f);
  border-radius: 3px;
  transform-origin: top center;
  transform: rotate(-32deg);
  transition: transform 0.5s cubic-bezier(0.34, 1.1, 0.4, 1);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
}

.turntable.is-playing .turntable__tonearm {
  transform: rotate(6deg);
}

.turntable__tonearm-head {
  position: absolute;
  bottom: -3px;
  left: 50%;
  transform: translateX(-50%);
  width: 12px;
  height: 12px;
  border-radius: 3px;
  background: #d8d8d8;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* "Now playing" line: small, above the record, right-aligned to it (flipped
   by script.js near the top/left edges). */
.turntable__now {
  position: absolute;
  right: 0; bottom: calc(100% + 12px);
  margin: 0; max-width: none;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(203, 232, 182, 0.94);
  box-shadow: 0 0 0 1px rgba(16, 17, 18, 0.12), 0 4px 12px rgba(0, 0, 0, 0.12);
  font-family: var(--font-body); font-size: 12px; line-height: 1.3; color: var(--ink);
  white-space: nowrap; pointer-events: none;
  opacity: 0; transform: translateY(4px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.turntable__now-label { color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.08em; font-size: 10px; margin-right: 4px; }
/* shown for a few seconds on each new song (.is-announcing, set by script.js),
   then again whenever the playing record is hovered or keyboard-focused */
.turntable.is-playing.is-announcing .turntable__now,
.turntable.is-playing:hover .turntable__now,
.turntable.is-playing:focus-visible .turntable__now { opacity: 1; transform: none; }
.turntable--now-below .turntable__now { bottom: auto; top: calc(100% + 12px); }
.turntable--now-left .turntable__now { right: auto; left: 0; }

@keyframes turntable-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (max-width: 700px) {
  .turntable {
    width: 76px;
    height: 76px;
  }
}

/* ============================================
   HOMEPAGE INTRO — plays once per browser session (see script.js).

   This uses a separate clone element, not the real nav brand — going
   back to that structure after a real-element version introduced a worse
   bug (a flash of the wrong state before JS could run, since the real
   element has to exist in its normal position from first paint). The
   clone avoids that entirely: its big/centered starting look is plain
   CSS with no JS-dependent values, so there's nothing to flash.

   THE KEY FIX vs the ORIGINAL clone version: shrinking used to be done
   with `transform: scale()`, which means the browser renders the text
   large and then visually shrinks that rendering — this does NOT
   pixel-match the same text rendered naturally at the smaller size
   (font hinting/antialiasing differ at different render scales), which
   is what caused the "not seamless" handoff. The fix is to transition
   the real `font-size` instead (to the exact pixel value measured from
   the actual nav brand via getComputedStyle) — the clone is always
   genuinely rendered at whatever size it's showing, never simulated, so
   its final frame matches the real element pixel-for-pixel.
   ============================================ */
.intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  background-color: var(--site-bg);
  background-image: url("textures/texture-bg.png");
  background-repeat: repeat;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.5s ease;
}

.intro-overlay.is-hidden {
  opacity: 0;
  pointer-events: none;
}

.intro-overlay__brand {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}

#introName {
  font-size: clamp(50px, 10vw, 110px);
  white-space: nowrap;
}

#introTitleWrap {
  font-size: clamp(28px, 7vw, 62px);
  white-space: nowrap;
}

.intro-cursor {
  display: inline-block;
  margin-left: 2px;
  animation: intro-cursor-blink 0.9s steps(1) infinite;
}

/* JG monogram opening (see script.js, "JG monogram opening"). The brand
   stays invisible until script.js has turned the name into the logo, so
   the full name never flashes first. */
.intro-overlay__brand {
  opacity: 0;
}

.intro-overlay__brand.intro-logo-in {
  opacity: 1;
  transition: opacity 0.35s ease;
}

#introName .intro-part {
  display: inline-block;   /* so the J and G can move independently */
  white-space: pre;
}

#introName .intro-baseline {
  display: inline-block;   /* zero-size marker sitting on the text baseline */
  width: 0;
  height: 0;
}

/* While the logo shows and unfolds, the name's J and G are invisible
   placeholders: script.js draws them on .intro-logo-canvas at exactly their
   positions, with the overlap cut out as real transparency. (Blending
   tricks were tried first and made the negative space visibly build up
   during the fade-in, as a flat green shape.) */
#introName.intro-mono .intro-J,
#introName.intro-mono .intro-G {
  color: transparent;
}

.intro-logo-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
}

.intro-logo-canvas.is-in {
  opacity: 1;
}

#introName .intro-fill {
  opacity: 0;
  transform: translateX(-0.12em);   /* starts tucked behind the J / G */
}

#introName.intro-filled .intro-fill {
  opacity: 1;
  transform: none;
  transition: opacity 0.45s ease, transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes intro-cursor-blink {
  50% {
    opacity: 0;
  }
}

/* everything except the intro overlay is hidden while `intro-hide-rest`
   is present on <body> — removed only after the overlay has fully faded
   out (see script.js), so the brand settles into place, THEN disappears
   into the real element, THEN the rest of the page appears — one thing
   changing at a time, not simultaneous effects. */
.site-nav__links,
.crate-outer,
.site-footer-band,
.turntable {
  transition: opacity 0.6s ease;
}

body.intro-hide-rest .site-nav__links,
body.intro-hide-rest .crate-outer,
body.intro-hide-rest .site-footer-band,
body.intro-hide-rest .turntable {
  opacity: 0;
  pointer-events: none;
}

body.intro-hide-rest {
  overflow: hidden;
}

/* ============================================
   WORD-TRIGGER PHOTO POPUPS (About page bio)
   Hover reveals the image on real hover-capable devices (mouse); on
   touch, script.js toggles .is-active on tap instead, since touch
   devices don't have a hover state to key off of. Both paths show/hide
   the exact same popup element — only the trigger mechanism differs.
   ============================================ */
/* plain bold external links within the bio (photographer, vintage
   clothing dealer) — distinct from .word-trigger, which reveals an inline
   popup and doesn't navigate anywhere. These are real links, so they get
   a real underline as the standard "this is clickable" signal. */
.bio-link {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.word-trigger {
  position: relative;
  display: inline;
  cursor: pointer;
}

/* opt-in modifier, since not every trigger word should be bold — DJ uses
   this, Strava deliberately doesn't */
.word-trigger--bold {
  font-weight: 600;
}

.word-trigger__popup {
  position: absolute;
  bottom: 100%;
  left: 50%;
  --popup-x: -50%;
  transform: translateX(var(--popup-x)) translateY(-8px) scale(0.94);
  transform-origin: bottom center;
  width: 220px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* visibility gets its own entry: 0s duration, 0.25s delay. Since this
     rule governs the "hiding" direction (cursor leaving), that delay
     means visibility only flips to hidden AFTER the opacity/transform
     fade-out has fully played — without it, visibility jumps to hidden
     the instant the cursor leaves, cutting the fade off before it's
     visually shown at all, which is why it looked like a sudden
     disappearance instead of a reverse of the entrance animation. */
  transition: opacity 0.25s ease, transform 0.25s ease, visibility 0s 0.25s;
  z-index: 50;
}

/* "above and to the right" variant — the popup's left edge sits just past
   the word's right edge instead of centering over it */
.word-trigger--right .word-trigger__popup {
  left: calc(100% + 10px);
  --popup-x: 0%;
  transform-origin: bottom left;
}

.word-trigger__popup img {
  display: block;
  width: 100%;
  height: auto;
}

.word-trigger__placeholder {
  display: flex;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 4px;
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
  background: var(--c-blue);
  color: rgba(255, 255, 255, 0.85);
  font-family: var(--font-display);
  font-size: 13px;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 12px;
}

/* real hover devices only — see the matching (hover: none) touch handling
   in script.js, which uses .is-active instead since this rule won't apply there */
@media (hover: hover) and (pointer: fine) {
  .word-trigger:hover .word-trigger__popup,
  .word-trigger:focus-visible .word-trigger__popup {
    opacity: 1;
    visibility: visible;
    transform: translateX(var(--popup-x)) translateY(-14px) scale(1);
    transition-delay: 0s;
  }
}

.word-trigger.is-active .word-trigger__popup {
  opacity: 1;
  visibility: visible;
  transform: translateX(var(--popup-x)) translateY(-14px) scale(1);
  transition-delay: 0s;
}

/* ============================================
   PROJECT MEDIA LIGHTBOX — click any project image/video to see it fit
   entirely within the screen, no scrolling needed. Fully opaque white
   background (not semi-transparent) so the page's own content — including
   a scroll-triggered video that might still be playing behind it — is
   completely hidden, not doubled up with the lightbox's own copy. No
   close button by design: clicking anywhere outside the media closes it.
   Built generically (see script.js) so this applies automatically to any
   .project-media/.project-media-grid image or .project-video on any
   page, not just Jackson Hole.
   ============================================ */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: #fafaf8;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  opacity: 0;
  visibility: hidden;
  cursor: default; /* plain arrow, by request — was zoom-out (a magnifying glass) */
  transition: opacity 0.3s ease, visibility 0s 0.3s;
}

.lightbox-overlay.is-active {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

.lightbox-overlay__media {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 4px;
  cursor: default;
  transform: scale(0.96);
  transition: transform 0.3s ease;
}

.lightbox-overlay.is-active .lightbox-overlay__media {
  transform: scale(1);
}

/* prev / next arrows when the lightbox holds a set (Deep Cuts) */
.lightbox-overlay__arrow {
  position: absolute; top: 50%;
  width: 48px; height: 48px; margin-top: -24px; padding: 0;
  border: 0; border-radius: 50%; background: rgba(255, 255, 255, 0.9); color: #262626;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.18);
  display: grid; place-items: center; cursor: pointer;
}
.lightbox-overlay__arrow[hidden] { display: none; }
.lightbox-overlay__arrow svg { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.lightbox-overlay__arrow--prev { left: 16px; }
.lightbox-overlay__arrow--next { right: 16px; }
.lightbox-overlay__arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* mockups in the lightbox (moved in by script.js): as big as the screen
   allows, still live (carousel arrows, sound button, iMessage playback) */
.project-ig .ig-phone, .project-video-frame--phone, .project-ims .im-phone { cursor: pointer; }
/* the phone frames and phone videos have the page green baked into their
   corners, so mockups open on the page green instead of the off-white */
.lightbox-overlay--mock { background: var(--site-bg); }
.lightbox-overlay__mock {
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  transform: scale(0.96); transition: transform 0.3s ease;
}
.lightbox-overlay.is-active .lightbox-overlay__mock { transform: none; }
.lightbox-overlay__mock .ig-phone { width: min(calc(100vw - 48px), calc((100svh - 48px) * 856 / 1840)); max-width: none; margin: 0; cursor: default; }
.lightbox-overlay__mock .project-video-frame--phone { width: min(calc(100vw - 48px), calc((100svh - 104px) * 856 / 1840)); max-width: none; margin: 0; border-radius: 8.42% / 3.92%; cursor: default; }
.lightbox-overlay__mock .project-ims { width: auto; margin: 0; }
.lightbox-overlay__mock .im-phone { width: min(calc((100vw - 48px - 2 * var(--im-gap)) / 3), calc((100svh - 48px) * 856 / 1840)); cursor: default; }
/* narrow screens: the three phones go full size in a sideways-swipe row */
@media (max-width: 700px) {
  .lightbox-overlay__mock--ims {
    width: calc(100% + 48px); margin: 0 -24px;
    overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none;
    align-items: flex-start;              /* start at the first phone (centring would push it off the left edge) */
  }
  .lightbox-overlay__mock--ims::-webkit-scrollbar { display: none; }
  .lightbox-overlay__mock--ims .project-ims { width: max-content; justify-content: flex-start; padding: 0 32px; gap: 16px; }
  .lightbox-overlay__mock--ims .im-phone { width: min(calc(100vw - 64px), calc((100svh - 48px) * 856 / 1840)); scroll-snap-align: center; }
}

/* ============================================
   EXECUTION LABELS (project pages)
   Each execution (one asset or a group of related assets) can carry a
   category label (OOH, DOOH, SOCIAL, EXPERIENTIAL, STUNT, AUDIO, MERCH), an
   optional short title and an optional explanation; see the "executions"
   filter in eleventy.config.js and design-guidelines.md. Two layouts:
   - side:  compact assets (phones, Instagram, radio). The label sits in a
            narrow column to the LEFT, top-aligned, while the asset stays
            centred on the page. Further assets of the same execution
            (e.g. the stills under an Instagram phone) span the full width
            below.
   - stack: full-bleed images, sets and videos. The label sits ABOVE.
   Phones and tablets (<= 860px) always stack. Labels never sit on artwork.
   Only the VVDS Fifties Medium Bold weight exists in the project, so labels
   use it small and letter-spaced rather than a faked lighter weight.
   ============================================ */
.execution .execution__label {         /* .execution prefix beats .project-body p */
  max-width: none;
  font-family: "VVDS Fifties", var(--font-display);
  font-weight: 700;
  font-size: clamp(36px, 3.8vw, 54px);   /* Jake: ~3x the first pass (which rendered at 18px) */
  line-height: 1;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);                     /* labels and titles black; only explanations are grey */
  margin: 0;
}
.execution .execution__title {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 18px;
  line-height: 1.3;
  color: var(--ink);
  margin: 10px 0 0;
}
.execution .execution__desc {
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink-soft);                /* same grey as the project description */
  max-width: 44ch;
  margin: 8px 0 0;
  white-space: pre-line;                 /* a blank line in the description starts a new paragraph */
}
.execution--stack {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.execution--stack .execution__item + .execution__item { margin-top: clamp(24px, 5vh, 48px); }
/* labels above full-width work line up with the site header's name (the
   same left-shift the nav brand uses above 1280px, see .site-nav__brand) */
@media (min-width: 1280px) {
  .execution--stack .execution__info,
  .execution--side .execution__info { margin-left: calc(-47.313vw + 536px); }
}

@media (min-width: 861px) {
  .execution--side {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    column-gap: 44px;
    row-gap: clamp(40px, 8vh, 72px);
    align-items: start;
  }
  .execution--side .execution__info {
    grid-column: 1;
    grid-row: 1;
    justify-self: start;                   /* same left edge as every other label (Jake, Sept 30) */
    width: min(230px, 100%);
    padding-top: 6px;
  }
  .execution--side .execution__item--lead {
    grid-column: 2;
    grid-row: 1;
    width: 380px;                          /* phone and Instagram mockups */
  }
  .execution--side .execution__item--lead:has(.project-radio) { width: 600px; }
  .execution--side .execution__item--lead .project-media {
    width: auto;
    margin-left: 0;
    margin-right: 0;
  }
  .execution--side .execution__item:not(.execution__item--lead) { grid-column: 1 / -1; }
  .execution--side .execution__more { grid-column: 1 / -1; }
}
/* side layout, extra assets: a row beneath the lead (like execution--row) */
.execution__more { display: flex; flex-wrap: wrap; justify-content: center; column-gap: clamp(32px, 5vw, 72px); row-gap: 32px; }
.execution__more .execution__item { flex: 0 1 380px; min-width: 0; }
.execution__more .execution__item .project-media { width: auto; margin-left: 0; margin-right: 0; }
@media (min-width: 861px) {
  .execution__more--3up { --g3: clamp(24px, 3vw, 48px); column-gap: var(--g3); }
  .execution__more--3up .execution__item { flex: 0 1 calc((100% - 2 * var(--g3)) / 3); max-width: 380px; }
}
@media (max-width: 860px) {
  .execution--side {
    display: flex;
    flex-direction: column;
    gap: 20px;
  }
  .execution--side .execution__item + .execution__item { margin-top: 24px; }
}

/* row: label above, assets side by side (e.g. two Instagram posts);
   they wrap onto separate lines when there isn't room (phones) */
.execution--row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  column-gap: clamp(32px, 5vw, 72px);
  row-gap: 32px;
}
.execution--row .execution__info { flex: 0 0 100%; margin-bottom: -12px; }
.execution--row .execution__item { flex: 0 1 380px; min-width: 0; }
.execution--row .execution__item .project-media { width: auto; margin-left: 0; margin-right: 0; }
/* a plain image in a row of phones (Diet Coke's tour poster) sits centred on them */
.execution--row .execution__item:has(> .project-media--framed) { align-self: center; }
@media (min-width: 1280px) {
  .execution--row .execution__info {
    margin-left: calc(-47.313vw + 536px);    /* aligned with the header name, like stack */
    margin-right: calc(47.313vw - 536px);
  }
}

/* a row of three (e.g. three Instagram posts) stays on one line from 861px
   up, each up to 380px wide; below that they wrap like any row */
@media (min-width: 861px) {
  .execution--3up { --g3: clamp(24px, 3vw, 48px); column-gap: var(--g3); }
  .execution--3up .execution__item { flex: 0 1 calc((100% - 2 * var(--g3)) / 3); max-width: 380px; }
}
/* a row of four (e.g. three posts and a Story): four across from 861px up,
   wider than the content column (up to 1440px, with a side gutter) so each
   phone stays readable; a 2 x 2 grid on tablets, and one per row on phones
   (like every other row of mockups) so captions stay readable */
.execution--4up { --g4: clamp(14px, 2.4vw, 40px); column-gap: var(--g4); row-gap: 28px; }
.execution--4up .execution__item { flex: 0 0 calc((100% - var(--g4)) / 2); max-width: 380px; }
@media (max-width: 700px) {
  .execution--4up .execution__item { flex: 0 1 380px; }
}
@media (min-width: 861px) {
  .execution--4up {
    --w4: min(100vw - 2 * clamp(16px, 3vw, 48px), 1440px);
    width: var(--w4);
    margin-left: calc(50% - var(--w4) / 2);
    margin-right: calc(50% - var(--w4) / 2);
  }
  .execution--4up .execution__item { flex: 0 1 calc((100% - 3 * var(--g4)) / 4); }
}
/* the label still lines up with the header name (2.687vw from the left above
   1280px, see .execution--stack); measured from this wider row's left edge */
@media (min-width: 1280px) {
  .execution--row.execution--4up .execution__info { margin-left: calc(var(--w4) / 2 - 47.313vw); margin-right: calc(47.313vw - var(--w4) / 2); }
}

/* a set of four as a 2 x 2 grid (gallery with columns: 2), content width */
.project-media-grid--2col {
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  width: auto;
  margin-left: 0;
  margin-right: 0;
}
@media (max-width: 700px) {
  .project-media-grid--2col { grid-template-columns: 1fr; gap: 16px; }
}

/* rows of two at a shared height (gallery with columns: pairs; an odd set
   ends on a row of three), for sets of mixed shapes, e.g. billboards next
   to bus shelters. Each image's flex
   grow is its width/height (--ar, from the imgRatio filter), so both images
   in a row come out the same height. Wider than content width, with a
   gutter; one per row on phones. */
.project-media-grid--pairs {
  gap: clamp(12px, 1.6vw, 24px);
  --pw: min(100vw - 2 * clamp(16px, 3vw, 48px), 1600px);
  width: var(--pw);
  margin-left: calc(50% - var(--pw) / 2);
  margin-right: calc(50% - var(--pw) / 2);
}
.project-media-pair {
  display: flex;
  gap: clamp(12px, 1.6vw, 24px);
  /* never taller than the screen: a row's height is its width / the sum of
     its images' width-to-height ratios (--row-ar, set in the template) */
  width: min(100%, calc(88svh * var(--row-ar, 99)));
  margin: 0 auto;
}
.project-media-grid--pairs img {
  flex: var(--ar, 1.5) 1 0;
  min-width: 0;
}
/* a row with a single image (rows: [2, 1] etc.): centred, never taller than the screen */
.project-media-pair--single img { flex: none; width: auto; max-width: 100%; max-height: 88svh; margin: 0 auto; }
@media (max-width: 700px) {
  .project-media-pair { flex-direction: column; gap: 16px; }
  .project-media-grid--pairs { gap: 16px; }
  .project-media-grid--pairs img { flex: none; max-height: 80svh; width: auto; max-width: 100%; margin: 0 auto; }
  .project-media-pair { width: 100%; }
}

/* ============================================
   FRAMED MEDIA (site standard since Sept 25, per Jake: full-bleed felt
   overwhelming). Single images, stacked image sets and landscape videos,
   plus their placeholders, sit in the content column instead of running
   edge to edge. A tall image is capped so it fits on one screen (e.g. the
   near-square Jackson Hole Manifesto). Phones keep them full width, as
   before. Not affected: rows of two (gallery "pairs", which have their own
   width), the 2 x 2 grid (already content width), and the phone,
   Instagram, iMessage and radio mockups. The lightbox still gives the
   full-screen view.
   ============================================ */
.project-media__img { display: block; width: 100%; height: auto; }
@media (min-width: 701px) {
  .project-media--framed,
  .project-media-grid:not(.project-media-grid--pairs):not(.project-media-grid--2col) {
    width: auto;
    margin-left: 0;
    margin-right: 0;
  }
  .project-media__img,
  .project-media-grid:not(.project-media-grid--pairs):not(.project-media-grid--2col) img {
    width: auto;
    max-width: 100%;
    max-height: 88svh;
    margin: 0 auto;
  }
  .project-media-grid:not(.project-media-grid--pairs):not(.project-media-grid--2col) { gap: 24px; }
}

/* ============================================
   CROSSFADE SEQUENCE (the "sequence" section type)
   Same-size frames stacked in one box at the first frame's shape (--ar,
   from the imgRatio filter); the one with .is-on shows. script.js
   ("Crossfade sequences") steps .is-on through the frames. Framed like a
   single image: content width, never taller than the screen.
   ============================================ */
.project-seq { position: relative; aspect-ratio: var(--ar, 4 / 3); width: 100%; margin: 0 auto; overflow: hidden; }
.project-seq__frame {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; pointer-events: none;
  transition: opacity 0.7s ease;
}
.project-seq__frame.is-on { opacity: 1; pointer-events: auto; }
@media (min-width: 701px) {
  .project-seq { width: min(100%, calc(88svh * var(--ar, 4 / 3))); }
}
@media (prefers-reduced-motion: reduce) {
  .project-seq__frame { transition: none; }
}

/* ============================================
   FACEBOOK MOCKUP (shared component)
   Markup: _includes/facebook/post.njk (spec: tools/instagram/README.md,
   "Facebook posts"). A single Facebook feed post, usually a sponsored ad,
   in the same phone frame and --r units as the Instagram mockup (it reuses
   .ig-phone, .ig-screen and .ig-clock). Don't restyle per post.
   ============================================ */
.fb-screen { color: #080809; background: #fff; }
.fb-screen svg { fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; }

/* top bar under the status bar: back, the Page name, search */
.fb-nav { position: relative; height: calc(236 * var(--r)); border-bottom: calc(2 * var(--r)) solid #e4e6eb; }
.fb-nav__back, .fb-nav__search { position: absolute; top: calc(150 * var(--r)); width: calc(58 * var(--r)); height: calc(58 * var(--r)); stroke-width: 2.4; }
.fb-nav__back { left: calc(26 * var(--r)); }
.fb-nav__search { right: calc(30 * var(--r)); stroke-width: 2.2; }
.fb-nav__title { position: absolute; left: 0; right: 0; top: calc(158 * var(--r)); text-align: center; font-size: calc(34 * var(--r)); line-height: calc(42 * var(--r)); font-weight: 700; }

/* post header: avatar, Page name + badge, "Sponsored · globe", more, close */
.fb-head { display: flex; align-items: center; padding: calc(26 * var(--r)) calc(26 * var(--r)) calc(18 * var(--r)) calc(30 * var(--r)); }
.fb-head__avatar { width: calc(84 * var(--r)); height: calc(84 * var(--r)); border-radius: 50%; border: 1px solid #e4e6eb; box-sizing: border-box; object-fit: cover; flex: none; display: block; }
.fb-head__text { display: flex; flex-direction: column; margin-left: calc(20 * var(--r)); min-width: 0; }
.fb-head__name { display: inline-flex; align-items: center; gap: calc(10 * var(--r)); font-size: calc(30 * var(--r)); line-height: calc(38 * var(--r)); font-weight: 650; }
.fb-screen .fb-head__badge { width: calc(26 * var(--r)); height: calc(26 * var(--r)); stroke: none; flex: none; }
.fb-head__sub { display: inline-flex; align-items: center; gap: calc(6 * var(--r)); font-size: calc(24 * var(--r)); line-height: calc(32 * var(--r)); color: #65686c; }
.fb-head__globe { width: calc(24 * var(--r)); height: calc(24 * var(--r)); stroke-width: 2; }
.fb-head__more, .fb-head__close { width: calc(46 * var(--r)); height: calc(46 * var(--r)); color: #65686c; flex: none; }
.fb-head__more { margin-left: auto; fill: currentColor; stroke: none; }
.fb-screen .fb-head__more { fill: currentColor; stroke: none; }
.fb-head__close { margin-left: calc(26 * var(--r)); stroke-width: 2.2; }

/* post text (shown in full) */
.fb-screen .fb-text { max-width: none; margin: 0; padding: 0 calc(30 * var(--r)) calc(22 * var(--r)); font-size: calc(29 * var(--r)); line-height: calc(39 * var(--r)); color: #080809; white-space: pre-line; }

/* the creative, edge to edge at its own shape */
.fb-media { width: 100%; background: #f0f2f5; }
.fb-media img { display: block; width: 100%; height: 100%; object-fit: cover; user-select: none; -webkit-user-drag: none; }

/* ad link bar: domain + headline, and the button */
.fb-cta { display: flex; align-items: center; gap: calc(20 * var(--r)); background: #f0f2f5; padding: calc(22 * var(--r)) calc(30 * var(--r)); }
.fb-cta__text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.fb-cta__domain { font-size: calc(22 * var(--r)); line-height: calc(30 * var(--r)); color: #65686c; text-transform: uppercase; letter-spacing: 0.02em; }
.fb-cta__headline { font-size: calc(28 * var(--r)); line-height: calc(36 * var(--r)); font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fb-cta__button { flex: none; background: #e2e5e9; border-radius: calc(12 * var(--r)); padding: calc(16 * var(--r)) calc(28 * var(--r)); font-size: calc(27 * var(--r)); line-height: calc(34 * var(--r)); font-weight: 650; }

/* reactions and counts */
.fb-counts { display: flex; align-items: center; justify-content: space-between; padding: calc(20 * var(--r)) calc(30 * var(--r)); font-size: calc(25 * var(--r)); line-height: calc(32 * var(--r)); color: #65686c; }
.fb-counts__reactions { display: inline-flex; align-items: center; }
.fb-react { width: calc(36 * var(--r)); height: calc(36 * var(--r)); border: calc(3 * var(--r)) solid #fff; border-radius: 50%; box-sizing: content-box; }
.fb-screen .fb-react circle { stroke: none; }
.fb-screen .fb-react path { fill: #fff; stroke: none; }
.fb-screen .fb-react--like circle { fill: #0866ff; }
.fb-screen .fb-react--love circle { fill: #f33e58; }
.fb-react--love { margin-left: calc(-10 * var(--r)); }
.fb-counts__n { margin-left: calc(10 * var(--r)); }

/* Like / Comment / Share */
.fb-actions { display: flex; justify-content: space-around; border-top: calc(2 * var(--r)) solid #e4e6eb; margin: 0 calc(30 * var(--r)); padding: calc(20 * var(--r)) 0 calc(24 * var(--r)); font-size: calc(26 * var(--r)); line-height: calc(34 * var(--r)); font-weight: 600; color: #65686c; }
.fb-action { display: inline-flex; align-items: center; gap: calc(12 * var(--r)); }
.fb-action svg { width: calc(40 * var(--r)); height: calc(40 * var(--r)); stroke-width: 1.9; }

/* the next post in the feed: a thin grey gap, then its header */
.fb-next { border-top: calc(14 * var(--r)) solid #c9ccd1; background: #fff; }

/* ============================================
   INSTAGRAM STORY (the "story" section type)
   Jake's Story screenshot already includes the Story interface (progress
   bars, account, reply bar), so it's shown as-is: full screen width, just
   under a dark status bar, on black. Same phone frame as the Instagram
   mockups, in its dark-status-bar variant (instagram-phone-frame-dark.png:
   black bar, white icons).
   ============================================ */
.ig-story { background: #000; }
.ig-story .ig-clock { color: #fff; }
.ig-story__img {
  position: absolute; left: 0; top: calc(150 * var(--r));
  width: 100%; height: auto; display: block;
  border-radius: calc(22 * var(--r));
  user-select: none; -webkit-user-drag: none;
}

/* ============================================
   IMESSAGE MOCKUP (the "imessage" section type)
   Markup: _includes/imessage/thread.njk; playback: "iMessage threads" in
   script.js. Measured from Jake's iOS 26 Messages screenshot (920 x 2000
   px): --r = 1/920 of the screen width. The phone chrome (header, input
   bar) is drawn to scale; the thread itself is sized in em from --fs, the
   reference's 34px text, which has a legibility floor so three phones can
   sit side by side even on a phone screen (the text then runs a little
   larger than life, the phones keep real proportions).
   ============================================ */
.project-ims {
  display: flex;
  flex-wrap: nowrap;                    /* never stacked */
  justify-content: center;
  align-items: flex-start;
  gap: clamp(8px, 3vw, 48px);
  padding: 0 clamp(12px, 3vw, 24px);
  box-sizing: border-box;
  --im-gap: clamp(8px, 3vw, 48px);
}
.im-phone {
  position: relative;
  flex: none;
  /* as wide as three can be side by side, never taller than the screen */
  width: min(330px,
             calc((100vw - 2 * clamp(12px, 3vw, 24px) - 2 * var(--im-gap)) / 3),
             calc(86svh * 856 / 1840));
  aspect-ratio: 856 / 1840;
}
.im-screen {
  position: absolute;
  left: calc(39 / 856 * 100%); top: calc(38 / 1840 * 100%);
  width: calc(776 / 856 * 100%); height: calc(1764 / 1840 * 100%);
  overflow: hidden;
  background: #fff;
  container-type: inline-size;
  --r: calc(100cqw / 920);
  --fs: max(calc(34 * var(--r)), 7.5px);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: #000;
  -webkit-font-smoothing: antialiased;
  text-align: left;
}
.im-screen svg { fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; }
.im-screen .ig-clock { z-index: 6; }

/* the thread: scrolls like the app (older messages move up and out) */
.im-thread {
  position: absolute; inset: 0;
  overflow: hidden;
  display: flex; flex-direction: column;
  padding: calc(352 * var(--r)) 0 calc(178 * var(--r));
  box-sizing: border-box;
  font-size: var(--fs);
  line-height: 1.147;                 /* 39 / 34 */
}
.im-item { display: grid; grid-template-rows: 1fr; flex: none; }
.im-inner { min-height: 0; overflow: hidden; display: flex; flex-direction: column; padding: 0 1.09em 0.7em; }   /* 37px sides, 24px between */
.im-item--in .im-inner { align-items: flex-start; }
.im-item--out .im-inner { align-items: flex-end; }
.im-item--stamp .im-inner { align-items: center; padding-bottom: 0.95em; }
.im-screen p.im-stamp { margin: 0; max-width: none; font-size: 0.74em; line-height: 1.2; color: #8a8a8e; }
.im-stamp b { font-weight: 600; }

/* bubbles */
.im-screen .im-bub {
  position: relative;
  margin: 0;
  max-width: 70cqw;
  padding: 0.647em 0.97em;            /* 22px / 33px */
  border-radius: 1.24em;
  font-size: 1em; line-height: 1.147;
  overflow-wrap: break-word;
}
.im-item--in .im-bub { background: #e9e9eb; color: #000; }
.im-item--out .im-bub,
.im-item--out .im-tail {
  color: #fff;
  /* iOS 26 blue: a gradient fixed to the screen (lighter near the top);
     script.js keeps --by = the element's offset from the screen top */
  background: linear-gradient(180deg, #6ac7fb 0%, #3aa9fd 45%, #1b94ff 100%) 0 calc(var(--by, 0) * -1px) / 100% var(--sh, 600px) no-repeat;
}
.im-tail {
  position: absolute; bottom: -0.42em;
  width: 0.9em; height: 0.52em;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 17'%3E%3Cpath d='M0 0H30C24 5 14 12 3 16.5C1.6 17 .8 16 1.6 14.8C4 11 5.4 5.6 5 0Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 17'%3E%3Cpath d='M0 0H30C24 5 14 12 3 16.5C1.6 17 .8 16 1.6 14.8C4 11 5.4 5.6 5 0Z'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.im-item--in .im-tail { left: 0.3em; background: #e9e9eb; }
.im-item--out .im-tail { right: 0.3em; transform: scaleX(-1); }
.im-item--in .im-inner, .im-item--out .im-inner { padding-bottom: 1.1em; }   /* room for the tail + the gap */

/* shared location: a map card on the sender's side */
.im-map { width: calc(560 * var(--r)); border-radius: calc(38 * var(--r)); overflow: hidden; background: #e9e9eb; }
.im-map img { display: block; width: 100%; aspect-ratio: 600 / 360; object-fit: cover; }
.im-screen p.im-map__label { margin: 0; max-width: none; padding: 0.45em 0.8em 0.5em; font-size: 0.82em; line-height: 1.2; color: #000; }
.im-map__label b { font-weight: 600; }

/* "Delivered" under the last sent message */
.im-item[data-kind="receipt"] .im-inner { padding-top: 0; margin-top: -0.55em; padding-bottom: 0.75em; }
.im-screen p.im-receipt { margin: 0; max-width: none; font-size: 0.7em; line-height: 1.2; font-weight: 600; color: #8a8a8e; }

/* typing indicator */
.im-item--typing .im-bub { padding: 0.647em 0.8em; }
.im-dots { display: inline-flex; gap: 0.2em; height: 1.147em; align-items: center; }
.im-dots i { width: 0.32em; height: 0.32em; border-radius: 50%; background: #8e8e93; animation: im-dot 1.2s infinite ease-in-out; }
.im-dots i:nth-child(2) { animation-delay: 0.18s; }
.im-dots i:nth-child(3) { animation-delay: 0.36s; }
@keyframes im-dot { 0%, 60%, 100% { opacity: 0.35; transform: none; } 30% { opacity: 1; transform: translateY(-0.08em); } }

/* stickers: no bubble */
.im-sticker { display: block; height: calc(360 * var(--r)); width: auto; }   /* images scale with the phone, not the text */

/* header: frosted fade, back button, avatar + name pill, video button */
.im-head {
  position: absolute; left: 0; right: 0; top: 0; height: calc(372 * var(--r)); z-index: 5;
  /* content scrolling under the header fades out, as in the screenshot */
  background: linear-gradient(180deg, rgba(255,255,255,0.97) 0%, rgba(255,255,255,0.92) 62%, rgba(255,255,255,0.6) 82%, rgba(255,255,255,0) 100%);
}
.im-phone .ig-frame { z-index: 10; }
.im-btn {
  position: absolute; width: calc(100 * var(--r)); height: calc(100 * var(--r));
  border-radius: 50%; background: #fff; box-sizing: border-box;
  box-shadow: 0 calc(3 * var(--r)) calc(26 * var(--r)) rgba(0,0,0,0.08);
  display: grid; place-items: center; color: #0c1014;
}
.im-btn svg { width: 100%; height: 100%; }
.im-btn--back { left: calc(37 * var(--r)); top: calc(143 * var(--r)); }
.im-btn--back svg { stroke-width: 4.5; }
.im-btn--ghost { left: calc(745 * var(--r)); top: calc(140 * var(--r)); width: calc(106 * var(--r)); height: calc(84 * var(--r)); background: #cfeaf6; opacity: 0.7; box-shadow: none; }
.im-btn--video { left: calc(783 * var(--r)); top: calc(143 * var(--r)); background: #f3fbff; }
.im-btn--video svg { width: 60%; height: 60%; stroke-width: 4; }
.im-avatar {
  position: absolute; left: calc(392 * var(--r)); top: calc(143 * var(--r));
  width: calc(136 * var(--r)); height: calc(136 * var(--r)); border-radius: 50%;
  background: linear-gradient(180deg, #a6b6de 0%, #7c86c3 100%);
  color: #fff; font-weight: 600; font-size: calc(72 * var(--r)); line-height: calc(136 * var(--r)); text-align: center;
}
.im-name {
  position: absolute; left: 50%; top: calc(268 * var(--r)); transform: translateX(-50%);
  height: calc(72 * var(--r)); padding: 0 calc(26 * var(--r)) 0 calc(30 * var(--r)); border-radius: calc(36 * var(--r));
  background: #fff; box-shadow: 0 calc(2 * var(--r)) calc(16 * var(--r)) rgba(0,0,0,0.06);
  display: inline-flex; align-items: center; gap: calc(12 * var(--r));
  font-weight: 700; font-size: max(calc(36 * var(--r)), 5.5px); white-space: nowrap;
}
.im-name svg { width: calc(12 * var(--r)); height: calc(22 * var(--r)); color: #b3b3b8; stroke-width: 2.5; }

/* input bar */
.im-foot {
  position: absolute; left: 0; right: 0; bottom: 0; height: calc(200 * var(--r)); z-index: 5;
  background: linear-gradient(0deg, rgba(255,255,255,0.98) 55%, rgba(255,255,255,0));
}
.im-btn--plus { left: calc(64 * var(--r)); top: auto; bottom: calc(63 * var(--r)); width: calc(92 * var(--r)); height: calc(92 * var(--r)); }
.im-btn--plus svg { width: 46%; height: 46%; stroke-width: 3.4; }
.im-field {
  position: absolute; left: calc(182 * var(--r)); right: calc(64 * var(--r)); bottom: calc(63 * var(--r)); height: calc(92 * var(--r));
  border-radius: calc(46 * var(--r)); background: #fff; box-shadow: 0 calc(3 * var(--r)) calc(26 * var(--r)) rgba(0,0,0,0.07);
  display: flex; align-items: center; justify-content: space-between; padding: 0 calc(36 * var(--r)) 0 calc(38 * var(--r)); box-sizing: border-box;
  color: #c4c4c8; font-size: max(calc(34 * var(--r)), 5.5px);
}
.im-field svg { width: calc(24 * var(--r)); height: calc(36 * var(--r)); color: #8e8e93; stroke-width: 2.2; }

/* playback (only when script.js runs): hidden until revealed */
.im-js .im-item { grid-template-rows: 0fr; transition: grid-template-rows 0.32s cubic-bezier(0.2, 0.7, 0.3, 1); }
.im-js .im-item.is-in { grid-template-rows: 1fr; }
.im-js .im-thread { transition: opacity 0.38s ease; }     /* the loop clears the threads with a quick fade */
.im-js.im-fading .im-thread { opacity: 0; }
.im-js .im-item .im-body, .im-js .im-item .im-receipt { opacity: 0; transform: translateY(0.5em) scale(0.92); transition: opacity 0.22s ease, transform 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.15); }
.im-js .im-item--in .im-body { transform-origin: 0 100%; }
.im-js .im-item--out .im-body { transform-origin: 100% 100%; }
.im-js .im-item.is-in .im-body, .im-js .im-item.is-in .im-receipt { opacity: 1; transform: none; }
.im-js .im-item[data-kind="sticker"] .im-body { transform: scale(1.35) rotate(-6deg); }
.im-js .im-item[data-kind="sticker"].is-in .im-body { transform: none; }
.im-item--typing { display: none; }             /* only shown during playback */
.im-js .im-item--typing { display: grid; }
@media (prefers-reduced-motion: reduce) {
  .im-js .im-item, .im-js .im-item .im-body, .im-js .im-item .im-receipt { transition: none; }
  .im-dots i { animation: none; opacity: 0.6; }
}

/* ============================================
   RADIO AD PLAYER (the "radio" section type in project.njk)
   A boombox silhouette: one SVG in the site's black ink with real cut-outs
   (dial window, speaker grilles, button ring and icon, the time), so the
   page itself shows through: no colour of its own, no band around it (Jake:
   black-and-white silhouette "for now"). Plays the campaign's file as-is.
   The FM dial is generic (no station); its needle shows progress and the
   invisible slider over the scale seeks. Positions below are fractions of
   the SVG's 640 x 400 viewBox, so the radio scales as one piece.
   ============================================ */
.project-radio { padding: 0 20px; }
.radio {
  position: relative;
  width: min(560px, 100%);
  margin: 0 auto;
  aspect-ratio: 640 / 400;
}
.radio__art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.radio__ink { fill: var(--ink); }
.radio__scale text { font-family: "VVDS Fifties", var(--font-display); font-weight: 700; font-size: 22px; }
.radio__scale .radio__band { font-size: 30px; }
.radio__cut-pause, .radio.is-playing .radio__cut-play { display: none; }
.radio.is-playing .radio__cut-pause { display: inline; }
.radio__grille { transform-box: fill-box; transform-origin: center; }
.radio.is-playing .radio__grille { animation: radio-thump 0.46s ease-in-out infinite alternate; }
@keyframes radio-thump { to { transform: scale(1.03); } }
@media (prefers-reduced-motion: reduce) { .radio.is-playing .radio__grille { animation: none; } }
.radio__needle {                   /* black needle over the dial window */
  position: absolute;
  top: calc(80 / 400 * 100%); height: calc(66 / 400 * 100%);
  left: calc((122 + var(--p, 0) * 428 - 2.5) / 640 * 100%);
  width: calc(5 / 640 * 100%);
  background: var(--ink);
  pointer-events: none;
}
.radio__seek {                     /* invisible slider over the scale: tap/drag to seek, keyboard too */
  position: absolute;
  top: calc(82 / 400 * 100%); height: calc(62 / 400 * 100%);
  left: calc(122 / 640 * 100%); width: calc(428 / 640 * 100%);
  margin: 0; -webkit-appearance: none; appearance: none; background: transparent;
  opacity: 0; cursor: pointer;
}
.radio__seek::-webkit-slider-thumb { -webkit-appearance: none; width: 1px; height: 40px; }   /* 1px thumb: a tap lands exactly where the needle goes */
.radio__seek::-moz-range-thumb { width: 1px; height: 40px; border: 0; }
.radio:has(.radio__seek:focus-visible) .radio__needle { box-shadow: 0 0 0 2px var(--accent); }
.radio__play {                     /* invisible hit area over the drawn button */
  position: absolute;
  left: calc(268 / 640 * 100%); top: calc(198 / 400 * 100%);
  width: calc(104 / 640 * 100%); height: calc(104 / 400 * 100%);
  border: 0; border-radius: 50%; padding: 0;
  background: transparent; cursor: pointer;
}
.radio__play:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* ============================================
   MOBILE PASS (Sept 2026), phones only (max-width: 700px, the site's phone
   breakpoint). Everything phone-specific from the mobile pass lives in
   this one block so it's easy to review and revise; desktop is untouched.
   ============================================ */
@media (max-width: 700px) {

  /* ---- header: name on one line, nav on one row beneath it ---- */
  .site-nav {
    flex-direction: column;
    align-items: flex-start;
    row-gap: 12px;
    padding: 16px 20px 14px;
  }
  .site-nav__name {
    font-size: min(34px, 7.9vw);          /* "Jake Gunther" fits one line from 320px up */
    white-space: nowrap;
  }
  .site-nav__title {
    font-size: 15px;
    margin-top: 4px;
  }
  /* three links (The Hits, Liner Notes, Deep Cuts) on one row: the type
     and spacing scale with the screen so they fit from 320px up */
  .site-nav__links {
    gap: min(16px, 2.6vw);
  }
  .site-nav__links li:not(:first-child) {
    padding-left: min(16px, 2.6vw);
  }
  .site-nav__links a {
    font-size: min(19px, 4.6vw);
    white-space: nowrap;
    padding: 8px 0;                        /* ~44px tap height */
  }

  /* project pages: the JG logo stands in for the full name (Jake's call),
     on one row with the nav, so the header is compact. The name and title
     stay in the link, visually hidden, for screen readers and search. */
  .project-page .site-nav {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    column-gap: 12px;                      /* keeps the logo off "The Hits" */
    padding-top: 14px;
    padding-bottom: 14px;
  }
  .project-page .site-nav__jg {
    display: block;
    height: 42px;
    width: auto;
  }
  .project-page .site-nav__links {
    gap: min(12px, 2.2vw);
  }
  .project-page .site-nav__links a {
    font-size: min(18px, 3.6vw);
  }
  .project-page .site-nav__links li:not(:first-child) {
    padding-left: min(12px, 2.2vw);
  }
  .project-page .site-nav__name,
  .project-page .site-nav__title {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /* ---- footer: same content, sized for a phone ---- */
  .site-footer,
  .site-footer--home {
    padding: 30px 20px 34px;
    gap: 14px 24px;
  }
  .site-footer .site-footer__prompt {
    font-size: 22px;
    padding: 6px 0 8px;                    /* 44px tap height */
  }
  .site-footer__icons {
    gap: 14px;
  }
  .site-footer__icons img {
    height: 32px;                          /* one row with the prompt down to ~360px */
  }
  .site-footer__icons a {
    padding: 6px;                          /* 44px tap area, same visual spacing */
    margin: -6px;
  }

  /* ---- homepage: no turntable on phones (too fiddly to use) ---- */
  .turntable {
    display: none !important;
  }

  /* ---- homepage crate: bigger records, swipe sideways through them ----
     The crate scene is shown at 290% of the screen width inside a
     horizontal scroller (each record about half the screen), snapping
     record by record. Records rest in the crate as on desktop; a tap plays
     the desktop pop-up (record lifts, vinyl slides out) and then opens the
     project (see the crate block in script.js). */
  .crate-outer {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    margin-top: 0;
    margin-bottom: 0;
    /* the homepage is one screen tall: the crate fills the space between
       header (~140px) and footer (~104px), records centred in it */
    min-height: calc(100svh - 244px);
    display: flex;
    align-items: center;
  }
  .crate-outer::-webkit-scrollbar {
    display: none;
  }
  .crate-box {
    flex: none;
    width: 290vw;
    margin-top: calc(-290vw * 887 / 1749 * 0.2);     /* trim the empty sky above the stacks */
    margin-bottom: calc(-290vw * 887 / 1749 * 0.05);
    padding-left: 0;
  }
  .crate-hit {
    scroll-snap-align: center;
  }

  /* ---- project pages ---- */
  .project-header {
    padding: 28px 0 8px;
    margin-bottom: 36px;
  }
  .project-header__summary {
    font-size: 18px;
  }
  .project-body {
    gap: 44px;
  }
  .project-nav {
    padding-bottom: 64px;                  /* was 104px: less dead space above the footer */
  }
  /* phone and Instagram mockups keep the same 20px side margin as the text */
  .project-video-frame--phone,
  .ig-phone {
    max-width: min(380px, calc(100vw - 40px));
  }

  /* placeholder boxes (until the real media arrives): label inset and centred */
  .placeholder-block {
    padding: 0 24px;
    box-sizing: border-box;
    text-align: center;
    font-size: 16px;
  }

  /* ---- about: the photo uses the full column ---- */
  .about-layout {
    padding-top: 32px;
    gap: 36px;
  }
  .about-photo {
    max-width: none;
  }
}
