/* Betawi direct template — Cover only (rebuild from the original PSD,
   cover-betawi.psd, 1080x1920). Ordinary static HTML/CSS/JS: the whole
   scene is authored at its native PSD pixel coordinates and scaled as
   one unit to cover the viewport, the same way object-fit:cover scales
   an image -- no per-property unit conversion, no runtime PSD engine. */
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:#000;height:100%;}
body{font-family:'EB Garamond',serif;-webkit-font-smoothing:antialiased;}
img{max-width:none;}

.bw-app{
  position:relative;
  max-width:480px;
  margin:0 auto;
  overflow:hidden;
  background:#efe6d6;
}

.bw-cover{
  position:relative;
  width:100%;
  height:100dvh;
  overflow:hidden;
  background:#efe6d6;
  /* Exit transition lives HERE, never on .bw-cover-scene: the scene's own
     transform is reserved exclusively for the JS-computed cover-fit
     scale (translate(-50%,-50%) scale(S)) and must never be touched by
     any animation, entrance or exit -- the CRITICAL VISUAL INVARIANT. */
  transition: opacity .8s cubic-bezier(.22,.61,.36,1), translate .8s cubic-bezier(.22,.61,.36,1);
}
.bw-app.is-open .bw-cover{
  opacity:0;
  translate:0 -3%;
  pointer-events:none;
}

/* Scaled as one unit (JS sets transform), native PSD size, so every
   child's plain PSD-pixel left/top/width/height/font-size stays exactly
   proportional at any viewport width -- this is what makes 360-430px
   all render identically to the PSD, just smaller or larger. */
.bw-cover-scene{
  position:absolute;
  top:50%;
  left:50%;
  width:1080px;
  height:1920px;
  transform-origin:center center;
}

.bw-l{position:absolute;opacity:1;}
.bw-text{white-space:pre-wrap;}
.bw-photo{overflow:hidden;}
.bw-open-btn{padding:0;}

/* ================= Motion =================
   Property allocation is deliberate and fixed, so entrance and idle
   animations on the SAME element never fight over the same CSS property
   (two animations touching one property don't blend, the later one just
   wins each frame -- keeping them on disjoint properties is what makes
   this safe):
     entrance -> opacity, scale (grows from a transform-origin anchored at
                 the asset's own natural attachment point in the PSD --
                 bottom for rooted plants/structures, top for hanging
                 ornaments, left/right for edge-framing foliage, centre
                 for free-floating content -- "forwards" fill, one-shot)
     idle     -> rotate only (sway), pivoting around that SAME anchor
   Every animation drives scale/rotate/opacity -- GPU-composited, never
   top/left/width/height/filter, and translate is reserved for the exit
   transition on a different element (.bw-cover) entirely. */

.bw-m-fade,.bw-m-up,.bw-m-growY,.bw-m-growX,.bw-m-growXY,.bw-m-scale,.bw-m-pop{
  animation-duration: var(--e-dur, 700ms);
  animation-delay: var(--e-delay, 0ms);
  animation-timing-function: ease;
  animation-fill-mode: both;
}
.bw-m-fade{ animation-name: bwFade; }
.bw-m-up{ animation-name: bwRevealUp; }
/* Softer ease-out than a typical "pop" curve (owner feedback: entrances felt
   rushed/jarring) -- reuses the same gentle settle curve already trusted
   elsewhere in this file for the Cover's exit transition, rather than a
   fast-then-trailing curve that reads as a sudden snap. */
.bw-m-growY{ animation-name: bwGrowY; animation-timing-function: cubic-bezier(.22,.61,.36,1); }
.bw-m-growX{ animation-name: bwGrowX; animation-timing-function: cubic-bezier(.22,.61,.36,1); }
.bw-m-growXY{ animation-name: bwGrowXY; animation-timing-function: cubic-bezier(.22,.61,.36,1); }
.bw-m-scale{ animation-name: bwScaleReveal; }
.bw-m-pop{ animation-name: bwButtonPop; animation-timing-function: cubic-bezier(.34,1.56,.64,1); }

@keyframes bwFade{
  from{ opacity:0; }
  to{ opacity: var(--e-op, 1); }
}
@keyframes bwRevealUp{
  from{ opacity:0; translate: 0 var(--e-ty, 30px); }
  to{ opacity:1; translate: 0 0; }
}
/* Grows from a transform-origin set per element (top/bottom edge) --
   the asset visually emerges from its own root/attachment point instead
   of just fading in in place. */
@keyframes bwGrowY{
  from{ opacity:0; scale: 1 var(--e-sy, .3); }
  to{ opacity:1; scale: 1 1; }
}
@keyframes bwGrowX{
  from{ opacity:0; scale: var(--e-sx, .3) 1; }
  to{ opacity:1; scale: 1 1; }
}
/* Corner-anchored bloom (both axes), for floral clusters that sit in a
   corner rather than along one edge. */
@keyframes bwGrowXY{
  from{ opacity:0; scale: var(--e-s, .3); }
  to{ opacity:1; scale: 1; }
}
@keyframes bwScaleReveal{
  from{ opacity:0; scale: var(--e-s, .9); }
  to{ opacity:1; scale: 1; }
}
@keyframes bwButtonPop{
  0%{ opacity:0; scale:.6; }
  70%{ opacity:1; scale:1.05; }
  100%{ opacity:1; scale:1; }
}

/* Idle (infinite, alternate) -- rotate only, pivoting around the same
   transform-origin as the entrance grow, so a bottom-rooted plant sways
   from its base and a hanging ornament sways from its top attachment.
   Rotate can never collide with any entrance animation (disjoint
   property: grow drives scale, sway drives rotate).

   CRITICAL: an element needs BOTH its one-shot grow AND its infinite
   sway running at once, on the same animation-name/-duration/-delay/etc
   properties. Those longhands don't merge across two separate class
   rules -- each is a single list value, so the later rule in the
   stylesheet simply overwrites the earlier one's value outright (the
   same "later-wins" trap the property-disjoint design elsewhere in this
   file exists to avoid). The only safe way to run two animations on one
   element is ONE rule declaring both as a comma-separated list, exactly
   like .bw-i-zoom already does for fade+zoom below. These three classes
   do that for each grow direction, and replace using .bw-m-growY/X/XY
   together with a separate .bw-i-sway class on the same element. */
.bw-i-sway-y, .bw-i-sway-x, .bw-i-sway-xy{
  animation-duration: var(--e-dur, 700ms), var(--i-dur, 8000ms);
  animation-delay: var(--e-delay, 0ms), calc(var(--e-delay, 0ms) + var(--e-dur, 700ms));
  animation-timing-function: cubic-bezier(.22,.61,.36,1), ease-in-out;
  animation-iteration-count: 1, infinite;
  animation-direction: normal, alternate;
  animation-fill-mode: both, none;
}
.bw-i-sway-y{ animation-name: bwGrowY, bwSway; }
.bw-i-sway-x{ animation-name: bwGrowX, bwSway; }
.bw-i-sway-xy{ animation-name: bwGrowXY, bwSway; }
@keyframes bwSway{
  0%,100%{ rotate: calc(var(--i-deg, 2deg) * -1); }
  50%{ rotate: var(--i-deg, 2deg); }
}

/* Background zoom-settle -- verified against the Simanten reference's own
   background motion (scale(1.15) -> scale(1), ~6s, one-shot on entrance,
   the FIRST visible motion). Two disjoint-property animations (opacity,
   scale) combined into one rule so neither overwrites the other -- same
   safe pattern as .bw-i-sway-y/x/xy above. No infinite idle afterwards:
   the background settles once and holds still. */
.bw-m-bgsettle{
  animation-name: bwFade, bwBgSettle;
  animation-duration: var(--e-dur, 900ms), var(--bg-dur, 6000ms);
  animation-delay: var(--e-delay, 0ms), var(--e-delay, 0ms);
  animation-timing-function: ease, cubic-bezier(.22,.61,.36,1);
  animation-iteration-count: 1, 1;
  animation-fill-mode: both, both;
}
@keyframes bwBgSettle{
  from{ scale: 1.15; }
  to{ scale: 1; }
}

/* ================= Hero ================= (additive only -- nothing
   above this point, including the Cover section's own rules, was
   changed). Hero reuses the same .bw-l / .bw-m-* / .bw-i-sway-* motion
   classes and keyframes already defined above for Cover.

   Cover -> Hero lifecycle: Hero starts hidden (display:none) so it is
   never visible or scrollable-to before "Buka Undangan". script.js
   adds is-hero-active to #bwApp only once the Cover's own existing,
   unmodified exit transition has finished; that single class then (1)
   removes Cover from the document entirely -- display:none takes it
   out of layout, so it no longer occupies scroll height and cannot be
   scrolled back into view -- and (2) reveals Hero. Because Hero's
   children only get display:block at that moment, their entrance
   animations (declared on .bw-l elements below, same as Cover) start
   fresh from delay 0 right then -- no JS-driven animation restart
   needed. */
.bw-hero{
  display:none;
  position:relative;
  width:100%;
  height:100dvh;
  overflow:hidden;
  background:#efe6d6;
}
.bw-hero-scene{
  position:absolute;
  top:50%;
  left:50%;
  width:1080px;
  height:1920px;
  transform-origin:center center;
}
.bw-app.is-hero-active .bw-cover{ display:none; }
.bw-app.is-hero-active .bw-hero{ display:block; }

/* Hero birds -- the owner-approved POC (c8d06f3) as is. Direct children of the
   Hero SECTION (not the 1080px scaled scene, where the POC's screen-unit flight
   would be scaled again and end off-screen), clipped by the Hero's overflow,
   visible only while the Hero is displayed, never tappable. */
.ua-bird-flight{
  position:absolute;
  inset:0;
  z-index:28;
  overflow:visible;
  pointer-events:none;
}
.ua-bird-flight img{
  position:absolute;
  left:-45%;
  top:43%;
  width:clamp(220px, 72%, 320px);
  height:auto;
  pointer-events:none;
  user-select:none;
  transform-origin:50% 50%;
  will-change:transform, opacity;
  animation:betawiBirdFlight 12800ms 650ms linear infinite both;
}
@keyframes betawiBirdFlight{
  0%{opacity:0;transform:translate3d(0, 0, 0) scale(.62) rotate(-11deg);}
  8%{opacity:.88;}
  43%{opacity:.92;transform:translate3d(calc(min(100vw, 480px) * .55), -14dvh, 0) scale(.9) rotate(-7deg);}
  74%{opacity:.8;transform:translate3d(calc(min(100vw, 480px) * .98), -28dvh, 0) scale(.72) rotate(-3deg);}
  100%{opacity:0;transform:translate3d(calc(min(100vw, 480px) * 1.35), -39dvh, 0) scale(.54) rotate(1deg);}
}
/* burug-devi flock: the GIF carries its own flight, placed across the sky */
.bw-bird-flock{
  position:absolute;
  left:0;
  top:30%;
  width:100%;
  z-index:28;
  pointer-events:none;
}
.bw-bird-flock img{display:block;width:100%;height:auto;pointer-events:none;user-select:none;}
@media (prefers-reduced-motion: reduce){ .ua-bird-flight, .bw-bird-flock{ display:none; } }

/* ================= Ayat ================= (additive only -- nothing
   above this point, including Cover's and Hero's own rules, was
   changed). Ayat scrolls inline after Hero rather than filling the
   viewport, so its scene scales to the section's WIDTH only (JS sets
   the transform and the section's own height to match, object-fit
   "contain" style) instead of Cover/Hero's crop-to-viewport "cover"
   scaling -- same native-PSD-pixel authorship principle, adapted for
   a normal scrolling block.

   Entrance is gated by animation-play-state rather than display:none:
   Ayat must occupy real scroll height at all times so scrolling down
   from Hero can reach it. Elements sit paused at their "from" keyframe
   (fill-mode:both, already declared on the shared bw-m-* rules above)
   until script.js's IntersectionObserver adds is-ayat-active on first
   meaningful visibility, then every child's animation plays from
   delay 0 in one motion, exactly as intended -- entrance always plays
   from a true resting start, never mid-animation from a scroll
   position. */
.bw-ayat{
  position:relative;
  width:100%;
  overflow:hidden;
  background:var(--ua-theme, #c38588);
}
.bw-ayat-scene{
  position:absolute;
  top:0;
  left:0;
  width:1080px;
  height:1797px;
  transform-origin:top left;
}
.bw-ayat .bw-l{ animation-play-state:paused; }
.bw-ayat.is-ayat-active .bw-l{ animation-play-state:running; }

/* ================= Pasangan ================= (additive only --
   nothing above this point, including Cover's, Hero's and Ayat's own
   rules, was changed). Scrolls inline exactly like Ayat (width-fit
   scene, JS sets height to match). Unlike Ayat, this section's
   entrance REPLAYS every time it enters/leaves the viewport (owner
   requirement: "alive like Simanten" while scrolling up and down
   repeatedly) -- so entrance is controlled directly via the Web
   Animations API in script.js (currentTime=0 + play()/pause()) rather
   than a one-way CSS class. animation-play-state:paused here is just
   the shared starting condition; script.js is the only thing that
   ever calls play()/pause() on these animations, on every enter/leave,
   any number of times -- there is no "is-pasangan-active" class. */
.bw-pasangan{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#e9d9d2;
}
.bw-pasangan-scene{
  position:absolute;
  top:0;
  left:0;
  width:1080px;
  height:3884px;
  transform-origin:top left;
}
.bw-pasangan .bw-l{ animation-play-state:paused; }
.bw-pasangan a.bw-l{ display:block; text-decoration:none; }

/* Spacing fix (owner phone review): Pasangan name / parents / Instagram are
   stacked by script.js (Pasangan reflow) with fixed breathing room; the
   text may wrap instead of shrinking. Scoped to the six [data-pf] blocks. */
.bw-pasangan [data-pf$="-name"], .bw-pasangan [data-pf$="-parents"]{ overflow-wrap:anywhere; }
.bw-pasangan [data-pf$="-name"]{ line-height:1.15; }

/* Portrait entrance -- RIGHT->CENTER / LEFT->CENTER. Large translateX
   only (never left/top), same soft-settle easing already trusted
   elsewhere in this file, so geometry itself (the inline left/top/
   width/height that IS the PSD position) is never touched -- only the
   visual offset before settling is. */
.bw-m-fromRight, .bw-m-fromLeft{
  animation-duration: var(--e-dur, 900ms);
  animation-delay: var(--e-delay, 0ms);
  animation-timing-function: cubic-bezier(.22,.61,.36,1);
  animation-fill-mode: both;
}
.bw-m-fromRight{ animation-name: bwGrowFromRight; }
.bw-m-fromLeft{ animation-name: bwGrowFromLeft; }
@keyframes bwGrowFromRight{
  from{ opacity:0; translate: 150px 0; }
  to{ opacity:1; translate: 0 0; }
}
@keyframes bwGrowFromLeft{
  from{ opacity:0; translate: -150px 0; }
  to{ opacity:1; translate: 0 0; }
}

/* Pasangan idle sway -- Simanten-style continuous drift (translate +
   tilt), replacing the tiny rotate-only sway for THIS section only.
   Scoped under .bw-pasangan so Cover/Hero/Ayat keep their locked
   .bw-i-sway-* behaviour byte-for-byte. Same one-rule/two-animation
   comma-list pattern (entrance grow + infinite idle). translate/rotate
   are free here: the grow entrance drives scale only. Free-floating
   sprays drift both ways; edge-anchored assets (.bw-i-in: vines,
   figures, corners) drift only inward so their bleed edge never opens
   a gap. --i-x/--i-y are scene px (scene is scaled as one unit). */
.bw-pasangan .bw-i-sway-y, .bw-pasangan .bw-i-sway-x, .bw-pasangan .bw-i-sway-xy{
  --i-x: 34px; --i-y: 0px;
  /* idle runs 1.8x slower than the authored --i-dur: calm, not busy */
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
}
.bw-pasangan .bw-i-sway-y{ animation-name: bwGrowY, bwPasDrift; }
.bw-pasangan .bw-i-sway-x{ animation-name: bwGrowX, bwPasDrift; }
.bw-pasangan .bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDrift; }
.bw-pasangan .bw-i-in.bw-i-sway-y{ animation-name: bwGrowY, bwPasDriftIn; }
.bw-pasangan .bw-i-in.bw-i-sway-x{ animation-name: bwGrowX, bwPasDriftIn; }
.bw-pasangan .bw-i-in.bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDriftIn; }
@keyframes bwPasDrift{
  0%,100%{ translate: 0 0; rotate: 0deg; animation-timing-function: ease-in-out; }
  25%{ translate: var(--i-x) var(--i-y); rotate: calc(var(--i-deg, 2deg) * 1.6); animation-timing-function: ease-in-out; }
  75%{ translate: calc(var(--i-x) * -1) calc(var(--i-y) * -1); rotate: calc(var(--i-deg, 2deg) * -1.6); animation-timing-function: ease-in-out; }
}
/* Edge-anchored assets: --i-x is signed OUTWARD (toward the nearest
   page edge) so the asset can only ever slide further off the edge,
   never inward -- its bleed edge can never open a visible cut inside
   the invitation. Y swings both ways; tilt swings both ways. */
@keyframes bwPasDriftIn{
  0%,100%{ translate: 0 0; rotate: 0deg; animation-timing-function: ease-in-out; }
  25%{ translate: var(--i-x) var(--i-y); rotate: calc(var(--i-deg, 2deg) * 1.6); animation-timing-function: ease-in-out; }
  50%{ translate: calc(var(--i-x) * .3) 0; rotate: 0deg; animation-timing-function: ease-in-out; }
  75%{ translate: var(--i-x) calc(var(--i-y) * -1); rotate: calc(var(--i-deg, 2deg) * -1.6); animation-timing-function: ease-in-out; }
}
/* Scroll-linked parallax layer (JS sets inline transform only). */
/* will-change is now set by script.js only while a section is near the viewport */
[data-par-bg]{ transform-origin: 50% 50%; }
[data-par-img]{ transform-origin: 50% 50%; }

/* Ayat: scroll.js takes over Ayat's entrance (per-asset replay on every
   scroll in/out, same as Pasangan). Only once script.js has added
   .bw-js-replay is Ayat's own "running once" rule superseded, so Ayat
   still plays normally if that script step cannot run. */
.bw-ayat.bw-js-replay.is-ayat-active .bw-l{ animation-play-state: paused; }

/* ================= Acara ================= (additive only). Same
   scroll-replay system as Pasangan (script.js drives every .bw-l via the
   Web Animations API; CSS keeps them paused) and the same calm
   translate+tilt idle drift on ornaments, scoped to .bw-acara so no
   existing section is affected. Reuses the existing keyframes. */
.bw-acara{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#c9a25d;
}
.bw-acara-scene{
  position:absolute;
  top:0;
  left:0;
  width:1080px;
  height:2859px;
  transform-origin:top left;
}
.bw-acara .bw-l{ animation-play-state:paused; }
.bw-acara a.bw-l{ display:block; text-decoration:none; }
/* invisible tap-area growth: the PSD buttons are only ~29 CSS px tall on a phone */
.bw-acara a.bw-l::after{ content:""; position:absolute; inset:-22px -12px; }
.bw-acara .bw-i-sway-y, .bw-acara .bw-i-sway-x, .bw-acara .bw-i-sway-xy{
  --i-x: 12px; --i-y: 0px;
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
}
.bw-acara .bw-i-sway-y{ animation-name: bwGrowY, bwPasDrift; }
.bw-acara .bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDrift; }
.bw-acara .bw-i-in.bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDriftIn; }
/* Ondel-ondel figures enter from their own side (reuses Pasangan's
   side-entrance keyframes). */
.bw-acara .bw-m-fromRight, .bw-acara .bw-m-fromLeft{
  animation-duration: var(--e-dur, 900ms);
  animation-delay: var(--e-delay, 0ms);
  animation-timing-function: cubic-bezier(.22,.61,.36,1);
  animation-fill-mode: both;
}
.bw-acara .bw-m-fromRight{ animation-name: bwGrowFromRight; }
.bw-acara .bw-m-fromLeft{ animation-name: bwGrowFromLeft; }

/* ================= Galeri ================= (additive only). Same
   replay system as Pasangan/Acara (script.js drives every .bw-l; CSS
   keeps them paused). Photo rows are native scroll-snap strips (the same
   mechanism as the existing UA gallery carousel): 2 photos visible per
   the PSD, swipe for the rest. */
.bw-galeri{
  position:relative;
  width:100%;
  overflow:hidden;
  background:var(--ua-theme, #9d5562);
}
.bw-galeri-scene{
  position:absolute;
  top:0;
  left:0;
  width:1080px;
  height:1920px;
  transform-origin:top left;
}
.bw-galeri .bw-l{ animation-play-state:paused; }
/* Platinum theme colour: Galeri headings sit directly on the recoloured
   surface. Fallback = their existing inline #fff (unchanged default). */
#bwGaleriScene > .bw-text{ color:var(--ua-theme-on, #fff) !important; }
.bw-gal-row{ overflow:hidden; }
.bw-gal-track{
  display:flex;
  gap:17px;
  width:100%;
  height:100%;
  overflow-x:auto;
  overflow-y:hidden;
  scroll-snap-type:x mandatory;
  scrollbar-width:none;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
}
.bw-gal-track::-webkit-scrollbar{ display:none; }
.bw-gal-slide{
  flex:0 0 460px;
  height:100%;
  scroll-snap-align:start;
  border-radius:30px;
  overflow:hidden;
  background:rgba(255,255,255,.06);
}
.bw-gal-slide[hidden]{ display:none; }
.bw-gal-slide{ cursor:zoom-in; -webkit-tap-highlight-color:transparent; }
.bw-gal-slide:focus-visible{ outline:3px solid #f7f1e8; outline-offset:-3px; }

/* Photo popup. Lives on <body>, above every section. */
.bw-lb{
  position:fixed;
  inset:0;
  z-index:9999;
  display:flex;
  align-items:center;
  justify-content:center;
  background:rgba(24,9,14,.92);
  animation:bwLbIn .25s ease both;
}
.bw-lb[hidden]{ display:none; }
.bw-lb{ touch-action:none; overscroll-behavior:contain; }
.bw-lb-img{
  max-width:92vw;
  max-height:82vh;
  width:auto;
  height:auto;
  object-fit:contain;
  border-radius:14px;
  box-shadow:0 12px 40px rgba(0,0,0,.45);
  user-select:none;
  -webkit-user-drag:none;
}
.bw-lb button{
  position:absolute;
  display:flex;
  align-items:center;
  justify-content:center;
  width:46px;
  height:46px;
  border:0;
  border-radius:50%;
  background:rgba(247,241,232,.16);
  color:#f7f1e8;
  font:400 30px/1 'Quicksand',sans-serif;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.bw-lb button[hidden]{ display:none; }
.bw-lb button:focus-visible{ outline:2px solid #f7f1e8; outline-offset:2px; }
.bw-lb-x{ top:max(14px, env(safe-area-inset-top)); right:14px; font-size:34px; }
.bw-lb-nav{ top:50%; margin-top:-23px; }
.bw-lb-prev{ left:8px; }
.bw-lb-next{ right:8px; }
.bw-lb-count{
  position:absolute;
  bottom:max(20px, env(safe-area-inset-bottom));
  left:0; right:0;
  text-align:center;
  color:#f7f1e8;
  font:500 15px/1 'Quicksand',sans-serif;
  letter-spacing:.08em;
}
@keyframes bwLbIn{ from{ opacity:0; } to{ opacity:1; } }
@media (prefers-reduced-motion: reduce){ .bw-lb{ animation:none; } }
.bw-gal-slide img{ display:block; width:100%; height:100%; object-fit:cover; -webkit-user-drag:none; user-select:none; }
.bw-gal-dots{ display:flex; align-items:center; justify-content:center; gap:10px; }
.bw-gal-dots span{ display:block; width:10px; height:10px; border-radius:5px; background:#868b98; flex:0 0 auto; transition:width .3s ease, background-color .3s ease; }
.bw-gal-dots span.is-active{ width:29px; background:var(--ua-theme-on, #f7f1e8); }

/* ================= Love Story ================= (additive only). Same
   replay system as Pasangan/Acara/Galeri; ornaments reuse the scoped
   translate+tilt idle drift. Card text is live (the PSD's baked text was
   removed from the card art). */
.bw-love{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#e1d2b9;
}
.bw-love-scene{
  position:absolute;
  top:0;
  left:0;
  width:1080px;
  height:2364px;
  transform-origin:top left;
}
.bw-love .bw-l{ animation-play-state:paused; }
.bw-love .bw-i-sway-y, .bw-love .bw-i-sway-x, .bw-love .bw-i-sway-xy{
  --i-x: 12px; --i-y: 0px;
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
}
.bw-love .bw-i-sway-y{ animation-name: bwGrowY, bwPasDrift; }
.bw-love .bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDrift; }
.bw-love .bw-i-in.bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDriftIn; }
.bw-lv-strip{ position:absolute; left:0; top:0; width:68px; height:100%; overflow:hidden; }
.bw-lv-strip img{ display:block; width:68px; height:auto; }
.bw-lv-cardimg{ position:absolute; left:68px; top:0; width:791px; height:auto; display:block; }
.bw-lv-text{
  position:absolute;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  box-sizing:border-box;
  padding:4px 40px 0;
  color:#2d251d;
  font-family:'EB Garamond','Cochin',serif;
  font-weight:500;
  font-size:45px;
}
.bw-lv-year{ font-family:'Quicksand',sans-serif; font-weight:700; font-style:italic; font-size:.844em; line-height:1; margin-bottom:.62em; white-space:nowrap; }
.bw-lv-body{ line-height:1.467; letter-spacing:.022em; }

/* ================= RSVP ================= (additive only). Scene is the PSD's
   native 1882x4542. Decorations/headings use the shared replay system; the
   FORM never transforms (no movement under the user's finger) -- it only
   fades in once. --s (scene scale, set by script.js) lets text/controls keep
   a phone-readable minimum (>=16px on inputs so iOS never zooms on focus). */
.bw-rsvp{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#e2d3b8;
}
.bw-rsvp-scene{
  --s:.2;
  position:absolute;
  top:0;
  left:0;
  width:1882px;
  height:4542px;
  transform-origin:top left;
}
.bw-rsvp .bw-l{ animation-play-state:paused; }
.bw-rsvp .bw-i-sway-y{
  --i-x: 12px; --i-y: 0px;
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
  animation-name: bwGrowY, bwPasDrift;
}
.bw-rsvp-form{ position:absolute; }
.bw-rsvp-form [hidden]{ display:none !important; }
.rs-field{ margin:0 0 76px; }
.rs-label{
  display:block;
  height:60px;
  margin:0 0 35px -3px;
  font:700 max(52px, calc(13px / var(--s)))/60px 'Cormorant Garamond','Cochin',serif;
  letter-spacing:.06em;
  color:#701b22;
  white-space:nowrap;
}
.rs-ctl{
  display:block;
  width:100%;
  box-sizing:border-box;
  margin:0;
  border:2px solid #d9caae;
  border-radius:30px;
  background-color:#fff;
  color:#111;
  font:400 max(66px, calc(16px / var(--s)))/1.25 'EB Garamond','Cochin',serif;
  padding:0 58px;
  outline:0;
  -webkit-appearance:none;
  appearance:none;
  box-shadow:none;
}
.rs-ctl::placeholder{ color:#757575; opacity:1; }
.rs-ctl:focus{ border-color:#701b22; }
input.rs-ctl{ height:196px; }
.rs-select{
  height:204px;
  padding-right:120px;
  cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 20'%3E%3Cpath d='M3 3l14 14L31 3' fill='none' stroke='%23111' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 10px center;
  background-size:34px 20px;
}
.rs-select:invalid{ color:#111; }
.rs-area{ height:340px; padding-top:56px; padding-bottom:40px; resize:none; overflow:auto; line-height:1.35; }
.rs-submit-wrap{ position:relative; margin:107px -4px 0 -4px; }
.rs-submit{
  display:block;
  width:100%;
  height:192px;
  border:0;
  border-radius:30px;
  background:#701b22;
  color:#fff;
  font:600 max(54px, calc(15px / var(--s)))/1 'Cormorant Garamond','Cochin',serif;
  letter-spacing:.22em;
  text-indent:.22em;
  text-transform:uppercase;
  cursor:pointer;
  -webkit-appearance:none;
  appearance:none;
}
.rs-submit:disabled{ opacity:.72; cursor:default; }
.rs-status{
  position:absolute;
  left:0; right:0;
  top:calc(100% + 24px);
  margin:0;
  text-align:center;
  font:600 max(46px, calc(13px / var(--s)))/1.2 'Quicksand',sans-serif;
  color:#701b22;
}
.rs-status.is-ok{ color:#154800; }
.rs-cards{ margin-top:137px; width:1718px; }
.rs-cards:empty{ display:none; }
.rs-card{
  box-sizing:border-box;
  height:316px;
  margin:0 0 58px;
  padding:0 64px;
  display:flex;
  flex-direction:column;
  justify-content:center;
  border:6px solid #e4d4a4;
  border-radius:30px;
  background:#fff;
  overflow:hidden;
}
.rs-card-name{
  font:italic 600 max(68px, calc(14px / var(--s)))/1.15 'Cormorant Garamond','Cochin',serif;
  color:#701b22;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
.rs-card-msg{
  margin:34px 0 0;
  font:400 max(59px, calc(13px / var(--s)))/1.3 'EB Garamond','Cochin',serif;
  color:#3a2d25;
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

/* ================= Hadiah ================= (additive only). Scene is the PSD's
   native 1882x3693. Same replay system as the other sections; the card art
   is a shared PNG with the baked text removed, all card content is live.
   --s (scene scale, set by script.js) keeps every text a phone-readable
   minimum and makes the SALIN tap area comfortable (>=44px). */
.bw-hadiah{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#c38588;
}
.bw-hadiah-scene{
  --s:.2;
  position:absolute;
  top:0;
  left:0;
  width:1882px;
  height:3693px;
  transform-origin:top left;
}
.bw-hadiah .bw-l{ animation-play-state:paused; }
.bw-hadiah .bw-i-sway-y{
  --i-x: 10px; --i-y: 0px;
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
  animation-name: bwGrowY, bwPasDrift;
}
.hd-card{ font-family:'Poppins',sans-serif; color:#2a0f06; }
/* Spacing fix (owner phone review): the card content is a padded flex column
   of three rows -- [bank | logo], [label+number | SALIN], [label+holder |
   label+date] -- instead of fixed x/y boxes. Normal data keeps the PSD
   rhythm (rows spread over the 822px card); long data wraps and the card
   grows (min-height = the PSD card), never overlapping. Typography sizes
   are unchanged. Scoped to .hd-* (Hadiah only). */
.bw-hadiah .hd-card{ height:auto !important; min-height:822px; }
.hd-card-bg{ position:absolute; left:0; top:0; width:1205px; height:100%; min-height:822px; display:block; }
.hd-body{ position:relative; box-sizing:border-box; min-height:822px; padding:104px 92px 96px 92px; display:flex; flex-direction:column; justify-content:space-between; gap:64px; white-space:normal; }
.hd-row{ display:flex; gap:64px; }
.hd-row-head{ align-items:center; justify-content:space-between; }
.hd-row-num{ align-items:center; justify-content:space-between; }
.hd-row-meta{ align-items:flex-start; justify-content:space-between; }
.hd-col{ display:flex; flex-direction:column; gap:30px; flex:1 1 0; min-width:0; }
.hd-col-r{ flex:0 0 auto; max-width:46%; align-items:flex-end; text-align:right; }
.hd-title{ flex:1 1 0; min-width:0; overflow-wrap:break-word; font:600 max(53px, calc(13px / var(--s)))/1.15 'Poppins',sans-serif; }
.hd-label{ font:400 max(40.5px, calc(11px / var(--s)))/1.1 'Poppins',sans-serif; }
.hd-card .hd-num{ overflow-wrap:anywhere; font:600 max(50px, calc(15px / var(--s)))/1.15 'Poppins',sans-serif; letter-spacing:12.5px; word-spacing:20px; }
.hd-card .hd-holder{ overflow-wrap:break-word; font:500 max(46.7px, calc(13px / var(--s)))/1.2 'Poppins',sans-serif; }
.hd-date{ white-space:nowrap; font:500 max(46.5px, calc(12px / var(--s)))/1.2 'Poppins',sans-serif; }
.hd-logo{ flex:0 0 auto; max-width:40%; min-height:86px; display:flex; align-items:center; justify-content:flex-end; }
.hd-logo img{ display:block; max-width:294px; width:auto; max-height:86px; object-fit:contain; }
.hd-logo-t{ font:600 max(52px, calc(14px / var(--s)))/1.1 'Poppins',sans-serif; color:#1f2f6b; text-transform:uppercase; letter-spacing:.02em; text-align:right; }
.hd-copy{
  position:relative; flex:0 0 auto;
  width:309px; height:103px;
  display:flex; align-items:center; justify-content:center; gap:26px;
  border:3px solid #806a36;
  border-radius:52px;
  background:#997e43;
  color:#fff;
  font:600 max(38px, calc(11px / var(--s)))/1 'Poppins',sans-serif;
  letter-spacing:.02em;
  cursor:pointer;
  padding:0;
  -webkit-appearance:none;
  appearance:none;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
}
.hd-copy svg{ width:34px; height:40px; flex:0 0 auto; }
.hd-copy::after{ content:""; position:absolute; inset:calc(-12px / var(--s)) calc(-8px / var(--s)); }
.hd-copy:focus-visible{ outline:3px solid #fff; outline-offset:4px; }
.hd-copy.is-done{ background:#6f8a3a; border-color:#5b7330; }
.hd-copy-t{ white-space:nowrap; }
.hd-card[hidden], .hd-card [hidden]{ display:none !important; }

/* ================= Live Streaming ================= (additive only). Same
   replay system as the other sections; the CTA is a real link (new tab)
   and, like the Hadiah copy button, is never moved by an idle animation. */
.bw-live{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#c8a9b5;
}
.bw-live-scene{
  position:absolute;
  top:0;
  left:0;
  width:1080px;
  height:1480px;
  transform-origin:top left;
}
.bw-live .bw-l{ animation-play-state:paused; }
.bw-live .bw-i-sway-y, .bw-live .bw-i-sway-x, .bw-live .bw-i-sway-xy{
  --i-x: 12px; --i-y: 0px;
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
}
.bw-live .bw-i-sway-y{ animation-name: bwGrowY, bwPasDrift; }
.bw-live .bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDrift; }
.bw-live .bw-i-in.bw-i-sway-xy{ animation-name: bwGrowXY, bwPasDriftIn; }
.lv-btn{
  box-sizing:border-box;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:34px;
  border:2px solid #e0cfa3;
  border-radius:58px;
  background:#fff;
  color:#5d0005;
  text-decoration:none;
  -webkit-tap-highlight-color:transparent;
}
.lv-btn:focus-visible{ outline:4px solid #5d0005; outline-offset:6px; }
.lv-btn::after{ content:""; position:absolute; inset:-30px -20px; }
.lv-ico{ flex:0 0 auto; }
.lv-btn-t{ font:700 max(35px, calc(13px / var(--lv-s, .3))) 'Playfair Display','Didot',serif; white-space:nowrap; letter-spacing:.02em; }

/* ================= Video ================= (additive only). Decorations/titles
   use the shared replay system; the PLAYER FRAME is never transformed,
   animated or replayed (it only fades in once) so it stays put while the
   guest watches. Nothing but a small poster is loaded until PLAY is tapped. */
.bw-video{
  position:relative;
  width:100%;
  overflow:hidden;
  background:#333f2a;
}
.bw-video-scene{
  --s:.2;
  position:absolute;
  top:0;
  left:0;
  width:1882px;
  height:2658px;
  transform-origin:top left;
}
.bw-video .bw-l{ animation-play-state:paused; }
.bw-video .bw-i-sway-y{
  --i-x: 12px; --i-y: 0px;
  animation-duration: var(--e-dur, 700ms), calc(var(--i-dur, 8000ms) * 1.8);
  animation-iteration-count: 1, infinite;
  animation-direction: normal, normal;
  animation-name: bwGrowY, bwPasDrift;
}
.vd-frame{ position:absolute; background:#000; overflow:hidden; }
.vd-poster{
  position:absolute; inset:0; width:100%; height:100%;
  margin:0; padding:0; border:0; background:#000; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  -webkit-appearance:none; appearance:none; -webkit-tap-highlight-color:transparent;
}
.vd-poster:focus-visible{ outline:6px solid #dfbf7b; outline-offset:-10px; }
.vd-thumb{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; }
.vd-thumb[hidden]{ display:none; }
.vd-btn{
  position:relative;
  display:flex; align-items:center; justify-content:center;
  width:210px; height:210px; border-radius:50%;
  background:rgba(150,140,126,.88);
  box-shadow:0 0 0 10px rgba(255,255,255,.12);
}
.vd-btn svg{ width:118px; height:118px; margin-left:8px; }
.vd-poster:active .vd-btn{ background:rgba(120,110,98,.92); }
.vd-player{ position:absolute; border:0; background:#000; display:block; }
iframe.vd-player{ left:0; top:178px; width:100%; height:1056px; }
video.vd-player{ left:0; top:0; width:100%; height:100%; object-fit:contain; }

/* ================= Closing ================= (additive only). Purple -> the
   Betawi dusty pink #9d5562 (Galeri/Hadiah rose). Shared replay system; the
   music button and links are plain controls that stay put. --s (scene scale,
   set by script.js) keeps text phone-readable. */
.bw-closing{
  position:relative;
  width:100%;
  overflow:hidden;
  background:var(--ua-theme, #9d5562);
}
.bw-closing-scene{
  --s:.2;
  position:absolute;
  top:0;
  left:0;
  width:1882px;
  height:3971px;
  transform-origin:top left;
}
.bw-closing .bw-l{ animation-play-state:paused; }
.cl-photo{ overflow:hidden; background:transparent; }
.cl-slides{ position:absolute; inset:0; }
.cl-slide{ position:absolute; inset:0; background-size:cover; background-position:50% 12%; opacity:0; transition:opacity 1.4s ease; }
.cl-slide.is-active{ opacity:1; }
/* the PSD's purple veil, rebuilt in Betawi pink: transparent at the top,
   solid panel colour by ~3/4 of the photo (ramp measured from the PSD) */
.cl-veil{
  position:absolute; inset:0;
  background:linear-gradient(to bottom,
    rgba(var(--ua-theme-rgb, 157,85,98),0) 0%, rgba(var(--ua-theme-rgb, 157,85,98),.13) 9%, rgba(var(--ua-theme-rgb, 157,85,98),.23) 18%,
    rgba(var(--ua-theme-rgb, 157,85,98),.33) 27%, rgba(var(--ua-theme-rgb, 157,85,98),.43) 36%, rgba(var(--ua-theme-rgb, 157,85,98),.53) 45%,
    rgba(var(--ua-theme-rgb, 157,85,98),.68) 55%, rgba(var(--ua-theme-rgb, 157,85,98),.77) 64%, rgba(var(--ua-theme-rgb, 157,85,98),.93) 73%,
    rgb(var(--ua-theme-rgb, 157,85,98)) 78%, rgb(var(--ua-theme-rgb, 157,85,98)) 100%);
}
.cl-body{ position:absolute; left:0; top:0; width:1882px; height:3971px; }
.cl-text{
  position:absolute; left:0; top:2604px; width:1868px;
  display:flex; flex-direction:column; align-items:center; text-align:center; color:var(--ua-theme-on, #fff);
}
.cl-text > .bw-l{ position:relative; margin:0; }
.cl-kata{
  max-width:1160px; padding:0 20px; box-sizing:border-box;
  font:400 max(62px, calc(13.5px / var(--s)))/1.79 Arial,'Liberation Sans',Roboto,'Helvetica Neue',sans-serif;
}
.cl-blessed{
  margin-top:86px !important;
  font:400 max(52px, calc(13px / var(--s)))/1.3 Arial,'Liberation Sans',Roboto,'Helvetica Neue',sans-serif;
  color:var(--ua-theme-on, #fff5d4);
}
.cl-names{
  margin-top:39px !important; max-width:1700px; padding:0 20px; box-sizing:border-box;
  font:italic 600 max(120px, calc(22px / var(--s)))/1.15 'Cormorant Garamond','Cochin',serif;
  letter-spacing:.06em;
  color:var(--ua-theme-on, #f4eee6);
}
.cl-by{
  display:flex; align-items:center; justify-content:center; text-align:center; box-sizing:border-box;
  font:500 max(44px, calc(12px / var(--s)))/1 'Cormorant Garamond','Cochin',serif;
  letter-spacing:.12em; padding-left:.12em; color:var(--ua-theme-on, #f0dfd0);
}
.cl-logo, .cl-soc-a{ display:block; }
.cl-logo img{ display:block; width:100%; height:100%; }
.cl-soc{ pointer-events:none; }
.cl-soc-a{ position:absolute; width:136px; height:136px; border-radius:50%; }
.cl-soc-a::after, .cl-logo::after{ content:""; position:absolute; inset:calc(-10px / var(--s)); border-radius:50%; }
.cl-music{
  padding:0; border:0; background:transparent; cursor:pointer; border-radius:50%;
  -webkit-appearance:none; appearance:none; -webkit-tap-highlight-color:transparent;
}
.cl-music img{ display:block; width:100%; height:100%; opacity:.72; transition:opacity .3s ease; }
.cl-music.is-on img{ opacity:1; animation:bwClSpin 9s linear infinite; }
.cl-music:focus-visible{ outline:4px solid #fff; outline-offset:6px; }
@keyframes bwClSpin{ to{ transform:rotate(360deg); } }
.bw-closing .cl-music::after{ content:""; position:absolute; inset:calc(-6px / var(--s)); border-radius:50%; }
@media (prefers-reduced-motion: reduce){ .cl-music.is-on img{ animation:none; } .cl-slide{ transition:none; } }

/* ================= Adat Jawa motion parity (Ayat -> Closing) =================
   MOTION ONLY: no position, size, type or colour is touched. Reproduces the
   ua-master reveal vocabulary on the existing Web-Animations replay:
     .rv   up  32px      .rv-l from -44px X     .rv-r from +44px X
     .rv-z scale .86 (.95s)   ease cubic-bezier(.22,.61,.36,1)  .85s
   Distances are CSS px on the phone: --bwk (scene px per CSS px, set by
   script.js per section) converts them, because the scene is scaled as one
   unit. translate/scale/opacity only; nothing is promoted permanently. */
.bw-jm{ --bwk:3; --jw-ease:cubic-bezier(.22,.61,.36,1); }
@keyframes bwJwUp{
  from{ opacity:0; translate: calc(var(--rv-x,0px) * var(--bwk)) calc(var(--rv-y,32px) * var(--bwk)); }
  to{ opacity:1; translate: 0 0; }
}
@keyframes bwJwZ{
  from{ opacity:0; scale:.86; }
  to{ opacity:1; scale:1; }
}
@keyframes bwJwAyatPhoto{
  from{ opacity:0; translate: 0 calc(-80px * var(--bwk)); }
  to{ opacity:1; translate: 0 0; }
}
.bw-jm .bw-m-up{
  animation-name: bwJwUp; animation-duration:.85s; animation-timing-function: var(--jw-ease);
}
.bw-jm .bw-m-scale, .bw-jm .bw-m-pop{
  animation-name: bwJwZ; animation-duration:.95s; animation-timing-function: var(--jw-ease);
}
/* gallery rows = Jawa cards sliding in from the side (.rv-l / .rv-r) */
.bw-jm .bw-gal-row.bw-m-fromLeft{ animation-name: bwJwUp; animation-duration:.85s; animation-timing-function: var(--jw-ease); --rv-x:-44px; --rv-y:0px; }
.bw-jm .bw-gal-row.bw-m-fromRight{ animation-name: bwJwUp; animation-duration:.85s; animation-timing-function: var(--jw-ease); --rv-x:44px; --rv-y:0px; }
/* Jawa's decoration entrance curve (grow-in overshoot) for every asset that
   grows or slides in; idle drift is unchanged. */
.bw-jm .bw-m-growY, .bw-jm .bw-m-growX, .bw-jm .bw-m-growXY,
.bw-jm .bw-m-fromLeft:not(.bw-gal-row), .bw-jm .bw-m-fromRight:not(.bw-gal-row){
  animation-timing-function: cubic-bezier(.25,.85,.3,1.15);
}
.bw-jm .bw-i-sway-y, .bw-jm .bw-i-sway-x, .bw-jm .bw-i-sway-xy{
  animation-timing-function: cubic-bezier(.25,.85,.3,1.15), ease-in-out;
}
/* Ayat (Jawa .ayat-photo-anim / .ayat-content-anim: photo drops in 80px,
   content rises 60px, 1s ease, content .25s later) */
.bw-ayat .bw-photo.bw-m-scale{ animation-name: bwJwAyatPhoto; animation-duration:1s; animation-timing-function: ease; }
.bw-ayat .bw-text.bw-m-up{ --rv-y:60px; animation-duration:1s; animation-timing-function: ease; }
/* one-shot reveals for blocks that are not .bw-l (form, video, wish cards):
   driven by window.bwRv in script.js with el.animate(); nothing persists. */

@media (prefers-reduced-motion: reduce){
  .bw-l{ animation: none !important; opacity:1 !important; translate:none !important; scale:none !important; rotate:none !important; }
  .bw-l[style*="--e-op"]{ opacity: var(--e-op, 1) !important; }
  .bw-app.is-open .bw-cover{ transition:none; }
  * { scroll-behavior: auto !important; }
}
