/*
 * The envelope that opens into the invitation.
 *
 * Everything about the paper is a custom property, so replacing the placeholder
 * gradients with the real embossed artwork is a change to --envelope-face and
 * --card-paper and nothing else — the fold geometry doesn't move.
 *
 * Only transform and opacity are animated. Anything else (width, top, filter on
 * a large box) would paint on every frame, and this has to stay smooth on the
 * cheap phones half the guests will open it on.
 */

.envelope {
    /* Where the four folds meet. Measured off the artwork's printed seams, which cross a little
       right of centre and close at about y 49% — nudge these two if the creases don't sit on the
       folds when it animates. 53% was below the join, out in the open part of the lower flap. */
    --fold-x: 50.5%;
    --fold-y: 49%;

    /* Where the butterfly rests before it takes off. Separate from the fold on purpose: the
       fold is where the *animation* pivots, while this is where the artwork's printed edges
       actually converge, and the two are a few percent apart. Move this, not --fold-y, if the
       butterfly doesn't sit on the seam — changing the fold would re-cut the flaps. */
    --seal-x: 50.5%;
    --seal-y: 49%;

    /* Slow on purpose. Opening a real envelope is unhurried, and this is the one moment in
       the invitation where the guest is only watching. Both numbers scale everything else —
       the fade below is derived from the duration, not set independently. */
    --fold-duration: 3.8s;
    --fold-stagger: 140ms;

    /* A held beat between the tap and the first movement of paper. The card starts fading up
       inside the envelope during it, so the opening begins quietly rather than snapping. */
    --reveal-lead: 0.7s;

    --envelope-face: url("/img/envelope_v_2.jpg");

    position: fixed;
    inset: 0;
    z-index: 20;
    display: grid;
    place-items: center;
    padding: var(--space-md);
    /* Flat, all the way round the sealed letter. The card only ever appears inside the
       envelope's own outline — see .envelope-card.

       Its own property rather than --color-bg directly: the surround and the page behind the
       invitation are the same colour by default, but they do not have to be. What suits the
       artwork once it is on screen is not necessarily what the envelope wants to sit on. */
    background: var(--envelope-surround, var(--color-bg));
}

.envelope[hidden] {
    display: none;
}

.envelope-face {
    position: relative;
    display: block;
    /* The artwork is 576x1024. The third term is that ratio applied to the height cap, so
       the envelope never grows past 74vh — a plain max-height would clamp the box and
       stretch the image, since the flaps paint it at background-size: 100% 100%. */
    width: min(86vw, 27rem, calc(74vh * 9 / 16));
    aspect-ratio: 9 / 16;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    /* The flaps are children, so the vanishing point belongs here. */
    perspective: 1200px;
    transform-style: preserve-3d;
}

/* Each flap is the *same* full-size box showing the *same* image, differing only in
   which triangle it clips. That's what makes the seams line up for nothing — there
   are no per-flap background offsets to keep in step with the artwork. */
.flap {
    position: absolute;
    inset: 0;
    background-image: var(--envelope-face);
    background-size: 100% 100%;
}

/* Closed and open are declared per flap as the *same* rotate function, so the browser
   interpolates an angle rather than decomposing two different matrices — which is where
   a 180-degree flip goes strange. */
.flap-top {
    clip-path: polygon(0 0, 100% 0, var(--fold-x) var(--fold-y));
    transform-origin: 50% 0;
    --flap-closed: rotateX(0deg);
    --flap-open: rotateX(180deg);
    /* Folded paper never catches the light evenly. Slight per-flap shading is what
       stops four triangles of one image from reading as a flat rectangle. */
    filter: brightness(1.03);
}

.flap-right {
    clip-path: polygon(100% 0, 100% 100%, var(--fold-x) var(--fold-y));
    transform-origin: 100% 50%;
    --flap-closed: rotateY(0deg);
    --flap-open: rotateY(180deg);
    filter: brightness(0.975);
}

.flap-bottom {
    clip-path: polygon(100% 100%, 0 100%, var(--fold-x) var(--fold-y));
    transform-origin: 50% 100%;
    --flap-closed: rotateX(0deg);
    --flap-open: rotateX(-180deg);
    filter: brightness(0.955);
}

.flap-left {
    clip-path: polygon(0 100%, 0 0, var(--fold-x) var(--fold-y));
    transform-origin: 0 50%;
    --flap-closed: rotateY(0deg);
    --flap-open: rotateY(-180deg);
    filter: brightness(1.01);
}

/*
 * The card inside the envelope: exactly the letter's outline, and nothing outside it. The
 * artwork is 940x1672 and the envelope face is 9:16, near enough the same shape that `cover`
 * shows the whole card rather than a crop of it.
 *
 * translateZ pushes it a hair behind the flaps. They start out coplanar with it, and coplanar
 * surfaces inside a preserve-3d parent flicker against each other.
 */
.envelope-card {
    position: absolute;
    inset: 0;
    background: url("/img/pearl_bg.jpg") center / cover no-repeat;
    transform: translateZ(-1px);
    opacity: 0;
}

/* Fades up through the lead-in and on into the fold, so the paper is already there as each
   flap peels back rather than appearing behind it. */
.envelope.is-open .envelope-card {
    animation: card-reveal calc(var(--reveal-lead) + var(--fold-duration) * 0.45) ease forwards;
}

@keyframes card-reveal {
    to { opacity: 1; }
}

/*
 * Two animations, not one: the fold and the fade need different curves.
 *
 * A timing function is applied between every *pair of adjacent keyframes*, not once across the
 * whole animation — so adding opacity stops to flap-open would chop the rotation into five
 * separately-eased segments and the paper would visibly stutter as it turned. Splitting them
 * lets the fold keep its single easing while the fade runs on its own.
 *
 * One animation-delay value covers both, which is what keeps the per-flap staggers below
 * working unchanged.
 */
.envelope.is-open .flap {
    animation: flap-open var(--fold-duration) cubic-bezier(0.32, 0.72, 0.28, 1)
                   var(--reveal-lead) forwards,
               flap-dissolve var(--fold-duration) linear var(--reveal-lead) forwards;
}

/* Opened one after another rather than together, so it unfolds instead of snapping. Each is
   offset from the lead-in, not from zero. */
.envelope.is-open .flap-right {
    animation-delay: calc(var(--reveal-lead) + var(--fold-stagger));
}

.envelope.is-open .flap-bottom {
    animation-delay: calc(var(--reveal-lead) + var(--fold-stagger) * 2);
}

.envelope.is-open .flap-left {
    animation-delay: calc(var(--reveal-lead) + var(--fold-stagger) * 3);
}

@keyframes flap-open {
    from {
        transform: var(--flap-closed);
    }
    to {
        transform: var(--flap-open);
    }
}

/*
 * The paper thinning out as it folds back, rather than staying solid and then dropping.
 *
 * It also does the job backface-visibility: hidden would otherwise do — stopping the front
 * artwork showing mirrored on the back of a flap — without that property's hard cut at exactly
 * 90 degrees, which reads as a blink mid-motion. The curve holds nearly full opacity while the
 * flap is still face-on, then falls away as it turns edge-on and past.
 */
@keyframes flap-dissolve {
    0% {
        opacity: 1;
    }
    30% {
        opacity: 0.94;
    }
    55% {
        opacity: 0.6;
    }
    78% {
        opacity: 0.26;
    }
    100% {
        opacity: 0;
    }
}

/* Starts clearing while the flaps are still moving, so the invitation appears behind them
   rather than after them. */
.envelope.is-open {
    transition: opacity 1.4s ease calc(var(--reveal-lead) + var(--fold-duration) * 0.55);
    opacity: 0;
    pointer-events: none;
}

/* Nothing scrolls behind a sealed envelope. */
.is-sealed {
    overflow: hidden;
}

/* --- "Touch to open" --- */

/*
 * Sits under the butterfly on the seal, so it reads as one thing: the butterfly, then what to
 * do about it. Hung off --seal-y for that reason — move the seal and the hint follows, rather
 * than drifting away from the insect it belongs to.
 *
 * --hint-drop is half the flier's own --flier-size plus a gap, which is what clears its wings.
 * The size is repeated rather than shared because .envelope-flier is a *sibling* of .envelope,
 * not a child, so there is no element both can inherit it from — change one and change both.
 *
 * translateZ is not decoration: the flaps are coplanar with the face inside a preserve-3d
 * parent, and anything level with them z-fights instead of sitting cleanly in front.
 */
.envelope-hint {
    --hint-drop: calc(min(17vw, 4.75rem) * 0.5 + 0.85rem);

    position: absolute;
    left: var(--seal-x);
    top: calc(var(--seal-y) + var(--hint-drop));
    transform: translateX(-50%) translateZ(1px);
    /* The button is the target; the words must not swallow a tap meant for it. */
    pointer-events: none;
    white-space: nowrap;
    color: var(--color-ink-muted);
    font-size: 0.7rem;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    transition: opacity 0.5s ease;
}

/* Gone the moment it is obeyed, rather than blinking on over the folding paper. Only the
   wrapper fades — the pulse inside carries on, multiplied down to nothing with it, which is
   what keeps the fade from starting with a jump back to full opacity. */
.envelope.is-open .envelope-hint {
    opacity: 0;
}

.envelope-hint-pulse {
    display: inline-block;
    animation: hint-pulse 2.6s ease-in-out infinite;
}

/* A slow breath rather than a blink. It has to be noticeable on a page where nothing else
   moves without ever competing with the butterfly, which is the thing worth looking at. */
@keyframes hint-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.3; }
}

/* --- The butterfly that rides the envelope open --- */

/*
 * Rests on the seal, then flies up and grows into the big one on the cover.
 *
 * Fixed and above the envelope's own z-index, because the flight has to carry on over the top
 * of the folding paper and outlast it — the overlay is still fading long after the butterfly
 * has landed.
 *
 * Two elements: this one is parked on the fold point by envelope.js and never moves again, and
 * .envelope-flier-move carries the flight. Keeping them apart means the centring translate
 * can't be overwritten by the animation's own transform, which is what makes the landing land
 * exactly on the target rather than half a wingspan off it.
 */
.envelope-flier {
    /* Roughly a real butterfly against a hand-sized envelope. It ends up at the big one's size,
       so this is only where it starts. */
    --flier-size: min(17vw, 4.75rem);

    position: fixed;
    left: 0;
    top: 0;
    z-index: 21;
    pointer-events: none;
    /* Both offsets are set in pixels by the script, measured off the envelope itself — the fold
       point moves with the artwork's size, so it can't be written down here. */
    transform: translate(-50%, -50%);
    filter: drop-shadow(0 6px 8px rgba(60, 48, 34, 0.18));
}

.envelope-flier[hidden] {
    display: none;
}

.envelope-flier .butterfly {
    /* Overrides the cover butterfly's width. The flight scales up from here to whatever the
       real one measures, so the two never need to agree on a number. */
    width: var(--flier-size);
    /* The beat itself stays at the resting duration. Flight speed is applied by envelope.js as
       a playback rate instead: changing the *duration* mid-animation re-maps the animation's
       progress and the wings visibly restart, whereas a rate change carries on from wherever it
       is. That is what lets the beat ease off as it lands rather than snapping. */
}

/* Hides the real one for as long as its stand-in is in the air. Not visibility:hidden — the
   script measures this element to find out where to fly to, and it has to keep its box. */
.butterfly.is-awaiting {
    opacity: 0;
}

/* --- The butterfly --- */

/*
 * The big one is a mounted specimen: it sits at the top of the opening screen, wings open, and
 * scrolls away with it. No flight, no wing beat, no sticking to the top — the flock does all
 * the moving. That matches the reference artwork, where the butterfly is a still photograph
 * pinned to the paper.
 */
.butterfly-perch {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    pointer-events: none;
    /* The shadow lives here rather than on .butterfly because a filter forces its element's
       children out of 3D, which matters for the flock's wings using the same rules. */
    filter: drop-shadow(0 8px 10px rgba(60, 48, 34, 0.16));
}

.butterfly {
    /*
     * --wing-hinge is how much of the width, from each edge, is wing rather than body. The
     * strip left in the middle is what stays still while the wings beat, so it has to cover
     * the body, head and antennae. Checked against the artwork rather than guessed: at 39%
     * the antenna tips sat right on the hinge line, so 37% gives them room.
     */
    --butterfly-image: url("/img/butterfly.png");
    --wing-hinge: 37%;

    position: relative;
    /* Opening-screen size. The perched size is this scaled down, not a second width — one
       number to change, and the two states can't drift apart. */
    width: min(46vw, 16rem);
    aspect-ratio: 320 / 318;
    /* Gives the wing rotation real foreshortening rather than a flat horizontal squash. */
    perspective: 220px;
}

.butterfly > span {
    position: absolute;
    inset: 0;
    background-image: var(--butterfly-image);
    background-size: 100% 100%;
    background-repeat: no-repeat;
}

/* The strip that holds the body, head and antennae. Never moves. */
.butterfly-core {
    clip-path: inset(0 var(--wing-hinge) 0 var(--wing-hinge));
}

/* Every butterfly beats, big and small. --beat is inherited from whichever butterfly the wing
   belongs to, so the flock's inline rates apply here without a second rule; the fallback is the
   big one's, slow enough to read as resting rather than labouring. */
.wing {
    animation: wing-beat var(--beat, 1.9s) ease-in-out infinite alternate;
}

.wing-left {
    clip-path: inset(0 calc(100% - var(--wing-hinge)) 0 0);
    transform-origin: var(--wing-hinge) 50%;
}

.wing-right {
    clip-path: inset(0 0 0 calc(100% - var(--wing-hinge)));
    transform-origin: calc(100% - var(--wing-hinge)) 50%;
    /* Mirrored, so both outer edges lift towards the viewer together. */
    animation-name: wing-beat-right;
}

/* Flies only once the envelope has actually opened. The delay holds it back until the flaps
   are well into their fold, so it reads as coming out of the envelope rather than racing it
   — with the slower fold it would otherwise have landed before the paper finished moving. */
/*
 * The big butterfly settling onto the card as the envelope opens: half size and invisible, up
 * to full and solid.
 *
 * The delay is measured against the overlay, not the flaps. The butterfly sits behind the
 * envelope, so nothing of this is visible until the overlay starts clearing at 55% of the fold
 * — starting any earlier just wastes the first second of it. `both` holds the half-size,
 * transparent first frame through the delay; without it the butterfly would sit at full size
 * waiting to shrink.
 */
.butterfly.is-emerging {
    animation: butterfly-emerge 1.8s cubic-bezier(0.22, 0.9, 0.3, 1) 2.7s both;
}

@keyframes butterfly-emerge {
    from {
        transform: scale(0.5);
        opacity: 0;
    }
    to {
        transform: none;
        opacity: 1;
    }
}

@keyframes wing-beat {
    to { transform: rotateY(50deg); }
}

@keyframes wing-beat-right {
    to { transform: rotateY(-50deg); }
}

/* --- The flock --- */

/*
 * Fixed, so the small butterflies drift over the page rather than scrolling with it, and
 * below the big one. overflow: hidden is load-bearing: the paths run from -15vw to 115vw and
 * would otherwise give the whole page a horizontal scrollbar.
 */
.flock {
    position: fixed;
    /* Sized to the largest viewport rather than to inset: 0, which resolves against the
       *visual* viewport and therefore changes the moment a phone's address bar slides away. A
       decorative layer that re-lays-out every time the browser chrome animates is the bounce a
       guest sees when they stop scrolling. lvh never changes, so this simply runs on under the
       bar and stays still. */
    inset: 0 0 auto 0;
    height: 100vh;
    height: 100lvh;
    z-index: 4;
    overflow: hidden;
    pointer-events: none;
}

/*
 * --flock-elapsed is how long the guest has been on the invitation, set by envelope.js and
 * inherited from .flock. Subtracting it from every delay winds each animation forward to where
 * it would have been, so a page load — answering the RSVP, say — doesn't snap all four
 * butterflies back to their starting positions. The animations are periodic, so a large
 * negative delay just resolves further into the loop.
 */
.flock-path {
    position: absolute;
    top: var(--fly-top);
    left: 0;
    animation: flock-cross var(--fly-dur) infinite;
    animation-delay: calc(var(--fly-delay) - var(--flock-elapsed, 0s));
}

/* Mirrored rather than given its own keyframes, so there's one path to adjust, not two. */
.flock-path.from-right {
    animation-name: flock-cross-back;
}

.flock-drift {
    animation: flock-drift var(--drift-dur) ease-in-out infinite;
    animation-delay: calc(0s - var(--flock-elapsed, 0s));
}

.butterfly-sm {
    width: var(--fly-size);
    /* The big butterfly's 220px vanishing point is barely perceptible on something this
       small; scaling it with the butterfly keeps the same amount of turn in the wings. */
    perspective: calc(var(--fly-size) * 3);
    opacity: 0.75;
    animation: flock-depth var(--depth-dur) ease-in-out infinite;
    animation-delay: calc(0s - var(--flock-elapsed, 0s));
}


/*
 * The horizontal path is where "running along a rail" came from: a single linear tween holds
 * one speed for the whole crossing. Real flight is a series of darts and hesitations, so the
 * pace is broken up with per-stop easing — the butterfly presses forward, drifts, then presses
 * again. The distances between stops are uneven on purpose.
 */
@keyframes flock-cross {
    0% { transform: translateX(-15vw); animation-timing-function: ease-in; }
    26% { transform: translateX(18vw); animation-timing-function: ease-out; }
    41% { transform: translateX(31vw); animation-timing-function: ease-in-out; }
    58% { transform: translateX(43vw); animation-timing-function: ease-in; }
    79% { transform: translateX(86vw); animation-timing-function: ease-out; }
    100% { transform: translateX(115vw); }
}

@keyframes flock-cross-back {
    0% { transform: translateX(115vw); animation-timing-function: ease-in; }
    26% { transform: translateX(82vw); animation-timing-function: ease-out; }
    41% { transform: translateX(69vw); animation-timing-function: ease-in-out; }
    58% { transform: translateX(57vw); animation-timing-function: ease-in; }
    79% { transform: translateX(14vw); animation-timing-function: ease-out; }
    100% { transform: translateX(-15vw); }
}

/*
 * The vertical wander, and the other half of the fix. This used to be a two-stop alternate,
 * which is a pure sine — the most mechanical shape there is. Six uneven stops that begin and
 * end together instead, so it loops without a seam but never reads as a wave. The tilt rides
 * along with it: a butterfly banks into a climb rather than staying level.
 */
@keyframes flock-drift {
    0% { transform: translateY(0) rotate(-5deg); }
    17% { transform: translateY(-5.5vh) rotate(7deg); }
    33% { transform: translateY(1.5vh) rotate(-9deg); }
    51% { transform: translateY(-8.5vh) rotate(4deg); }
    69% { transform: translateY(-1.5vh) rotate(-6deg); }
    86% { transform: translateY(-10.5vh) rotate(8deg); }
    100% { transform: translateY(0) rotate(-5deg); }
}

/* Nearer and further away. Slight, but it stops all four sitting on one flat plane. */
@keyframes flock-depth {
    0%, 100% { transform: scale(1); }
    38% { transform: scale(0.84); }
    72% { transform: scale(1.1); }
}

/* --- Replay --- */

.envelope-replay {
    display: block;
    margin: var(--space-lg) auto 0;
    padding: 0.5rem 1rem;
    border: 0;
    background: none;
    /* Gold and small: the last line on the page, and the only thing on it that is not part of
       the invitation. Muted grey at 0.75rem read as a leftover control rather than as a closing
       flourish. */
    color: var(--color-accent);
    font-size: 0.6rem;
    letter-spacing: 0.35em;
    text-transform: uppercase;
    cursor: pointer;
}

/*
 * Reduced motion. Not a courtesy — this is full-screen 3D rotation, which is exactly
 * the kind of thing that makes people with vestibular disorders ill. The envelope
 * still opens, it just crossfades instead of tumbling.
 */
@media (prefers-reduced-motion: reduce) {
    .envelope {
        --fold-duration: 0.3s;
        --fold-stagger: 0ms;
    }

    .envelope.is-open .flap {
        animation: flap-fade var(--fold-duration) ease forwards;
    }

    @keyframes flap-fade {
        to { opacity: 0; }
    }

    .scroll-cue {
        animation: none;
    }

    /* The words stay — it is the only thing telling the guest what to do. Just still. */
    .envelope-hint-pulse {
        animation: none;
    }

    .wing {
        animation: none;
    }

    /* Cancelled rather than shortened. The fill holds the animation's first frame through its
       delay, so leaving it on would park the butterfly at half size and invisible for two
       seconds before it did anything. */
    .butterfly.is-emerging {
        animation: none;
    }

    /* Removed rather than frozen. Four butterflies parked mid-air across the page would look
       like a rendering fault; they are decoration, so nothing is lost by dropping them. */
    .flock {
        display: none;
    }
}
