/* ── Lee Whittaker — portfolio (matches leewhittaker.co.uk) ───── */
:root {
  --bg: #f2f3ee;          /* rgb(242,243,238) — cream, from live site */
  --tx: #111111;
  --mu: #6f6f6a;
  --pad: 60px;            /* side padding, from live site */
  --font: 'Circular', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* ── Colour themes (set via Settings → data-theme on <html>) ──── */
[data-theme="red"]        { --tx: #e4211e; --mu: #e4211e; }                  /* red type on cream */
[data-theme="blue"]       { --tx: #1f3dd1; --mu: #1f3dd1; }                  /* blue type on cream */
[data-theme="blue-white"] { --bg: #ffffff; --tx: #1f3dd1; --mu: #1f3dd1; }  /* blue type on white */
[data-theme="slate"]     { --tx: #7699c0; --mu: #7699c0; }                  /* soft slate-blue type on cream */

* { box-sizing: border-box; margin: 0; padding: 0; }
/* Always reserve the scrollbar so the gallery's measured width never
   shifts when images load and the page grows taller (prevents overflow). */
html { -webkit-font-smoothing: antialiased; overflow-y: scroll; scrollbar-gutter: stable; }
/* Settings → Smooth scroll OFF disables the browser's native elastic overscroll
   bounce. ON (the default) leaves it alone — the native macOS rubber-band is
   exactly the "bounce" the reference sites have, and it's far smoother than any
   JS reimplementation (we used to hijack the wheel to fake it, which is what
   read as clunky). */
html.no-bounce, html.no-bounce body { overscroll-behavior-y: none; }

body {
  font-family: var(--font);
  font-weight: 400;              /* Circular Book */
  background: var(--bg);
  color: var(--tx);
  font-size: 18px;
  line-height: 1.45;
}
a { color: inherit; text-decoration: none; }
img { -webkit-user-drag: none; user-select: none; }

/* ── Header (sticky, transparent — matches live) ─────────────── */
header {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: calc(28px + env(safe-area-inset-top)) calc(var(--pad) + env(safe-area-inset-right)) 28px calc(var(--pad) + env(safe-area-inset-left));
  background: transparent;
}
/* the site name is a real <h1> for SEO (search engines weight it heavily
   for "who is this page about") but must look exactly like it did as a
   plain link — reset the browser's default h1 margin/font-size here. */
.brand-h1 { margin: 0; font-size: inherit; font-weight: inherit; line-height: 1; }
.brand { font-size: 23px; font-weight: 450; }   /* 700 -> 500 -> 450: nudged slightly lighter again per request (400 = Book is the lightest available if you want it lighter still) */
nav { display: flex; gap: 46px; font-size: 17px; }
nav a { color: var(--tx); }
nav a.active { text-decoration: underline; text-underline-offset: 4px; }
nav a:hover { text-decoration: underline; text-underline-offset: 4px; }

/* hamburger — hidden on desktop */
.burger {
  display: none;
  background: none; border: none; cursor: pointer;
  font-family: var(--font); font-size: 20px; color: var(--tx);
  line-height: 1; padding: 0;
}

/* mobile full-screen menu */
.mobile-menu {
  position: fixed; inset: 0; z-index: 50;
  background: var(--bg);
  display: none;
  flex-direction: column; align-items: center; justify-content: center;
  gap: 22px; font-size: 22px;
}
.mobile-menu.open { display: flex; }
.mm-close {
  position: absolute; top: calc(20px + env(safe-area-inset-top)); right: calc(22px + env(safe-area-inset-right));
  background: none; border: none; cursor: pointer;
  font-family: var(--font); font-size: 16px; color: var(--tx);
}

/* ── Justified gallery ───────────────────────────────────────── */
main { padding: 10px var(--pad) 90px; }
#gallery .row { display: flex; }
#gallery .ph {
  display: block; padding: 0; border: none; background: #e7e6e1;
  overflow: hidden; cursor: pointer; flex: 0 0 auto;
}
#gallery .ph img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; transition: opacity 0.5s ease;
}
#gallery .ph img.loaded { opacity: 1; }

/* ── Lightbox (spaced out — text in the corners) ─────────────── */
.lightbox {
  position: fixed; inset: 0; z-index: 60;
  background: var(--bg);
  display: none; align-items: center; justify-content: center;
  --imgw: 58vw; --imgh: 72vh;                 /* slideshow image size — sized up, still well within the 2400px source so nothing upscales. --sizef scales per photo (100%/80%). */
}
.lightbox.open { display: flex; }
/* stage = arrows flanking the centered image, lots of whitespace */
.lb-stage { display: flex; align-items: center; justify-content: center; gap: 26px; }
#lb-img {
  max-width: calc(var(--imgw) * var(--sizef, 1)); max-height: calc(var(--imgh) * var(--sizef, 1)); object-fit: contain; display: block;
  cursor: inherit;   /* inherit the ‹/› arrow cursor the JS sets on the lightbox */
  opacity: 0; transition: opacity 0.28s ease;
}
#lb-img.ready { opacity: 1; }
/* stacked transition: new image drops in on top; previous lingers then fades */
#lb-img { position: relative; z-index: 2; }
#lb-img.stack-in { animation: stackIn 0.55s cubic-bezier(0.2, 0.7, 0.2, 1); }
@keyframes stackIn { from { opacity: 0; transform: translateY(16px) scale(0.985); } to { opacity: 1; transform: none; } }
.lb-ghost { z-index: 1; animation: ghostOut 0.6s ease forwards; }
@keyframes ghostOut { 0%, 35% { opacity: 1; } 100% { opacity: 0; } }

/* next-image transitions (Settings → Slideshow transition) */
#lb-img.tx-fade { animation: txFade 0.4s ease; }
#lb-img.tx-slide-next { animation: txSlideNext 0.45s cubic-bezier(0.2, 0.7, 0.2, 1); }
#lb-img.tx-slide-prev { animation: txSlidePrev 0.45s cubic-bezier(0.2, 0.7, 0.2, 1); }
@keyframes txFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes txSlideNext { from { opacity: 0; transform: translateX(50px); } to { opacity: 1; transform: none; } }
@keyframes txSlidePrev { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: none; } }

/* corner text — caption bottom-left, counter bottom-right, close top-right.
   The lightbox is edge-to-edge (position:fixed; inset:0), so on notched/
   rounded-corner iPhones these need the extra safe-area inset added on top
   of the normal offset, or they'd sit under the notch or Dynamic Island in
   landscape, or too close to the home-indicator bar at the bottom. */
.lb-caption {
  position: absolute; bottom: calc(34px + env(safe-area-inset-bottom)); left: calc(var(--pad) + env(safe-area-inset-left));
  font-size: 15px; color: var(--tx); max-width: 60vw; line-height: 1.4;
  font-style: italic;   /* client names read as an editorial credit */
  letter-spacing: 0.01em;
}
.lb-counter {
  position: absolute; bottom: calc(34px + env(safe-area-inset-bottom)); right: calc(var(--pad) + env(safe-area-inset-right));
  font-size: 15px; color: var(--tx); font-variant-numeric: tabular-nums;
}
.lb-brand {
  position: absolute; top: calc(34px + env(safe-area-inset-top)); left: calc(var(--pad) + env(safe-area-inset-left));
  font-size: 23px; font-weight: 400; color: var(--tx);   /* match the header brand's size (was 16px — shrank on entering the slideshow) and use the lightest weight, not bold */
  cursor: pointer;
  /* no CSS transition here — the scroll-drag bounce is a live JS spring
     (see startLbSpring in site.js) that paints transform every frame;
     a CSS transition would fight it and make the motion feel laggy */
}
.lb-brand:hover { opacity: 0.55; }
/* top-right nav, always available in the slideshow — Overview closes back to
   the grid, Contact navigates straight there (no need to close first) */
.lb-nav {
  position: absolute; top: calc(34px + env(safe-area-inset-top)); right: calc(var(--pad) + env(safe-area-inset-right));
  display: flex; gap: 28px; font-size: 15px;
}
.lb-nav a { color: var(--tx); cursor: pointer; user-select: none; }
.lb-nav a:hover { opacity: 0.55; }
.lb-nav a:focus, .lb-nav a:focus-visible { outline: none; }
/* ── Information / Contact (two columns, editorial) ──────────── */
.info-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 48px;
             max-width: 940px; margin: 14vh 0 0; padding: 0 var(--pad); }
/* bold section headers — kept prominent against the light body below */
.info-h { font-size: 23px; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 24px; }
.info-h--contact { margin-top: 46px; }
/* body copy — Circular Book is the lightest weight available (no Light/300 in
   the embedded set), so "thinner, more chic" is faked with wider letter-spacing
   and slightly looser leading rather than an actual weight change. */
.info-bio { font-weight: 400; }
.info-bio p { font-size: 16px; line-height: 1.8; letter-spacing: 0.02em;
              max-width: 44ch; margin-bottom: 1.15em; }
.info-bio p:last-child { margin-bottom: 0; }
.info-contact { display: flex; flex-direction: column; gap: 9px; margin-top: 2px; }
/* email and Instagram are both links — style them IDENTICALLY (same weight,
   size, underline). They used to disagree: email underlined, Instagram italic,
   which read as inconsistent. */
.info-line { width: fit-content; font-weight: 400; font-size: 16px; letter-spacing: 0.02em;
             color: var(--tx); border-bottom: 1px solid currentColor; padding-bottom: 1px; }
.info-line:hover { opacity: 0.6; }
.info-clients { display: flex; flex-direction: column; gap: 7px; }
.info-clients li { list-style: none; font-weight: 400; font-size: 16px; letter-spacing: 0.02em; }
@media (max-width: 700px) { .info-wrap { grid-template-columns: 1fr; gap: 36px; } }


/* ── Responsive ──────────────────────────────────────────────── */
@media (max-width: 1080px) { :root { --pad: 40px; } }

@media (max-width: 700px) {
  :root { --pad: 30px; }           /* matches live mobile side padding */
  header { padding: calc(22px + env(safe-area-inset-top)) calc(30px + env(safe-area-inset-right)) 22px calc(30px + env(safe-area-inset-left)); align-items: center; }
  nav { display: none; }
  .burger { display: block; }
  /* In the slideshow the brand (left) and Overview/Contact (right) share one
     line. At the desktop 23px the brand is wide enough to collide with the nav
     on a phone — "Lee Whittaker covering over Overview." Drop it to the nav's
     scale here (and pin the nav's top to match) so they sit cleanly apart. */
  .lb-brand { font-size: 15px; top: calc(30px + env(safe-area-inset-top)); }
  .lb-nav { top: calc(30px + env(safe-area-inset-top)); gap: 22px; }
  #lb-img { max-width: 90vw; max-height: 72vh; }
  .lb-caption, .lb-counter { bottom: calc(26px + env(safe-area-inset-bottom)); font-size: 13px; }
  .lb-caption { max-width: 62vw; }
}

@media (prefers-reduced-motion: reduce) {
  #gallery .ph img, #lb-img { transition: none; }
}

/* ── Interactions: cursor, colour, hover, transitions ────────── */
.lightbox { transition: background-color 0.6s ease; }
html.lb-active { overflow: hidden; }              /* hide page scrollbar while in lightbox */

/* index grid: normal by default; hovering ONE image applies the effect
   chosen in the editor Settings tab (class hx-* on #gallery). */
#gallery { transition: opacity 0.18s ease; }   /* snappy fade before lightbox — was 0.35s */
#gallery.fading { opacity: 0; pointer-events: none; }   /* fade grid before opening lightbox */
#gallery .ph { position: relative; transition: transform 0.35s ease, box-shadow 0.35s ease; }
#gallery .ph img { transition: opacity 0.5s ease, filter 0.4s ease, transform 0.5s ease; }
.ph-cap {
  position: absolute; left: 10px; right: 10px; bottom: 9px; z-index: 2;
  color: #fff; font-size: 12px; line-height: 1.3; opacity: 0;
  transition: opacity 0.3s ease; pointer-events: none;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
}
#gallery .ph::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: var(--wash, #111); opacity: 0;
  transition: opacity 0.3s ease; pointer-events: none;
}
#gallery .ph::before {
  content: ""; position: absolute; inset: 10px; z-index: 2;
  border: 1px solid rgba(255, 255, 255, 0.85); opacity: 0;
  transition: opacity 0.3s ease; pointer-events: none;
}
/* effect variants — gated to real hover-capable pointers (mouse/trackpad).
   Touch devices have no persistent :hover, so a tap can get "caught" as a
   hover state and need a second tap to actually open the lightbox. Scoping
   these to (hover: hover) means touch always gets a plain, immediate tap. */
@media (hover: hover) and (pointer: fine) {
  #gallery.hx-caption .ph:hover .ph-cap { opacity: 1; }
  #gallery.hx-caption .ph:hover::after { opacity: 0.26; }        /* darken so text reads */
  #gallery.hx-zoom .ph:hover img { transform: scale(1.05); }
  #gallery.hx-lift .ph:hover { transform: translateY(-7px); box-shadow: 0 14px 30px rgba(0, 0, 0, 0.15); }
  #gallery.hx-brighten .ph:hover img { filter: brightness(1.08) contrast(1.03) saturate(1.05); }
  #gallery.hx-wash .ph:hover::after { opacity: 0.5; }
  #gallery.hx-grayscale .ph img { filter: grayscale(1); }
  #gallery.hx-grayscale .ph:hover img { filter: grayscale(0); }
  #gallery.hx-focus .ph img { filter: blur(4px); transform: scale(1.03); }   /* slight overscale hides the blurred edge */
  #gallery.hx-focus .ph:hover img { filter: blur(0); transform: scale(1); }
  #gallery.hx-frame .ph:hover::before { opacity: 1; }
}
/* hx-none → no hover effect */

/* intro overlay — full-bleed stacked name (LEE / WHITTAKER / STUDIO) */
#intro {
  position: fixed; inset: 0; z-index: 90; background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; animation: introOut 0.6s ease 1.25s forwards;   /* snappier — was 2s hold */
}
.intro-name {
  display: flex; flex-direction: column;
  text-transform: uppercase; font-weight: 700; color: var(--tx);
  line-height: 0.84; letter-spacing: -0.03em; text-align: center;
}
.intro-name span {
  display: block; opacity: 0; transform: translateY(0.28em);
  animation: introLineIn 0.9s cubic-bezier(0.19, 1, 0.22, 1) forwards;
}
.intro-name span:nth-child(1) { animation-delay: 0.08s; }
.intro-name span:nth-child(2) { animation-delay: 0.20s; }
.intro-name span:nth-child(3) { animation-delay: 0.32s; }
/* full-bleed: the longest line (WHITTAKER) stretches close to both edges */
#intro.intro-fullscreen .intro-name { font-size: clamp(46px, 16vw, 260px); }
#intro.intro-lowercase .intro-name { text-transform: lowercase; }
/* small "logo" variant: compact, centred */
#intro:not(.intro-fullscreen) .intro-name { font-size: 26px; line-height: 1; letter-spacing: 0; }
#intro.gone { display: none; }
/* already shown this session (set in <head> before first paint) — never let the
   intro flash up again on a repeat load; see the note in index.html. */
html.intro-skip #intro { display: none; }
/* "Start on: Slideshow" — hide the overview grid from first paint until site.js
   opens the lightbox (it removes this class), so the grid never flashes before
   the slideshow. visibility (not display) keeps the grid's layout so it still
   measures its width and builds correctly behind the lightbox. */
html.landing-slideshow #page { visibility: hidden; }
@keyframes introLineIn { to { opacity: 1; transform: translateY(0); } }
@keyframes introOut { to { opacity: 0; visibility: hidden; } }

/* contact page — colour background. MUST match Settings → Contact background
   (galleries.json "contactColor"). On a real page load (transitions off, a
   refresh, a shared link) this CSS paints before galleries.js/site.js apply the
   setting, so a mismatched colour flashes for a beat first. It used to be an
   obvious orange placeholder (#ea5b1f) — that was the orange flash between the
   slideshow and Contact. Keep in sync with the setting (currently Slate Blue). */
body.contact { background: #7699c0; }
/* email underline + Instagram follow the theme text colour like everything else */

/* ── Slideshow modes: composed (per-photo left/right/top/centre) & stacked (pile) ── */
#lb-multi { position: fixed; inset: 0; display: none; z-index: 61; cursor: inherit; }
#lb-multi img, .lb-stage { cursor: inherit; }   /* let the JS-set ‹/› arrow cursor show through */
.lightbox.lb-mode-composed #lb-multi,
.lightbox.lb-mode-stacked  #lb-multi { display: block; }
.lightbox.lb-mode-composed .lb-stage,
.lightbox.lb-mode-stacked  .lb-stage { display: none; }

/* spread — images sit left / right / centre. Left/right are both anchored to
   the horizontal centre (50%) and pushed apart by a small fixed gap via
   transform, rather than pinned to the viewport edges — that way the gap
   between the pair stays tight and constant regardless of each image's
   actual rendered width (a narrow portrait crop won't leave a huge gap). */
.sp-img { position: absolute; top: 50%; max-height: calc(76vh * var(--sizef, 1)); object-fit: contain;
          opacity: 0; transition: opacity .45s ease, transform .45s ease; }
.sp-img.in { opacity: 1; }
.sp-left   { left: 50%; transform: translate(calc(-100% - 14px), -50%); max-width: calc(54vw * var(--sizef, 1)); }
.sp-right  { left: 50%; transform: translate(14px, -50%); max-width: calc(54vw * var(--sizef, 1)); }
.sp-centre { left: 50%;  transform: translate(-50%, -50%); max-width: calc(66vw * var(--sizef, 1)); }

/* stacked — overlapping pile, newest on top */
.st-img { position: absolute; top: 50%; left: 50%; max-width: calc(62vw * var(--sizef, 1)); max-height: calc(74vh * var(--sizef, 1));
          object-fit: contain; transition: transform .45s ease, opacity .45s ease; }

/* keep the corner controls clickable above the multi-image layer */
.lb-brand, .lb-nav { z-index: 65; }
.lb-caption, .lb-counter { z-index: 65; }

/* composed/spread/stacked on phones: the desktop vw/vh values above leave a
   single centred or stacked photo tiny on a narrow screen — size it like the
   single-mode image instead. Left/right pairs get more width too since
   there's no room to spare on a narrow viewport. Must come after the base
   rules above so it wins the cascade at equal specificity. */
@media (max-width: 700px) {
  .sp-img    { max-height: calc(60vh * var(--sizef, 1)); }
  .sp-centre { max-width: calc(90vw * var(--sizef, 1)); max-height: calc(72vh * var(--sizef, 1)); }
  .sp-left   { transform: translate(calc(-100% - 8px), -50%); max-width: calc(50vw * var(--sizef, 1)); }
  .sp-right  { transform: translate(8px, -50%); max-width: calc(50vw * var(--sizef, 1)); }
  .st-img    { max-width: calc(90vw * var(--sizef, 1)); max-height: calc(72vh * var(--sizef, 1)); }
}

/* Smooth scroll (Settings → Smooth scroll) is JS-driven — see initSmoothScroll
   in site.js. No CSS needed; it eases the real window scroll each frame. */

/* ── Page transition (Settings → Page transition) — fade | blur | slide | scroll ──
   Navigating between pages goes through pt2Navigate (site.js), never a real
   reload — see the .pt2-* rules further down for the actual per-style
   animation. What's here is ONLY the very first reveal on a genuine fresh page
   load (a direct visit, refresh, or back/forward — see the popstate handler in
   site.js): #page starts pre-hidden and settles in once the page has actually
   parsed, instead of only ever being instantly, fully there. Applied to #page
   (header+main), NEVER <body> — filter/opacity/transform on an ancestor of the
   lightbox/intro/mobile-menu would create a new CSS containing block and break
   their position:fixed. #page holds none. */
#page.pt-enabled { transition: opacity 0.55s cubic-bezier(0, 0, 0.58, 1),
                               filter 0.5s cubic-bezier(0, 0, 0.58, 1),
                               transform 0.55s cubic-bezier(0.5, 0, 0.2, 1); }

/* fade — content simply dissolves in */
#page.pt-fade              { opacity: 0; }
#page.pt-fade.pt-ready     { opacity: 1; }

/* blur — soft-focus dissolve in */
#page.pt-blur              { opacity: 0; filter: blur(10px); }
#page.pt-blur.pt-ready     { opacity: 1; filter: blur(0); }

@media (prefers-reduced-motion: reduce) {
  #page.pt-enabled { transition: opacity 0.2s ease; filter: none; transform: none; }
  #page.pt-fade, #page.pt-blur { filter: none; transform: none; }
}

/* slide — full content swap: old content slides fully off, new content
   slides in from the opposite edge, simultaneously (site.js → pt2Navigate).
   The header stays put — only the content region moves — so it reads as one
   continuous surface rather than two separate page loads. Positioning
   (position/top) is set inline by JS per-element; this file only supplies
   the motion. Timing/easing matches a typical fast, symmetric page-swap. */
/* no cursor:wait here — there's nothing to actually wait for (the new content
   is already fetched, this is just a local animation), and on macOS/Chrome a
   "wait" cursor renders as a spinning system busy-indicator that reads as the
   page being stuck loading rather than mid-transition */
body.pt2-active { position: fixed; inset: 0; width: 100%; height: 100%; overflow: hidden; }
/* Full-bleed backdrop each panel's content sits inside during the transition
   (site.js → pt2Navigate) — always covers the viewport regardless of the
   content's own CSS (e.g. .info-wrap's max-width), so the slide/colour never
   shows a narrow box with the old page still visible around it. top/left are
   set inline by JS. */
.pt2-panel { position: absolute; left: 0; width: 100%; min-height: 100%; }
/* All of these animate TRANSFORM, not top/left. That matters for the vertical
   "scroll": the outgoing panel is positioned at top:-scrollY (to freeze the
   current view in place), so animating its `top` to a fixed value made the
   travel distance = (target − scrollY), i.e. a huge epic scroll at the top of
   the page but a meek little one (or nothing) once scrolled down. transform is
   relative to wherever the element already sits, so the travel is ALWAYS the
   same distance no matter how far down the page you were. vh/vw units (not %)
   keep it relative to the viewport rather than the element's own height. */
/* horizontal slide — snappy symmetric ease-in-out */
.pt2-in-left   { animation: pt2InLeft   0.44s cubic-bezier(.645,.045,.355,1) forwards; }
.pt2-in-right  { animation: pt2InRight  0.44s cubic-bezier(.645,.045,.355,1) forwards; }
@keyframes pt2InLeft   { from { transform: translateX(100vw);  } to { transform: translateX(0); } }
@keyframes pt2InRight  { from { transform: translateX(-100vw); } to { transform: translateX(0); } }

/* Leaving the LIGHTBOX (slideshow → Contact). The 340vh epic scroll above only
   works because the outgoing panel (the gallery) is many screens tall, so its
   content fills the whole scroll. The lightbox is a single fixed viewport-height
   overlay showing one centred photo on a plain backdrop — scroll IT away and you
   get screens of empty page (and the photo's own surrounding backdrop) sliding
   past before Contact arrives, which reads as "it went back to the overview for
   a second." Instead, Contact rises straight UP over the still image and covers
   it (it's given a higher z-index than the lightbox in JS), so it's photo →
   Contact with nothing in between. */
/* vertical "scroll" in the COVER model — the incoming page travels exactly one
   viewport to land squarely over the (stationary) outgoing page. forward rises
   up from below, back descends from above. Also used leaving the lightbox. */
.pt2-cover-up   { animation: pt2CoverUp   0.72s cubic-bezier(.65,0,.35,1) forwards; }
.pt2-cover-down { animation: pt2CoverDown 0.72s cubic-bezier(.65,0,.35,1) forwards; }
@keyframes pt2CoverUp   { from { transform: translateY(100vh);  } to { transform: translateY(0); } }
@keyframes pt2CoverDown { from { transform: translateY(-100vh); } to { transform: translateY(0); } }

/* fade / blur page transitions — a plain cross-fade in place, not a spatial
   move. Only the incoming panel animates (opacity/blur in); the outgoing one
   just sits still underneath and gets covered — it paints first in DOM order,
   so the incoming panel is already on top with no z-index needed here. */
.pt2-in-fade { animation: pt2InFade 0.38s ease forwards; }
.pt2-in-blur { animation: pt2InBlur 0.38s ease forwards; }
@keyframes pt2InFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pt2InBlur { from { opacity: 0; filter: blur(10px); } to { opacity: 1; filter: blur(0); } }

@media (prefers-reduced-motion: reduce) {
  .pt2-in-left, .pt2-in-right,
  .pt2-cover-up, .pt2-cover-down, .pt2-in-fade, .pt2-in-blur { animation-duration: 0.01s; }
}

/* iPhone/iPad refinement: dvh (dynamic viewport height) tracks the actual
   visible viewport as Safari's address bar shows/hides, where vh is
   sometimes computed against the taller, address-bar-hidden height —
   using dvh keeps slideshow images sized against what's really on screen.
   @supports-gated so it only adds to the vh values above, never replaces
   them for browsers that don't know dvh. */
@supports (height: 1dvh) {
  .lightbox { --imgh: 66dvh; }
  .info-wrap { margin-top: 14dvh; }
  /* Keep the per-photo 80% factor (--sizef) — without it these dvh overrides,
     which are ALWAYS active (every modern browser supports dvh), pinned the
     height at full size, so 80% only ever shrank width. Portrait photos are
     height-bound, so 80% then did nothing visible. */
  .sp-img { max-height: calc(70dvh * var(--sizef, 1)); }
  .st-img { max-height: calc(68dvh * var(--sizef, 1)); }
  @media (max-width: 700px) {
    #lb-img { max-height: calc(72dvh * var(--sizef, 1)); }
    .sp-img { max-height: calc(60dvh * var(--sizef, 1)); }
    .sp-centre, .st-img { max-height: calc(72dvh * var(--sizef, 1)); }
  }
}
