/* Cross-page view transitions — experimental, ignored gracefully
   where unsupported. Elements sharing a view-transition-name across
   two pages morph between them on navigation; see
   #archive-hokkaido-roadtrip below and project-title-block on the
   project page itself. */
@view-transition {
  navigation: auto;
}

@font-face {
  font-family: "Suisse Int'l";
  src: url("/fonts/SuisseIntl-Light-WebS.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Suisse Int'l";
  src: url("/fonts/SuisseIntl-Regular-WebS.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --bg: #FAF9F7;
  --fg: #1A1A1A;
  --size-body: 16px;
  --size-small: 12px;

  --gutter: 24px;
  --margin: 20px;
  --content-width: calc(var(--real-vw, 100vw) - 2 * var(--margin));
  --col: calc((var(--content-width) - 11 * var(--gutter)) / 12);
  /* two columns to grow into — left-aligned, so a short title (the
     usual case) naturally stays close to the wordmark side and never
     needs to reach toward the gallery at all */
  --title-width: calc(2 * var(--col) + var(--gutter));
  /* two columns + two gutters, floored at 152px — "Chris Nakahara"
     renders at a fixed ~113px regardless of viewport, so on mobile
     and narrow desktop widths (where --col shrinks toward nothing)
     the grid-based amount alone isn't enough to clear it */
  --info-shift: max(calc(2 * var(--col) + 2 * var(--gutter)), 152px);

  --mobile-breakpoint: 700px;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: "Suisse Int'l", -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: var(--size-body);
  font-weight: 300;
  line-height: 1.5;
}

a {
  color: inherit;
}

img {
  max-width: 100%;
}

:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 4px;
}

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

  html.home {
    scroll-snap-type: none;
  }
}

.grid {
  display: grid;
  grid-template-columns: repeat(12, var(--col));
  column-gap: var(--gutter);
  padding-inline: var(--margin);
}

@media (max-width: 700px) {
  .grid {
    display: block;
    padding-inline: var(--margin);
  }
}

/* Fixed chrome — header, project info and language switch never scroll.
   Only .project-gallery moves. */

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding-block: 24px;
  mix-blend-mode: difference;
  pointer-events: none;
}

.site-header .grid {
  align-items: center;
  pointer-events: none;
}

.site-header a {
  pointer-events: auto;
}

.site-nav a {
  color: #fff;
  text-decoration: none;
}

.site-nav {
  grid-column: 9 / 13;
  display: flex;
  justify-content: flex-start;
  gap: 8px;
}

.site-nav a:hover {
  opacity: 0.6;
}

/* Vertically centered on the left edge, independent of the header bar
   — on desktop. On mobile it moves back into the header row instead
   (see below), trading places with the project title / current-slide
   label, which takes over the vertically-centered left spot there. */
.wordmark-fixed {
  position: fixed;
  left: var(--margin);
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  pointer-events: none;
  mix-blend-mode: difference;
  z-index: 10;
}

.wordmark {
  font-size: var(--size-body);
  font-weight: 400;
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
  pointer-events: auto;
}

.wordmark:hover {
  opacity: 0.6;
}

@media (max-width: 700px) {
  .site-header {
    padding-block: 16px;
    /* viewport-fit=cover (needed for the info box's safe-area fix
       below) also pulls the header under the top notch/status bar
       without this */
    padding-top: calc(16px + env(safe-area-inset-top, 0px));
  }

  .site-header .grid {
    display: flex;
    align-items: center;
    justify-content: flex-end;
  }

  /* Regular instead of the sitewide Light default — thin text reads
     poorly at mobile sizes/viewing distance for functional nav like
     this. Body copy (the description that opens under "+ Info", the
     contact page's bio) stays Light. */
  .site-nav a {
    font-weight: 400;
  }

  /* 16px text is still a small tap target, and the two links sit
     right next to each other in the header row. The invisible halo
     grows the hit area without moving anything or touching layout;
     kept tighter than the project page's "+ Info" halo (which stands
     alone) since Portfolio/Contact only have 8px between them —
     -4px left/right avoids the two halos swallowing each other's
     taps, -10px top/bottom is safe because the header row itself has
     no other tap targets above or below to collide with. */
  .site-nav a {
    position: relative;
  }

  .site-nav a::after {
    content: "";
    position: absolute;
    inset: -10px -4px;
  }

  /* back in the header row instead of vertically centered — matches
     the same top padding as .site-header so it lines up with "Portfolio
     Contact" on the same line */
  .wordmark-fixed {
    top: 0;
    bottom: auto;
    height: auto;
    padding-top: calc(16px + env(safe-area-inset-top, 0px));
  }
}

/* Project page

   The title's own line sits at exactly the wordmark's height — not
   the center of a taller title+info block, which would push it
   above that line instead. .project-info is centered on just
   .project-title-block (its only flex child, sized to the h1's own
   line); .project-meta hangs below that as an absolutely-positioned
   sibling, so it can't affect where the title lands. */

.project-info {
  position: fixed;
  left: calc(var(--margin) + var(--info-shift));
  top: 0;
  bottom: 0;
  width: var(--title-width);
  display: flex;
  align-items: center;
  pointer-events: none;
  mix-blend-mode: difference;
  z-index: 10;
}

.project-info a,
.project-info button,
.project-info .project-description-content {
  pointer-events: auto;
}

.project-title-block {
  position: relative;
  width: 100%;
}

.project-title-block h1 {
  /* shrink-wrapped to the text, not the full column — matching
     #archive-hokkaido-roadtrip .archive-info h2 a below, which is an
     inline <a> sized to its text by default. A view transition morphs
     between the two elements' actual boxes; block-vs-inline meant
     morphing from a text-width box to a full-column one, which reads
     as the title zooming rather than just moving */
  display: inline-block;
  font-size: var(--size-body);
  font-weight: 300;
  color: #fff;
  margin: 0;
  text-align: left;
  /* pairs with #archive-hokkaido-roadtrip .archive-info h2 on the
     home page — matching names morph the title between the two on
     navigation, where the browser supports it. Overridden below per
     project once more than one exists, since the name has to be
     unique on the home page's archive list but this rule is shared
     by every project page. */
  view-transition-name: project-title;
}

.project-two-days-in-abu-dhabi .project-title-block h1 {
  view-transition-name: project-title-two-days-in-abu-dhabi;
}

.project-the-polestar-2-x-cherrydeck-experiment .project-title-block h1 {
  view-transition-name: project-title-the-polestar-2-x-cherrydeck-experiment;
}

.project-aleksei-olympic-park .project-title-block h1 {
  view-transition-name: project-title-aleksei-olympic-park;
}

.project-reina-in-tokyo .project-title-block h1 {
  view-transition-name: project-title-reina-in-tokyo;
}

.project-3am-in-tokyo .project-title-block h1 {
  view-transition-name: project-title-3am-in-tokyo;
}

.project-midnight-club-tokyo .project-title-block h1 {
  view-transition-name: project-title-midnight-club-tokyo;
}

.project-meta {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  margin-top: 8px;
  font-size: var(--size-small);
  color: #fff;
}

.project-description-toggle {
  display: block;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
  transform: translateY(0);
  opacity: 1;
  transition: transform 0.3s ease, opacity 0.3s ease;
}

/* "+ Info" lifts and fades out (not removed) when open, so the toggle
   keeps its line-height and the box above never resizes — and settles
   smoothly back into place once aria-expanded flips back, instead of
   snapping. */
.project-description-toggle[aria-expanded="true"] {
  transform: translateY(-8px);
  opacity: 0;
  pointer-events: none;
}

/* Year + description sit one line below "+ Info", not on top of it —
   "+ Info" still fades out rather than being removed (so its own line
   stays reserved, see .project-description-toggle above), but the
   revealed content flows normally beneath it, same as mobile. */
.project-description-content {
  cursor: pointer;
}

.project-description-content[hidden] {
  display: none;
}

.project-year {
  margin: 0 0 8px;
  /* plain opacity transition, toggled by a class in info-toggle.js —
     appears and disappears together with the description, right at
     open()/close(), rather than the word-by-word keyframe animation
     below (which needs the more involved restart-on-click machinery
     that's overkill for a single line, and — turned out — fragile) */
  opacity: 0;
  transition: opacity 0.25s ease;
}

.project-year.is-visible {
  opacity: 1;
}

.project-description {
  margin: 0;
}

.project-description .word {
  display: inline-block;
  opacity: 0;
}

@keyframes word-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes word-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(4px);
  }
}

.project-gallery {
  grid-column: 5 / 11;
  display: flex;
  flex-direction: column;
  gap: 0;
  list-style: none;
  margin: 0;
  padding-left: 0;
}

.project-gallery figure {
  margin: 0;
}

.project-gallery img {
  display: block;
  width: 100%;
  height: auto;
  cursor: zoom-in;
}

/* Fixed chrome sits above the fullscreen view; both sit above the
   normal page flow. */
.site-header {
  z-index: 10;
}

/* Fullscreen click-and-scroll view: desktop only (see project notes —
   mobile images are already full-bleed, a second fullscreen mode is
   redundant there). js/fullscreen-view.js also skips wiring it up on
   mobile; this is the visual backstop. */
.fullscreen-view {
  position: fixed;
  inset: 0;
  z-index: 5;
  background: #000;
}

@media (max-width: 700px) {
  .fullscreen-view {
    display: none;
  }

  .project-gallery img {
    cursor: auto;
  }
}

.fullscreen-view[hidden] {
  display: none;
}

body.fullscreen-active {
  overflow: hidden;
}

/* Portfolio/Contact and the project title + info stay up during the
   fullscreen viewer — only the wordmark goes, for a bit more of an
   immersive feel without losing all context. The wordmark's own
   mix-blend-mode flicker (see the view-transition note further down)
   isn't an issue once it's simply not there. */
.wordmark-fixed,
.site-footer {
  transition: opacity 0.2s ease;
}

body.fullscreen-active .wordmark-fixed,
body.fullscreen-active .site-footer {
  opacity: 0;
  pointer-events: none;
}

/* The title + info group slides down to the bottom instead of
   staying centered, out of the photo's way. .project-info's own box
   still spans the full height as a flex container (that's what keeps
   the title exactly at the wordmark's line normally) — only where its
   child sits within it moves, via align-items, with room reserved
   below for .project-meta. .project-title-block carries the
   view-transition-name (not .project-info itself, whose height would
   otherwise be interpolated from 100vh down to content-height — the
   same zoom-instead-of-slide problem the cross-page title morph had)
   so the browser animates this as a plain position change, in the
   same transition that's already animating the image to fullscreen.
   .project-info's width does change below (one grid column wider),
   but that's a much smaller jump than its height would be, so it
   morphs cleanly as part of the same transition instead of needing
   its own view-transition-name. */
.project-title-block {
  view-transition-name: project-position;
}

body.fullscreen-active .project-info {
  align-items: flex-end;
  /* enough room for the title, the toggle, and the description at
     its full open height, all stacked below the title with the
     description expanding downward same as the normal (non-
     fullscreen) state — not just the closed toggle line */
  padding-bottom: 160px;
  /* one grid column wider than the normal --title-width: a longer
     description reads as a block of short, stacked lines at 2
     columns, less so at 3. Desktop only — mobile never gets this
     class (fullscreen-view.js bails out before adding it there), so
     no separate mobile override is needed. */
  width: calc(3 * var(--col) + 2 * var(--gutter));
}


.fullscreen-track {
  height: 100vh;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  /* kept visible on purpose — it's the one hint that this view
     scrolls — but with a transparent track/background instead of the
     default solid one, which read as a stripe against the photo */
  scrollbar-color: rgba(255, 255, 255, 0.4) transparent;
}

.fullscreen-track::-webkit-scrollbar {
  background: transparent;
}

.fullscreen-track::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.4);
}

.fullscreen-track::-webkit-scrollbar-track {
  background: transparent;
}

.fullscreen-slide {
  height: 100vh;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.fullscreen-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  cursor: zoom-out;
}

.fullscreen-slide[data-orientation="portrait"] img {
  object-fit: contain;
}

@media (max-width: 700px) {
  /* project pages only — their mobile images run edge-to-edge with
     no gutter, so a black body avoids a light flash between them.
     Scoped to .project-page-body: a bare `body` selector here used
     to apply to every page, blacking out the imprint/privacy pages'
     dark text too — only ever hidden because every other page has a
     full-bleed image or photo hero covering the body regardless. */
  .project-page-body {
    background: #000;
  }

  .project-page {
    display: block;
  }

  /* Title sits a fixed distance above the bottom of the screen —
     not centered at the wordmark's height like on desktop. Bottom-
     anchored rather than the desktop top:0;bottom:0 flex-centering,
     which would bounce as iOS Safari's dynamic toolbar changes the
     viewport height. The 112px is deliberate breathing room below
     the title (for the "+ Info" toggle to hang in, see .project-meta
     further up) plus a small additional lift, measured so the title
     sits at the same height as the portfolio view's own title
     (.archive-current-project below).

     Crucially, .project-title-block and .project-meta keep their
     desktop position:relative/absolute pairing (declared further up,
     not overridden here): .project-meta hangs below the title as an
     absolutely-positioned sibling rather than a normal-flow one, so
     it can't push the title's own position around. Without that, a
     longer or shorter description — different per project, and
     always laid out in full even while "closed", so the toggle is
     just a fade with no layout shift — would change this block's
     total height and thus where a bottom anchor puts the title,
     landing it at a different spot on every project page. */
  .project-info {
    top: auto;
    bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px) + 112px);
    left: var(--margin);
    width: calc(100vw - 2 * var(--margin));
    display: block;
  }

  /* Regular instead of the sitewide Light default — the title is the
     one line of text every visitor reads here, thin weight hurt
     legibility at mobile sizes. The description that opens below (+
     the toggle itself) stays Light on purpose, see .project-meta. */
  .project-title-block h1 {
    font-weight: 400;
  }

  .project-description-content[hidden] {
    display: block;
  }

  /* The closed description is laid out (so nothing shifts when it
     opens) but invisible — it must not swallow taps meant for the
     toggle just above it. */
  .project-info .project-description-content[hidden] {
    pointer-events: none;
  }

  /* 30x18px text is far below a comfortable touch target. The invisible
     halo grows the hit area to roughly 74x50 without moving anything;
     skewed downward because thumbs tend to land low. */
  .project-description-toggle {
    position: relative;
  }

  .project-description-toggle::after {
    content: "";
    position: absolute;
    inset: -14px -24px -18px -20px;
  }

  .project-gallery {
    margin-inline: calc(-1 * var(--margin));
  }

  /* not shown on project pages on mobile for now */
  .project-page-body .site-footer {
    display: none;
  }
}

/* Home — highlight sequence + archive, one continuous page.

   Desktop uses "proximity", not "mandatory". The two don't behave the
   same when a snap point has nothing past it: "mandatory" always
   resolves to the single nearest snap point, forward or back, no
   matter the distance — with the archive strips deliberately carrying
   none of their own, the 5 hero slides were the only points on the
   whole page, so stopping anywhere below them (even at the very
   bottom, after scrolling through ten archive projects) still snapped
   all the way back up to the nearest one. A release point right after
   the hero (tried here at one point) only relocated the same problem
   — now IT was the sole point below the hero, pulling back to itself
   from arbitrarily far into the archive instead.

   "proximity" has no such pull from a distance — it only resolves to
   a snap point when the resting scroll position is already close to
   one, so the archive can carry zero snap points and still never get
   yanked anywhere. scroll-snap-stop:always on .hero-slide below still
   forces each highlight to be visited (not skipped over) during a
   fast scroll through the sequence itself.

   Mobile doesn't have this conflict — every project is already its
   own full-screen slide with nothing unsnapped between or after them
   — so it stays on "mandatory", which snaps more reliably on touch. */

html.home {
  scroll-snap-type: y proximity;
}

@media (max-width: 700px) {
  html.home {
    scroll-snap-type: y mandatory;
  }
}

.hero {
  margin: 0;
  position: relative;
}

.hero-slide {
  display: block;
  height: 100vh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.hero-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* vertically centered on the left, matching the project-page title —
   shows whichever highlight is currently in view */
.hero-current-title {
  position: fixed;
  left: calc(var(--margin) + var(--info-shift));
  /* two columns wider than the project-page / archive-strip title
     (--title-width): the longer hero titles ("The Polestar 2 ×
     Cherrydeck Experiment") otherwise wrap onto a second line here,
     and the title should sit on one line across all five slides.
     nowrap guarantees it at narrow desktop widths where the columns
     get small. */
  width: calc(var(--title-width) + 2 * (var(--col) + var(--gutter)));
  white-space: nowrap;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  text-align: left;
  font-size: var(--size-body);
  color: #fff;
  mix-blend-mode: difference;
  pointer-events: none;
  z-index: 10;
  margin: 0;
}

.archive {
  padding-block: 96px;
}

.archive-mobile {
  display: none;
}

.archive-project {
  /* stretch (default) so .archive-info matches the strip's height —
     that's what lets the title sit flush with its top */
  margin-bottom: 96px;
}

.archive-project:last-child {
  margin-bottom: 0;
}

.archive-info {
  /* two columns to grow into, left-aligned — same reasoning as
     --title-width on the project page */
  grid-column: 3 / 5;
}

.archive-info h2 {
  font-size: var(--size-body);
  font-weight: 300;
  margin: 0;
  text-align: left;
}

.archive-info h2 a {
  text-decoration: none;
}

/* always visible, static — no toggle/reveal interaction like the
   project page's own info box, just year + description sitting
   under the title */
.archive-year {
  margin: 8px 0 0;
  font-size: var(--size-small);
}

.archive-description {
  margin: 8px 0 0;
  font-size: var(--size-small);
}

/* each project gets its own view-transition-name, paired with a
   matching #title-<slug> rule further down on that project's own
   page — view-transition-name must be unique within a single
   document, and both archive entries sit on this page at once, so
   they can't all share one generic name the way the h1 side can
   (only one project page is ever open at a time there) */
#archive-hokkaido-roadtrip .archive-info h2 a {
  view-transition-name: project-title;
}

#archive-two-days-in-abu-dhabi .archive-info h2 a {
  view-transition-name: project-title-two-days-in-abu-dhabi;
}

#archive-the-polestar-2-x-cherrydeck-experiment .archive-info h2 a {
  view-transition-name: project-title-the-polestar-2-x-cherrydeck-experiment;
}

#archive-aleksei-olympic-park .archive-info h2 a {
  view-transition-name: project-title-aleksei-olympic-park;
}

#archive-reina-in-tokyo .archive-info h2 a {
  view-transition-name: project-title-reina-in-tokyo;
}

#archive-3am-in-tokyo .archive-info h2 a {
  view-transition-name: project-title-3am-in-tokyo;
}

#archive-midnight-club-tokyo .archive-info h2 a {
  view-transition-name: project-title-midnight-club-tokyo;
}

.archive-strip {
  /* same start column as the project-page gallery, so the image
     column lines up as one consistent edge across the site — but
     extended past the grid's own right padding all the way to the
     true edge of the screen, not just to the margin, since it's
     meant to look cut off/continuing rather than framed */
  grid-column: 5 / 13;
  width: calc(100% + var(--margin));
  display: flex;
  gap: 8px;
  /* every item shares this height; width follows from it per item
     (see .archive-strip-item) — scales with --col like the rest of
     the grid, so it doesn't change apparent shape at other viewport
     widths, just size */
  --strip-height: calc(3 * var(--col));
  height: var(--strip-height);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}

.archive-strip::-webkit-scrollbar {
  display: none;
}

.archive-strip-item {
  /* width is derived from the shared height, not a fixed share of
     the row — landscape images render properly wide/"strip-like"
     instead of being squeezed into a box sized for portrait content.
     Portrait images get their own fixed 4:5 crop (see below) rather
     than their native ratio, so a 2:3 source doesn't end up far
     narrower than everything else in the row. Two fixed ratios
     instead of one means widths are still just two predictable
     values, not arbitrary per image. */
  flex: 0 0 auto;
  height: 100%;
  width: calc(var(--strip-height) * 3 / 2);
  display: block;
}

.archive-strip-item[data-orientation="portrait"] {
  width: calc(var(--strip-height) * 4 / 5);
}

.archive-strip-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

@media (max-width: 700px) {
  /* no separate highlight sequence on mobile — every project is
     already a full-screen slide in the archive below, so a hero
     reprise of 5 of them would just be redundant */
  .hero,
  .hero-current-title {
    display: none;
  }

  /* every slide is full-bleed and they butt up with no gaps, so the
     body colour only ever shows in Safari's overscroll and behind its
     translucent toolbars — black there instead of a white fade, same
     as the project pages */
  html.home body {
    background: #000;
  }

  .archive {
    padding-block: 0;
  }

  .archive-desktop {
    display: none;
  }

  .archive-mobile {
    display: block;
  }

  .archive-mobile-slide {
    display: block;
    height: 100vh;
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }

  .archive-mobile-slide img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  /* Matches where the title TEXT itself sits on a project page, not
     just .project-info's own bottom anchor — that block grows upward
     from var(--margin), and the "+ Info" toggle plus the description
     space it reserves beneath the title (see .project-meta below)
     add roughly 88px under the title before reaching that anchor, so
     matching only the anchor would leave this title sitting visibly
     lower. The extra 24px on top is a deliberate further lift, not
     part of that measurement — Chris's call, for better balance.
     Landing the title at the same spot in the portfolio view and on
     its own project page means nothing needs to animate between
     them: no element sits mid-image either way, which is the point —
     it keeps the photo itself the one thing with visual weight. */
  .archive-current-project {
    /* Same rules as .project-title-block h1 (sits inside the shared
       .project-info container, see index.html), so title and "+ Info"
       land exactly where they do on the project pages. Regular, not
       the sitewide Light default, like that title. */
    display: inline-block;
    font-size: var(--size-body);
    font-weight: 400;
    color: #fff;
    text-align: left;
    margin: 0;
  }

  /* The fixed info block follows whichever slide is current (home.js
     sets which .project-meta is shown and toggles visibility). */
  .archive-current-info {
    visibility: hidden;
  }
}

/* Footer — Imprint/Privacy, present on every page. Ordinary document
   flow, right after each page's real content — reached only by
   scrolling there, never overlaying earlier content — then pulled up
   with a negative top margin so it overlaps the bottom of the last
   image instead of adding its own row below it. Deliberately not
   `position: absolute` against `main`: that depended on `main`'s full
   auto height being settled at the moment it's measured, which held
   up in testing here but not in practice on the project page. Normal
   flow has no such dependency — its position falls straight out of
   the actual rendered content, nothing to get out of sync with. */
.site-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding-right: var(--margin);
  /* 18px is this element's own rendered height (12px text at the
     body's 1.5 line-height); the margin pulls it up exactly that far
     plus one more --margin, landing it var(--margin) above the true
     bottom edge instead of directly on it */
  margin-top: calc(-18px - var(--margin));
  font-size: var(--size-small);
  color: #fff;
  mix-blend-mode: difference;
}

.site-footer a {
  color: #fff;
  text-decoration: none;
}

.site-footer a:hover {
  opacity: 0.6;
}

@media (max-width: 700px) {
  /* Same reasoning as .site-nav a's halo: 12px text, two links only
     8px apart, so the invisible hit area stays tighter than the
     standalone "+ Info" halo to avoid Imprint/Privacy swallowing
     each other's taps. */
  .site-footer a {
    position: relative;
  }

  .site-footer a::after {
    content: "";
    position: absolute;
    inset: -10px -4px;
  }
}

/* Contact page

   One full-bleed photo (placeholder gradient for now — see the HTML
   comment where a real <img> can drop in later without any layout
   change), with a scrim for legibility and two text blocks — English
   and Japanese, side by side, not a language switcher, just this
   page's own bilingual content — over it. Sized to exactly 100vh so
   it never needs to scroll on desktop. .site-footer needs no special
   handling here — its negative margin-top pulls it up over the
   bottom of this hero the same as it does everywhere else. */

.contact-hero {
  height: 100vh;
  overflow: hidden;
}

.contact-bg,
.contact-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.contact-bg {
  /* Leftover from before the real photo: this was the placeholder's
     own look (a lighter grey gradient) and stayed in place after the
     <picture> swap, now only visible for the moment the photo itself
     is still loading/decoding — which is exactly the grey flash (and
     grey status-bar strip, composited under Safari's translucent bar)
     Chris was seeing, not a Safari quirk. Solid black instead, same
     fallback color as .project-page-body uses while its own images
     load. */
  background: #000;
}

.contact-scrim {
  position: absolute;
  inset: 0;
  /* lighter still — was 0.45/0.6 originally (tuned for the old
     placeholder gradient), then 0.2/0.35; Chris asked for even less,
     the photo itself is already dark enough to carry the text. */
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.18));
}

.contact-content {
  position: relative;
  height: 100%;
  /* align-content centers the row as a whole in the 100vh hero;
     align-items keeps the EN/JP blocks flush at the top of that row
     relative to each other — with align-items:center instead, the
     shorter block (whichever it is) centers independently within the
     taller one's own height, so their first lines didn't match up */
  align-content: center;
  align-items: start;
  color: #fff;
}

.contact-block {
  grid-column: 5 / 8;
}

.contact-block-jp {
  grid-column: 8 / 11;
  /* Suisse Int'l has no Japanese glyphs — system fonts only, no
     Japanese webfont (too large to justify loading) */
  font-family: "Hiragino Sans", "Yu Gothic", "Noto Sans JP", sans-serif;
  /* at 300 the system Japanese font reads visibly heavier than Suisse
     Light next to it; 200 picks the next lighter cut where one exists
     (macOS Hiragino, Yu Gothic Light) and is a no-op where it doesn't */
  font-weight: 200;
}

.contact-block p {
  /* tighter than the site's usual 1.5 and 16px — five paragraphs in
     two languages need the room to fit 100vh without scrolling on
     desktop (see the spec note); a bit denser here beats cutting text */
  line-height: 1.35;
  margin: 0 0 8px;
}

.contact-block p:last-child {
  margin-bottom: 0;
}

.contact-links {
  /* a full extra line's worth of space (not just the usual 8px
     paragraph gap), so this reads as its own group, set apart from
     the bio text rather than one more line of it */
  margin-top: 40px;
  display: flex;
  gap: 16px;
  font-size: var(--size-body);
}

/* Tokyo clock (filled by js/tokyo-time.js, empty without JS): sits above
   the email row as the first line of the contact group. With the clock
   present, the email row follows it at the closer 16px instead of
   opening the group with its own 40px gap. */
.contact-time {
  margin-top: 40px;
}

.contact-time:empty {
  display: none;
}

.contact-time:not(:empty) + .contact-links {
  margin-top: 16px;
}

/* email stands alone; Instagram/Behance/Cherry Deck form their own
   line below it, one line break down — less separation than the
   email row gets from the bio text above, since these two rows
   belong together as one group */
.contact-links-social {
  margin-top: 16px;
}

.contact-links a,
.contact-links span {
  text-decoration: none;
}

.contact-links a:hover {
  opacity: 0.7;
}

/* not yet links — no Behance/Cherry Deck handles exist yet, see the
   HTML comment — styled the same but visually inert */
.contact-links span {
  opacity: 0.5;
}

@media (max-width: 700px) {
  /* Chris's call: on mobile the photo stays put as a fixed backdrop
     (one full viewport, pinned) and only the text scrolls over it —
     not the "stretch the background to match content height" approach
     used before. position:fixed takes .contact-bg/.contact-scrim out
     of the flow entirely, so .contact-hero's own height just follows
     .contact-content now (auto), no grid trick needed to make a
     background "keep up" with taller content. */
  .contact-hero {
    height: auto;
  }

  /* the dark hero covers the whole page on mobile, so the body colour
     only shows in Safari's overscroll and behind its translucent
     toolbars — black there, not a white fade (same as home and the
     project pages). Imprint/privacy stay light: dark text on cream. */
  .contact-page {
    background: #000;
  }

  .contact-bg,
  .contact-scrim {
    position: fixed;
    /* Left at inset:0 the photo runs under the notch/status-bar strip
       too, and Safari's "automatic" toolbar tint then samples that
       strip's actual pixels — which aren't black here, unlike on the
       project pages, so the bar reads as grey instead of true black.
       Starting the fixed layer below env(safe-area-inset-top) leaves
       that strip showing .contact-page's plain #000 body background
       instead, same true black Safari picks up there on the project
       pages. */
    top: env(safe-area-inset-top, 0px);
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: calc(100vh - env(safe-area-inset-top, 0px));
  }

  /* Replaces the desktop gradient with a flat black whose opacity
     js/contact-scrim.js drives from scroll position: 0.1 at the top
     (matching the desktop scrim's own starting darkness) ramping up to
     0.5 as the page scrolls, then holding there. Darkens the WHOLE
     fixed photo at once rather than a band tied to the text's position
     — the earlier version (a gradient on .contact-content, fading in
     just above the text) left a visible seam sweeping up the photo as
     that gradient's edge scrolled past underlying image detail; a
     scroll-linked overlay on the full image has no edge to show. 0.1
     here is just the pre-JS starting value, so there's no flash of a
     brighter, unscrimmed photo before the script runs. */
  .contact-scrim {
    background: #000;
    opacity: 0.1;
  }

  /* The mobile crop is much narrower than tall, so object-fit:cover's
     default centered crop cuts off the left edge — and Chris (holding
     the camera up) sits left-of-center in the frame, not dead center.
     Shifted left so he stays in the visible strip instead of being
     cropped out entirely. */
  .contact-bg img {
    object-position: 30% center;
  }

  .contact-content {
    /* .contact-bg/.contact-scrim are position:fixed (removed from the
       flow), and this is the next/last element in source order with
       no z-index of its own — plain DOM-order stacking already puts
       it on top, same as the desktop rule relies on, no z-index
       needed. */
    position: relative;
    height: auto;
    /* starts in the lower third of the first screen, clearing Chris's
       face in the fixed photo behind it; only this text scrolls from
       here on, the photo itself never moves. The extra 24px (~1 line
       of body text) nudges it down a bit further still. */
    padding-top: calc(66vh + 24px);
    padding-bottom: 96px;
  }

  .contact-block-jp {
    margin-top: 32px;
  }

  /* four links now (Instagram, Behance, Cherrydeck, LinkedIn): they
     just fit on one line at 375px, but not below ~360px — let them
     wrap there instead of running off the edge. Mobile only; on
     desktop the row keeps its single line. */
  .contact-links {
    flex-wrap: wrap;
  }
}

/* Imprint / Privacy — same header/footer chrome as everywhere else.
   Column span matches .project-gallery, so legal text reads at the
   same width as the photos on a project page. Spacing is top-margin
   only (h1 stays margin:0, flush with the page's own top padding) so
   adjoining elements never fight over a shared margin. */
.legal-page {
  padding-block: 128px 96px;
  min-height: 100vh;
}

.legal-content {
  grid-column: 5 / 11;
}

.legal-content h1 {
  font-size: var(--size-body);
  /* Regular instead of the site's usual Light, for better legibility
     in this much longer block of running legal text */
  font-weight: 400;
  margin: 0;
}

.legal-content h2 {
  font-size: var(--size-body);
  font-weight: 400;
  margin: 48px 0 0;
}

.legal-content p,
.legal-content ul,
.legal-content address {
  font-size: var(--size-body);
  font-style: normal;
  margin: 16px 0 0;
  padding-left: 0;
}

.legal-content ul {
  padding-left: 24px;
}

.legal-content li {
  margin-top: 8px;
}

.legal-content li:first-child {
  margin-top: 0;
}

@media (max-width: 700px) {
  .legal-page {
    padding-block: 96px 64px;
  }
}
