/* ============================================================================
   fang.work — /photos page
   Mirrors tenn.design /photos: max-w-3xl intro w/ pull-quote, then a centered
   gallery of large 3:2 photos with hover ring + reveal-on-scroll. Each photo
   gets a CSS-only colour-block placeholder (no images required) tinted via
   --c1/--c2/--l custom properties.
   ============================================================================ */

/* ---- Intro section ------------------------------------------------------- */
.photos-intro {
    position: relative;
    width: 100%;
    max-width: 48rem;            /* max-w-3xl */
    margin-inline: auto;
    padding: 5rem 1rem 1rem;
}
@media (min-width: 640px) {
    .photos-intro { padding: 8rem 2rem 1rem; }
}

/* Section label — same as about page */
.section-label {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    line-height: 1;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted-foreground);
}

/* Headline */
.photos-title {
    margin-top: 1rem;
    max-width: 28ch;
    font-family: var(--font-display);
    font-size: 1.5rem;
    line-height: 1.2;
    letter-spacing: -0.02em;
    color: var(--foreground);
}
@media (min-width: 640px) {
    .photos-title { font-size: 1.875rem; }
}

/* ---- Per-character flip animation (heading) ----------------------------
   Each Chinese character (and punctuation) is wrapped in a <span.char>.
   They start rotated -90deg on the X-axis, below baseline, invisible,
   and washed-out. The JS adds .is-flipped which kicks off a 3D rotateX
   back to 0 with a per-character transition-delay — producing a slow,
   elegant "card-flip" reveal with a subtle settle bounce.
   ----------------------------------------------------------------------- */
.photos-title {
    perspective: 800px;          /* depth for rotateX */
}
.photos-title .char {
    display: inline-block;
    transform-origin: 50% 100%;  /* pivot at bottom-centre for a card-flip */
    transform:
        translate3d(0, 32px, 0)
        rotateX(-92deg)
        scale(0.92);
    opacity: 0;
    filter: blur(2px) saturate(0.6);    /* initial: dreamy, washed-out */
    color: var(--muted-foreground);    /* start muted, lift to full at end */
    /* staggered per-character via inline transition-delay (set by JS) */
    transition:
        transform 1.05s cubic-bezier(0.22, 0.61, 0.36, 1),
        opacity 0.65s ease,
        filter 1.05s ease,
        color 1.05s ease;
    will-change: transform, opacity, filter;
}
.photos-title.is-flipped .char {
    transform:
        translate3d(0, 0, 0)
        rotateX(0)
        scale(1);
    opacity: 1;
    filter: blur(0) saturate(1);
    color: var(--foreground);
}
/* Settle bounce: a tiny final wobble that runs after the main flip lands.
   Triggered by an even later transition-delay on the same class — purely
   additive: it reuses translate3d as a vertical jiggle. The bounce is
   subtle (4px peak) so it reads as physical settle, not a hiccup. */
@keyframes char-settle {
    0%   { transform: translate3d(0, 0, 0)     rotateX(0) scale(1); }
    35%  { transform: translate3d(0, -3px, 0)  rotateX(0) scale(1); }
    70%  { transform: translate3d(0, 1px, 0)   rotateX(0) scale(1); }
    100% { transform: translate3d(0, 0, 0)     rotateX(0) scale(1); }
}
/* Hover micro-interaction — each char can be re-flipped on hover for a
   tactile response (cursor on the title lets you play). */
.photos-title .char:hover {
    transform: rotateX(360deg);
    transition: transform 0.85s cubic-bezier(0.22, 0.61, 0.36, 1),
                color 0.3s ease;
}
/* When reduced motion is on, skip the 3D flip and just fade */
@media (prefers-reduced-motion: reduce) {
    .photos-title .char {
        transform: none;
        filter: none;
        transition: opacity 0.4s ease, color 0.4s ease;
    }
}

/* Pull-quote */
.photos-quote {
    margin-top: 4rem;
    font-family: var(--font-display);
    font-style: italic;
    font-size: 1.875rem;
    line-height: 1.15;
    letter-spacing: -0.02em;
    color: var(--foreground);
}
.photos-quote p { margin: 0; }
.photos-quote footer {
    margin-top: 1.5rem;
    font-family: var(--font-sans);
    font-style: normal;
    font-size: 0.75rem;
    line-height: 1;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted-foreground);
}
@media (min-width: 640px) {
    .photos-quote { font-size: 2.25rem; }
}
@media (min-width: 768px) {
    .photos-quote { font-size: 3rem; }
}

/* ---- Gallery ------------------------------------------------------------- */
.photos-gallery {
    margin-top: 5rem;
    display: flex;
    flex-direction: column;
    gap: 5rem;                  /* space-y-20 */
}
@media (min-width: 640px) {
    .photos-gallery { gap: 6rem; }  /* sm:space-y-24 */
}

.photo {
    /* The horizontal "padding" wrapper trick used by tenn.design: leaves
       a small breathing margin on either side at desktop widths. */
    padding-inline: clamp(0px, calc((100vw - 1440px) / 2 + 70px), 70px);
}
@media (max-width: 700px) {
    .photo { padding-inline: 0; }
}

/* ---- Per-photo elegant reveal ------------------------------------------
   Each photo starts hidden + slightly down + slightly scaled-down.
   JS adds an .is-revealed class once IntersectionObserver fires; the
   transition then runs once, slowly and softly. After the reveal the
   element stops animating (the scroll-driven transform takes over).
   ---------------------------------------------------------------------- */
.photo .photo-frame,
.mv-item .mv-frame {
    opacity: 0;
    transform: translate3d(0, 28px, 0) scale(0.965);
    transition:
        opacity 1.4s cubic-bezier(0.16, 0.84, 0.44, 1),
        transform 1.4s cubic-bezier(0.16, 0.84, 0.44, 1),
        border-color 0.6s ease;
    will-change: opacity, transform;
}
.photo.is-revealed .photo-frame,
.mv-item.is-revealed .mv-frame {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
    /* After the entrance reveal completes, slow the JS-driven
       scroll-zoom follow-up to a buttery 0.4s ease-out so the
       scale response to scroll feels weighted, not snappy. */
    transition:
        opacity 1.4s cubic-bezier(0.16, 0.84, 0.44, 1),
        transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1),
        border-color 0.6s ease;
}

/* Caption + label fade in slightly later than the frame */
.photo .photo-cap,
.photo .photo-label,
.mv-item .mv-cap,
.mv-item .mv-label {
    opacity: 0;
    transition: opacity 1.1s ease 0.80s;
}
.photo.is-revealed .photo-cap,
.mv-item.is-revealed .mv-cap { opacity: 1; }
.photo-frame:hover .photo-label,
.mv-frame:hover .mv-label { opacity: 1; transition-delay: 0s; }

/* Unsplash photographer credit, appended under the figcaption by JS */
.photo .photo-cap[data-photographer]::after,
.mv-item .mv-cap[data-photographer]::after {
    content: " · by " attr(data-photographer);
    color: color-mix(in oklab, var(--muted-foreground) 60%, transparent);
    font-weight: 400;
    margin-left: 0.25em;
}

/* Tinted "wipe" inside the placeholder — the colour block grows from the
   top edge down as the photo enters, mimicking a slow-developing print. */
.photo-frame::before {
    /* keep base colour fully transparent until reveal */
    opacity: 0;
    transition: opacity 1.2s ease 0.80s;
}
.photo.is-revealed .photo-frame::before { opacity: 1; }

.photo-frame {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    max-width: 90rem;            /* max-w-[1440px] */
    margin-inline: auto;
    overflow: hidden;
    background-color: var(--muted);
    border: 1px solid color-mix(in oklab, var(--foreground) 10%, transparent);
    aspect-ratio: 2880 / 1920;   /* 3:2 */
    /* Initial hover transform transitions have to remain snappy; the
       reveal transition is owned by `.photo .photo-frame` above. */
    transition: border-color 0.6s ease;
    /* Reserve a hair of vertical breathing room so the scroll-driven
       scale-up never crops into the figure above/below. */
    margin-block: 0.75rem;
}
.photo-frame:hover {
    border-color: color-mix(in oklab, var(--foreground) 30%, transparent);
}

/* ---- Real <img> rendering --------------------------------------------
   Each .photo-frame now contains an actual <img>. The image fills the
   frame, fades in as the photo reveals, and keeps a subtle grain overlay
   via the existing ::after rule.
   ----------------------------------------------------------------------- */
.photo-frame > img,
.mv-frame > img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
    opacity: 0;
    transition: opacity 0.9s ease 0.2s;
}
.photo.is-revealed .photo-frame > img,
.mv-item.is-revealed .mv-frame > img { opacity: 1; }

/* While the image is still loading, show a soft shimmer on the muted
   background so the placeholder doesn't look broken. */
.photo-frame:not(.is-loaded),
.mv-frame:not(.is-loaded) {
    background-image: linear-gradient(
        100deg,
        transparent 20%,
        color-mix(in oklab, var(--foreground) 4%, transparent) 50%,
        transparent 80%
    );
    background-size: 200% 100%;
    animation: photo-shimmer 1.6s linear infinite;
}
.photo-frame.is-loaded,
.mv-frame.is-loaded {
    animation: none;
    background-image: none;
}
@keyframes photo-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* Colour-block placeholder using two CSS gradients based on --c1/--c2.
   Kept for legacy/no-image frames; when an <img> is present we hide the
   placeholder so the photo shows through. */
.photo-frame:has(> img)::before,
.mv-frame:has(> img)::before { display: none; }

.photo-frame::before {
    content: "";
    position: absolute; inset: 0;
    background:
        radial-gradient(ellipse at 30% 20%, color-mix(in oklab, var(--c2, #2b2b2b) 60%, transparent), transparent 60%),
        radial-gradient(ellipse at 75% 80%, color-mix(in oklab, var(--c2, #2b2b2b) 40%, transparent), transparent 70%),
        linear-gradient(135deg, var(--c1, #1a1a1a) 0%, var(--c2, #2b2b2b) 100%);
    z-index: 0;
}
/* Subtle film-grain overlay for tactile photo feel */
.photo-frame::after {
    content: "";
    position: absolute; inset: 0;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.4'/></svg>");
    opacity: 0.05;
    mix-blend-mode: overlay;
    z-index: 1;
    pointer-events: none;
}

/* Corner label badge (subtle, tenn.design-like) */
.photo-label {
    position: absolute;
    top: 0.75rem;
    left: 0.75rem;
    font-family: var(--font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    color: rgba(255, 255, 255, 0.6);
    padding: 0.25rem 0.5rem;
    background-color: rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(4px);
    z-index: 2;
    opacity: 0;
    transition: opacity 0.3s ease;
}
.photo-frame:hover .photo-label { opacity: 1; }

/* Caption below each photo */
.photo-cap {
    margin-top: 0.875rem;
    padding-inline: 0.25rem;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted-foreground);
    text-align: center;
}

/* ---- Lightbox (full-screen overlay) ------------------------------------- */
body.lightbox-open,
body.mv-lightbox-open { overflow: hidden; }

.photo-lightbox,
.mv-lightbox {
    position: fixed; inset: 0;
    z-index: 80;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    padding: 4rem 1rem;
    background-color: rgba(0, 0, 0, 0.92);
    backdrop-filter: blur(20px);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.45s ease, visibility 0s linear 0.45s;
}
.photo-lightbox.is-open,
.mv-lightbox.is-open {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.45s ease, visibility 0s linear 0s;
}
.photo-lightbox-close,
.mv-lightbox-close {
    position: absolute;
    top: 1rem; right: 1rem;
    -webkit-appearance: none; appearance: none;
    background: transparent; border: 0;
    padding: 0.75rem;
    color: rgba(255, 255, 255, 0.7);
    cursor: pointer;
    transition: color 0.2s ease, transform 0.2s ease;
}
.photo-lightbox-close:hover,
.mv-lightbox-close:hover { color: #fff; }
.photo-lightbox-close:active,
.mv-lightbox-close:active { transform: scale(0.95); }

.photo-lightbox-stage,
.mv-lightbox-stage {
    width: 100%;
    max-width: 90rem;
    display: flex; flex-direction: column; align-items: center;
    gap: 1.25rem;
}
.photo-lightbox-stage > .photo-frame,
.mv-lightbox-stage > .mv-frame {
    width: 100%;
    max-width: min(90rem, 95vw, calc(95vh * 1.5));   /* honour 3:2 ratio bound by viewport */
    margin-block: 0;
    border-color: color-mix(in oklab, var(--foreground) 20%, transparent);
}
.photo-lightbox-cap,
.mv-lightbox-cap {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.65);
    text-align: center;
}

/* ---- Mobile tweaks ------------------------------------------------------ */
@media (max-width: 639px) {
    .photo-lightbox,
    .mv-lightbox { padding: 3rem 0.5rem; }
}

/* Reveal animation tweaks — figure appears slightly later than header */
.photos-gallery .reveal { will-change: transform, opacity; }