/* hanny-and-andra.css — base styles: reset, tokens, sections, nav dots, form,
   gallery. Colour comes from palettes.css via the data-palette tokens. */

/* Type. Both families are served from this repo, not from Google. The page used to
   pull them from fonts.googleapis.com at runtime; when that request does not land the
   display script drops to the next family in --font-display — Comic Sans MS, via the
   generic `cursive` on Windows — and a guest's first impression is the wrong one.
   These are Google's own latin and latin-ext woff2 files, committed under
   /static/fonts/, so the page makes no third-party font request at all.

   One file per family, style and subset: Cormorant Garamond ships as a variable font,
   so a single file covers every weight the page uses. unicode-range keeps latin-ext
   optional — a browser only downloads it if a character in that range is drawn. */
@font-face {
  font-family: "Pinyon Script";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/pinyon-script-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Pinyon Script";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/pinyon-script-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Cormorant Garamond, upright. Google serves one variable file for 400, 500 and 600,
   which are the weights this page uses (400 body, 500 body, 600 labels). */
@font-face {
  font-family: "Cormorant Garamond";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/cormorant-garamond-roman-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Cormorant Garamond";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/cormorant-garamond-roman-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Cormorant Garamond, italic — the Ar-Rum verse — 400 to 600 from the variable file. */
@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/cormorant-garamond-italic-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Cormorant Garamond";
  font-style: italic;
  font-weight: 400 600;
  font-display: swap;
  src: url("/static/fonts/cormorant-garamond-italic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Minimal preflight. This replaces the reset the Tailwind browser build was
   supplying implicitly. Every rule here is one the page was quietly relying on:
   without them, UA defaults come back and the layout shifts — the root
   line-height, element margins/paddings, the fieldset/legend box, heading
   weight, and link colour and underline. Limited to what can match this markup. */

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  border: 0 solid;
}

html {
  /* The page's baseline line-height used to come from Tailwind's html rule. */
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: inherit;
  font-weight: inherit;
}

a {
  color: inherit;
  text-decoration: inherit;
}

ol,
ul,
menu {
  list-style: none;
}

img,
svg,
video,
canvas {
  display: block;
  vertical-align: middle;
}

img,
video {
  max-width: 100%;
  height: auto;
}

button,
input,
select,
optgroup,
textarea {
  font: inherit;
  color: inherit;
  background-color: transparent;
  border-radius: 0;
  opacity: 1;
}

/* The document is the scroller, so scroll-linked libraries (AOS) see real scroll. */
html {
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
  overflow-x: hidden;
  /* Nothing scrolls sideways, so horizontal overscroll buys nothing — and containing it
     is what some Chrome versions check before handing a drag near the left edge to the
     back gesture. */
  overscroll-behavior-x: contain;
}

:root {
  --bg: #0f1418;
  --ink: #f5efe6;
  --muted: #c3b8a6;
  /* The third text tone: brighter than --muted and a step under --accent. The cover's
     guest line and the countdown's city use it. palettes.css sets one per palette; this
     is the fallback for a page rendered without a palette. */
  --ink-soft: #d6b97a;
  --accent: #c9a24b;
  --panel: rgba(15, 20, 24, 0.55);
  --keyline: rgba(201, 162, 75, 0.45);
  --radius: 18px;
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 2rem;
  --space-4: 4rem;
  --slide-pad: clamp(1.25rem, 4vw, 4rem);
  /* A script stack in this order on purpose: the generic `cursive` is Comic Sans MS on
     Windows, so it is the last resort rather than the first fallback. */
  --font-display: "Pinyon Script", "Segoe Script", "Brush Script MT", "Snell Roundhand", "Apple Chancery", cursive;
  --font-body: "Cormorant Garamond", Georgia, "Times New Roman", serif;
}

body {
  font-family: var(--font-body);
  font-size: clamp(1.05rem, 0.5vw + 1rem, 1.2rem);
  font-weight: 500;
  color: var(--ink);
  background: var(--bg);
}

.deck {
  position: relative;
  z-index: 1;
}

.slide {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 100vh is the LARGE viewport on a phone — the height with the browser toolbar
     hidden — so a snapped slide runs past the bottom of the screen. Whatever a
     slide pins to its bottom edge (the thank-you line) then needs a second swipe
     to a section that is not there, and the snap pulls the page back. 100svh is
     the small viewport, so a slide always fits what the phone is showing. */
  min-height: 100vh; /* fallback: no svh support */
  min-height: 100svh;
  padding: var(--slide-pad);
  background: var(--bg);
  scroll-snap-align: start;
}

/* Opaque by default so only the sections named below let the fixed scene-1
   backdrop (.hero__bg) show through. */
.slide--hero,
.slide--pria,
.slide--wanita,
.slide--thanks {
  background: transparent;
}

.slide__inner {
  width: 100%;
  max-width: 900px;
  text-align: center;
}

.kicker {
  margin: 0 0 var(--space-2);
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.35em;
  text-transform: uppercase;
}

/* Section nav */
.dots {
  position: fixed;
  top: 50%;
  right: clamp(0.75rem, 2vw, 1.5rem);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 0;
  transform: translateY(-50%);
  /* The pager is chrome over the content, not content. Half opacity keeps it
     usable without pulling the eye away from the section being read. */
  opacity: 0.5;
}

.dots button {
  width: 12px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.dots button::before {
  content: "";
  display: block;
  width: 10px;
  height: 10px;
  margin: 0 auto;
  border: 1px solid var(--muted);
  border-radius: 50%;
  transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
}

.dots button.is-active::before {
  background: var(--accent);
  border-color: var(--accent);
  transform: scale(1.4);
}

/* Hero */

.hero {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: space-between;
  gap: var(--space-2);
  /* Inset from the slide edges: lifts the bottom line up and keeps the title
     clear of the top and side edges. */
  padding: var(--space-2) var(--space-2) var(--space-3);
  /* svh, not vh: a phone's 100vh is taller than its screen. See .slide. */
  min-height: calc(100vh - 2 * var(--slide-pad));
  min-height: calc(100svh - 2 * var(--slide-pad));
}

.hero__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.15;
  text-align: left;
  /* Base the two lines scale from: "Wedding" x1.25, "Invitation" x2.
     "Invitation" is 3.35em wide, so 2x is 6.7em: 9.6vw + a 1.8rem floor keeps
     it inside the slide padding even on a ~280px-wide screen. These values are
     exactly 0.8x of 2.25rem / 12vw / 6rem. */
  --title-size: clamp(1.8rem, 9.6vw, 4.8rem);
}

.hero__title-sm {
  font-size: calc(var(--title-size) * 1.25);
}

.hero__title-lg {
  font-size: calc(var(--title-size) * 2);
}

/* The cover's top-right corner holds two lines: the couple's names, and under them the
   guest the link was addressed to. One column, so a long guest name can never land on the
   names' line. */
.hero__top {
  position: absolute;
  top: calc(var(--slide-pad) + var(--space-2));
  right: calc(var(--slide-pad) + var(--space-2));
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.2rem;
  max-width: min(70%, 22rem);
  text-align: right;
}

.hero__sign {
  margin: 0;
  color: var(--ink);
  /* A step up from the 0.95rem the couple's names were set in: the corner is a signature,
     and it reads across a phone on a photo. The corner's max-width is unchanged, so the
     line still holds one line at 320px. */
  font-size: 1.2rem;
  letter-spacing: 0.18em;
}

/* The addressee from the link: the guest's own name, so the soft-ink tone — brighter than
   the muted body colour, a step under the accent the couple's names above it carry — set
   large enough to read at arm's length. It wraps inside the corner rather than pushing the
   block wider. */
.hero__guest {
  margin: 0;
  color: var(--ink-soft);
  font-size: 1.05rem;
  line-height: 1.5;
  letter-spacing: 0.04em;
  overflow-wrap: anywhere;
}

.hero__foot {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
}

/* One row per day, spanning the hero: the date hard against the left edge, the city
   against the right. Normal case and lighter tracking keep the pair a detail rather
   than a headline. */
.hero__date {
  display: flex;
  flex-wrap: wrap;
  margin: 0;
  color: var(--muted);
  font-size: 0.85rem;
  letter-spacing: 0.08em;
}

/* margin-left: auto holds the city against the right edge even when it wraps onto its
   own row; the padding is the gap between the two halves. */
.hero__date-city {
  margin-left: auto;
  padding-left: var(--space-1);
}

.hero__invite {
  margin: 0;
  color: var(--muted);
  font-size: calc(1.05rem * 0.75);
  line-height: 1.7;
  text-align: justify;
  text-justify: inter-word;
}

/* Hero background slideshow */
#hero {
  overflow: hidden;
}

#hero .slide__inner {
  position: relative;
  z-index: 1;
}

/* Hero background slideshow — one fixed layer pinned behind the whole page, so
   the hero, both mempelai slides and the thank-you slide share the same photo.
   #hero is position:relative with z-index:auto, so it creates no stacking
   context and this fixed layer joins .deck's (z-index 1) instead. */
.hero__bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
}

.hero__bg-img {
  position: absolute;
  inset: 0;
  opacity: 0;
  background-position: center 30%;
  background-repeat: no-repeat;
  background-size: cover;
  /* 0.84s is 70% of the original 1.2s: the crossfade scales with the cycle. */
  transition: opacity 0.84s ease;
}

.hero__bg-img.is-active {
  opacity: 1;
}

/* Film artifacts CamanJS cannot do: light leaks, grain, a vignette and edge
   aberration. All overlays — no per-pixel pass, no JPEG re-encode. */
.film-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.film {
  position: absolute;
  inset: 0;
  /* Above the ::after scrim (which is positioned with z-index auto), because
     grain belongs over a darkened frame — under it the dark areas eat it. Still
     inside .hero__bg, which is z-index 0, so all page text stays above and crisp. */
  z-index: 2;
  pointer-events: none;
}

/* Light entering the camera: screen-blended so it blooms instead of washing out. */
.film__leak {
  position: absolute;
  inset: 0;
  background-image:
    radial-gradient(58% 40% at 100% 2%, rgba(255, 178, 98, 0.46), rgba(255, 150, 80, 0) 62%),
    radial-gradient(52% 38% at 0% 100%, rgba(255, 214, 146, 0.3), rgba(255, 190, 120, 0) 66%);
  mix-blend-mode: screen;
}

/* A tiled feTurbulence tile: cheaper and more even than a per-pixel pass, and it
   never has to survive a JPEG re-encode. stitchTiles hides the tile seam. */
.film__grain {
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  mix-blend-mode: overlay;
  opacity: 0.075;
}

/* Matte finish: clear across the centre, darkening only the extreme corners. */
.film__matte {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(
    122% 92% at 50% 45%,
    rgba(0, 0, 0, 0) 54%,
    rgba(26, 15, 8, 0.42) 100%
  );
}

/* Edge aberration, on the ACTIVE slide only, so one layer is filtered rather than
   nine. This is the one item here with a real performance question on a phone: to
   drop it, delete this single rule. */
.hero__bg-img.is-active {
  filter: url("#film-ca");
}

/* Ar-Rum */

.panel {
  max-width: 720px;
  margin: 0 auto;
  padding: var(--space-3);
  /* No keyline: the verse sits on the tint alone. */
  border-radius: var(--radius);
  background: var(--panel);
  backdrop-filter: blur(6px);
}

.ayat__text {
  margin: 0 0 var(--space-2);
  font-size: clamp(1rem, 2.4vw, 1.4rem);
  font-style: italic;
  line-height: 1.7;
}

.ayat__source {
  margin: 0;
  color: var(--accent);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* Mempelai */


/* The photo behind these two slides is blurred and darkened, so the card — not the
   backdrop — is what reads. A later slide paints after the earlier one that holds
   .hero__bg, so this overlay lands on top of the photo; .slide__inner is lifted to
   z-index 1 so the overlay stays under the kicker and the card. */
.slide--pria::before,
.slide--wanita::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(6, 8, 11, 0.5);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.slide--pria .slide__inner,
.slide--wanita .slide__inner {
  position: relative;
  z-index: 1;
}

/* The card is the photo: the image fills it, the names sit on its darker bottom edge,
   and the nickname runs down its right side. */
.mempelai {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-1);
  width: min(100%, 420px);
  /* 3:4 keeps the card inside one phone screen; the sources are narrower still
     (900x1600 and 1066x1600), so cover fills it and crops a little of the height. */
  aspect-ratio: 3 / 4;
  max-height: 70svh;
  margin: 0 auto;
  padding: var(--space-3);
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  background: var(--bg);
  overflow: hidden;
  /* Lifted off the blurred backdrop. */
  box-shadow: 0 1.1rem 2.4rem rgba(0, 0, 0, 0.5);
  transform-style: preserve-3d;
}

/* The card's own colour still sits under the photo, so a photo that has not landed yet
   shows the slide colour rather than the backdrop. */
.slide--pria .mempelai,
.slide--wanita .mempelai {
  background: var(--bg);
}

/* The photo is the card's surface, not a thumbnail inside it. */
.mempelai__photo {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.mempelai__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A vignette, not a veil: the middle of the frame stays as the photo was and only the
   border and the corners darken. Todo 059 veiled the whole photo so the vertical nickname
   had something behind it, which dimmed the couple with it; a `circle` is sized from the
   centre to the farthest corner, so the corner the nickname runs down is still the darkest
   stop while the centre is untouched, and the nickname's own text shadow carries it. The
   overlay is a positioned ::after inside the photo wrapper, so it paints over the image
   without touching the pixels the grade works on, and the names' band (.mempelai::after)
   still sits above it. */
.mempelai__photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(
    circle at 50% 50%,
    rgba(4, 6, 9, 0) 40%,
    rgba(4, 6, 9, 0.34) 72%,
    rgba(4, 6, 9, 0.82) 100%
  );
  pointer-events: none;
}

/* The names' own band: the lower part of the card is darkened so the type never has to
   fight the photo. ::after is painted after the photo, so the text is lifted above it.
   It stops at 42% of the card and is 0 well before its top edge: it covers the type block
   (name, nickname, lineage) and leaves the middle of the frame to the vignette above. */
.mempelai::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  background: linear-gradient(
    to top,
    rgba(4, 6, 9, 0.92) 8%,
    rgba(4, 6, 9, 0.58) 48%,
    rgba(4, 6, 9, 0) 100%
  );
  pointer-events: none;
}

.mempelai__name,
.mempelai__profile,
.mempelai__parents {
  position: relative;
  z-index: 1;
  /* The band above the type is shorter and lighter than it was, so the type carries its
     own shadow: a bright frame cannot wash the names out, the same way the nickname in
     the corner has one. */
  text-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.6);
}

.mempelai__name {
  margin: 0;
  font-family: var(--font-display);
  /* Exactly 70% of the original clamp(1.75rem, 4.5vw, 2.5rem): the full name with
     its ", S.T." overran the card. Only this face moves. */
  font-size: clamp(1.225rem, 3.15vw, 1.75rem);
  font-weight: 400;
  line-height: 1.25;
}

/* Down the card's right edge, in the top corner: a label, not part of the name block, so
   the names own the bottom band. vertical-rl turns it 90° without a transform, which
   keeps it inside the card. */
.mempelai__nick {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 2;
  margin: 0;
  color: var(--accent);
  font-size: 0.8rem;
  letter-spacing: 0.25em;
  text-transform: uppercase;
  writing-mode: vertical-rl;
  text-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.75);
}

.mempelai__parents {
  margin: 0;
  color: var(--muted);
  font-size: 0.9rem;
}

/* The lineage runs as two lines: the order on its own, then the parents' names under it.
   A guest reading the card wants the names, so they are the brighter of the two and the
   order stays a muted lead-in. Both spans are blocks, so no <br> is added to the markup. */
.mempelai__parents-order,
.mempelai__parents-names {
  display: block;
}

.mempelai__parents-names {
  margin-top: 0.15rem;
  color: var(--ink);
}

/* Tanggal Acara */

.section-title {
  margin: 0 0 var(--space-3);
  padding-bottom: 0.08em;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(2rem, 6vw, 3.25rem);
  font-weight: 400;
  line-height: 1.25;
}

.events {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: var(--space-3);
}

.event {
  padding: var(--space-3);
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  background: var(--panel);
  text-align: left;
}

.event__city {
  margin: 0 0 var(--space-1);
  color: var(--accent);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
}

.event__title {
  margin: 0 0 var(--space-2);
  font-size: 1.15rem;
}

.event__line {
  margin: 0 0 0.25rem;
  font-size: 0.95rem;
  overflow-wrap: anywhere;
}

.event__muted {
  color: var(--muted);
}

.event__map {
  display: inline-block;
  margin-top: var(--space-2);
  color: var(--accent);
  font-size: 0.9rem;
  text-decoration: none;
  border-bottom: 1px solid var(--keyline);
}

/* A day with parts is a list, and reads as one: the preflight strips list markers from
   every ul, so the akad and the resepsi used to look like two more lines of the card. The
   marker is drawn rather than left to the browser's disc, so its size and colour are the
   page's. Only a day with `sessions` has it — a single-`time` day keeps a plain line. */
.event__sessions {
  display: grid;
  gap: 0.15rem;
  margin: 0 0 0.25rem;
}

.event__sessions .event__line {
  position: relative;
  margin: 0;
  padding-left: 1.05rem;
}

.event__sessions .event__line::before {
  content: "";
  position: absolute;
  top: 0.5em;
  left: 0.1rem;
  width: 0.32rem;
  height: 0.32rem;
  border-radius: 50%;
  background: var(--accent);
}

/* Countdown — the nearest day, and the four numbers to it. */

.countdown__meta {
  margin: 0 0 var(--space-3);
  color: var(--muted);
  font-size: 0.95rem;
}

/* The city is the part of the line a guest scans for, so it is set a step larger than the
   date beside it and in the soft-ink tone: a town name has to read at a glance, the date
   can be read carefully. The script rebuilds this span on a day's rollover. */
.countdown__city {
  color: var(--ink-soft);
  font-size: 1.2em;
  font-weight: 600;
}

/* Four tiles, and never wider than the phone: minmax(0, 1fr) lets a tile shrink below
   its content instead of pushing the row past the edge. */
.countdown {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-2);
  max-width: 640px;
  margin: 0 auto;
}

.countdown__tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: var(--space-2) 0.4rem;
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  background: var(--panel);
}

/* Tabular figures keep a tile from twitching as the seconds turn over. */
.countdown__value {
  color: var(--ink);
  font-size: clamp(1.35rem, 6vw, 2.1rem);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.countdown__label {
  color: var(--accent);
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.countdown__done {
  margin: var(--space-3) 0 0;
  color: var(--accent);
}

/* Konfirmasi Kehadiran */

.rsvp {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 520px;
  margin: 0 auto;
  padding: var(--space-3);
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  background: var(--panel);
  text-align: left;
}

.rsvp__field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.rsvp__label {
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.rsvp__input {
  width: 100%;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--keyline);
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.25);
  color: var(--ink);
  font: inherit;
}

.rsvp__radios {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.rsvp__radios legend {
  margin-bottom: 0.35rem;
}

.rsvp__radio {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.95rem;
}

.rsvp__submit {
  align-self: flex-start;
  min-height: 44px;
  padding: 0.6rem 1.4rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--accent);
  color: #10161a;
  font: inherit;
  cursor: pointer;
}

.rsvp__status {
  min-height: 1.2em;
  margin: 0;
  color: var(--accent);
  font-size: 0.9rem;
}

.rsvp__status.is-error {
  color: #e5857a;
}

/* List Ucapan */

.ucapan__count {
  margin: 0 0 var(--space-2);
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.28em;
  text-transform: uppercase;
}

/* The list scrolls inside the slide, so the section never grows past one screen. */
.ucapan {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-height: 60vh;
  margin: 0 auto;
  padding: 0 0.5rem 0 0;
  overflow-y: auto;
  list-style: none;
  text-align: left;
  scrollbar-width: thin;
  scrollbar-color: var(--keyline) transparent;
  -webkit-overflow-scrolling: touch;
}

/* Set by the script only while the list is long enough to scroll: one card must not have
   its last line dimmed by a fade that has nothing to hide. */
.ucapan.is-scrollable {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent);
}

.ucapan::-webkit-scrollbar {
  width: 4px;
}

.ucapan::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: var(--keyline);
}

/* Monogram on the left, everything else in a column: the card reads as one voice. */
.ucapan__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0) 60%), var(--panel);
}

/* One script capital is the page's own ornament, and it gives a card an anchor without a
   photo. */
.ucapan__avatar {
  display: grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  border: 1px solid var(--keyline);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.25);
  color: var(--accent);
  font-family: var(--font-display);
  font-size: 1.3rem;
  line-height: 1;
}

.ucapan__body {
  min-width: 0;
}

.ucapan__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.6rem;
  margin: 0 0 0.35rem;
}

.ucapan__name {
  font-size: 1.05rem;
  font-weight: 600;
}

/* The attendance is a status, not a sentence. */
.ucapan__meta {
  padding: 0.1rem 0.55rem;
  border: 1px solid var(--keyline);
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The wish itself: a quote behind a hairline, in the body face. */
.ucapan__msg {
  margin: 0 0 0.35rem;
  padding-left: 0.7rem;
  border-left: 1px solid var(--keyline);
  font-style: italic;
  line-height: 1.65;
  overflow-wrap: anywhere;
}

.ucapan__time {
  display: block;
  color: var(--muted);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
}

.ucapan__empty {
  color: var(--muted);
}

/* A wish sent from this page lands with a short rise, so it is obvious where it went. */
@keyframes ucapan-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
}

.ucapan__item.is-new {
  animation: ucapan-in 500ms ease-out;
}

/* Wedding Gift */

/* A card, not a banner: the grid is capped and the column floor lowered, so four accounts
   settle into a compact 2x2 block of ~212px cards. The 180px floor is the number's own
   width — "1XXX XXXX XXXX X" in tabular figures — plus the card's padding, so it still
   fits one line. */
.gift {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
  gap: var(--space-2);
  max-width: 440px;
  margin: 0 auto;
}

/* The card is a surface: a soft diagonal wash, a corner sheen and a chip — the shape a
   guest already knows from their own card. Colour comes from the palette tokens only, so
   the accent never has to carry text on itself. */
.gift__card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.35rem;
  padding: 0.75rem var(--space-2);
  overflow: hidden;
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  background: linear-gradient(150deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 55%), var(--panel);
  text-align: left;
}

/* The sheen: a low-opacity accent disc off the bottom-right corner. */
.gift__card::after {
  content: "";
  position: absolute;
  right: -30%;
  bottom: -60%;
  z-index: 0;
  width: 9rem;
  height: 9rem;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent), transparent 70%);
  opacity: 0.16;
  pointer-events: none;
}

/* Everything the card says sits above the sheen. */
.gift__chip,
.gift__name,
.gift__number,
.gift__copy {
  position: relative;
  z-index: 1;
}

.gift__chip {
  width: 1.4rem;
  height: 0.95rem;
  border: 1px solid var(--keyline);
  border-radius: 5px;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.04));
}

.gift__name {
  margin: 0;
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/* Tabular figures and a wide track: a number that has to be read back, digit by digit. */
.gift__number {
  margin: 0;
  font-size: clamp(0.95rem, 4.2vw, 1.1rem);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.1em;
  overflow-wrap: anywhere;
}

/* An icon button: the words the pill used to print live in the markup's aria-label and
   title, the glyph in an inline SVG. Pinned to the card's bottom-right, and 2.75rem
   across — 44px, the touch floor from todo 023, kept although the glyph is smaller. */
.gift__copy {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: flex-end;
  width: 2.75rem;
  min-height: 2.75rem;
  /* Pinned to the bottom, so the buttons line up across a row of taller cards. */
  margin-top: auto;
  padding: 0;
  border: 1px solid var(--keyline);
  border-radius: 50%;
  background: transparent;
  color: var(--accent);
  cursor: pointer;
}

.gift__copy svg {
  width: 1.2rem;
  height: 1.2rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Two glyphs, one shown at a time: the clipboard while the button rests, the check for
   the flash. The script only toggles .is-copied, so it never has to write the button's
   content. */
.gift__copy-done {
  display: none;
}

.gift__copy.is-copied .gift__copy-icon {
  display: none;
}

.gift__copy.is-copied .gift__copy-done {
  display: block;
}

.gift__copy:hover,
.gift__copy:focus-visible {
  border-color: var(--accent);
}

.gift__copy.is-copied {
  border-color: var(--accent);
  background: var(--accent);
  color: #10161a;
}

/* Gallery — the pointer trail below is the whole section. */

/* The trail is the content of this slide, so its label sits at the top rather than in the
   middle of an empty stage. The wash lifts the slide off its neighbours so it reads as a
   surface, and the three gesture rules keep a drag a drag: no text selection, no
   long-press callout, no double-tap zoom delay. manipulation still allows pan and pinch,
   so scrolling and zooming are untouched. */
.slide--gallery {
  align-items: flex-start;
  background-image: linear-gradient(rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.045));
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}

/* Four words telling a guest how the photos arrive. Quiet and normal case: the kicker
   above already carries the tracked caps. */
.gallery-hint {
  margin: var(--space-1) 0 0;
  color: var(--muted);
  font-size: 0.85rem;
  letter-spacing: 0.06em;
  line-height: 1.6;
}

/* Pointer trail — Hyperplexed's "Mousemove Image Gallery". Every step the pointer takes,
   the next photo lands where it is and the oldest of six drops out, so the pointer drags
   a short trail of the couple's photos. The layer is pointer-events: none, so nothing can
   be clicked through it, and overflow: hidden keeps a photo from ever reaching the next
   section. */
.gallery-trail {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
}

.gallery-trail__img {
  position: absolute;
  width: min(40vmin, 260px);
  transform: translate(-50%, -50%);
  border: 1px solid var(--keyline);
  border-radius: var(--radius);
  box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.45);
}

/* visibility, not display: the film grader finds an image through an IntersectionObserver
   and a display: none element never intersects, so these would stay ungraded. The
   data-status toggle is still what caps the trail at six photos. */
.gallery-trail__img[data-status="inactive"] {
  visibility: hidden;
}

.gallery-trail__img[data-status="active"] {
  display: block;
  animation: gallery-trail-in 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes gallery-trail-in {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.85);
  }

  to {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
}

/* Thank You — the title is pinned to the top of the slide and the thanks words
   to the bottom, leaving the middle open to the backdrop. */

.thanks {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  /* svh, not vh: see .slide. The foot is pinned to the bottom of this box, so it is
     the part a viewport taller than the screen used to hide. */
  min-height: calc(100vh - 2 * var(--slide-pad));
  min-height: calc(100svh - 2 * var(--slide-pad));
  /* Clear of the fixed footer credit on a narrow, short viewport. */
  padding-bottom: var(--space-2);
}

.thanks__head {
  width: 100%;
}

.thanks__foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
}

.slide--thanks .section-title {
  margin-bottom: 0;
  font-size: clamp(1.6rem, 4.5vw, 2.4rem);
}

.thanks__text {
  margin: 0;
  font-size: clamp(0.85rem, 2vw, 1rem);
  line-height: 1.6;
}

.thanks__names {
  margin: var(--space-1) 0 0;
  color: var(--accent);
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 4.5vw, 2rem);
  font-weight: 400;
  line-height: 1.25;
}

/* Reference-style copy blocks */
.mempelai__profile {
  margin: var(--space-1) 0;
  padding: 0;
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.7;
  list-style: none;
}

.event__access {
  color: var(--muted);
  font-style: italic;
}

/* One sentence, and one line at every width. Body size plus 0.05em tracking broke the
   line after "sebelum" on a phone, so the size is tied to the viewport and the tracking
   gives back the rest: ~12.5px at 320px, ~14px at 390px, capped at 15.2px. It stays the
   highlighted sentence — weight and colour are unchanged. */
.rsvp__deadline {
  margin: 0 0 var(--space-1);
  color: var(--accent);
  font-size: clamp(0.78rem, 3.6vw, 0.95rem);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.rsvp__note {
  max-width: 520px;
  margin: 0 auto var(--space-2);
  color: var(--muted);
  font-size: 0.95rem;
  line-height: 1.6;
}

.site-foot {
  position: fixed;
  bottom: 0.75rem;
  left: 50%;
  z-index: 3;
  transform: translateX(-50%);
  color: var(--muted);
  font-size: 0.75rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  opacity: 0.7;
  pointer-events: none;
}

/* Loading screen */
@keyframes loader-expire {
  to {
    opacity: 0;
    visibility: hidden;
  }
}

.loader {
  position: fixed;
  inset: 0;
  z-index: 9;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: var(--bg);
  transition: opacity 900ms cubic-bezier(0.39, 0.575, 0.565, 1);
  /* Failsafe: if the script never runs (JS enabled but broken), the overlay
     still clears itself. A pending animation does not fight the fade. */
  animation: loader-expire 400ms linear 6000ms forwards;
}

.loader.is-out {
  opacity: 0;
  pointer-events: none;
}

.loader__names {
  margin: 0;
  color: var(--accent);
  font-family: var(--font-display);
  font-size: clamp(2rem, 9vw, 4rem);
  line-height: 1.2;
}

.loader__kicker {
  margin: 0;
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.35em;
  text-transform: uppercase;
}

.loader__bar {
  width: min(60vw, 240px);
  height: 2px;
  margin-top: var(--space-2);
  overflow: hidden;
  background: var(--keyline);
}

.loader__fill {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
  transition: width 300ms linear;
}

.loader__pct {
  margin: 0;
  color: var(--muted);
  font-size: 0.75rem;
  letter-spacing: 0.2em;
}

/* If AOS never boots, keep reveal targets visible. */
html:not(.aos-ready) [data-aos] {
  opacity: 1 !important;
  transform: none !important;
}

/* Mobile pass */
@media (max-width: 480px) {
  .dots {
    right: 0.5rem;
  }

  .hero__top {
    max-width: 70%;
  }

  .hero__sign {
    font-size: 1rem;
    letter-spacing: 0.08em;
  }

  .hero__guest {
    font-size: 0.9rem;
  }

  .events,
  .gift {
    grid-template-columns: 1fr;
  }

  .ucapan {
    max-height: 52vh;
  }

  .countdown {
    gap: var(--space-1);
  }

  .countdown__tile {
    padding: var(--space-1) 0.25rem;
  }

  .site-foot {
    position: static;
    margin: 0 auto var(--space-2);
    transform: none;
    text-align: center;
    opacity: 0.6;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .dots button::before {
    transition: none;
  }

  .gallery-trail__img[data-status="active"] {
    animation: none;
  }

  .ucapan__item.is-new {
    animation: none;
  }

  .loader,
  .loader__fill {
    transition: none;
  }
}
