/* ═══════════════════════════════════════════
   gift-of-flowers — beat 6: finale (6.0 s, the last scene; tap; hold `heart` at 2.6 s)
   A small outlined heart floats up and waits. Tapped, it opens, a ripple
   leaves it, a fourth sheet of bubbles rises from it, and the closing line
   is written on in script, completing on swell's hit, when a white lift
   brightens the page and twelve glints twinkle in. The sign-off fades up
   beneath. Then it rests: the line breathes, the glints twinkle, the bridge's
   bubbles and wash drift on. Never a frozen frame; nothing ever replays.

   The section is TRANSPARENT over the bridge's #gof-ground (screens.md,
   cross-beat contract 1). The fourth bubble sheet is this beat's own.
   The glints use the library's _shared/effects/sparkles.css (linked by the
   bridge) with its light-ground variant: .sparkle-layer--light / .sparkle.

   Play-state classes on .finale-container, toggled by js/finale.js:
     --is-rising   0.4 s   the heart rises from 84% to 52% y (1.8 s, drift)
     --is-hinted   2.2 s   "tap the heart" fades in; the heart pulses (1.8 s, breath)
     --is-armed    2.6 s   the hold `heart`: the heart takes the tap
     --is-opened   tap     the heart presses and opens, the ripple, the bubble sheet (+0.15 s)
     --is-writing  hit - write   the closing line's clip wipe, row by row, and its 3 vh rise
     --is-lit      hit     the white lift and the glints
     --is-signed   tap + 2.6 s   the sign-off fades up 8 px
     --is-resting  tap + 3.6 s   the line breathes; the spent heart is parked
   Only transform, opacity, one small SVG fill and a small-area clip-path move.
   ═══════════════════════════════════════════ */

.finale-container {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
}

/* ───── The white lift (behind everything) ─────
   --finale-lift is already white at 0.6 alpha, so the layer's opacity runs
   0 -> 1 -> 0.3: the lift peaks at 0.6 and settles at 0.18 (shots.md). */
.finale-lift {
    position: absolute;
    left: 50%;
    top: 46%;
    width: 150vw;
    height: 90vh;
    transform: translate(-50%, -50%);
    background: radial-gradient(closest-side, var(--finale-lift), rgba(255, 255, 255, 0));
    opacity: 0;
    will-change: opacity;
}

.finale-container.finale-is-lit .finale-lift {
    animation: finale-lift-in 1.65s linear forwards;
}

@keyframes finale-lift-in {
    0% { opacity: 0; animation-timing-function: var(--ease-expo-out); }
    15.2% { opacity: 1; animation-timing-function: var(--ease-silk); }
    100% { opacity: 0.3; }
}

/* ───── The fourth bubble sheet ─────
   One layer: its fourteen bubbles are static; the sheet grows out of the
   heart (transform-origin on the heart) while it rises past the top. Every
   bubble ends off-screen and the sheet at opacity 0, so it comes to rest. */
.finale-bubbles {
    position: absolute;
    inset: 0;
    overflow: hidden;
}

.finale-bubbles-sheet {
    position: absolute;
    inset: 0;
    transform-origin: 50% 52%;
    transform: translate3d(0, 0, 0) scale(0.2);
    opacity: 0;
}

.finale-bubble {
    position: absolute;
    width: var(--finale-b, 10px);
    height: var(--finale-b, 10px);
    margin: calc(var(--finale-b, 10px) / -2) 0 0 calc(var(--finale-b, 10px) / -2);
    border-radius: 50%;
    background: var(--bubble);
    opacity: min(var(--finale-bo, 0.4), var(--bubble-opacity-max));
}

.finale-container.finale-is-opened .finale-bubbles-sheet {
    will-change: transform, opacity;
    animation: finale-bubbles-rise 17s var(--ease-drift) 0.15s both;
}

@keyframes finale-bubbles-rise {
    0% { opacity: 0; transform: translate3d(0, 0, 0) scale(0.2); }
    6% { opacity: 1; }
    90% { opacity: 1; }
    100% { opacity: 0; transform: translate3d(0, -135%, 0) scale(1.5); }
}

/* ───── The words ───── */
.finale-words {
    position: absolute;
    inset: 0;
}

/* Centred on the heart's place, 46% y. Its width is the column the line is
   fitted in; the sign-off hangs beneath it, out of flow, so the line's
   centre never moves with the sign-off's length. */
.finale-line-box {
    position: absolute;
    left: 50%;
    top: 46%;
    width: min(88vw, 34rem);
    transform: translate(-50%, -50%);
}

.finale-line-rise {
    transform: translateY(3vh);
}

.finale-line-breath {
    transform-origin: 50% 50%;
}

.finale-line {
    --finale-line-size: var(--type-script-closing);
    --finale-ink-pad: 0.12em;        /* the ink's side padding: drawn rows and the measure share it */
    margin: 0;
    font-family: var(--font-script);
    font-weight: 400;
    font-size: var(--finale-line-size);
    line-height: 1.3;
    color: var(--wine);
    -webkit-text-stroke: var(--script-stroke) var(--wine);
    text-align: center;
    text-wrap: balance;
    overflow-wrap: anywhere;
}

/* Measuring pass only (js/finale.js): the words, balanced, so their rows can
   be read. It carries the ink's own side padding (--finale-ink-pad), so the
   column it wraps in is exactly the room a drawn row has: what is measured
   as one row is drawn as one line. */
.finale-measure {
    display: block;
    box-sizing: border-box;
    padding: 0 var(--finale-ink-pad);
    text-wrap: balance;
}

/* Each wipe row is exactly one line: the fit has already broken the words. */
.finale-row {
    display: block;
    white-space: nowrap;
}

/* Only when even the 2.1 rem floor cannot hold a word in the column (a single
   unbroken word wider than the screen) may a row wrap inside itself. */
.finale-line.finale-line-loose .finale-row {
    white-space: normal;
}

/* The writing-on: each row is a left-to-right clip wipe, paced to its glyph
   count (--finale-wf of the write, after --finale-wo of it). The negative
   insets let Pinyon's swashes show once written. */
.finale-ink {
    display: inline-block;
    max-width: 100%;
    padding: 0 var(--finale-ink-pad);
    clip-path: inset(-0.6em calc(100% + 0.8em) -0.6em -0.8em);
}

.finale-container.finale-is-writing .finale-ink {
    clip-path: inset(-0.6em -0.8em -0.6em -0.8em);
    transition: clip-path calc(var(--finale-write, 2.1s) * var(--finale-wf, 1)) var(--ease-silk) calc(var(--finale-write, 2.1s) * var(--finale-wo, 0));
}

.finale-container.finale-is-writing .finale-line-rise {
    transform: translateY(0);
    transition: transform var(--finale-write, 2.1s) var(--ease-expo-out);
}

/* From tap + 3.6 s: the line breathes (never a frozen frame). */
.finale-container.finale-is-resting .finale-line-breath {
    animation: finale-breathe 5s var(--ease-breath) infinite;
}

@keyframes finale-breathe {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.012); }
}

/* ───── The glints (the library's light sparkles) ─────
   Sized to the widest fitted row by js/finale.js; all sit at or above the
   line's last row, so none is behind the sign-off. Parked (no animation)
   until the hit, then each twinkles in from its keyframe's 0% within the
   first second and keeps its own phase. */
.finale-glints {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: var(--finale-glint-w, 100%);
    transform: translateX(-50%);
    opacity: 0;
}

.finale-glints .sparkle {
    animation-fill-mode: backwards;
}

.finale-container:not(.finale-is-lit) .finale-glints .sparkle {
    animation: none;
}

.finale-container.finale-is-lit .finale-glints {
    opacity: 1;
}

/* ───── The sign-off ───── */
.finale-signoff {
    position: absolute;
    left: 50%;
    top: 100%;
    width: min(80vw, 22rem);
    margin: 0.35em 0 0;
    font-family: var(--font-hand);
    font-weight: 600;
    font-size: var(--type-hand-signoff);
    line-height: 1.2;
    color: var(--wine);
    text-align: center;
    text-wrap: balance;
    overflow-wrap: anywhere;
    opacity: 0;
    transform: translate(-50%, 8px);
}

.finale-container.finale-is-signed .finale-signoff {
    opacity: 1;
    transform: translate(-50%, 0);
    transition:
        opacity 0.9s var(--ease-expo-out),
        transform 0.9s var(--ease-expo-out);
}

/* ───── The heart ───── */
.finale-heart-stage {
    position: absolute;
    left: 50%;
    top: 52%;
    width: 0;
    height: 0;
}

/* From 84% y to 52% y: 32 vh below its place, and faded out, until 0.4 s. */
.finale-heart-rise {
    position: absolute;
    left: 0;
    top: 0;
    transform: translateY(32vh);
    opacity: 0;
}

.finale-container.finale-is-rising .finale-heart-rise {
    transform: translateY(0);
    opacity: 1;
    transition:
        transform 1.8s var(--ease-drift),
        opacity 0.9s var(--ease-silk);
}

/* The 76 px invisible round hit area, centred on the heart's place. */
.finale-heart {
    position: absolute;
    left: -38px;
    top: -38px;
    width: 76px;
    height: 76px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: default;
    pointer-events: auto;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
}

.finale-container.finale-is-armed .finale-heart {
    cursor: pointer;
}

.finale-heart:focus {
    outline: none;
}

.finale-heart:focus-visible {
    outline: 1.5px solid var(--wine);
    outline-offset: -4px;
}

.finale-heart-pulse,
.finale-heart-open {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 40px;
    height: 40px;
    margin: -20px 0 0 -20px;
}

.finale-heart-svg {
    display: block;
    width: 40px;
    height: 40px;
    overflow: visible;
}

.finale-heart-path {
    fill: var(--accent-warm-deep);
    fill-opacity: 0;
    stroke: var(--accent-celestial);
    stroke-width: 1.6;
    stroke-linejoin: round;
}

/* 2.2 s: the pulse, scale 1 -> 1.1. */
.finale-container.finale-is-hinted .finale-heart-pulse {
    animation: finale-heart-pulse 1.8s var(--ease-breath) infinite;
}

@keyframes finale-heart-pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.1); }
}

/* The tap: press (0.88, 90 ms snap), then open (1.3, 0.5 s expo-out) as the
   outline fills with accent-warm-deep and the heart dissolves. */
.finale-container.finale-is-opened .finale-heart {
    pointer-events: none;
    cursor: default;
}

.finale-container.finale-is-opened .finale-heart-open {
    animation: finale-heart-open 0.59s linear forwards;
}

@keyframes finale-heart-open {
    0% { transform: scale(1); animation-timing-function: var(--ease-snap); }
    15.25% { transform: scale(0.88); animation-timing-function: var(--ease-expo-out); }
    100% { transform: scale(1.3); }
}

.finale-container.finale-is-opened .finale-heart-svg {
    animation: finale-heart-dissolve 0.5s var(--ease-silk) 0.12s forwards;
}

@keyframes finale-heart-dissolve {
    from { opacity: 1; }
    to { opacity: 0; }
}

.finale-container.finale-is-opened .finale-heart-path {
    fill-opacity: 1;
    stroke: var(--accent-warm-deep);
    transition:
        fill-opacity 0.18s var(--ease-silk),
        stroke 0.18s var(--ease-silk);
}

/* The one-shot ripple ring. */
.finale-heart-ripple {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 40px;
    height: 40px;
    margin: -20px 0 0 -20px;
    border: 1.5px solid var(--wine);
    border-radius: 50%;
    opacity: 0;
    transform: scale(1);
}

.finale-container.finale-is-opened .finale-heart-ripple {
    animation: finale-ripple 1s var(--ease-expo-out) forwards;
}

@keyframes finale-ripple {
    from { opacity: 0.5; transform: scale(1); }
    to { opacity: 0; transform: scale(7); }
}

/* Once the payoff has played the spent heart is parked: no looping pulse,
   no hit area, nothing to focus. */
.finale-container.finale-is-resting .finale-heart-rise {
    visibility: hidden;
}

.finale-container.finale-is-resting .finale-heart-pulse,
.finale-container.finale-is-resting .finale-heart-open,
.finale-container.finale-is-resting .finale-heart-svg,
.finale-container.finale-is-resting .finale-heart-ripple {
    animation: none;
}

/* ───── "tap the heart" ───── */
.finale-hint {
    position: absolute;
    left: 0;
    top: 46px;
    margin: 0;
    transform: translateX(-50%);
    white-space: nowrap;
    font-family: var(--font-body);
    font-weight: 400;
    font-size: var(--type-hint);
    line-height: 1.4;
    letter-spacing: 0.14em;
    text-transform: lowercase;
    color: var(--wine);
    opacity: 0;
}

.finale-container.finale-is-hinted .finale-hint {
    opacity: 1;
    transition: opacity 0.6s var(--ease-silk);
}

.finale-container.finale-is-opened .finale-hint {
    opacity: 0;
    transition: opacity 0.4s var(--ease-silk);
}

/* ───── Screen-reader line (visually hidden, top-left, never under the badge) ───── */
.finale-sr {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Entry states are applied instantly (prepare), never animated in. */
.finale-instant,
.finale-instant *,
.finale-instant *::before,
.finale-instant *::after {
    transition: none !important;
    animation: none !important;
}

/* ───── Reduced motion: plays in full, calmer ─────
   The heart appears in place (no float, no pulse); the tap turns it to the
   filled heart with a cross-fade and no ripple, then it fades; the closing
   line and the sign-off fade in whole (0.6 s each) on the same schedule
   (js/finale.js lands the line's fade on the hit); the lift is an opacity
   fade with no glints; the fourth bubble sheet is not added. */
@media (prefers-reduced-motion: reduce) {
    .finale-heart-rise {
        transform: none;
    }

    .finale-container.finale-is-rising .finale-heart-rise {
        transform: none;
        transition: opacity 0.6s var(--ease-silk);
    }

    .finale-container.finale-is-hinted .finale-heart-pulse {
        animation: none;
    }

    .finale-container.finale-is-opened .finale-heart-open {
        animation: none;
    }

    .finale-container.finale-is-opened .finale-heart-svg {
        animation: finale-heart-dissolve 0.6s var(--ease-silk) 0.5s forwards;
    }

    .finale-container.finale-is-opened .finale-heart-path {
        transition:
            fill-opacity 0.4s var(--ease-silk),
            stroke 0.4s var(--ease-silk);
    }

    .finale-heart-ripple,
    .finale-bubbles,
    .finale-glints {
        display: none;
    }

    .finale-line-rise,
    .finale-container.finale-is-writing .finale-line-rise {
        transform: none;
        transition: none;
    }

    .finale-ink {
        clip-path: none;
    }

    .finale-line {
        opacity: 0;
    }

    .finale-container.finale-is-writing .finale-ink {
        clip-path: none;
        transition: none;
    }

    .finale-container.finale-is-writing .finale-line {
        opacity: 1;
        transition: opacity var(--finale-write, 0.6s) var(--ease-silk);
    }

    .finale-signoff {
        transform: translate(-50%, 0);
    }

    .finale-container.finale-is-signed .finale-signoff {
        transition: opacity 0.6s var(--ease-silk);
    }

    .finale-container.finale-is-lit .finale-lift {
        animation: none;
        opacity: 0.3;
        transition: opacity 0.6s var(--ease-silk);
    }

    .finale-container.finale-is-resting .finale-line-breath {
        animation: none;
    }
}
