/* ==========================================================================
   S1 Design Studio — design system
   Rebuilt from scratch 2026-07-06, every value measured off the original
   Wix site at 1280px (see scripts/capture/extract-full.mjs dumps).
   ========================================================================== */

/* ---- fonts ---------------------------------------------------------------
   Originals are premium: Neue Haas Grotesk Display Pro (headlines/nav) and
   Neue Haas Unica Pro (body/buttons). Substituted with TeX Gyre Heros, a
   free true Helvetica clone. Only 400/700 exist, so 100–500 map to regular
   and 600–900 to bold. Playfair Display (name accent) and Raleway (form)
   are the exact free faces the original uses. Mulish stands in for the
   form-input face (Avenir LT 35 Light). */
@font-face {
  font-family: "TeX Gyre Heros";
  src: url("../assets/fonts/tex-gyre-heros-regular.woff2") format("woff2");
  font-weight: 100 500;
  font-display: swap;
}
@font-face {
  font-family: "TeX Gyre Heros";
  src: url("../assets/fonts/tex-gyre-heros-bold.woff2") format("woff2");
  font-weight: 600 900;
  font-display: swap;
}

:root {
  /* palette — measured */
  --ink: #363636;          /* all text, dark buttons */
  --ink-soft: #686868;     /* current nav item, footer line */
  --btn-border: #556b8d;   /* 1px border on dark buttons */
  --input-border: rgba(54, 54, 54, 0.6);
  --page: #ffffff;

  /* type */
  /* Grotesk role = Arimo (Google, well-hinted, Arial-metric Helvetica-alike).
     Replaced TeX Gyre Heros site-wide 2026-07-08 for even stroke rendering on
     Windows (TeX Gyre's rough hinting made stems look uneven at small sizes).
     TeX Gyre kept as fallback. Note the metric shift vs TeX Gyre @100px:
     cap 69 vs 73 (all-caps ×1.058), x-height 53 vs 52 (mixed-case ×0.981 —
     within tolerance, so mixed-case sizes are left at their measured values;
     only the all-caps nav + buttons are cap-re-matched). */
  --sans: "Arimo", "TeX Gyre Heros", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* body copy was Neue Haas Unica Pro *Light* (300); TeX Gyre Heros has no
     light cut, so it fell back to 400 and read too heavy. Inter has a true
     300 and is a clean neutral grotesque — use it for the light body text so
     the strokes stay thin & faint like the original (same #363636 colour). */
  --sans-light: "Inter", var(--sans);
  --serif: "Playfair Display", Georgia, serif;
  --form: "Raleway", var(--sans);
  --input: "Mulish", "Avenir Next", var(--sans);

  /* layout */
  --page-w: 1280px;
  --header-h: 110px;
  --gutter: 10px;          /* hero + portfolio grids (gap BETWEEN cards) */
  --edge: 5px;             /* outer L/R inset for hero + carousel strip */
  --g-gutter: 8px;         /* project galleries */
  --sbw: 0px;              /* scrollbar width, set by JS — keeps full-bleed centred */
}

/* full-bleed helper: spans the exact content width (viewport minus the
   scrollbar), centred on it — so left/right gaps stay equal regardless of
   the scrollbar. Used by the header, hero, and carousel strip. */
.full-bleed {
  width: calc(100vw - var(--sbw));
  margin-left: calc(50% - 50vw + var(--sbw) / 2);
}

/* ---- reset ---------------------------------------------------------------*/
*, *::before, *::after { box-sizing: border-box; }
/* overflow-y: scroll keeps the vertical scrollbar gutter reserved on EVERY page
   (inert on short pages) so the content width never shifts when switching
   between a tall page and a short one — and keeps --sbw stable across swaps */
html { -webkit-text-size-adjust: 100%; overflow-x: hidden; overflow-y: scroll; }
body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 400;
  font-size: 15px;
  line-height: 1.8;
  /* render strokes lighter/thinner (grayscale AA) — the original site does
     this, and it's what lets the light body copy read faint & elegant */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
p { margin: 0; }
h1, h2, h3 { margin: 0; font-weight: 400; }
button { font: inherit; background: none; border: 0; padding: 0; cursor: pointer; color: inherit; }

.page {
  max-width: var(--page-w);
  margin: 0 auto;
  position: relative;
}

/* ==========================================================================
   Header — logo dead-centre (185×32), nav right: three 102px cells ending
   30px from the page edge, 13px/50px, current page #686868
   ========================================================================== */
/* full-bleed header: spans the viewport; logo stays centred, nav pins to the
   right edge — content below stays inside the centred .page column */
.site-header {
  width: calc(100vw - var(--sbw));
  margin-left: calc(50% - 50vw + var(--sbw) / 2);
  height: var(--header-h);
  /* sticky so it stays put across page swaps and can hide/reveal on scroll.
     white bg + z-index so content scrolls cleanly underneath it */
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--page);
  /* plain slow fade in on load */
  animation: headerFade 2s ease both;
  /* slide up when scrolling down, glide back on scroll up */
  transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.3s ease;
  will-change: transform;
}
/* hidden: slid up out of view (used when scrolling down the page) */
.site-header.is-hidden { transform: translateY(-100%); }
/* stuck: page has been scrolled — a whisper of shadow lifts it off content */
.site-header.is-stuck { box-shadow: 0 6px 22px rgba(0, 0, 0, 0.06); }
@keyframes headerFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Wix-style content swap: fade the old page out, the new page in. The header
   above never re-renders — only <main> is replaced (see main.js router). */
main { transition: opacity 0.2s ease; }
main.is-swapping { opacity: 0; }
/* Entrance flourishes replay on SPA nav, not just on load/refresh — Samuel
   wants the launch animation every time you land on a page, incl. via nav.
   This matches Portfolio (tiles fade up on scroll) and Contact (contactRise/PhotoIn); Home's
   hero rise + carousel settle now play on swap too (carousel handled in JS). */
.site-header .logo {
  position: absolute;
  left: 50%;
  top: 39px;
  transform: translateX(-50%);
}
.site-header .logo img { width: 185px; height: 32px; object-fit: cover; }

.nav {
  position: absolute;
  right: 30px;
  top: 30px;
  display: flex;
}
.nav a {
  display: block;
  width: 102px;
  text-align: center;
  /* Nav is all-caps, so size is cap-height-driven. Arimo (now --sans) has a
     smaller cap than TeX Gyre, so bump 11.7 → 12.4px to hold the same cap. */
  font-size: 12.4px;
  letter-spacing: 0.5px;
  line-height: 50px;
  color: var(--ink);
  transition: color 0.15s ease;
}
.nav a:hover { color: var(--ink-soft); }
.nav a.active { color: var(--ink-soft); }

.nav-toggle { display: none; }

/* ==========================================================================
   Footer — icons 22px / 10px apart, line 10px 0.8ls #686868, 21px bottom pad
   ========================================================================== */
.site-footer { text-align: center; padding-bottom: 21px; }
.site-footer .socials {
  display: flex;
  justify-content: center;
  gap: 10px;
}
.site-footer .socials .social-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--ink);
  transition: opacity 0.2s ease;
}
.site-footer .socials .social-icon svg { width: 22px; height: 22px; display: block; }
.site-footer .socials .social-icon:hover { opacity: 0.7; }
.site-footer .copyright {
  margin-top: 10px;
  /* original is Neue Haas Grotesk Display Pro 10px / 0.8px tracking. That
     cut is more condensed than our TeX Gyre Heros, which runs ~7% wider at
     the same px. Nudging to 9.6px keeps the original's open 0.6px tracking
     rhythm while matching its exact line width (~227px). */
  font-size: 9.6px;
  letter-spacing: 0.6px;
  line-height: normal;
  color: var(--ink-soft);
}
.site-footer .legal-link {
  display: inline-block;
  margin-top: 6px;
  font-size: 9.6px;
  letter-spacing: 0.6px;
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: 2px;
  transition: color 0.15s ease;
}
.site-footer .legal-link:hover { color: var(--ink); }

/* per-page gap between last content and the footer icons — measured */
.home main          { padding-bottom: 114px; }
.legal-page main    { padding-bottom: 96px; }
.portfolio-page main{ padding-bottom: 86px; }
.contact-page main  { padding-bottom: 0; } /* image meets the footer band cleanly, like it meets the header */
/* footer mirrors the header: same-height band with its content centred, so the
   page is framed symmetrically top and bottom (still one screen, no scroll) */
.contact-page .site-footer {
  /* matches the header's 110px band on normal/tall screens (symmetric framing);
     eases smaller only on short screens where a full band would squeeze the
     form off the fold. Content stays vertically centred in the band. */
  min-height: clamp(80px, 12.2vh, var(--header-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0;
}
.project-page main  { padding-bottom: 51px; }
/* short galleries: the original keeps the footer under the content and
   pads the page out to 900px */
.project-page .page { min-height: 900px; }

/* ==========================================================================
   Dark button — 230×50, #363636, 1px #556b8d, r10, 12px/500 white label
   ========================================================================== */
.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 230px;
  height: 50px;
  margin: 0 auto;
  background: var(--ink);
  border: 1px solid var(--btn-border);
  border-radius: 10px;
  color: #fff;
  /* all-caps label — cap-height-driven; bump 12 → 12.7px for Arimo's smaller cap */
  font-size: 12.7px;
  font-weight: 500;
  line-height: 16.8px;
  transition: background 0.25s ease, color 0.25s ease;
}
/* invert to white on hover (dark label + border stay) */
.btn:hover { background: #fff; color: var(--ink); }

/* ==========================================================================
   HOME
   ========================================================================== */

/* hero — three 3:4 tiles, full-bleed across the viewport (as the original),
   10px outer margins and gutters */
.hero {
  width: calc(100vw - var(--sbw));
  margin-left: calc(50% - 50vw + var(--sbw) / 2);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
  padding: 0 var(--edge);
}
.hero-card {
  position: relative;
  margin: 0; /* kill the default <figure> margin so the card fills its grid cell */
  border-radius: 14px;
  overflow: hidden; /* clips the hover zoom to the rounded frame */
  /* fade + rise in on load/refresh, gently staggered across the three cards.
     Duration history: 1.8s → 1.4s → 0.7s (matched Contact) → 1.1s. Samuel kept
     the Contact-era 0.10s gap between cards but wanted each card's own motion
     slower and gentler, so hero and `contactRise` now deliberately DIVERGE —
     they are no longer meant to be kept in sync.
     Easing softened from cubic-bezier(0.22, 0.61, 0.36, 1) to easeOutQuad: the
     old curve was ~55% travelled a quarter of the way in (a visible surge that
     then braked); this one is ~43% there — less snap, a longer even settle. */
  animation: heroRise 1.1s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
/* gap BETWEEN cards tightened 0.20s → 0.10s (Samuel) so the three read as one
   cascade rather than three separate arrivals; Contact's rows step 0.07s. The
   0.15s lead-in before card 1 is unchanged. */
.hero-card:nth-child(1) { animation-delay: 0.15s; }
.hero-card:nth-child(2) { animation-delay: 0.25s; }
.hero-card:nth-child(3) { animation-delay: 0.35s; }
.hero img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  /* gentle premium zoom — starts fast, decelerates hard into a settle */
  transform: scale(1);
  transition: transform 1.8s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
/* subtle darken veil, bottom-weighted for a refined vignette; fades in on hover */
.hero-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(20, 20, 20, 0.42), rgba(20, 20, 20, 0.14) 55%, rgba(20, 20, 20, 0.04));
  opacity: 0;
  transition: opacity 0.9s ease;
  pointer-events: none;
}
.hero-card:hover img { transform: scale(1.07); }
.hero-card:hover::after { opacity: 1; }
@keyframes heroRise {
  from { opacity: 0; transform: translateY(34px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* tagline — 26px/36.4, 117px above, strip starts 122px below the line */
.tagline {
  text-align: center;
  font-size: 25px;
  line-height: 35px;
  letter-spacing: -0.01em;
  padding: 117px 20px 119px;
}

/* carousel strip — inset 10px each side to align with the hero cards,
   675×506 slides, 10px apart, arrow r23 */
.strip {
  position: relative;
  width: calc(100vw - var(--sbw) - 2 * var(--edge));
  margin-left: calc(50% - 50vw + var(--sbw) / 2 + var(--edge));
  height: 506px;
  overflow: hidden;
}
.strip-track {
  display: flex;
  gap: var(--gutter);
  height: 100%;
  will-change: transform;
  backface-visibility: hidden;      /* GPU compositing — avoids subpixel shimmer during slide */
  -webkit-backface-visibility: hidden;
}
.strip-track img {
  flex: 0 0 675px;
  width: 675px;
  height: 506px;
  object-fit: cover;
  border-radius: 14px;      /* match the hero cards */
}
.strip-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 35px;
  height: 35px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s ease;
  z-index: 2;
}
.strip-arrow:hover { background: rgba(255, 255, 255, 0.95); }
.strip-arrow svg { width: 13px; height: 13px; stroke: #000; fill: none; stroke-width: 1.6px; }
.strip-arrow.next { right: 23px; }
.strip-arrow.prev { left: 23px; }
.strip-arrow[hidden] { display: none; }

/* MORE PROJECTS sits 50px under the strip */
.strip + .btn { margin-top: 50px; }

/* services — equal ~82px gap above the heading and below the grid (before the
   founder band). NOTE: measure with the scroll-reveal in its FINAL state —
   .services/.founder are .reveal elements, and their pre-reveal translateY
   offsets the numbers (this bit us once: looked 95/95, was really 69/95). */
.services { text-align: center; margin-top: 82px; }
.services h2 {
  /* mixed-case heading — x-height-driven; Arimo x ≈ TeX Gyre x, so keep 34px */
  font-size: 34px;
  line-height: 47.6px;
  letter-spacing: -0.01em;
}
.services .intro {
  margin-top: 17px;
  /* Inter Light: TeX Gyre Heros has no light cut, and Inter (300) is both
     lighter and more evenly hinted than the free Grotesk clone — cleaner,
     more consistent stroke, especially on Windows */
  font-family: var(--sans-light);
  font-weight: 300;
  /* Inter x-height is ~6% larger than TeX Gyre at the same px (55 vs 52 @100px),
     so 18 -> 17px keeps the visual size identical — only weight/rendering change */
  font-size: 17px;
  line-height: 32.4px;
  letter-spacing: -0.02em;
}
.services .intro .sub {
  display: block;
  font-size: 15px;
}
.services-grid {
  width: 982px;
  max-width: 100%;
  margin: 70px auto 0;
  display: grid;
  grid-template-columns: repeat(3, 300px);
  justify-content: space-between;
}
.services-grid h3 {
  /* original is Neue Haas Unica Pro bold 20px; our TeX Gyre Heros bold has a
     ~6% larger x-height, so match the original's x-height (20 -> 18.9px) */
  font-size: 18.9px;
  font-weight: 700;
  line-height: 28px;
}
.services-grid p {
  margin-top: 18px;
  font-family: var(--sans-light);
  font-weight: 300;
  /* original body is Neue Haas Unica Pro Light 15px; Inter's x-height is ~8%
     larger, so match the original's x-height (15 -> 13.9px) */
  font-size: 13.9px;
  line-height: 27px;
}
/* The three service blurbs use explicit <br class="svc-br"> to reproduce the
   original Wix line distribution at the fixed 300px-column desktop layout.
   Our substitute fonts (Inter/TeX Gyre) are narrower than the original Neue
   Haas Unica, so auto-wrapping (incl. text-wrap:balance/pretty) breaks at
   different points and orphans the last line — forced breaks match faithfully.
   The layout stays locked (fixed 300px columns + these breaks) at every width
   until the 982px grid no longer fits (~981px), where breaks switch off and
   columns go fluid. Each <br> is authored with a leading space so hiding it
   still leaves a word gap (no "conceptto" joins). */
.svc-br { display: inline; }

/* founder — 943px band: 462×616 portrait, 51px gap, 430px text column */
.founder {
  width: 943px;
  max-width: 100%;
  margin: 82px auto 0; /* equal to .services margin-top — see note there */
  display: grid;
  grid-template-columns: 462px 430px;
  justify-content: space-between;
}
.founder .portrait {
  width: 462px;
  height: 616px;
  object-fit: cover;
  border-radius: 14px;
}
.founder .name {
  margin-top: 54px;
  font-family: var(--serif);
  font-size: 28px;
  font-weight: 700;
  line-height: normal;
}
.founder .role {
  margin-top: 7px;
  font-family: var(--serif);
  font-size: 12px;
  font-weight: 600;
  line-height: normal;
}
.founder .bio { margin-top: 51px; font-family: var(--sans-light); font-weight: 300; font-size: 13.9px; line-height: 27px; }
.founder .bio + .bio { margin-top: 34px; }
.founder .btn { margin-top: 73px; }

/* ==========================================================================
   PORTFOLIO — 3-across tiles, full-bleed like the hero: --edge (5px) outer
   inset, --gutter (10px) between tiles. Locked at 3 columns at every width
   (Samuel's call — no responsive collapse). Tiles are square until squares
   would push the 2nd row off screen, then they cap to landscape so the first
   6 always fit the viewport (see --tile-h below).
   ========================================================================== */
.portfolio-grid {
  /* One column's width, and the tile height we actually use. Tiles are SQUARE
     (height = column width) until that would push the second row off screen —
     then they cap, so the first 6 are always visible without scrolling
     (Samuel's ask at a wide ~1990px window, where 653px squares left room for
     only one row). The cap is half the space under the header, less the row
     gutter and a bottom breath. Capping crops the tile to landscape; it does
     NOT shrink it away from its column. */
  --col: calc((100vw - var(--sbw) - var(--edge) * 2 - var(--gutter) * 2) / 3);
  --tile-h: min(var(--col),
                calc((100dvh - var(--header-h) - var(--gutter) - var(--edge)) / 2));
  width: calc(100vw - var(--sbw));
  margin-left: calc(50% - 50vw + var(--sbw) / 2);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: var(--tile-h);
  gap: var(--gutter);
  padding: 0 var(--edge);
}
.portfolio-grid a {
  position: relative;
  display: block;
  /* height comes from grid-auto-rows (--tile-h) — deliberately NOT aspect-ratio:
     a max-height on an aspect-ratio box transfers back through the ratio and
     shrinks the WIDTH too, which detaches tiles from their columns */
  height: 100%;
  overflow: hidden;
  border-radius: 14px;      /* match the hero cards + carousel slides */
  /* Scroll-driven "shuffle up": each tile is dealt into place as it enters the
     viewport (JS adds .in via IntersectionObserver — see initTiles in main.js).
     Reads as a card being pushed up rather than a dissolve: it travels far
     (86px) from slightly small (0.94), the opacity ramps FAST (0.3s) so the
     tile is already solid for most of the move, and the transform easing
     overshoots a touch before settling. */
  opacity: 0;
  transform: translateY(86px) scale(0.94);
  transition: opacity 0.3s ease-out,
              transform 0.82s cubic-bezier(0.18, 0.86, 0.26, 1.06);
  will-change: opacity, transform;
}
.portfolio-grid a.in { opacity: 1; transform: none; }
.portfolio-grid img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* same premium hover as the hero cards: gentle zoom that eases in and settles */
  transform: scale(1);
  transition: transform 1.8s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
/* darken veil, bottom-weighted, fades in on hover — matches the hero cards */
.portfolio-grid a::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(20, 20, 20, 0.42), rgba(20, 20, 20, 0.14) 55%, rgba(20, 20, 20, 0.04));
  opacity: 0;
  transition: opacity 0.9s ease;
  pointer-events: none;
}
.portfolio-grid a:hover img { transform: scale(1.07); }
.portfolio-grid a:hover::after { opacity: 1; }
/* NO transition-delay here on purpose: the order is driven entirely by the JS
   queue in initTiles (strictly one card at a time, 1→10, each waiting on the
   one before). A CSS delay can't express that — a whole row crosses the fold
   in the same frame, so the cadence has to be timed, not declared. */

/* ==========================================================================
   CONTACT — left column x161 w478; photo stack right (396×495 @ 684.5,132)
   ========================================================================== */
/* viewport-locked split — the whole page (header + split + footer) fits the
   screen, so the form is fillable with ZERO scroll. Form left, vertically
   centred; full-height portrait photo right, running header-to-footer.
   Vertical spacings clamp() against vh so the column compresses gracefully
   on shorter laptop screens instead of overflowing. */
.contact-page .page {
  min-height: 100dvh; /* min-, not fixed: if a screen is truly too short the
                         page grows and scrolls rather than clipping fields */
  display: flex;
  flex-direction: column;
}
/* two equal halves so the gap channel sits dead-centre on the page — directly
   above the (centred) Instagram icon in the footer. The form hugs the channel
   (justify-self:end); the photo fills the right half. */
.contact-main {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 64px;
  align-items: stretch;
}
.contact-col {
  align-self: center;
  justify-self: end;
  width: 478px;
  margin: 0;
  padding: clamp(4px, 0.6vh, 18px) 0;
}
.contact-photo {
  position: relative;
  margin: 0; /* kill the default <figure> margin so it fills its grid cell */
  overflow: hidden;
  border-radius: 14px;
  background: #f0efec; /* neutral placeholder while the image loads */
}
.contact-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  /* slideshow: each photo fades in, holds while slowly zooming, dissolves to
     the next. 3 photos, 21s loop, staggered a third apart so the fade-out of
     one overlaps the fade-in of the next (true crossfade, no blank frame). */
  animation: contactCross 21s ease-in-out infinite;
  will-change: opacity, transform;
}
/* img 1 starts at its hold point (−1.7s ≈ the 8% mark) so it's steady & fully
   visible the instant the page loads — no mid-fade "jump" on refresh. imgs 2
   and 3 use positive delays, so they stay hidden until their turn (the loop
   still crossfades continuously once it gets going). */
.contact-photo img:nth-child(1) { animation-delay: -1.7s; }
.contact-photo img:nth-child(2) { animation-delay: 5.3s; }
.contact-photo img:nth-child(3) { animation-delay: 12.3s; }
@keyframes contactCross {
  0%   { opacity: 0; transform: scale(1.05); }
  8%   { opacity: 1; }
  33%  { opacity: 1; }
  41%  { opacity: 0; transform: scale(1.13); }
  100% { opacity: 0; transform: scale(1.05); }
}

/* entrance — the form cascades up (same rise+fade as the hero cards /
   portfolio tiles), the photo panel dissolves in. Plays on load AND on SPA
   nav (not disabled for .spa-swap), so landing on Contact always animates. */
@keyframes contactRise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
@keyframes contactPhotoIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.contact-col > .heading,
.contact-col > .lead,
.contact-col > .mail,
.contact-col > .tel,
.contact-form > * {
  animation: contactRise 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
.contact-col > .heading { animation-delay: 0.12s; }
.contact-col > .lead    { animation-delay: 0.19s; }
.contact-col > .mail    { animation-delay: 0.26s; }
.contact-col > .tel     { animation-delay: 0.31s; }
.contact-form > *:nth-child(1) { animation-delay: 0.38s; } /* names row */
.contact-form > *:nth-child(2) { animation-delay: 0.45s; } /* email */
.contact-form > *:nth-child(3) { animation-delay: 0.52s; } /* message */
.contact-form > *:nth-child(4) { animation-delay: 0.59s; } /* referral */
.contact-form > *:nth-child(5) { animation-delay: 0.66s; } /* SEND */
.contact-photo { animation: contactPhotoIn 1.1s ease 0.15s both; }

.contact-col .heading {
  text-align: center;
  /* matches the Wix original's 50px on normal-height screens; the vh term only
     compresses it on genuinely short viewports so the form stays above the fold */
  font-size: clamp(31px, 5.6vh, 50px);
  font-weight: 700;
  line-height: 1.38;
  letter-spacing: -0.01em;
}
.contact-col .lead {
  margin-top: clamp(8px, 1.5vh, 23px);
  text-align: center;
  font-family: var(--sans-light);
  font-weight: 300;
  font-size: 13px;
  line-height: 19.6px;
}
.contact-col .mail {
  display: block;
  margin-top: clamp(5px, 1vh, 13px);
  text-align: center;
  font-family: var(--sans-light);
  font-weight: 300;
  font-size: 13px;
  line-height: 19.6px;
  text-decoration: underline;
}
.contact-col .tel {
  display: block;
  margin-top: 1px;
  text-align: center;
  font-family: var(--sans-light);
  font-weight: 300;
  font-size: 13px;
  line-height: 19.6px;
}

.contact-form { margin-top: clamp(16px, 2.4vh, 49px); }
.contact-form .row { display: flex; gap: 24px; }
.contact-form .field { flex: 1; }
.contact-form .field + .field-block,
.contact-form .field-block + .field-block { margin-top: clamp(9px, 1.2vh, 28px); }
.contact-form .field-block.referral { margin-top: clamp(8px, 1.5vh, 21px); }
.contact-form label {
  display: block;
  font-family: var(--form);
  font-size: 14px;
  line-height: 19.6px;
  margin-bottom: 9px;
}
.contact-form input,
.contact-form textarea {
  width: 100%;
  height: 42px;
  border: 1px solid var(--input-border);
  border-radius: 15px;
  background: #fff;
  padding: 0 13px;
  font-family: var(--input);
  font-weight: 300;
  font-size: 16px;
  color: var(--ink);
}
.contact-form textarea {
  height: clamp(42px, 5.5vh, 92px);
  padding-top: 9px;
  resize: none;
}
.contact-form ::placeholder { color: #757575; opacity: 1; }
.contact-form input:focus,
.contact-form textarea:focus { outline: none; border-color: var(--ink); }
/* required-field errors (shown on submit; cleared as the visitor types) */
.contact-form input.invalid,
.contact-form textarea.invalid { border-color: #b3261e; }

.contact-form .send {
  margin-top: clamp(14px, 2.1vh, 38px);
  min-width: 102px;
  height: 41px;
  padding: 11px 29px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  border-radius: 100px;
  color: #fff;
  font-family: var(--form);
  font-size: 16px;
  line-height: 19.2px;
  transition: opacity 0.2s ease;
}
.contact-form .send:hover { opacity: 0.88; }
.form-note {
  margin-top: 14px;
  font-size: 14px;
  line-height: 19.6px;
  display: none;
}
.form-note.show { display: block; }
.form-note.error { color: #b3261e; }

/* ==========================================================================
   PROJECT PAGES — the photos inside a project carry the Portfolio card FORMAT
   (full-bleed --edge inset, --gutter spacing, locked 3 columns, 14px radius,
   same zoom + darken hover) but keep their OWN aspect ratio: 3 masonry columns
   of natural-height photos, dealt round-robin across the columns so the
   numbering still reads left-to-right. Nothing is cropped — Samuel's call, the
   photos are framed the way they were shot (portraits stay portrait).
   MORE PROJECTS stays 80px below the gallery.
   ========================================================================== */
.gallery {
  width: calc(100vw - var(--sbw));
  margin-left: calc(50% - 50vw + var(--sbw) / 2);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
  padding: 0 var(--edge);
  align-items: start;       /* columns run to their own natural height */
}
.gallery .col {
  display: flex;
  flex-direction: column;
  gap: var(--gutter);
}
.gallery .tile {
  position: relative;
  margin: 0;                /* <figure> default margin would break the column */
  overflow: hidden;         /* clips the hover zoom to the rounded frame */
  border-radius: 14px;
  cursor: pointer;          /* the whole tile opens the lightbox */
}
.gallery .tile img {
  width: 100%;
  height: auto;             /* natural aspect ratio — no object-fit crop */
  display: block;
  cursor: pointer;
  /* same premium hover as the hero cards + portfolio tiles */
  transform: scale(1);
  transition: transform 1.8s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
.gallery .tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(20, 20, 20, 0.42), rgba(20, 20, 20, 0.14) 55%, rgba(20, 20, 20, 0.04));
  opacity: 0;
  transition: opacity 0.9s ease;
  pointer-events: none;     /* clicks fall through to the <img> (lightbox) */
}
.gallery .tile:hover img { transform: scale(1.07); }
.gallery .tile:hover::after { opacity: 1; }
.gallery + .btn { margin-top: 80px; }

/* lightbox — approximation of the Wix expand view */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(255, 255, 255, 0.98);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.lightbox.open { opacity: 1; pointer-events: auto; }
.lightbox img {
  max-width: 90vw;
  max-height: 86vh;
  object-fit: contain;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
}
.lightbox .lb-close,
.lightbox .lb-prev,
.lightbox .lb-next {
  position: absolute;
  color: var(--ink);
  line-height: 1;
  padding: 12px;
}
.lightbox .lb-close { top: 18px; right: 24px; font-size: 30px; }
.lightbox .lb-prev { left: 14px; top: 50%; transform: translateY(-50%); font-size: 38px; }
.lightbox .lb-next { right: 14px; top: 50%; transform: translateY(-50%); font-size: 38px; }
.lightbox button:hover { opacity: 0.6; }

/* ==========================================================================
   Legal pages (Privacy Policy) — simple centred prose column
   ========================================================================== */
/* Type is scaled to ~0.8 of the site's default prose size so the whole policy
   reads as fine print. The column runs as wide as the page's safe area allows
   — 920px, which stays inside the header nav's 30px page inset — so the block
   fills the width without crowding the edges. Leading is opened up (1.85)
   because the long measure needs the extra air to track line-to-line. */
.legal {
  max-width: 920px;
  margin: 0 auto;
  padding: 24px 24px 0;
}
.legal h1 {
  font-size: 30px;        /* 40 × 0.75 */
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
}
.legal .updated {
  margin-top: 10px;
  font-family: var(--sans-light);
  font-weight: 300;
  font-size: 11.3px;      /* 13.9 × 0.81 */
  color: var(--ink-soft);
}
.legal h2 {
  margin-top: 30px;
  font-size: 15.2px;      /* 18.9 × 0.80 */
  font-weight: 700;
  line-height: 1.3;
}
.legal p,
.legal li {
  margin-top: 11px;
  font-family: var(--sans-light);
  font-weight: 300;
  font-size: 12.2px;      /* 15 × 0.81 */
  line-height: 1.85;
  /* no single-word last lines (orphans); falls back to normal wrapping
     in browsers without text-wrap: pretty */
  text-wrap: pretty;
}
.legal ul { margin: 6px 0 0; padding-left: 18px; }
.legal a { text-decoration: underline; text-underline-offset: 2px; }
.legal a[href^="tel:"] { white-space: nowrap; }  /* never split the phone number across lines */
.legal a:hover { color: var(--ink-soft); }

/* ==========================================================================
   WeChat popup — the original opens a small "Wechat ID" lightbox
   ========================================================================== */
.wechat-modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(0, 0, 0, 0.35);
  display: none;
  align-items: center;
  justify-content: center;
}
.wechat-modal.open { display: flex; }
.wechat-modal .box {
  background: #fff;
  border-radius: 10px;
  padding: 36px 48px;
  text-align: center;
  position: relative;
}
.wechat-modal .box svg { width: 44px; height: 44px; margin: 0 auto 12px; color: var(--ink); }
.wechat-modal .box p { font-family: var(--sans-light); font-weight: 300; font-size: 15px; line-height: 27px; }
.wechat-modal .box .close {
  position: absolute;
  top: 8px;
  right: 14px;
  font-size: 22px;
}

/* ==========================================================================
   Entrance motion — approximation of the Wix fade-up reveals; remove or
   retune freely (Samuel: tweak duration/offset here)
   ========================================================================== */
.reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94),
              transform 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.reveal.in { opacity: 1; transform: none; }
/* MORE PROJECTS CTA: same on-scroll fade-up, but with a subtle scale "pop"
   so the button gets its own little entrance moment. Transition must list the
   entrance props AND the hover props (background/color), else .reveal's
   transition overrides .btn's and the hover invert snaps instantly. */
.btn.reveal {
  transform: translateY(20px) scale(0.96);
  transition: opacity 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94),
              transform 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94),
              background 0.25s ease, color 0.25s ease;
}
.btn.reveal.in { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .btn.reveal { transform: none; }
  .contact-photo img { animation: none; transform: none; opacity: 0; }
  .contact-photo img:first-child { opacity: 1; }
  .contact-col > .heading, .contact-col > .lead, .contact-col > .mail,
  .contact-col > .tel, .contact-form > *, .contact-photo {
    animation: none; opacity: 1; transform: none;
  }
  .hero-card { animation: none; }
  .hero-card:hover img { transform: none; }
  .hero img { transition: none; }
  .portfolio-grid a { opacity: 1; transform: none; transition: none; }
  .portfolio-grid a:hover img { transform: none; }
  .portfolio-grid img { transition: none; }
  .site-header { animation: none; transition: none; }
  main { transition: none; }
}

/* ==========================================================================
   Responsive — the original is a fixed 1280 desktop design; below that we
   scale gracefully (original Wix had a separate mobile layout; fine-tune
   with Samuel later)
   ========================================================================== */
/* Services blurbs AND the founder bio stay locked in the faithful desktop
   layout (services: fixed 300px columns + forced <br> line distribution;
   founder: fixed 462px portrait + 430px text column) at every width down to
   here, where the ~982px grids no longer fit the viewport. Only then do the
   forced breaks switch off and both blocks go fluid so the text wraps
   naturally. The spaces authored before each <br class="svc-br"> keep word
   gaps intact when the breaks are hidden (so no "conceptto" glue-ups). */
@media (max-width: 981px) {
  .svc-br { display: none; }
  .services-grid { width: auto; padding: 0 40px; grid-template-columns: repeat(3, 1fr); gap: 40px; }
  .founder { width: auto; padding: 0 40px; grid-template-columns: 1fr 1fr; gap: 48px; }
  .founder .portrait { width: 100%; height: auto; aspect-ratio: 3 / 4; }
}

@media (max-width: 900px) {
  .strip-track img { flex-basis: 52vw; width: 52vw; height: 100%; }
  .strip { height: 40vw; min-height: 300px; }
  /* stacked mobile: the no-scroll constraint is dropped — form first, photo below */
  .contact-main { grid-template-columns: 1fr; gap: 44px; padding: 0 5vw; }
  .contact-col { margin: 0 auto; max-width: 478px; width: 100%; }
  .contact-photo { min-height: 300px; }
}

@media (max-width: 760px) {
  .nav {
    display: none;
    position: absolute;
    top: var(--header-h);
    right: 0;
    left: 0;
    background: #fff;
    flex-direction: column;
    z-index: 40;
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
  }
  .nav.open { display: flex; }
  .nav a { width: 100%; }
  .nav-toggle {
    display: block;
    position: absolute;
    right: 22px;
    top: 40px;
    width: 30px;
  }
  .nav-toggle span {
    display: block;
    height: 1.5px;
    background: var(--ink);
    margin: 6px 0;
  }
  .hero { grid-template-columns: 1fr; }
  .hero img { aspect-ratio: 4 / 5; }
  .tagline { padding: 64px 24px 64px; font-size: 20px; line-height: 1.45; }
  .services { margin-top: 56px; }
  .services-grid { grid-template-columns: 1fr; gap: 40px; margin-top: 48px; }
  .founder { grid-template-columns: 1fr; gap: 40px; margin-top: 72px; }
  .founder .name { margin-top: 0; }
  .founder .btn { margin: 48px auto 0; }
  /* .portfolio-grid and .gallery both stay locked at 3 columns at every width
     (Samuel's call) — the project photos keep the Portfolio card format */
  .home main { padding-bottom: 90px; }
  .portfolio-page main { padding-bottom: 64px; }
  .contact-page main { padding-bottom: 72px; }
}
