* { box-sizing: border-box; }

/* self-hosted — a Thai handwriting font, replaces Sriracha as the Thai half
   of the handwritten/script pairing (real handwriting, not a marker font).
   weight range covers the single Thin file regardless of requested weight. */
@font-face {
  font-family: 'JaoCherry';
  src: url('../assets/fonts/JaoCherry-Thin.ttf') format('truetype');
  font-weight: 100 700;
  font-style: normal;
  font-display: fallback;
  /* the only weight available is Thin — its strokes read noticeably smaller
     than Caveat's at the same declared size, so bump it up here once instead
     of hand-tuning every font-size that pairs the two */
  size-adjust: 118%;
}

/* second Thai handwriting face, used only for the closing message (.pc-note)
   — user supplied this one directly instead of Mali/JaoCherry. */
@font-face {
  font-family: 'JaoTomato';
  src: url('../assets/fonts/JaoTomato-Thin.ttf') format('truetype');
  font-weight: 100 700;
  font-style: normal;
  font-display: fallback;
}

/* ---- hand-drawn icon set (CSS shapes, no emoji/image assets) ---- */
.icon-heart {
  position: relative;
  display: inline-block;
  width: 1em; height: 0.9em;
  color: inherit;
  vertical-align: -0.05em;
}
.icon-heart::before, .icon-heart::after {
  content: "";
  position: absolute;
  top: 0; left: 0.5em;
  width: 0.5em; height: 0.8em;
  background: currentColor;
  border-radius: 0.5em 0.5em 0 0;
  transform: rotate(-45deg);
  transform-origin: 0 100%;
}
.icon-heart::after { left: 0; transform: rotate(45deg); transform-origin: 100% 100%; }

.icon-star {
  display: inline-block;
  width: 0.9em; height: 0.9em;
  background: currentColor;
  clip-path: polygon(50% 0%, 62% 36%, 100% 50%, 62% 64%, 50% 100%, 38% 64%, 0% 50%, 38% 36%);
  vertical-align: -0.1em;
}

.icon-photo {
  position: relative;
  display: inline-block;
  width: 1.5em; height: 1.15em;
  border: 0.13em solid currentColor;
  border-radius: 0.2em;
}
.icon-photo::before {
  content: "";
  position: absolute;
  top: 0.16em; left: 0.16em;
  width: 0.28em; height: 0.28em;
  border-radius: 50%;
  background: currentColor;
}
.icon-photo::after {
  content: "";
  position: absolute;
  right: -0.02em; bottom: -0.02em;
  width: 0; height: 0;
  border-left: 0.5em solid transparent;
  border-bottom: 0.6em solid currentColor;
}

.icon-pin {
  display: inline-block;
  width: 0.8em; height: 0.8em;
  border-radius: 50% 50% 50% 0;
  background: var(--pink);
  transform: rotate(-45deg);
  box-shadow: 0 2px 3px rgba(0,0,0,0.2);
}

/* Step 1 centerpiece (2026-07-20: "ทำหน้าแรกใหม่เลยไม่เอาแบบจดหมาย" — the
   envelope+wax-seal icon read as "a letter", which Step 6 already is; swapped
   for the couple sticker instead so Step 1 doesn't front-load that motif). */
.hero-centerpiece {
  display: block;
  width: 110px;
  height: auto;
  margin: 14px auto 20px;
  filter: drop-shadow(0 6px 10px rgba(0,0,0,0.16));
}

/* Recolored 2026-07-19 ("เปลี่ยนสี theme เอาให้เหมาะกับ sticker") — palette
 * sampled directly from the HandDrawn Web Elements Pack (assets/stickers/):
 * deep crimson-maroon (~rgb(122,32,37), sampled off fingerprint_heart.png)
 * instead of bright pink/purple, kraft-paper cream (~rgb(210,200,175),
 * sampled off the star sticker's halo) instead of the near-white pink tint.
 * Variable names kept as-is (--pink/--purple/--cream/--text) since every
 * component below already references them — only the values change. */
:root {
  /* 2026-07-20: lightened ~12% across the board per "ปรับโทนสีให้จางลง" — same
     kraft/maroon palette, just a touch softer. */
  --pink: #8a3b3f;
  --pink-light: #ecdcc0;
  --purple: #b15448;
  --cream: #f3e8d3;
  --text: #604c3b;
}

html, body {
  height: 100%;
  overflow: hidden; /* ponytail: no page scroll — it's a locked step flow, not a document */
  /* 2026-07-22: overflow:hidden alone doesn't stop iOS Safari's native
     "scroll focused input into view" on the pin-slot inputs — it scrolls the
     layout viewport itself past #app, revealing blank white below ("เห็นพื้นที่ขาว").
     position:fixed fully locks the viewport, no scroll possible at any level. */
  position: fixed;
  width: 100%;
}

body {
  margin: 0;
  font-family: 'Mali', 'Prompt', sans-serif;
  background: var(--cream); /* base tint under the image layer, so nothing ever flashes white */
  color: var(--text);
  display: flex;
  justify-content: center;
  height: 100%;
}
/* The paper background lives on its own fixed layer rather than on `body` with
   `background-attachment: fixed`. iOS Safari treats background-attachment:fixed
   as unreliable at the best of times, and body here is itself position:fixed —
   that combination made the background simply not paint on phones
   ("เปิดบนทรศ.พื้นหลังหายอะ"). A plain fixed layer gives the same single
   continuous crop behind everything, on every browser.
   It's out of flow (position:fixed), so body's flex layout is unaffected. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0.3)), url('../assets/bg/image/paper-2.webp') center / cover no-repeat;
  pointer-events: none;
}

#app {
  width: 100%;
  max-width: 540px; /* was 480px — same fixed cap at every viewport, still no mobile/desktop split */
  height: 100%;
  position: relative;
  overflow: hidden;
  background: transparent;
  /* no box-shadow: #app is transparent and shares one continuous background
     with body now — a shadow here just drew two dark vertical lines flanking
     the card wherever open paper showed beside it (barely visible on mobile
     where #app fills the screen, very visible on desktop — looked like a
     mobile/desktop split, which it effectively was). */
}

.progress-dots {
  display: none; /* ponytail: hidden by default, shown only in ?dev mode (body.dev-mode override below) */
  position: sticky;
  top: 0;
  z-index: 5;
  justify-content: center;
  gap: 6px;
  padding: 14px 0;
  background: linear-gradient(180deg, var(--cream) 60%, transparent);
}
body.dev-mode .progress-dots { display: flex; }
.progress-dots span {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #d9c49a;
  transition: all 0.3s ease;
}
.progress-dots span.done { background: var(--pink); }
.progress-dots span.current { width: 18px; border-radius: 4px; background: linear-gradient(90deg, var(--pink), var(--purple)); }

.script {
  font-family: 'Caveat', 'JaoCherry', cursive; /* Caveat has no Thai glyphs — falls through to JaoCherry, a real Thai handwriting font */
  /* 2.4rem was sized for Caveat; JaoCherry's size-adjust (118%, see its
     @font-face) makes Thai text render noticeably bigger at the same
     font-size, which was overflowing headings (step 4's question, step 6's
     "ยิ้มหน่อยยย") into the elements above/around them. */
  font-size: 1.6rem;
  line-height: 1.35;
  margin: 0.3em 0 0.2em;
  color: var(--pink);
}
/* gradient text reserved for the two emotional peaks (opening + final reveal) —
   using it on every heading read as generic/AI-templatey, so it's dialed back here */
.script-hero {
  background: linear-gradient(100deg, var(--pink), var(--purple));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.step {
  display: none;
  height: 100%;
  box-sizing: border-box;
  padding: 32px 20px;
  flex-direction: column;
  justify-content: center;
  animation: fadeIn 0.4s ease;
  position: relative; /* so a step without a .card (e.g. Step 4) still positions absolute children like .sticker-tag-img correctly */
}
.step.active { display: flex; }
.step.center { text-align: center; }

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

/* "ค่อยๆขยับลอยขึ้นมา" instead of popping in instantly — used on step 4's card
   and stickers, retriggered by JS (see replayFloat() in app.js) every time the
   question changes, not just on first entering the step. Uses the standalone
   `translate` property rather than `transform`, so it doesn't clobber the
   stickers' `transform: rotate(...)` from their .pos-1/.pos-2 rules — same
   trick as .cam-arrow's nudge animation. */
@keyframes qFloatUp {
  from { opacity: 0; translate: 0 16px; }
  to { opacity: 1; translate: 0 0; }
}
.q-float-in { animation: qFloatUp 0.5s ease both; }

.card {
  width: 100%;
  background: #fff;
  border-radius: 20px;
  padding: 28px 22px;
  box-shadow: 0 12px 30px rgba(122,32,37,0.16), 0 2px 6px rgba(122,32,37,0.1);
  position: relative;
  z-index: 1;
}
/* Recheck 2026-07-19: .paper-tag's rotated bounding box dips ~9px below its
   own content into whatever's right under it (measured: tag bottom edge at
   37px from card top, .eyebrow starting at 28px) — clips the first line of
   text on any card that has one (seen on Steps 3/5/6). Scoped extra top
   clearance only for cards that actually have a paper-tag, so cards without
   one keep the normal 28px. */
.card:has(> .paper-tag) { padding-top: 46px; }
.card::before {
  /* ponytail: washi-tape decoration, CSS only, no image asset */
  content: "";
  position: absolute;
  top: -12px;
  left: 28px;
  width: 56px;
  height: 22px;
  background: repeating-linear-gradient(135deg, #d9c49a, #d9c49a 4px, #e6dbbd 4px, #e6dbbd 8px);
  opacity: 0.85;
  transform: rotate(-4deg);
  border-radius: 2px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
/* tape color cycles per step so the run of cards doesn't read as one flat pink block */
.step[data-step="2"] .card::before { display: none; } /* removed per "เอาสองอันนี้ออก" — the pin+paper-tag date tag was the other */
.step[data-step="4"] .card::before { background: repeating-linear-gradient(135deg, #ffe3ae, #ffe3ae 4px, #fff3d9 4px, #fff3d9 8px); }
.step[data-step="5"] .card::before { background: repeating-linear-gradient(135deg, #bdeecb, #bdeecb 4px, #e0f9e7 4px, #e0f9e7 8px); }
.step[data-step="6"] .card::before { background: repeating-linear-gradient(135deg, #ffc7b0, #ffc7b0 4px, #ffe3d6 4px, #ffe3d6 8px); }

.card.center { text-align: center; }

/* Step 1 cover — redesigned to match reference: envelope floats on grid paper,
   watercolor flowers flanking, CSS stars & hearts scattered around.
   No card background — everything sits directly on the page texture. */
.cover {
  position: relative;
  width: 100%; height: 100%; /* full step area so absolute decos reach corners */
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  cursor: pointer;
  animation: cover-float 5s ease-in-out infinite;
}
@keyframes cover-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-7px); }
}
.cover-title {
  font-size: clamp(2.4rem, 10vw, 3rem);
  font-family: 'Itim', 'Mali', cursive;
  margin: 0 0 4px;
  letter-spacing: 0.04em;
  text-decoration: underline;
  text-underline-offset: 6px;
  text-decoration-thickness: 2px;
}
.cover-hint {
  position: absolute;
  /* pinned to .cover's own top-right corner — independent of .envelope's
     rotation and position, so it stays upright with the envelope below it */
  top: 8%; right: 6%;
  margin: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
}
.cover-hint[hidden] { display: none; } /* the author `display: flex` above beats the UA [hidden] rule otherwise */

.envelope {
  position: relative;
  width: min(70vw, 260px);
  aspect-ratio: 612 / 757; /* tall enough for the open envelope's standing flap */
  margin-top: -160px; /* CLICK now lives outside this column (pinned to .cover's corner instead), so this only needs a small nudge, not the big offset that used to make room for it */
  transform: rotate(-2deg); /* nothing hand-placed sits perfectly square */
  filter: drop-shadow(0 16px 22px rgba(0,0,0,0.22));
}
/* both images anchored to the same bottom-center point — an envelope's
   bottom edge stays put as it opens, only the flap rises above it, so
   sharing that one anchor is what keeps closed/open from jumping around */
.env-img {
  position: absolute;
  left: 50%; bottom: 0;
  transform: translateX(-50%);
  width: 100%;
  height: auto;
  transition: opacity 0.5s ease;
}
.env-closed { opacity: 1; z-index: 4; }
.env-back { opacity: 0; z-index: 1; }
.env-front { opacity: 0; z-index: 3; }
.cover.opening .env-closed { opacity: 0; }
.cover.opening .env-back,
.cover.opening .env-front { opacity: 1; }

/* the letter rises from behind .env-back, up past the pocket notch, and
   tucks behind .env-front's lower flaps — real depth now that the envelope
   is split into a back sheet and a front pocket instead of one flat photo. */
.env-letter {
  position: absolute;
  left: 50%; bottom: 8%;
  width: 82%;
  transform: translate(-50%, 0) rotate(1.5deg);
  z-index: 2;
  /* opacity 0 at rest, not 1 — .env-closed (z4) used to be the only thing
     hiding it, but while .env-closed and .env-front crossfade (both
     mid-transparent for that ~0.5s) the fully-opaque letter showed through
     both of them, flickering into view early. Fading the letter in on its
     own timeline, after the crossfade is basically done, removes that gap. */
  opacity: 0;
  /* not clickable until opened — otherwise it sits on top of the closed
     envelope and swallows the tap meant to open it */
  pointer-events: none;
  transition: opacity 0.3s ease 0.35s, transform 2.4s cubic-bezier(0.22,0.9,0.3,1) 0.55s;
}
.cover.opening .env-letter {
  opacity: 1;
  pointer-events: auto;
  cursor: pointer;
  transform: translate(-50%, -90%) rotate(1.5deg);
}
.env-letter-paper {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 4px 10px rgba(0,0,0,0.25));
}
.cover.opening { animation: none; } /* stop the idle float once opening starts */
.cover.opening .cover-hint { opacity: 0; transition: opacity 0.3s ease; }

/* "เปิดสิ" + a down arrow, centered above the closed envelope — the
   envelope graphic's own top edge sits at ~44% down .cover, same anchor
   point .cover-hint used to use before it moved to the corner. */
.cover-open-hint {
  position: absolute;
  left: 50%; top: 40%;
  transform: translate(-50%, -100%);
  margin: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  pointer-events: none;
  /* animation: cover-open-hint-bounce 1.4s ease-in-out infinite; */
}
.cover-open-text {
  font-size: 1.6rem;
  color: var(--pink);
  margin-bottom: 26px;
}
.cover-open-arrow {
  font-size: 1.6rem;
  line-height: 1;
  color: var(--pink);
}
.cover.opening .cover-open-hint { opacity: 0; transition: opacity 0.3s ease; }
@keyframes cover-open-hint-bounce {
  0%, 100% { transform: translate(-50%, -100%); }
  50% { transform: translate(-50%, -80%); }
}
.cover-deco {
  position: absolute;
  opacity: 0.95;
  pointer-events: none;
  z-index: 5;
  filter: drop-shadow(0 4px 8px rgba(0,0,0,0.12));
}
/* flowers flanking the envelope — positioned in the middle-left/right area */
.cover-deco-1 { width: 100px; top: 30%; left: 0;  transform: rotate(-5deg); }
.cover-deco-2 { width: 100px; bottom: 20%; right: 0; transform: rotate(5deg); }
.click-letters {
  display: inline-flex;
  align-items: center;
  animation: click-bounce 2.4s ease-in-out infinite;
}
.click-letters img {
  height: 30px;
  width: auto;
}
@keyframes click-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
.cover-title .icon-heart { color: var(--pink); }

.sticker-pencil-img {
  position: absolute;
  width: 30px;
  bottom: 38%;
  right: 22%;
  filter: drop-shadow(0 6px 8px rgba(0,0,0,0.25));
  cursor: pointer;
  z-index: 3;
  transform-origin: 50% 50%; /* center pivot so the +180 flip below doesn't shift the bounding box */
  /* +180deg from the previous 35/38deg: flips which end (tip vs eraser) sits at which
     corner ("กลับหัวกลับหาง") while keeping the pencil on the same diagonal line */
  animation: pencil-idle 6s ease-in-out infinite;
  -webkit-user-drag: none;
  user-select: none;
}
.sticker-pencil-img:hover,
.sticker-pencil-img:focus-visible {
  animation: pencil-wiggle 2.5s ease-in-out infinite;
}
@keyframes pencil-idle {
  0%, 100% { transform: rotate(218deg); }
  50% { transform: rotate(215deg); }
}
@keyframes pencil-wiggle {
  0%, 100% { transform: rotate(210deg) scale(1.05); }
  25% { transform: rotate(224deg) scale(1.05); }
  75% { transform: rotate(204deg) scale(1.05); }
}
.card.dark {
  background: #1a1220;
  color: #fff;
}
.card.dark::before { background: repeating-linear-gradient(135deg, #c9855a, #c9855a 4px, #e0cda3 4px, #e0cda3 8px); }

/* torn-paper caption tag — kraft-paper scrap with a handwritten note,
   one per step max (a solid-circle "sticker" badge was tried before this
   and rejected as too much/not pretty — this replaces that idea) */
.paper-tag {
  position: absolute;
  top: -16px;
  right: 18px;
  /* fibrous kraft-paper look: base tone + faint diagonal grain streaks, no image asset */
  background:
    repeating-linear-gradient(115deg, rgba(138,59,59,0.05) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(25deg, rgba(255,255,255,0.35) 0 1px, transparent 1px 6px),
    #e6d5b3;
  color: #7a2f2f;
  font-family: 'Caveat', cursive;
  font-weight: 600;
  font-size: 1.2rem;
  padding: 10px 16px 14px;
  transform: rotate(6deg);
  box-shadow: 0 4px 8px rgba(0,0,0,0.16);
  z-index: 2;
  clip-path: polygon(
    0% 10%, 8% 2%, 16% 8%, 26% 0%, 36% 7%, 46% 1%, 56% 8%, 66% 2%, 76% 7%, 86% 1%, 94% 6%, 100% 0%,
    100% 90%, 92% 100%, 84% 93%, 74% 100%, 64% 94%, 54% 100%, 44% 93%, 34% 100%, 24% 94%, 14% 100%, 6% 95%, 0% 100%
  );
}
/* 2026-07-20: "อยากให้ Sticker i love you อยู่ซ้าย All my Heart อยู่ขวาแทน" —
   swapped back to the default top-right (was moved top-left on 2026-07-19 to
   dodge .sticker-ransom-img, which is now on the left instead — see its own
   comment below). No override needed here anymore, default position is right. */

/* ---- scrapbook decorations ----
   2026-07-20: the pack's named PNGs for record/i_love_you/favorite_person
   (previously corrupted — see git history / old comment) were replaced with
   real cut-out stickers from a new source image (user-provided sticker
   sheet, hand-cropped + alpha-verified transparent). CSS recreations below
   are gone; real images take their place. */

/* Recheck 2026-07-19: measured overlap with .paper-tag on Step 5 (both were
   top-left there). Moved to bottom-left — Step 5's card is short on content
   below the audio player, plenty of clear space. */
.sticker-vinyl-img {
  position: absolute;
  bottom: -30px;
  left: -18px;
  width: 78px;
  height: auto;
  transform: rotate(-8deg);
  filter: drop-shadow(0 6px 10px rgba(0,0,0,0.25));
  z-index: 2;
  animation: spin 9s linear infinite;
}
@keyframes spin { to { transform: rotate(352deg); } }



/* Step 2, 2026-07-21: "Mission" heading + couple sticker removed, replaced by
   a single unlock.png graphic acting as the title — centered, in normal flow. */
.step[data-step="2"] .card {
  text-align: center;
}
.step2-unlock-img {
  /* 2026-07-22: centered on top edge of the pin-slot graphic, not overlapping
     the paper pieces ("ตรงกลางด้านบน" — top-center, not dead-center).
     Width now scales like .pin-inputs (% of card, capped) instead of a fixed
     px so it shrinks on narrow screens instead of overflowing ("behavior
     แบบ pin slot ตอนย่อ"). Nudged slightly left of true-center per request. */
  position: absolute;
  top: 0;
  left: calc(50% - 14px);
  transform: translate(-50%, -100%);
  width: 78%;
  max-width: 420px;
  height: auto;
  pointer-events: none;
  filter: drop-shadow(0 8px 16px rgba(122,32,37,0.18));
}
/* slide the whole pin sprite in from below on step enter */
.step[data-step="2"].active .pin-inputs {
  animation: pin-slide-in 0.6s cubic-bezier(0.22, 0.9, 0.3, 1) 0.1s both;
}
.step[data-step="2"].active .step2-unlock-img {
  animation: unlock-drop 0.5s cubic-bezier(0.34, 1.4, 0.64, 1) 0.35s both;
}
@keyframes pin-slide-in {
  from { opacity: 0; transform: translateX(-50%) translateY(24px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes unlock-drop {
  from { opacity: 0; transform: translate(-50%, -120%); }
  to   { opacity: 1; transform: translate(-50%, -100%); }
}
/* tip text, positioned below the whole pin-slot sprite (not just slot 1) so
   it never overlaps the sprite's own bottom decorations (button/tag art) —
   sprite's own box ends at top:100% in this same % coordinate system. */
.pin-slot-tip {
  position: absolute;
  left: 2%;
  top: 106%;
  margin: 0;
  font-family: 'Mali', sans-serif; /* smaller/cleaner Thai than the JaoCherry script fallback, reads better this tiny */
  font-size: 0.7rem;
  color: var(--text);
  opacity: 0.45;
}

/* handwritten "favorite person" tag — separate slot from .paper-tag so both
   can appear on the same card without colliding. Recheck 2026-07-19: this
   slot's rotated bounding box was overlapping the washi tape corner when
   off-center; centered clears it (see .card:has(> .paper-tag) padding fix
   above too, for the paper-tag/eyebrow overlap that affected every card). */
.sticker-tag-img {
  position: absolute;
  top: -10px;
  left: 50%;
  transform: translateX(-50%) rotate(-3deg);
  width: 84px;
  height: auto;
  filter: drop-shadow(0 3px 6px rgba(0,0,0,0.14));
  z-index: 2;
}

.sticker-star-img {
  position: absolute;
  top: -30px;
  right: 20px;
  width: 64px;
  height: auto;
  transform: rotate(10deg);
  filter: drop-shadow(0 3px 5px rgba(0,0,0,0.16));
  z-index: 2;
}

/* Step 3 games-intro only (removed from Step 6 — it collided with the flip
   grid there). Sits at the script-title band on the right, above the long
   hint line so it never covers text. */
/* Step 3: plain fade in/out text, no card — a brief "next chapter" beat
   between the mission unlock and the camera. Auto-advances, see app.js. */
.journey-text {
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 2rem;
  color: var(--pink);
  text-align: center;
  margin: 0;
  margin-top: -120px;
  animation: journey-fade 2.8s ease forwards;
}
@keyframes journey-fade {
  0% { opacity: 0; }
  20%, 70% { opacity: 1; }
  100% { opacity: 0; }
}

/* Step 3 (Heart Hunt card): this card has no paper-tag/washi of its own to
   dodge, so a plain top-right corner is clear. */
.sticker-firework-img {
  position: absolute;
  top: -18px;
  right: 6px;
  width: 74px;
  height: auto;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,0.16));
  z-index: 2;
}

/* .sticker-always-img (always-banner.png) removed 2026-07-20 — the source
   file was deleted by a concurrent edit to assets/stickers/ with no
   equivalent replacement among the new crops, and the Step 5 card doesn't
   need it now that the cassette graphic is the visual centerpiece. */

/* small accent for the Love Quiz card (Step 3) — previously had no sticker at all. */
.sticker-happy-place-img {
  position: absolute;
  top: -22px;
  right: 8px;
  width: 76px;
  height: auto;
  transform: rotate(-6deg);
  filter: drop-shadow(0 3px 5px rgba(0,0,0,0.16));
  z-index: 2;
}

#app.shake { animation: shake 0.4s ease; }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-8px); }
  40%, 80% { transform: translateX(8px); }
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.75rem;
  color: var(--pink);
  font-weight: 600;
  margin: 0;
}

.hint { color: #a97a8a; font-size: 0.9rem; }
.hint.light { color: #d9b8c9; }
/* pinned-note look for Step 2: a little pushpin sticking through the card top */
.pin {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #ff8a7a, #b15448 70%);
  box-shadow: 0 3px 6px rgba(0,0,0,0.35);
  margin: 0 auto -9px;
  position: relative;
  z-index: 2;
}

.btn-primary {
  width: 100%;
  padding: 14px;
  border: none;
  border-radius: 999px;
  background: linear-gradient(135deg, #b15448, var(--pink));
  color: #fff;
  font-weight: 600;
  font-size: 1rem;
  cursor: pointer;
  margin-top: 16px;
  box-shadow: 0 6px 16px rgba(122,32,37,0.35), inset 0 1px 0 rgba(255,255,255,0.5);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.btn-primary:active { transform: scale(0.97); box-shadow: 0 3px 8px rgba(122,32,37,0.3); }

/* Step 3 quiz — paper-cutout style options, one per row */
.quiz-options {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 18px;
}
.quiz-option {
  padding: 13px 16px;
  border: 1.5px solid var(--pink);
  border-radius: 14px;
  background: rgba(255,255,255,0.75);
  color: var(--text);
  font-family: 'Mali', sans-serif;
  font-size: 0.95rem;
  cursor: pointer;
  transition: transform 0.15s ease, background 0.2s ease;
}
.quiz-option:active { transform: scale(0.97); }
.quiz-option.chosen { background: var(--pink); color: #fff; } /* brief tap feedback before the next question */
.quiz-praise {
  text-align: center;
  font-family: 'JaoTomato', 'Mali', sans-serif; /* matches step 7's .pc-note, per request */
  font-size: 1.1rem;
  line-height: 1.5;
  color: var(--purple);
  margin: 4px 0 0;
}

/* Step 6 dressing — the quiz card as a taped-down note rather than a form.
   Each option is a slightly crooked paper strip; the tilt alternates so a
   column of them reads hand-stuck instead of grid-aligned. */
.step[data-step="4"] .card { transform: rotate(-1deg); border-radius: 18px 22px 18px 24px; }

/* decorative stickers — assets/stickers/question/ (torn-paper question mark,
   hand-drawn flower, paper heart). All three have clean alpha — question3's
   apparent dark "vignette" is just its transparent background rendering dark
   in image previews, not baked into the file. */
.q-sticker {
  position: absolute;
  pointer-events: none;
  z-index: 1;
  /* a position:absolute image resolves width/height:auto per-axis, independently of
     each other — so ANY two-constraint combo (width+max-height, or max-width+max-height)
     still distorts it ("โดนบีบ"). Only a single explicit dimension is safe; the other
     stays auto and the browser is then forced to keep the intrinsic ratio. Same pattern
     as .q-card-flower below. */
  height: 110px; width: auto;
  filter: drop-shadow(0 4px 8px rgba(60,45,30,0.25));
}
/* Two position variants per sticker slot (mirrored left/right), rotated per
   question in app.js — otherwise every question puts its sticker in exactly
   the same spot and only the picture changes. Kept to 2, not 3: a 3rd, less
   obvious spot was landing somewhere odd relative to the taped-note corner. */
.q-sticker-mark.pos-1 { top: 4%; right: 8%; transform: rotate(12deg); }
.q-sticker-mark.pos-2 { top: 4%; left: 8%; transform: rotate(-12deg); }
.q-sticker-heart.pos-1 { bottom: 6%; left: 6%; transform: rotate(-20deg); }
.q-sticker-heart.pos-2 { bottom: 6%; right: 6%; transform: rotate(20deg); }
/* pinned to the question card itself, corner peeking over the edge, rather
   than floating loose on the page like .q-sticker. Two variants (right/left
   corner), rotated per question in app.js. */
.q-card-flower {
  position: absolute;
  top: -30px;
  height: 100px; width: auto; /* capped by height, not width — the animal/draw stickers are taller-than-wide, unlike the original flowers, so a fixed width let them grow tall enough to cover the question text */
  pointer-events: none;
}
.q-card-flower.pos-1 { right: -10px; transform: rotate(10deg); } /* right pulled in from -25px — at narrow phone widths (~375px) the card only has ~18px of margin to the viewport edge, so -25px pushed the flower past it into #app's overflow:hidden clip ("โดนตัด") */
.q-card-flower.pos-2 { left: -10px; transform: rotate(-10deg); }
/* whole step (question text AND the score summary) shares one font now — matches step 7's .pc-note, per request */
.step[data-step="4"] .card .script { text-align: center; line-height: 1.35; margin-top: 1.1em; font-size: 1.3rem; font-family: 'JaoTomato', 'Mali', sans-serif; font-weight: 500; }
.step[data-step="4"] .eyebrow { text-align: center; font-size: 1.1rem; font-family: 'JaoTomato', 'Mali', sans-serif; font-weight: 500; }
.quiz-option {
  box-shadow: 0 3px 0 rgba(122,32,37,0.08);
  transform: rotate(-0.6deg);
}
.quiz-option:nth-child(even) { transform: rotate(0.7deg); }
.quiz-option:active { transform: scale(0.97); }
.quiz-option.chosen { transform: rotate(0deg) scale(1.02); }

.btn-secondary {
  flex: 1;
  padding: 12px;
  border-radius: 999px;
  border: 2px solid var(--pink);
  background: #fff;
  color: var(--pink);
  font-weight: 600;
  cursor: pointer;
}

.ending-actions { display: flex; gap: 10px; margin-top: 16px; }

/* Step 2 card, 2026-07-20: dropped the white card fill so the paper-scrap pin
   slots below sit directly on the shared body texture ("เอาพื้นหลังขาวๆออก"). */
.step[data-step="2"] .card {
  background: transparent;
  box-shadow: none;
  padding-top: 0px; /* 2026-07-22: reduced further from 10px per follow-up ("ขออีกนิด") */
  padding-bottom: 325px; /* reserves flow space for .pin-inputs + .step2-subtext now that they're positioned absolute below */
}
/* 2026-07-20: switched from 6 cropped pieces to the one uncropped
   assets/stickers/pin-slot.webp sprite as a single background ("ใช้รูปที่ทำมา
   เลยไม่ต้องมาตัด") — each .pin-slot is now just an invisible, positioned
   hit-area for its input, sized up from before ("ปรับขนาดให้ใหญ่ขึ้น"). */
.pin-inputs {
  position: absolute; /* freed from the header's flow — its own space, positioned off the card ("space" only, same pin-slot.png) */
  top: 100px; /* 2026-07-22: raised further from 140px per follow-up ("ขออีกนิด") */
  left: 50%;
  transform: translateX(-50%);
  width: calc(100% + 20px); /* small bleed past the card's 22px side padding, but keeps a margin off the screen edge on mobile */
  max-width: 540px; /* matches #app's new cap — same budget at every viewport, no mobile/desktop split */
  aspect-ratio: 1849 / 726;
  background: url('../assets/stickers/pin-slot.webp') center / contain no-repeat;
}
.pin-slot {
  position: absolute;
  transform: translateX(-50%);
  /* pencil sticker as the cursor, tilted 135deg so the tip points down-right
     like it's actually writing on the paper piece — hotspot near the tip */
  cursor: url('../assets/stickers/pencil-cursor-135.webp') 42 41, text;
}
/* Each slot's left/top/height/width/tilt is measured off its own piece in
   pin-slot.png (center x, writable-paper y-range) — not one uniform row,
   since the 6 pieces sit at different heights/sizes in the photo itself. */
/* nth-of-type (not nth-child): counts only sibling <span>s, so the
   .step2-unlock-img <img> now living inside #mission-pin doesn't shift the index */
.pin-slot:nth-of-type(1) { left: 10.3%; top: 33%; height: 45%; width: 13%; transform: translateX(-50%) rotate(-3deg); }  /* kraft square — short piece */
.pin-slot:nth-of-type(2) { left: 27.6%; top: 26%; height: 58%; width: 14%; transform: translateX(-50%) rotate(2deg); }  /* gray, tall below the ribbon */
.pin-slot:nth-of-type(3) { left: 40.8%; top: 29%; height: 49%; width: 13%; transform: translateX(-50%) rotate(-1deg); } /* grid, below the tape */
.pin-slot:nth-of-type(4) { left: 54.5%; top: 33%; height: 48%; width: 14%; transform: translateX(-50%) rotate(1deg); }  /* lined, below the tape */
.pin-slot:nth-of-type(5) { left: 73.1%; top: 28%; height: 48%; width: 16%; transform: translateX(-50%) rotate(2deg); }  /* newsprint, above the button */
.pin-slot:nth-of-type(6) { left: 89.2%; top: 41%; height: 42%; width: 14%; transform: translateX(-50%) rotate(-4deg); } /* tag, below the hole */

.pin-digit {
  width: 100%;
  height: 100%;
  text-align: center;
  font-size: 1.3rem;
  font-family: inherit;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: transparent; /* the typed digit stays hidden — .pin-digit-img shows the mapped number.png instead */
  caret-color: var(--pink); /* cute pink caret instead of plain text-color */
}
.pin-digit:focus { outline: none; }
.pin-slot:focus-within .pin-digit-img { animation: pin-pop 0.3s ease; }
@keyframes pin-pop { 0% { transform: scale(1); } 60% { transform: scale(1.22); } 100% { transform: scale(1.08); } }
.pin-slot:focus-within .pin-digit-img { transform: scale(1.08); }
.pin-digit-img {
  position: absolute;
  inset: 8%;
  width: 84%;
  height: 84%;
  object-fit: contain;
  pointer-events: none;
  display: none; /* shown via JS once a digit is entered */
}

.toast {
  position: fixed;
  top: 24px;
  left: 50%;
  transform: translateX(-50%);
  color: #d33;
  font-size: 0.85rem;
  z-index: 10;
  white-space: nowrap;
  animation: toast-fade 1.8s ease forwards; /* matches the auto-dismiss timing in checkMission() */
}
.toast-success {
  color: var(--pink);
  animation: toast-fade 1.2s ease forwards; /* matches the setTimeout in checkMission() before goToStep(3) */
}
@keyframes toast-fade {
  0%, 60% { opacity: 1; }
  100% { opacity: 0; }
}

/* Heart Hunt */
.hunt-field {
  position: relative;
  height: 320px;
  background: var(--pink-light);
  border-radius: 14px;
  overflow: hidden;
  margin-top: 12px;
}
.hunt-heart {
  position: absolute;
  font-size: 1.6rem;
  color: var(--pink);
  cursor: pointer;
  background: none;
  border: none;
  transition: transform 0.15s;
  opacity: 0;
  animation:
    huntEnter 0.5s ease-out var(--enter-delay, 0s) forwards,
    bob var(--bob-duration, 1.8s) ease-in-out calc(var(--enter-delay, 0s) + var(--delay, 0s)) infinite;
}
.hunt-heart.collected { opacity: 0; pointer-events: none; transform: scale(1.6) !important; animation: none; }
.hunt-count { text-align: center; font-weight: 600; margin-top: 10px; }
@keyframes huntEnter {
  from { opacity: 0; transform: translateY(16px) scale(0.6); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-8px) rotate(var(--tilt, 6deg)); }
}

/* Gallery */
.relationship-label {
  font-family: 'Caveat', cursive;
  font-size: 1.3rem;
  color: var(--purple);
  margin: 0;
}
/* Step 4 runs tighter than other steps — a 200px camera + photo pile is tall,
   so this pulls the title block up and shrinks the gaps around it to avoid
   the page needing to scroll. */
.step[data-step="6"] { padding-top: 48px; justify-content: flex-start; }
.step[data-step="6"] .script { margin: 0.05em 0 0; }
/* sizing/centering moved here from .camera-frame so .cam-arrow (now a sibling
   of the button, see index.html) shares the same box without inheriting the
   button's own rotate animation */
.camera-wrap {
  position: relative;
  width: 180px;
  margin: 34px auto 6px;
}
.camera-frame {
  display: block;
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: transform 0.15s ease;
  position: relative;
  z-index: 10; /* camera stays above the photo pile/stickers at all times */
}
.camera-frame:active { transform: scale(0.9); } /* press feedback */
.camera-frame:disabled { cursor: default; animation: none; }

/* idle wiggle loop — brief wiggle then a long rest, not constant */
/* .camera-frame:not(:disabled) { animation: camera-invite 2.5s ease-in-out infinite; } */
.camera-frame:not(:disabled):active { animation-play-state: paused; }
@keyframes camera-invite {
  0%, 55%, 100% { transform: rotate(0deg); }
  58% { transform: rotate(-4deg); }
  61% { transform: rotate(3deg); }
  64% { transform: rotate(-2deg); }
  67% { transform: rotate(0deg); }
}

/* ---- the camera itself, drawn in CSS (was assets/stickers/camera.webp) ----
   Everything below is sized in % of .cam, so the camera scales purely from
   .camera-frame's width above — no magic numbers to re-tune per size. */
.cam {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 0.92;
  pointer-events: none; /* the <button> owns the click, not its parts */
  filter: drop-shadow(0 10px 18px rgba(60,40,35,0.22));
}
/* cream upper body */
.cam-top {
  position: absolute;
  top: 0; left: 2.5%;
  width: 95%; height: 66%;
  background: #f7f5f0;
  border-radius: 11% 11% 4% 4% / 15% 15% 4% 4%;
}
/* black film-door lower body, slightly wider than the top */
.cam-bottom {
  position: absolute;
  bottom: 0; left: 0;
  width: 100%; height: 37%;
  background: #22201e;
  border-radius: 3% 3% 7% 7% / 6% 6% 18% 18%;
}
/* flash unit — vertical striped window, top-left */
.cam-flash-unit {
  position: absolute;
  top: 9%; left: 7%;
  width: 15%; height: 45%;
  border-radius: 3px;
  background:
    repeating-linear-gradient(90deg,
      rgba(130,130,130,0.42) 0 1px,
      rgba(255,255,255,0.92) 1px 4px),
    #e9e9e7;
  box-shadow: inset 0 0 0 2.5px #cfcbc2, 0 1px 2px rgba(0,0,0,0.14);
}
/* lens: three nested rings, glass gets the iridescent tint */
.cam-lens {
  position: absolute;
  top: 8%; left: 50%;
  transform: translateX(-50%);
  width: 42%; aspect-ratio: 1;
  border-radius: 50%;
  background: #2b2927;
  box-shadow: 0 3px 7px rgba(0,0,0,0.3), inset 0 1px 1px rgba(255,255,255,0.16);
  display: flex;
  align-items: center;
  justify-content: center;
}
.cam-lens-mid {
  width: 74%; height: 74%;
  border-radius: 50%;
  background: #171615;
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
}
.cam-lens-glass {
  width: 62%; height: 62%;
  border-radius: 50%;
  /* flat teal glass with one small hard highlight — no shading blends, to
     match the flat treatment across the rest of the camera */
  background: #2b4a4a;
  box-shadow: inset 0 0 4px rgba(0,0,0,0.6);
  position: relative;
}
.cam-lens-glass::after {
  content: "";
  position: absolute;
  top: 18%; left: 20%;
  width: 26%; aspect-ratio: 1;
  border-radius: 50%;
  background: rgba(255,255,255,0.42);
}
/* viewfinder block, top-right */
.cam-viewfinder {
  position: absolute;
  top: 8%; right: 6%;
  width: 20%; aspect-ratio: 1 / 0.86;
  border-radius: 14%;
  background: #2b2926;
  box-shadow: inset 0 1px 1px rgba(255,255,255,0.14);
}
.cam-viewfinder::after {
  content: "";
  position: absolute;
  top: 22%; left: 22%;
  width: 56%; height: 56%;
  border-radius: 22%;
  background: #100f0f;
}
/* the one orange accent — kept because it's the detail that reads
   "Polaroid" fastest; the −/+ marks around it were fussy at this size */
.cam-slider-knob {
  position: absolute;
  top: 60%; right: 11%;
  width: 11%; height: 4.5%;
  border-radius: 999px;
  background: #f0ad20;
  box-shadow: 0 1px 2px rgba(0,0,0,0.28);
}
/* red shutter button, bottom-left of the cream body */
.cam-shutter {
  position: absolute;
  bottom: 8%; left: 9%;
  width: 12.5%; aspect-ratio: 1;
  border-radius: 50%;
  background: #d8352a;
  box-shadow: 0 0 0 3px #f7f5f0, 0 1px 3px rgba(0,0,0,0.22);
}
/* the dark handle bridging the two bodies */
.cam-handle {
  position: absolute;
  top: -7%; left: 50%;
  transform: translateX(-50%);
  width: 34%; height: 15%;
  border-radius: 999px;
  background: #3a3835;
}
/* the sliding catch on the handle — a rounded bar, not a circle: a dot there
   read as a stray hole rather than part of the mechanism */
.cam-handle::after {
  content: "";
  position: absolute;
  top: 50%; left: 52%;
  transform: translateY(-50%);
  width: 34%; height: 52%;
  border-radius: 999px;
  background: #6d6a66;
}
/* the slot the photos come out of */
.cam-eject {
  position: absolute;
  top: 30%; left: 5%;
  width: 90%; height: 9%;
  border-radius: 3px;
  background: #08080a;
  box-shadow: inset 0 2px 3px rgba(0,0,0,0.9), 0 1px 0 rgba(255,255,255,0.08);
}
/* the rainbow stripe, bottom-left */
.cam-rainbow {
  position: absolute;
  bottom: 12%; left: 10%;
  width: 17%; height: 30%;
  background: linear-gradient(180deg,
    #4a90d9 0 25%,
    #57b85e 25% 45%,
    #f2c53d 45% 62%,
    #ef8c3a 62% 80%,
    #e0483c 80% 100%);
  border-radius: 1px;
}
/* camera shutter flash on tap — plain white overlay that pops then fades */
/* Shutter flash — blinks inside the top-right box on the camera itself.
   It sits in that box in the markup, so it's positioned against it, and the
   box-shadow lets the light bleed a little past the box's edges. */
.camera-frame .flash {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  z-index: 2; /* above the viewfinder's ::after, which is painted as its last child */
}
.camera-frame .flash.fire {
  animation: camera-flash 0.42s ease-out;
}
/* two quick pulses rather than one fade — a blink reads as a flash firing,
   a single fade reads as a light being switched off */
@keyframes camera-flash {
  0%   { opacity: 0;   box-shadow: 0 0 0 0 rgba(255,255,255,0); }
  12%  { opacity: 1;   box-shadow: 0 0 16px 7px rgba(255,255,255,0.9); }
  34%  { opacity: 0.2; box-shadow: 0 0 6px 2px rgba(255,255,255,0.25); }
  52%  { opacity: 0.95; box-shadow: 0 0 13px 6px rgba(255,255,255,0.75); }
  100% { opacity: 0;   box-shadow: 0 0 0 0 rgba(255,255,255,0); }
}

/* One photo comes out per camera tap and lands in this pile — no grid of
   empty frames up front. Tapping the pile itself moves on to the next
   step. See revealNextPhoto() in app.js. */
.photo-stack {
  position: relative;
  width: 144px; /* grown to match the padding bump, so the photo itself stays the same size */
  height: 150px;
  margin: 20px auto 0;
  cursor: pointer;
}
/* The resting jitter/tilt + the "start near the camera" offset are set from
   JS as plain literal transform strings (see revealNextPhoto in app.js) —
   NOT via CSS custom properties/calc(). Transitioning a transform built from
   var()/calc() didn't reliably animate here (jumped straight to the end
   value), so the transform itself is just declared to transition below and
   JS drives its actual values directly. Frame+photo move as one rigid piece
   so they never drift apart ("กรอบขาวไม่ตรง"). */
.photo-stack .photo-slot {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* opacity is fast on purpose — it should become visible almost right away
     (right at the camera), THEN the slow part is watching it travel/grow/
     focus down into place over 1.6s linear. A front-loaded ease curve on
     everything at once made the whole thing read as an instant pop. */
  transition: opacity 0.2s ease, transform 1.6s linear;
}
.photo-stack .photo-slot.revealed {
  opacity: 1;
}
/* was deliberately left blurred here (no grow/scale-in on the photo itself,
   just the slot's own move) — with real photos in now it just reads as
   broken, so dropped per user request. */
/* polaroid look — shared by the camera pile (.photo-stack) and the photo
   viewer modal (.photo-modal-stack), no image asset needed */
.photo-stack .photo-slot, .photo-modal-stack .photo-slot {
  background: #fff;
  border-radius: 4px;
  padding: 10px 10px 30px; /* a bit more white polaroid margin, esp. the bottom strip */
  display: flex;
  align-items: center;
  justify-content: center;
  color: #c48;
  font-size: 0.75rem;
  overflow: hidden;
  box-shadow: 0 6px 14px rgba(122,32,37,0.18);
}
.photo-stack .photo-inner, .photo-modal-stack .photo-inner {
  background: var(--pink-light);
  width: 100%; height: 100%;
  display: flex; flex-direction: column; gap: 6px;
  align-items: center; justify-content: center;
  border-radius: 2px;
  color: #c48;
}
.photo-stack .photo-inner img, .photo-modal-stack .photo-inner img { width: 100%; height: 100%; object-fit: cover; }
.photo-stack .photo-inner img { filter: blur(6px); } /* stays blurred on the pile until opened in the modal viewer */
#photo-stack.viewed .photo-inner img { filter: none; } /* modal was closed at least once — unblur the pile too */

/* transparent text-only "continue" affordance — only shown after the photo
   modal has been opened & closed at least once, see closePhotoModal() */
.btn-transparent {
  display: block;
  margin: 48px auto 0;
  background: none;
  border: none;
  color: var(--pink);
  font-family: 'Mali', sans-serif;
  font-size: 0.9rem;
  text-decoration: underline;
  cursor: pointer;
}
.btn-transparent[hidden] { display: none; } /* author `display` rules above beat the UA [hidden] rule otherwise — this was showing regardless of the hidden attribute */

/* Step 6's continue link — text only, per request (the pill/border/bounce
   version read as too much). Scoped to the id, not .btn-transparent itself,
   since that base class is shared with the retired notebook step below.
   Handwriting font to match this screen's "ยิ้มหน่อยยย" heading (.script). */
#gallery-continue {
  margin: 32px auto 0;
  background: none;
  border: none;
  color: var(--pink);
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 1.05rem;
}
#gallery-continue[hidden] { display: none; }

/* Photo viewer modal — full-screen blurred backdrop, one polaroid focused
   in the middle at a time. Tapping the front card sends it to the back
   (z-index reshuffle) so the next one shows; loops forever. See
   openPhotoModal()/cyclePhotoModal() in app.js. */
.photo-modal {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(40, 20, 25, 0.45);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  transition: opacity 0.25s ease;
}
.photo-modal[hidden] { display: none; }
.photo-modal.active { opacity: 1; }
.modal-close {
  position: absolute;
  top: 18px;
  left: 18px;
  background: none;
  border: none;
  color: #fff;
  font-size: 2rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.8;
}
.photo-modal-stack {
  position: relative;
  width: 244px;
  height: 284px; /* taller than the pile's polaroid — the extra is the caption's bottom white strip below, see padding-bottom override next */
}
/* each card's transform (offset + scale, so the ones behind visibly peek
   out from under the front card) is set inline from JS per its depth in
   the stack — see applyModalDepths() in app.js. This rule only owns the
   transition, so changing depth animates as a real "shuffle to the back"
   move instead of an instant swap. */
.photo-modal-stack .photo-slot {
  position: absolute;
  inset: 0;
  cursor: pointer;
  transition: transform 0.35s ease;
  padding-bottom: 64px; /* focused view only — room for a 2-3 line caption, unlike the compact pile */
}
.photo-modal-stack .photo-slot.front { cursor: pointer; }
/* caption text sits in the polaroid's own white bottom margin, like a
   handwritten note under the photo. Anchored from the TOP of the caption
   strip (not bottom) so line 1 lands at the same spot for every photo —
   bottom-anchoring made 1-line captions sit lower and 2-line captions push
   their first line up, so the top edge never matched across photos. */
.photo-modal-stack .photo-caption {
  position: absolute;
  /* .photo-slot reserves a fixed 64px strip below the photo (padding-bottom
     above) — anchor the caption's TOP 6px into that strip, not its bottom,
     so line 1 always lands here regardless of whether the caption wraps to
     1 or 2 lines. */
  top: calc(100% - 72px);
  left: 12px; right: 12px;
  text-align: center;
  font-family: 'JaoTomato', 'Mali', sans-serif;
  /* font-family: 'Caveat', cursive; */
  font-size: 1rem;
  line-height: 1.35;
  color: #6b4a35;
}

/* #bg-music — persistent background-music iframe, lives outside #app in
   index.html so it survives every step change. Tiny + off-screen since it's
   audio-only. See startBgMusic() in app.js / DATA.song in data.js. */
#bg-music {
  position: fixed;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

.credit {
  text-align: center;
  font-size: 0.7rem;
  color: #c99;
  padding: 6px 0 14px;
}
.credit a { color: inherit; }

/* native <audio>, tinted to match the theme instead of default grey (native accent-color, no custom player needed) */
audio { width: 100%; margin-top: 12px; accent-color: var(--pink); border-radius: 999px; }

/* Letter — inside the closing scrapbook spread, so no box of its own:
   the .sb-paper page underneath already IS the paper. */
.letter {
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 1.25rem;
  line-height: 1.6;
  white-space: pre-wrap;
  min-height: 150px;
  color: var(--text);
}

/* ---- Step 8 (current): Closing Scrapbook — a ruled sheet with a photo-booth
   strip, a month calendar circling DATA.metDate, a flower and the closing
   message layered over it. See CONTEXT.md "Step 8 ending is a Closing
   Scrapbook". Every piece is absolutely positioned in % of the step, so the
   whole collage scales with the viewport rather than breaking apart. ---- */
.scrapbook-end { padding: 0; justify-content: center; }

/* ROOT-CAUSE FIX for pieces getting clipped (the flower's petals, the strip's
   bottom, the message's last line): the collage used to be positioned in % of
   the step while its type, tape and paperclip were frozen in px, and the
   flower deliberately hung outside on `right: -5%`. #app clips overflow, so on
   any viewport shorter or narrower than the one it was tuned on, whatever
   spilled past the edge was simply cut off.
   Now the whole thing lives on one fixed-ratio canvas that always fits inside
   the viewport — `min()` on both axes shrinks it to whichever axis runs out
   first — and every piece is sized in cqw (% of the canvas's own width) so it
   scales with the canvas instead of overflowing it. Nothing is positioned
   outside the canvas any more, so there is nothing left to clip. */
/* The canvas is deliberately NARROWER than #app (92%), not full-bleed. Every
   card here is rotated a few degrees for the hand-placed look, and rotating a
   tall box swings its corners sideways by roughly height/2 * sin(angle) — for
   the 500px-tall photo strip at 2.5deg that is ~11px, which is how the strip
   ended up at x:-7 and got clipped even though its `left` was positive. The
   gutter is the room for that swing, so no piece can reach #app's edge.
   Width is capped off the viewport height too, so the ratio holds either way. */
.pb-canvas {
  position: relative;
  /* 400px cap: without it the canvas grows to fill #app (max-width 540) on
     desktop — ~448px against a phone's ~345px, which read as oversized. The
     cap is above every phone width (430px viewport -> 396px canvas) so it only
     ever bites on desktop, keeping this screen the phone-width frame the rest
     of the site is designed as. */
  width: min(92%, calc((100vh - 12px) * 375 / 660), 400px);
  aspect-ratio: 375 / 660; /* the ratio the composition actually fills */
  margin: auto;
  container-type: size; /* makes cqw below resolve against this box */
}

/* ---- Step 8 (current): Photo Collage — overlapping tilted prints with the
   closing message handwritten across them. Sits on .pb-canvas above, so it
   inherits the fit-and-scale behaviour. ---- */
.pc-photo {
  position: absolute;
  background: #fffdf8;
  padding: 2.1cqw 2.1cqw 6.5cqw; /* deep bottom margin = reads as a print, not a framed image */
  filter: drop-shadow(0 4px 10px rgba(60,45,30,0.35));
}
.pc-photo img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: #e7e0d4; /* placeholder tint until assets/photos/N.webp exist */
}
.pc-photo img.empty { background: #e7e0d4; }
/* hand-placed, deliberately uneven — an even fan reads as a gallery grid */
.pc-photo-1 { top: -1%; left: 0%;  width: 62%; transform: rotate(-3.5deg); z-index: 2; }
.pc-photo-2 { top: 6%;  left: 40%; width: 57%; transform: rotate(3deg);    z-index: 3; }
.pc-photo-3 { top: 67%; left: 3%;  width: 40%; transform: rotate(2.5deg);  z-index: 2; } /* moved down + shrunk so it mostly clears .pc-note's bottom edge (28.5%-68.9%) instead of covering the note's text or sitting fully hidden under it */
.pc-photo-4 { top: 68%; left: 33%; width: 62%; transform: rotate(-2deg);   z-index: 1; }

/* Stickers. Positions are hand-tuned in % of the canvas and sizes in cqw so
   the whole arrangement scales as one unit. Every rotated piece is kept a few
   % off the canvas edge: rotating swings the corners sideways by roughly
   height/2 * sin(angle), and #app clips overflow. */
.pc-sticker { position: absolute; display: block; pointer-events: none; }
/* the star sits ON the first print's corner, like the reference — in front of
   the photos, not behind them */
.pc-star-a { top: -12%; left: -10%; width: 36%; transform: rotate(-14deg); z-index: 4; } /* moved up + to the left side */
/* scribble hearts on the open paper at the top right */
.pc-heart-a { top: -9%; left: 71%; width: 14%; transform: rotate(8deg);   z-index: 6; }
.pc-heart-e { top: -1%; left: 66%; width: 8%; transform: rotate(-14deg); z-index: 6; }
.pc-heart-b { top: 2%;  left: 76%; width: 7%; transform: rotate(-6deg);  z-index: 6; }
/* the two small ones punctuate the handwriting */

/* buttons: on top of the prints, half-off the photo edge like real ones */
.pc-buttons      { top: 4%; left: 85%; width: 21%; transform: rotate(6deg);   z-index: 7; }
.pc-button-heart { top: 68%; left: 14%; width: 24%; transform: rotate(-10deg); z-index: 7; }
.pc-flower-2 { top: 39%;  left: -15%; width: 28%; transform: rotate(8deg) scaleX(-1); z-index: 6; } /* pink flower — note's left edge, mid-height, bigger, tilted a bit right */
.pc-flower-3 { top: 68%;  left: 83%;  width: 24%; transform: rotate(10deg); z-index: 2; } /* other-color flower — top of the bottom-right photo */
.pc-camera-2 { top: 85%; left: -4%;  width: 32%; transform: rotate(-8deg); z-index: 7; } /* nudged up + right */
.pc-clip { top: 25%; left: 88%; width: 12%; transform: rotate(80deg); z-index: 7; } /* turned near-horizontal/diagonal so it sits along the note's top edge instead of hanging down across the text */
.pc-arrow-text { top: 90%; left: 55%; width: 34%; transform: rotate(-6deg); z-index: 7; } /* combined luv-you-with-hearts-and-bow sticker, replaces the separate arrow + text */
/* extra bottom-left heart, per reference mockup */
.pc-heart-c { top: 80%; left: -6%; width: 12%; transform: rotate(-10deg); z-index: 7; }

/* The handwriting. It used to sit straight on the photos with a white band
   behind each line (box-decoration-break on an inline span); it now lives on
   the ruled note card below, which does that job better. */
.pc-hand {
  position: absolute;
  margin: 0;
  z-index: 5;
  font-family: 'Caveat', 'JaoCherry', cursive;
  color: #2e241d;
}
.pc-note {
  /* The ruled note card IS this element's background, not a separate <img>:
     one box means the card and the words in it cannot drift apart, and the
     padding below is what keeps the text off the wavy pink border.
     aspect-ratio comes from the asset (1536x1024) so the rules stay unstretched. */
  top: 29%; left: -2%;
  width: 104%;
  aspect-ratio: 1536 / 1024;
  background: url('../assets/stickers/final-decorate-page/note-card.webp') center / 100% 100% no-repeat;
  filter: drop-shadow(0 4px 10px rgba(60,45,30,0.28));
  /* text sits toward the top of the card, per request — a longer message grows
     downward into the empty rules instead of pushing off the top edge */
  display: flex;
  align-items: flex-start;
  justify-content: center;
  box-sizing: border-box;
  padding: 9% 11% 7%;
  text-align: center;
  /* JaoTomato, a second Thai handwriting face — user-supplied, swapped in for
     Mali/JaoCherry after both read poorly at this message's small size. */
  font-family: 'JaoTomato', 'Mali', sans-serif;
  /* smaller type on a bigger card, per request: 3.7cqw = 13px phone / 15px
     desktop. Below this the Thai handwriting face stops being legible on a
     phone, so treat it as the floor. */
  font-size: 5cqw;
  line-height: 1.3;
  transform: rotate(-1deg);
}

/* Washi tape — CSS-drawn (no matching asset in the project yet), small
   half-transparent strips pinning a couple of the prints down like the
   reference. Kept plain/solid rather than a busy pattern so it reads as
   tape, not another sticker competing with the photos themselves. */
.pc-tape {
  position: absolute;
  z-index: 8;
  width: 14cqw;
  height: 6cqw;
  opacity: 0.75;
  box-shadow: 0 2px 4px rgba(60,45,30,0.15);
}
.pc-tape-1 { top: -2.5cqw; right: 6cqw; background: #f3c9d6; transform: rotate(-8deg); }
.pc-tape-2 { top: -2.5cqw; left: 28cqw; background: #cdd3ef; transform: rotate(6deg); }
.pc-tape-4 { top: -2.5cqw; right: 26cqw; background: #f3c9d6; transform: rotate(5deg); }

/* ---- Step 8, previous layout (hidden, see index.html): ruled sheet +
   photo-booth strip + month calendar. ----
   shared paper look for the three "cut out and stuck down" cards */
.pb-note, .pb-strip, .pb-calendar, .pb-message {
  position: absolute;
  background: #fffdf8;
  filter: drop-shadow(0 6px 12px rgba(60,45,30,0.28)); /* filter, not box-shadow — masked/torn edges keep their shadow */
}

/* the big ruled sheet everything else sits on */
.pb-note {
  top: 5%; left: 25%;
  width: 72%; height: 56%;
  background-image: repeating-linear-gradient(0deg, transparent 0 5.6cqw, rgba(150,120,90,0.18) 5.6cqw 5.87cqw);
  transform: rotate(1.5deg);
}
/* punched holes down the sheet's left edge */
.pb-punch {
  position: absolute;
  left: 1.9cqw;
  width: 1.9cqw; height: 2.4cqw;
  border-radius: 2px;
  background: rgba(90,60,45,0.55);
}
.pb-punch:nth-of-type(1) { top: 7%; }
.pb-punch:nth-of-type(2) { top: 12%; }
.pb-punch:nth-of-type(3) { top: 17%; }

/* washi tape holding a card down */
.pb-tape {
  position: absolute;
  width: 14.4cqw; height: 5.3cqw;
  background: repeating-linear-gradient(135deg, rgba(196,164,116,0.55) 0 5px, rgba(226,202,160,0.55) 5px 10px);
}
.pb-tape-note { top: -2.7cqw; left: 32%; transform: rotate(-2deg); }
.pb-tape-cal  { top: -2.4cqw; left: 18%; transform: rotate(-6deg); }

.pb-happy {
  position: absolute;
  top: 8%; left: 14%;
  margin: 0;
  font-family: 'Yellowtail', cursive; /* already loaded — no extra font request for one heading */
  font-size: 6.6cqw;
  font-weight: 700;
  color: var(--pink);
}
/* CSS-drawn hearts, same construction as .icon-heart */
.pb-heart-solid, .pb-heart-doodle {
  position: relative;
  display: inline-block;
  width: 1em; height: 0.9em;
  color: var(--pink);
  vertical-align: -0.05em;
}
.pb-heart-solid::before, .pb-heart-solid::after,
.pb-heart-doodle::before, .pb-heart-doodle::after {
  content: "";
  position: absolute;
  top: 0; left: 0.5em;
  width: 0.5em; height: 0.8em;
  background: currentColor;
  border-radius: 0.5em 0.5em 0 0;
  transform: rotate(-45deg);
  transform-origin: 0 100%;
}
.pb-heart-solid::after, .pb-heart-doodle::after { left: 0; transform: rotate(45deg); transform-origin: 100% 100%; }
/* the loose heart doodled on the sheet's right side */
.pb-heart-doodle {
  position: absolute;
  top: 19%; right: 13%;
  font-size: 5.9cqw;
  opacity: 0.5;
  transform: rotate(-10deg);
}

/* --- photo-booth strip --- */
.pb-strip {
  top: 11%; left: 1%;
  width: 34%;
  padding: 7px 6px 5px;
  text-align: center;
  transform: rotate(-2.5deg);
  /* perforated edge down BOTH sides — two mask layers intersected, so each
     side keeps its own row of bites out of the paper */
  -webkit-mask-image:
    radial-gradient(circle 4px at 0 8px, transparent 4px, #000 4.5px),
    radial-gradient(circle 4px at 100% 8px, transparent 4px, #000 4.5px);
  mask-image:
    radial-gradient(circle 4px at 0 8px, transparent 4px, #000 4.5px),
    radial-gradient(circle 4px at 100% 8px, transparent 4px, #000 4.5px);
  -webkit-mask-size: 100% 16px;
  mask-size: 100% 16px;
  -webkit-mask-repeat: repeat-y;
  mask-repeat: repeat-y;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.pb-strip-top, .pb-strip-bottom {
  margin: 0;
  font-family: Georgia, 'Times New Roman', serif; /* system serif — nothing new to download */
  color: var(--text);
}
.pb-strip-top { font-size: 2.56cqw; letter-spacing: 0.02em; padding: 2px 0 6px; }
.pb-strip-bottom { font-size: 3.6cqw; font-weight: 700; padding: 6px 0 2px; }
.pb-shot + .pb-shot { margin-top: 5px; }
.pb-img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  background: #e7e0d4; /* placeholder tint until assets/photos/N.webp exist */
}
.pb-img.empty { background: #e7e0d4; }
/* photo-booth strips are black and white — but only once there IS a photo,
   otherwise this greys the warm placeholder tint out to a dead slab */
.pb-img[src] { filter: grayscale(1) contrast(1.05); }
/* the little heart tucked beside the strip's bottom caption */
.pb-heart-strip {
  position: absolute;
  bottom: 24%; left: 32%;
  font-size: 4.3cqw;
  transform: rotate(9deg);
  z-index: 2;
}

/* --- calendar --- */
.pb-calendar {
  top: 42%; right: 1%;
  width: 54%;
  padding: 12px 10px 14px;
  text-align: center;
  transform: rotate(-1deg);
}
.pb-cal-year {
  display: block;
  font-family: Georgia, 'Times New Roman', serif; /* system serif — nothing new to download */
  font-size: 2.56cqw;
  color: var(--text);
}
.pb-cal-month {
  display: block;
  font-family: Georgia, 'Times New Roman', serif; /* system serif — nothing new to download */
  font-size: 4.7cqw;
  color: var(--text);
  margin-bottom: 1.6cqw;
}
.pb-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.5cqw 0;
  font-size: 2.13cqw;
  color: var(--text);
}
.pb-cal-head { opacity: 0.65; padding-bottom: 3px; }
.pb-cal-cell {
  /* square-ish cells so the circled date's ring lands on the number, not
     an oval around it */
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pb-cal-cell.is-met {
  background: var(--pink);
  color: #fff;
  border-radius: 50%;
  font-weight: 700;
}

.pb-flower {
  position: absolute;
  bottom: 12%; right: 0;
  width: 34%;
  /* the reference's flowers are dried/bleached — desaturating the daffodil
     asset gets it into the same washed-out family as the paper */
  filter: sepia(0.45) saturate(0.5) brightness(1.05) drop-shadow(0 5px 9px rgba(60,45,30,0.3));
  transform: rotate(6deg);
  z-index: 1;
}

/* --- the closing message, on its own torn-off scrap --- */
.pb-message {
  bottom: 2%; left: 22%;
  width: 62%;
  font-size: 3.84cqw; /* the text inherits this, so the em-based ruling below tracks it */
  padding: 1em 1em 1.1em 1.7em;
  transform: rotate(-1.5deg);
  /* ruling under the text, plus a torn bottom edge. In em, so the lines stay
     locked to the text's line-height at every canvas scale. */
  background-image: repeating-linear-gradient(0deg, transparent 0 1.73em, rgba(150,120,90,0.2) 1.73em 1.8em);
  background-position: 0 0.7em;
  -webkit-mask-image: radial-gradient(circle 5px at 6px 100%, transparent 5px, #000 5.5px);
  mask-image: radial-gradient(circle 5px at 6px 100%, transparent 5px, #000 5.5px);
  -webkit-mask-size: 12px 100%;
  mask-size: 12px 100%;
  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  z-index: 2;
}
.pb-message-text {
  margin: 0;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 1em;
  line-height: 1.8; /* matches the ruling above so the text sits ON the lines */
  text-align: center;
  color: var(--text);
}
/* paperclip on the scrap's left edge */
.pb-clip {
  position: absolute;
  top: 3.2cqw; left: -0.8cqw;
  width: 2.9cqw; height: 9.1cqw;
  border: 2px solid #c9a86a;
  border-radius: 6px;
  transform: rotate(-6deg);
  z-index: 3;
}
.pb-clip::before {
  content: "";
  position: absolute;
  top: 1.3cqw; left: 0.3cqw;
  width: 1.3cqw; height: 5.9cqw;
  border: 2px solid #c9a86a;
  border-top: 0;
  border-radius: 0 0 4px 4px;
}

/* ---- Step 8 (dead, kept per CONTEXT.md — was the Wish Jar): glass jar of
   paper cranes, tap the jar to open it, tap each crane to reveal its
   message. Unused since Step 8 became the Closing Scrapbook above. ---- */
.wish-jar {
  position: relative;
  width: min(52vw, 190px);
  margin: 0 auto;
  cursor: pointer;
  animation: cover-float 5s ease-in-out infinite;
}
.wj-glass {
  width: 100%;
  height: auto;
  display: block;
  filter: drop-shadow(0 14px 18px rgba(0,0,0,0.2));
  transition: transform 0.4s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.wish-jar.open .wj-glass { transform: scale(1.04) translateY(-4px); }

/* sits over the jar itself (nested inside .wish-jar) so the cranes can start
   clustered at the jar's neck, visible before it's ever tapped */
.wj-cranes {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.wj-crane {
  position: absolute;
  top: 46%; left: 50%;
  width: 118px;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  /* not clickable until popped out — taps on an unpopped crane should fall
     through to the jar itself (data-action="open-jar"), not reveal early */
  pointer-events: none;
  opacity: 1;
  /* closed: scattered around inside the jar's body (not stacked in one
     spot), each rotated/flipped a different way so it reads as a pile of
     distinct cranes rather than one copy-pasted 5 times */
  transform:
    translate(-50%, -50%)
    rotate(calc((var(--i) - 2) * 22deg))
    scale(0.5) scaleX(var(--flip, 1));
  transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.22, 0.9, 0.3, 1), top 0.6s cubic-bezier(0.22, 0.9, 0.3, 1), left 0.6s cubic-bezier(0.22, 0.9, 0.3, 1);
}
/* scatter positions down at the jar's bottom — hand-placed, not formulaic,
   so they don't line up in an obvious row/grid */
.wj-crane:nth-child(1) { top: 68%; left: 40%; }
.wj-crane:nth-child(2) { top: 78%; left: 60%; }
.wj-crane:nth-child(3) { top: 84%; left: 44%; }
.wj-crane:nth-child(4) { top: 76%; left: 32%; }
.wj-crane:nth-child(5) { top: 70%; left: 58%; }
/* each crane pops out on its own tap of the jar (one per tap, not all at
   once) — see the click handler in app.js, which adds .popped in order */
.wj-crane.popped {
  pointer-events: auto;
  top: 100%; left: 50%;
  transform:
    translate(calc(-50% + (var(--i) - 2) * 52px), 20px)
    rotate(calc((var(--i) - 2) * 9deg))
    scale(1) scaleX(var(--flip, 1));
}
.wj-crane img {
  width: 100%;
  height: auto;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.15));
}
.wj-msg {
  position: absolute;
  bottom: 100%; left: 50%;
  transform: translate(-50%, 8px);
  width: 132px;
  background: #fff8ef;
  border-radius: 10px;
  padding: 8px 10px;
  margin-bottom: 8px;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 0.8rem;
  line-height: 1.4;
  text-align: center;
  color: var(--text);
  box-shadow: 0 4px 10px rgba(0,0,0,0.15);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.wj-crane.revealed .wj-msg { opacity: 1; transform: translate(-50%, 0); }

.neon { color: #ff6fa5; text-shadow: 0 0 12px rgba(255,111,165,0.7); }

/* one-time shimmer sweep on entry, not an infinite pulse loop — draws the eye
   once without nagging at it forever */
.card.dark .btn-primary {
  position: relative;
  overflow: hidden;
}
.card.dark .btn-primary::after {
  content: "";
  position: absolute;
  top: 0; left: -60%;
  width: 40%; height: 100%;
  background: linear-gradient(115deg, transparent, rgba(255,255,255,0.55), transparent);
  animation: shimmer 2.2s ease-out 0.6s 1;
}
@keyframes shimmer {
  to { left: 130%; }
}

/* floating background decoration — img now (assets/bg/effect-background/), was a CSS-drawn icon */
.bg-heart {
  position: fixed;
  bottom: -10%;
  opacity: var(--o, 0.5);
  width: var(--size, 1.2rem);
  height: auto;
  animation: var(--anim, floatUp) linear forwards;
  pointer-events: none;
  z-index: 0; /* behind .card (z-index 1) — was 5 (on top of everything), asked to sit behind the paper instead */
}
/* straight-up float, gentle sway */
@keyframes floatUp {
  from { transform: translateY(0) translateX(0) rotate(0deg); opacity: var(--o, 0.5); }
  50% { transform: translateY(-55vh) translateX(var(--sway, 20px)) rotate(15deg); }
  80% { opacity: var(--o, 0.5); }
  to { transform: translateY(-110vh) translateX(0) rotate(-10deg); opacity: 0; }
}
/* drifts diagonally sideways as it rises, bigger horizontal travel */
@keyframes floatDrift {
  from { transform: translate(0, 0) rotate(0deg); opacity: var(--o, 0.5); }
  50% { transform: translate(var(--sway, 60px), -50vh) rotate(-20deg); }
  80% { opacity: var(--o, 0.5); }
  to { transform: translate(var(--sway, 60px), -100vh) rotate(20deg); opacity: 0; }
}
/* lazy side-to-side wobble while barely rising, more "floating in place" */
@keyframes floatWobble {
  from { transform: translate(0, 0) rotate(0deg); opacity: var(--o, 0.5); }
  25% { transform: translate(var(--sway, 30px), -20vh) rotate(10deg); }
  50% { transform: translate(calc(var(--sway, 30px) * -1), -45vh) rotate(-10deg); }
  75% { transform: translate(var(--sway, 30px), -70vh) rotate(10deg); }
  80% { opacity: var(--o, 0.5); }
  to { transform: translate(0, -105vh) rotate(0deg); opacity: 0; }
}

/* 2026-07-20: explicitly asked to stop splitting mobile vs desktop styling —
   the two breakpoints that used to widen #app/.card at 700px/1000px caused a
   visible seam/rectangle where the wider box met the body background. One
   fixed layout at every viewport width now (#app stays 480px, see above). */

/* ---- Step 7: the notebook -------------------------------------------------
   The scrapbook page and the timeline in one screen. html/body lock scrolling
   site-wide (see the note on html,body above), so the scrolling happens inside
   .spread-scroll, never on the page. */
.step:has(> .spread) { padding: 0; justify-content: flex-start; } /* full-bleed — the paper IS the screen here. Keyed off .spread, not the step number, so renumbering steps can't hand this to a different screen. */

.spread {
  position: relative;
  z-index: 1; /* same trick as .card — keeps the floating .bg-heart stickers behind the page */
  flex: 1;
  width: 100%;
  min-height: 0; /* lets the flex child actually shrink, which is what makes it scroll */
  margin: 0 auto;
  overflow: hidden;
  background: #fdfbf7; /* base tint only — the paper itself is drawn on the scroller below */
}

/* The paper — grid lines and spiral binding — is painted on the SCROLLER, with
   background-attachment: local, so it travels with the entries instead of
   sitting still behind them. That is the whole trick: one long sheet moving
   past the window, not a window onto a fixed sheet. (A background on a scroll
   container is glued to its border box by default; `local` is what unglues it.)
   The spiral is two of these layers rather than an element, because an element
   would need a wrapper as tall as the content to scroll along. */
.spread-scroll {
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 18px 18px 0; /* was `0 40px` on the left to clear the spiral binding, which is gone */
  scrollbar-width: none; /* the scrollbar reads as a browser chrome artifact on a paper page */
  background-image:
    /* faint cream ruling, warmer than the old blue grid — closer to a real journal page */
    repeating-linear-gradient(0deg, rgba(150,120,90,0.16) 0 1px, transparent 1px 16px),
    /* paper texture/grain underneath everything. The wash went 0.55 -> 0.80:
       at 0.55 the paper.webp grain showed through as a muddy brown-beige,
       noticeably dingier than the rest of the site. Ruling nudged up a touch
       to stay visible against the lighter ground. */
    linear-gradient(rgba(255,253,248,0.8), rgba(255,253,248,0.8)), url('../assets/bg/image/paper.webp');
  background-position: 0 0, center;
  background-size: auto, cover;
  background-repeat: repeat, no-repeat;
  background-attachment: local, local;
}
.spread-scroll::-webkit-scrollbar { display: none; }
.sp-heading {
  margin: 0 0 10px;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 1.5rem;
  color: var(--pink);
  text-align: center;
}

/* ---- collage page — the whole of Step 7, one fixed scrapbook spread (no
   more scrolling timeline list below it). Same washi-tape/torn-paper
   language as the Step 8 letter (.sb-*), laid out in three loose columns
   like a real journal page. ---- */
/* hidden unless #spread has .spread-alt (set by app.js only for ?page=7-1) —
   the old scrolling timeline (.sp-heading/.sp-entries) is Step 7 by default.
   The :not() override below has higher specificity than the base rule, so
   it wins regardless of source order. */
.spread:not(.spread-alt) .sp-collage { display: none; }
.spread.spread-alt .sp-heading,
.spread.spread-alt .sp-entries { display: none; }

.sp-collage {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) auto minmax(0, 0.85fr);
  gap: 10px;
  padding: 18px 6px 30px;
}
.sp-col { display: flex; flex-direction: column; align-items: center; gap: 14px; position: relative; }
.sp-col-left { align-items: flex-start; padding-top: 6px; }
.sp-col-right { align-items: flex-end; padding-top: 40px; }

.sp-love-title {
  display: block;
  width: 128px;
  margin: -4px 0 -6px -4px;
  transform: rotate(-3deg);
}
.sp-love-hearts { display: block; font-size: 0.9rem; letter-spacing: 2px; color: var(--pink); transform: rotate(4deg); }

.sp-tag {
  display: inline-block;
  background: #2c2622;
  color: #fdf8ee;
  font-family: 'JaoCherry', cursive;
  font-size: 0.68rem;
  padding: 3px 8px;
  border-radius: 2px;
  transform: rotate(-1deg);
}

.sp-doodle-star, .sp-doodle-heart {
  color: var(--pink);
  font-size: 1.1rem;
  opacity: 0.8;
  align-self: flex-start;
}
.sp-col-right .sp-doodle-star, .sp-col-right .sp-doodle-heart { align-self: flex-end; }

/* torn sticky note — reuses the Step 8 letter's paper-and-tape recipe at a smaller scale */
.sp-sticky {
  position: relative;
  background: #fffdf8;
  padding: 10px 12px;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 0.95rem;
  line-height: 1.35;
  color: var(--purple);
  box-shadow: 0 3px 8px rgba(60,45,30,0.22);
  transform: rotate(-2deg);
}
.sp-sticky p { margin: 0; }

.sp-note-scrap {
  position: relative;
  background: #fff;
  padding: 10px 12px;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 0.95rem;
  color: var(--purple);
  box-shadow: 0 3px 8px rgba(60,45,30,0.2);
  transform: rotate(2deg);
  text-align: right;
}

/* washi tape strip, reused wherever a paper scrap needs pinning down */
.sp-tape {
  position: absolute;
  width: 46px; height: 16px;
  background: repeating-linear-gradient(135deg, rgba(214,180,150,0.55) 0 5px, rgba(245,228,206,0.6) 5px 10px);
  box-shadow: 0 1px 3px rgba(60,45,30,0.15);
}
.sp-note-tape { top: -8px; right: 6px; transform: rotate(8deg); }

/* ticket stub — notched sides via clip-path, dashed tear line down the middle */
.sp-ticket {
  position: relative;
  width: 100px;
  padding: 8px 10px;
  background: #e7a7b0;
  color: #5b2530;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%,
    0 62%, 6% 58%, 0 54%,
    0 46%, 6% 42%, 0 38%);
  transform: rotate(2deg);
  box-shadow: 0 3px 6px rgba(60,45,30,0.2);
}
.sp-ticket span { display: block; font-family: 'JaoCherry', cursive; font-size: 0.72rem; font-weight: 700; }
.sp-ticket small { font-size: 0.6rem; }

.sp-tag-label {
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 0.95rem;
  color: var(--pink);
  background: #f2e6cf;
  padding: 4px 10px;
  border-radius: 3px;
  transform: rotate(-2deg);
  box-shadow: 0 2px 5px rgba(60,45,30,0.18);
}

/* photo-booth strip — a vertical black strip like a real photobooth print */
.sp-booth {
  position: relative;
  width: 128px;
  background: #221f1c;
  padding: 10px 8px 12px;
  box-shadow: 0 4px 10px rgba(60,45,30,0.3);
  transform: rotate(-1.5deg);
}
.sp-booth-tape { top: -9px; left: 50%; margin-left: -23px; transform: rotate(-3deg); }
.sp-booth-label {
  margin: 0 0 6px;
  font-family: 'JaoCherry', cursive;
  font-size: 0.7rem;
  color: #fdf8ee;
  text-align: center;
}
.sp-booth .sp-img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: #4a443d;
  margin-bottom: 4px;
}
.sp-booth .sp-img.empty { background: #4a443d; }
.sp-love-forever {
  margin: 4px 0 0;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 1.05rem;
  color: var(--pink);
}

/* speech bubbles — a real tail via clip-path corner triangle */
.sp-bubble {
  position: relative;
  max-width: 118px;
  background: #fff;
  border: 1.5px solid #2c2622;
  border-radius: 16px;
  padding: 8px 10px;
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 0.92rem;
  line-height: 1.25;
  color: #2c2622;
  text-align: center;
}
.sp-bubble::after {
  content: "";
  position: absolute;
  bottom: -8px; right: 18px;
  width: 12px; height: 12px;
  background: #fff;
  border-right: 1.5px solid #2c2622;
  border-bottom: 1.5px solid #2c2622;
  clip-path: polygon(0 0, 100% 100%, 0 100%);
}

/* --- one milestone: a polaroid beside its words, sides alternating --- */
.sp-entry {
  display: flex;
  align-items: center;
  gap: 14px;
  /* Exactly two entries per screenful, then scroll for the next two. Sized off
     the viewport rather than in px so it holds on any phone; a % of the
     scroller can't work here because .sp-entries' own height is auto, which
     makes a percentage indefinite. svh, not vh, so the mobile URL bar
     collapsing doesn't reflow the page mid-scroll. */
  min-height: 44vh;
  min-height: 44svh;
  margin-bottom: 8px;
  /* start hidden — .in is added by the observer as the entry scrolls in */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.sp-entry.in { opacity: 1; transform: none; }
.sp-entry:nth-child(even) { flex-direction: row-reverse; }

/* the polaroid print */
.sp-frame {
  flex: none;
  width: 50%; /* balance — wider prints squeeze .sp-words until Thai titles break badly */
  background: #fffdf8;
  padding: 5px 5px 7px;
  box-shadow: 0 3px 8px rgba(60,45,30,0.28);
}
.sp-frame.sp-polaroid { padding: 8px 8px 26px; } /* the wide bottom margin is what makes it read as a polaroid */
.sp-entry:nth-child(odd)  .sp-frame { transform: rotate(-2.5deg); }
.sp-entry:nth-child(even) .sp-frame { transform: rotate(2.5deg); }

.sp-frame .sp-img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4; /* portrait — fills the taller row without going wide enough to squeeze .sp-words */
  object-fit: cover;
  background: #e7e0d4; /* matched to the lighter paper — #ded5c4 became the darkest thing on the page */
}
/* placeholder — no photo filled in yet. A src-less <img> has no content, so the
   background colour alone stands in for the print. */
.sp-frame .sp-img.empty { background: #e7e0d4; }

/* washi tape holding the print down */
.sp-frame .sp-tape {
  position: absolute;
  top: -8px; left: -12px;
  width: 40px; height: 14px;
  background: repeating-linear-gradient(135deg, rgba(212,175,120,0.8) 0 4px, rgba(236,212,170,0.8) 4px 8px);
  box-shadow: 0 1px 3px rgba(60,45,30,0.2);
}
.sp-frame { position: relative; }
.sp-entry:nth-child(even) .sp-frame .sp-tape { left: auto; right: -12px; transform: rotate(28deg); }
.sp-entry:nth-child(odd)  .sp-frame .sp-tape { transform: rotate(-30deg); }

.sp-words { flex: 1; min-width: 0; }
.tl-date {
  display: block;
  font-size: 0.76rem;
  letter-spacing: 0.04em;
  color: var(--purple);
  margin-bottom: 10px;
}
.tl-title {
  font-family: 'Caveat', 'JaoCherry', cursive;
  font-size: 0.75rem;
  /* 1.15 was too tight for JaoCherry — its tall tone marks/ascenders from one
     line were overlapping into the line above it on multi-line titles */
  line-height: 1.6;
  color: var(--pink);
  margin: 0;
}
.tl-note {
  margin: 4px 0 0;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text);
}

/* last item in the scroller, so reaching it means having scrolled the lot */
.spread-continue { display: block; margin: 4px auto 34px; }

/* Palette lifted from LoveMemo's starry-night template (the "question" feature
   there is the same fake-chat idea) — a dreamy pastel pink that reads sweeter
   than this site's kraft/maroon. Scoped to the chat only; the rest of the
   site keeps its own palette. */
.step[data-step="3"] {
  --chat-ink: #5b3a4a;
  --chat-dim: #b98aa0;
  --chat-accent: #ffb3d1;
  --chat-line: rgba(255, 111, 160, 0.25);
}

/* ---- Step 3: the chat ------------------------------------------------------
   A messaging-app conversation. Only .chat-scroll scrolls (html/body lock
   scrolling site-wide); the replies sit under it in a fixed row, the way a
   keyboard would. */
.step[data-step="3"] { padding: 20px 10px 14px; justify-content: flex-start; }

/* ponytail: bigger frame just for the chat step, via :has instead of a JS-toggled class */
body:has(.step[data-step="3"].active) #app { max-width: 640px; }

.chat-scroll {
  flex: 1;
  position: relative;
  z-index: 1; /* same trick as .card — the floating .bg-heart stickers sit at 0 and would drift over the bubbles */
  min-height: 0; /* lets the flex child shrink, which is what makes it scroll */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.chat-scroll::-webkit-scrollbar { display: none; }

.chat-log { display: flex; flex-direction: column; gap: 8px; }

.chat-msg {
  /* Sized against the typing indicator below, which is the reference: its
     three 6px dots in 12px/14px padding make a ~32px pill, while the bubbles
     at 0.98rem/1.45 in 10px/15px padding came out ~47px and read as a
     different, much heavier scale next to it. */
  max-width: 72%;
  padding: 8px 13px 9px;
  border-radius: 17px;
  font-size: 0.88rem;
  line-height: 1.4;
  box-shadow: 0 6px 18px rgba(255, 111, 160, 0.14);
  animation: chat-pop 0.28s ease;
  overflow-wrap: anywhere; /* Thai has no spaces to break at */
}
@keyframes chat-pop {
  from { opacity: 0; transform: translateY(8px) scale(0.97); }
  to { opacity: 1; transform: none; }
}
/* the sender — left, and the one wearing the accent pink */
.chat-msg.chat-me {
  align-self: flex-start;
  background: var(--chat-accent);
  border: 1px solid var(--chat-accent);
  color: var(--chat-ink);
  border-bottom-left-radius: 6px;
}
/* the recipient — right, the accent colour, because these are her words */
.chat-msg.chat-her {
  align-self: flex-end;
  margin-left: auto; /* belt and braces — pushes the bubble right even where align-self is overridden */
  background: rgba(255, 255, 255, 0.78);
  border: 1px solid var(--chat-line);
  color: var(--chat-ink);
  border-bottom-right-radius: 6px;
}

/* three dots, staggered — the pause that sells it as someone typing */
.chat-typing {
  /* .chat-scroll is a plain block, so align-self does nothing here — the bubble
     has to size itself to its three dots or it stretches the full width */
  width: fit-content;
  display: flex;
  gap: 4px;
  margin-top: 8px;
  padding: 12px 14px;
  background: var(--chat-accent); /* matches the sender's bubble — it's the same person about to speak */
  border: 1px solid var(--chat-accent);
  border-radius: 17px 17px 17px 6px; /* same corner language as the bubbles */
  box-shadow: 0 6px 18px rgba(255, 111, 160, 0.14);
}
.chat-typing[hidden] { display: none; } /* an author display rule beats the UA [hidden] rule otherwise */
.chat-typing span {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.85); /* on the accent bubble now, so the dots have to be light */
  animation: chat-dot 1.1s ease-in-out infinite;
}
.chat-typing span:nth-child(2) { animation-delay: 0.16s; }
.chat-typing span:nth-child(3) { animation-delay: 0.32s; }
@keyframes chat-dot {
  0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-3px); }
}

/* her replies — a visual-novel choice panel across the bottom of the screen.
   .chat-scroll takes flex:1 above it, so the panel never moves no matter how
   long the conversation gets. */
.chat-options {
  flex: none;
  position: sticky;
  bottom: 0;
  z-index: 1; /* keeps the floating .bg-heart stickers behind the panel */
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 12px;
  padding: 12px 8px 4px;
}
.chat-options:empty { display: none; } /* no panel chrome while it's his turn */

.chat-option {
  width: 100%;
  /* kept in step with .chat-msg above — the bubbles came down to 0.88rem and
     these were left looking like the heavier element in the same conversation */
  padding: 10px 14px 11px;
  font-family: inherit;
  font-size: 0.88rem;
  line-height: 1.4;
  text-align: center;
  color: var(--chat-ink);
  background: rgba(255, 255, 255, 0.72);
  border: 1px solid rgba(255, 111, 160, 0.35);
  border-radius: 14px;
  box-shadow: 0 6px 18px rgba(255, 111, 160, 0.14);
  cursor: pointer;
  animation: chat-pop 0.28s ease backwards;
}
/* stagger them in, so the panel deals itself out like a hand of cards */
.chat-option:nth-child(2) { animation-delay: 0.07s; }
.chat-option:nth-child(3) { animation-delay: 0.14s; }
.chat-option:nth-child(4) { animation-delay: 0.21s; }
.chat-option:active {
  transform: scale(0.985);
  background: var(--chat-accent);
  border-color: var(--chat-accent);
  color: var(--chat-ink);
}

/* curved motion arrow, tucked close to the camera's top-right corner so its
   tip reads as pointing straight at it — arrow.webp has a white (not
   transparent) background, multiply blends it away against the cream page.
   Positioned off .camera-wrap now (a sibling of the button, not a child of
   it) specifically so its own motion doesn't compound with the camera's
   rotate jiggle — that combination was reading as one blurry wiggle instead
   of a separate pointing gesture. */
.cam-fx { position: absolute; inset: 0; pointer-events: none; }
.cam-arrow {
  position: absolute;
  top: -20px; right: -56px;
  width: 72px;
  mix-blend-mode: multiply;
  opacity: 0.9;
  pointer-events: none;
}
/* a clear side-to-side nudge toward the camera and back — "เข้าออกซ้ายขวา" per
   request, on its own faster clock so it visibly reads as separate motion
   even while the camera is also jiggling on its own beat. */
.cam-arrow { 
  animation: cam-arrow-nudge 1.3s ease-in-out infinite; 
  z-index: 10;
}
@keyframes cam-arrow-nudge {
  0%, 100% { translate: 8px 0; }
  50% { translate: -6px 0; }
}

/* small stars scattered loosely around the camera — spaced apart so they
   read as scattered sparkle, not a cluster fighting the arrow */
.cam-star {
  position: absolute;
  font-style: normal;
  color: #f0b429; /* warm gold, like the reference sparkles */
}
.cam-star-1 { top: -18px; left: 10px; font-size: 15px; opacity: 0.9; }
.cam-star-2 { bottom: 10px; right: -18px; font-size: 12px; opacity: 0.8; }
.cam-star-3 { bottom: -16px; left: -6px; font-size: 9px; opacity: 0.65; }
.camera-frame:not(:disabled) .cam-star { animation: cam-star-twinkle 2.5s ease-in-out infinite; }
.camera-frame:not(:disabled):active .cam-star { animation-play-state: paused; }
.cam-star-1 { animation-delay: 0.12s; }
.cam-star-2 { animation-delay: 0.3s; }
.cam-star-3 { animation-delay: 0.48s; }
@keyframes cam-star-twinkle {
  0%, 40%, 100% { scale: 1; }
  50% { scale: 1.35; }
  60% { scale: 0.9; }
}
