/* Digital invitation chrome — diamond envelope intro overlay + phone-frame for the
   published page underneath. Loaded only when shared.php detects mode='invitation'.
   Adapted from invitacion-boda-digital/envelop/index2.html. */:root{--inv-cream-light:#fafaf8;--inv-cream-deep:#ece4cc;--inv-border:#d8d8d6;--inv-border-dark:#c8c2af;--inv-gold:#a58a4f;--inv-ink:#5e5848;--inv-frame-max:448px}/* Body backdrop and viewport lock — only when the body is in invitation mode. */body.is-invitation{background:linear-gradient(135deg, rgb(83 88 83 / 55%), rgb(83 88 83 / 85%)),radial-gradient(ellipse at center, #3f433f, #232624);overflow:hidden;-webkit-font-smoothing:antialiased}body.is-invitation .invitation-backdrop{position:fixed;inset:0;background:linear-gradient(135deg, rgb(83 88 83 / 50%), rgb(83 88 83 / 80%));z-index:0}/* Phone frame for the published page. Centered column on desktop, full-bleed on mobile.
   Published invitation pages render their content inside an iframe so the template's
   media queries match the phone-frame width (not the desktop viewport). */body.is-invitation #preview-content,body.is-invitation .invitation-frame{position:fixed;top:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--inv-frame-max);height:100vh;min-height:0;overflow-x:hidden;overflow-y:auto;background:var(--inv-cream-light);box-shadow:0 0 60px rgba(0, 0, 0, 0.45);z-index:20}body.is-invitation .invitation-frame{border:0;display:block}/* Loading screen: phone-frame shaped (dark backdrop shows around it), kept ON TOP of
   the envelope so the heart is visible while the envelope + stamp image load. shared.php
   fades it out once the stamp has loaded, revealing the envelope underneath. */body.is-invitation .loading-container{position:fixed;top:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--inv-frame-max);height:100vh;background:var(--inv-cream-light);z-index:200}/* ===== Envelope overlay ===== */body.is-invitation .envelope{position:fixed;top:0;left:50%;transform:translateX(-50%);z-index:50;width:100%;max-width:var(--inv-frame-max);height:100vh;overflow:hidden;background:transparent;border:0;padding:0;cursor:pointer;-webkit-tap-highlight-color:transparent;font-family:'Georgia','Times New Roman',serif}body.is-invitation .envelope .env-inner{position:absolute;inset:0;z-index:40;display:flex;height:100%;width:100%;flex-direction:column;justify-content:center}/* Diamond base shared by all envelope panels */body.is-invitation .invitation-panel-tile{position:absolute;display:block;height:70vh;width:70vh;border:1px solid var(--inv-border);background-color:var(--inv-cream-light)}/* Lower half — three back panels + hint */body.is-invitation .env-bottom{position:absolute;bottom:0;left:0;width:100%;height:75vh;will-change:transform;transition:transform 1.8s cubic-bezier(0.68, 0.02, 0.28, 1)}body.is-invitation .env-back-a{top:10%;left:50%;transform:translate(-100%, 5%) rotate(45deg);border-radius:6px}body.is-invitation .env-back-b{top:10%;left:50%;transform:translate(0%, 5%) rotate(45deg);border-radius:6px}body.is-invitation .env-back-c{bottom:10%;left:50%;transform:translate(-50%, 55%) rotate(45deg);border-radius:24px;border-width:2px;z-index:2}body.is-invitation .hint{position:absolute;bottom:0;left:0;right:0;height:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:3;pointer-events:none;color:var(--inv-ink);text-align:center;transition:opacity 0.35s ease-out;padding:0 24px}body.is-invitation .hint .intro{display:block;font-style:italic;font-size:17px;margin-bottom:10px;opacity:0.8;line-height:1.4}body.is-invitation .hint .cta{display:inline-block;font-weight:700;font-size:15px;letter-spacing:1.5px;color:#879696;transform-origin:center}@keyframes invitationCtaZoomPulse{0%,100%{opacity:0.5;transform:scale(1)}50%{opacity:1;transform:scale(1.12)}}body.is-invitation .hint .cta{animation:invitationCtaZoomPulse 2s ease-in-out infinite}/* Top flap (front face) */body.is-invitation .env-top{position:absolute;display:flex;align-items:flex-end;justify-content:flex-end;height:70vh;width:70vh;border-radius:24px;border:2px solid var(--inv-border-dark);background-color:var(--inv-cream-light);will-change:transform;top:10%;left:50%;z-index:3;box-shadow:7px 3px 14px 1px rgba(0, 0, 0, 0.15),0 2px 4px -2px rgba(0, 0, 0, 0.15);transform:translateX(-50%) translateY(-50%) rotate(45deg)}/* Names — counter-rotated to read upright. */body.is-invitation .env-names{position:absolute;inset:0;z-index:30;display:flex;height:100%;width:100%;flex-direction:column;align-items:center;justify-content:flex-start;padding-top:64px;color:#879696;transform:translate(25%, 25%) rotate(-45deg);will-change:opacity;transition:opacity 0.6s ease-out}body.is-invitation .env-names .name{font-family:'Brush Script MT','Lucida Handwriting',cursive;font-size:clamp(34px, 9vmin, 46px);line-height:1;text-shadow:0 1px 2px rgba(100, 110, 110, 0.12);white-space:nowrap}body.is-invitation .env-names .amp{font-family:'Brush Script MT','Lucida Handwriting',cursive;font-size:clamp(20px, 5vmin, 26px);font-style:italic;margin:4px 0;opacity:0.9}/* Stamp — counter-rotated, sits in the bottom-right corner of the diamond. */body.is-invitation .env-stamp{position:relative;z-index:30;transform:translate(25%, 25%) rotate(-45deg);filter:drop-shadow(0 8px 14px rgba(80, 50, 10, 0.35));will-change:transform,opacity;transition:transform 0.65s cubic-bezier(0.5, 0, 0.75, 0),opacity 0.5s ease-out}body.is-invitation .env-stamp img{display:block;width:clamp(80px, 16vmin, 108px);object-fit:contain;user-select:none;-webkit-user-drag:none}@keyframes invitationStampPulse{0%,100%{transform:translate(25%, 25%) rotate(-45deg) scale(1)}50%{transform:translate(25%, 25%) rotate(-45deg) scale(1.05)}}body.is-invitation .envelope:not(.opened) .env-stamp{animation:invitationStampPulse 2.6s ease-in-out infinite}/* Editor mode (preview iframe inside app.php): the stamp + hint text act as
   shortcuts that open the bottom edit sheet on mobile. Re-enable pointer
   events on the hint children (the wrapper has pointer-events:none above)
   and give them an explicit pointer cursor. */body.is-invitation-editor .env-stamp,body.is-invitation-editor .env-names,body.is-invitation-editor .hint .intro,body.is-invitation-editor .hint .cta{cursor:pointer}body.is-invitation-editor .hint .intro,body.is-invitation-editor .hint .cta{pointer-events:auto}/* Opened state — stamp shrinks, names fade, top flap flies off (handled by JS),
   bottom panels slide down. */body.is-invitation .envelope.opened{pointer-events:none}body.is-invitation .envelope.opened .hint{opacity:0}body.is-invitation .envelope.opened .env-stamp{transform:translate(25%, 25%) rotate(-45deg) scale(0.3);opacity:0}body.is-invitation .envelope.opened .env-names{opacity:0;transition-delay:0.1s}body.is-invitation .envelope.opened .env-bottom{transform:translateY(120%);transition-delay:0.3s}/* Editor preview only: applied for one frame while the intro-preview snaps
   the envelope back to its closed first frame, so the flap and inner panels
   reset instantly instead of easing back via their .opened transitions. */body.is-invitation .envelope.inv-preview-instant,body.is-invitation .envelope.inv-preview-instant *{transition:none !important;animation:none !important}/* Music toggle — small floating button revealed after envelope opens. */.invitation-music-toggle{position:fixed;bottom:18px;right:18px;width:44px;height:44px;border-radius:50%;background:rgba(255, 255, 255, 0.92);border:1px solid rgba(0, 0, 0, 0.08);box-shadow:0 4px 14px rgba(0, 0, 0, 0.12);cursor:pointer;z-index:100;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--inv-ink);transition:transform 0.2s ease,opacity 0.2s ease;opacity:0;transform:scale(0.8);pointer-events:none}.invitation-music-toggle.is-visible{opacity:1;transform:scale(1);pointer-events:auto}.invitation-music-toggle:hover{transform:scale(1.08)}.invitation-music-toggle.is-muted{color:#9ca3af}.invitation-music-toggle.is-muted::after{content:'';position:absolute;width:60%;height:2px;background:currentColor;transform:rotate(-45deg);pointer-events:none}/* Persistent "playing" indicator: the note glyph gently bounces while
   music is on, so the button doubles as a now-playing status. Frozen
   when muted (.is-muted overrides via the selector below). */@keyframes invitationMusicNoteBounce{0%,100%{transform:translateY(0) rotate(0deg)}25%{transform:translateY(-2px) rotate(-6deg)}50%{transform:translateY(0) rotate(0deg)}75%{transform:translateY(-2px) rotate(6deg)}}.invitation-music-toggle.is-playing .music-icon{display:inline-block;animation:invitationMusicNoteBounce 1.4s ease-in-out infinite;transform-origin:center}.invitation-music-toggle.is-muted .music-icon{animation:none}/* One-shot onboarding hint: a soft pulse ring + scale wobble that runs
   for ~4s right after the envelope opens, to draw the eye to the button
   the first time it appears. Removed on click or timeout. */@keyframes invitationMusicHintPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.12)}}@keyframes invitationMusicHintRing{0%{box-shadow:0 0 0 0 rgba(165, 138, 79, 0.55),0 4px 14px rgba(0, 0, 0, 0.12)}70%{box-shadow:0 0 0 16px rgba(165, 138, 79, 0),0 4px 14px rgba(0, 0, 0, 0.12)}100%{box-shadow:0 0 0 0 rgba(165, 138, 79, 0),0 4px 14px rgba(0, 0, 0, 0.12)}}.invitation-music-toggle.is-hint{animation:invitationMusicHintPulse 1.4s ease-in-out 3,invitationMusicHintRing 1.4s ease-out 3}@media (prefers-reduced-motion: reduce){.invitation-music-toggle.is-playing .music-icon,.invitation-music-toggle.is-hint{animation:none}}/* ===== Video intro (intro-video-player.js) =====
   Replaces the envelope when the couple picked a published intro clip. The
   overlay sits on the envelope layer (z 50): above the page frame (z 20),
   below the heart loader (z 200). Ported from mockups/intros/index.html. *//* Cream cover between the page frame and the intro while a normal/blend intro
   plays: hides the page (so it isn't visible behind the video) and, in blend
   mode, gives the video a clean light surface to multiply against. Hidden for
   lumakey, where the page must show through the keyed canvas from the start. */.invitation-intro-backdrop{position:fixed;top:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--inv-frame-max);height:100vh;background:var(--inv-cream-light);z-index:40;pointer-events:none}/* The intro overlay — positioned identically over the phone frame. */.invitation-intro{position:fixed;top:0;left:50%;transform:translateX(-50%);width:100%;max-width:var(--inv-frame-max);height:100vh;z-index:50;overflow:hidden;background:#000;box-shadow:0 0 60px rgba(0, 0, 0, 0.45)}#invitation-intro-video{display:block;position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#000;z-index:2}/* Static opaque poster behind the media layers (iOS Safari anti-flash): stays
   on screen through the tap→play handoff; the video fades in OVER it once a
   real frame painted, so the swap can never flash black. Rendered only for
   normal/blend intros with a catalog thumb (.has-poster on the overlay) —
   lumakey intros key their poster onto the canvas instead (single layer). */.invitation-intro-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:1;opacity:1;pointer-events:none;background:#000;/* Painted only while .has-poster is on the overlay: the editor keeps the
       img in the DOM across intro switches, and a stale poster must not show
       through a later lumakey/pretapKey intro's transparent canvas. */display:none}.invitation-intro.has-poster .invitation-intro-poster{display:block}/* Keyed output — the canvas renders at a capped resolution (≤544px wide) and
   is stretched to the overlay box, mirroring the video's object-fit: cover. */#invitation-intro-canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:none;z-index:3}/* White layer used to fade the video to white before revealing the page. */#invitation-intro-flash{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:4}/* Blend mode: the overlay multiplies with the cream backdrop behind it, so the
   video's white areas read as the light page surface instead of flat white.
   Requires a transparent overlay so the blend reaches the layer beneath. */.invitation-intro.is-blend{mix-blend-mode:multiply;background:transparent;box-shadow:none;isolation:auto}.invitation-intro.is-blend #invitation-intro-video{background:transparent}/* Lumakey mode: the canvas keys out the clip's key colour per pixel, so the
   page behind shows THROUGH the keyed area while the video plays. */.invitation-intro.is-lumakey{background:transparent;box-shadow:none}.invitation-intro.is-lumakey #invitation-intro-canvas{display:block}.invitation-intro.is-lumakey #invitation-intro-video{visibility:hidden}/* Pre-tap (published): show the clip's own element — it displays the poster
   natively, also on iOS, where JS cannot read video frames before the tap.
   Removed by JS the instant the key loop paints its first keyed frame. */.invitation-intro.is-lumakey.is-pretap #invitation-intro-video{visibility:visible}/* Poster-behind fade-in (.has-poster, normal/blend only): the video starts
   invisible over the opaque poster img and is faded in by JS on the first
   REAL painted frame — never before, so iOS Safari's black repaint between
   the paused poster and playback is covered. The lumakey canvas is NEVER
   opacity-gated: its keyed poster must cover from the first paint. */.invitation-intro.has-poster #invitation-intro-video{opacity:0;transition:opacity 260ms ease}/* Frozen rehearsal frame (editor Intro tab): after the preview clip ends the
   overlay parks on the clip's first frame. Normal/blend intros park on the
   raw video, rendered opaquely — canvas off, no blending. Must come AFTER
   the is-blend/is-lumakey rules to win at equal specificity. The color-cast
   CSS filter stays on the video, matching playback. */.invitation-intro.is-frozen{mix-blend-mode:normal;background:#000}.invitation-intro.is-frozen #invitation-intro-canvas{display:none}.invitation-intro.is-frozen #invitation-intro-video{visibility:visible;/* has-poster starts the video at opacity 0 — the parked rehearsal frame
       must not be blanked by it (these rules come later, so they win). */opacity:1}.invitation-intro.is-frozen .invitation-intro-poster{opacity:0}/* Keyed (lumakey) intros stay keyed while frozen: the parked frame 0 is
   rendered through the keyer onto the canvas (JS draws it once at freeze
   time), the key colour stays transparent and the page shows through —
   exactly like playback. Higher specificity, so it wins over the raw-video
   frozen rules above. */.invitation-intro.is-lumakey.is-frozen{background:transparent}.invitation-intro.is-lumakey.is-frozen #invitation-intro-canvas{display:block}.invitation-intro.is-lumakey.is-frozen #invitation-intro-video{visibility:hidden}/* "Tap to start" cue — covers the intro until the guest taps (the tap is the
   user gesture that unlocks unmuted playback + background-video autoplay). */.invitation-intro-start{position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;border:0;padding:0;cursor:pointer;color:#fff;/* Fully transparent: the pre-tap first frame must show untouched, and the
       tap→play handover must not change the frame's brightness at all — only
       the play indicator itself fades out (the .hidden opacity transition). */background:transparent;-webkit-tap-highlight-color:transparent;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:opacity 0.3s ease}.invitation-intro-start.hidden{opacity:0;pointer-events:none}.invitation-intro-start .play-ring{width:76px;height:76px;border-radius:50%;border:2px solid rgba(255, 255, 255, 0.85);display:flex;align-items:center;justify-content:center;font-size:30px;padding-left:5px;/* The overlay is fully transparent, so the indicator carries its own
       contrast: a faint dark fill inside the ring plus soft shadows keep it
       readable on bright first frames without darkening the frame itself. */background:rgba(0, 0, 0, 0.18);text-shadow:0 1px 8px rgba(0, 0, 0, 0.45);box-shadow:0 2px 18px rgba(0, 0, 0, 0.25);transition:transform 0.15s ease,background 0.15s ease}.invitation-intro-start:hover .play-ring{background:rgba(0, 0, 0, 0.3);transform:scale(1.05)}.invitation-intro-start .play-label{font-size:13px;letter-spacing:2px;text-transform:uppercase;opacity:0.85;text-shadow:0 1px 8px rgba(0, 0, 0, 0.45)}