/* The background is a 4-stop vertical gradient (pale grey sky → dusty rose →
   warm tan → sage). The stops are registered <color> properties so a mood
   change (see setMood in app.js) cross-fades smoothly instead of snapping. */
@property --bg-1 { syntax: "<color>"; inherits: true; initial-value: #dcdad9; }
@property --bg-2 { syntax: "<color>"; inherits: true; initial-value: #c4a8aa; }
@property --bg-3 { syntax: "<color>"; inherits: true; initial-value: #b8a687; }
@property --bg-4 { syntax: "<color>"; inherits: true; initial-value: #86997d; }

:root {
  --bg-rest-1: #dcdad9;
  --bg-rest-2: #c4a8aa;
  --bg-rest-3: #b8a687;
  --bg-rest-4: #86997d;

  --bg-yes-1: #e6dcdb;
  --bg-yes-2: #d2aeb2;
  --bg-yes-3: #c6ab84;
  --bg-yes-4: #8fa97f;

  --bg-no-1: #d0cece;
  --bg-no-2: #b19ca0;
  --bg-no-3: #a39a82;
  --bg-no-4: #6c7f66;

  --bg-1: var(--bg-rest-1);
  --bg-2: var(--bg-rest-2);
  --bg-3: var(--bg-rest-3);
  --bg-4: var(--bg-rest-4);

  --ink: #2f3b30;
  --pill-active: rgba(139, 104, 84, 0.62);
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overscroll-behavior: none;
}

html {
  transition:
    --bg-1 900ms ease,
    --bg-2 900ms ease,
    --bg-3 900ms ease,
    --bg-4 900ms ease;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--ink);
  background-image: linear-gradient(
    180deg,
    var(--bg-1) 0%,
    var(--bg-2) 30%,
    var(--bg-3) 55%,
    var(--bg-4) 100%
  );
}

/* Cloudy texture overlay, faint (14%) and fixed to the viewport, sitting
   behind everything else. A separate layer (not folded into body's own
   background-image) so its opacity can be controlled independently of
   the mood gradient beneath it. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  background-image: url("assets/texture-overlay.png");
  background-size: cover;
  background-position: center;
  opacity: 0.14;
  pointer-events: none;
  z-index: 0;
}

/* --corner is the one number the whole decorative frame (and the top bar's
   inset inside it) scales from; --u is "one unit" of the corner artwork's
   218-wide drawing, so every frame measurement below is written in the
   artwork's own units and stays proportional at any screen size. */
#app {
  --frame-inset: clamp(6px, 1.9vw, 30px);
  --corner: clamp(88px, 15vw, 218px);
  --u: calc(var(--corner) / 218);

  position: relative;
  height: 100vh;
  overflow: hidden;
}

/* ---------- Decorative frame ----------
   Four corner brackets (one symbol, mirrored into each corner) joined by
   four edge-centre segments. The segments are built from CSS — two rules
   and a diamond at each end — rather than stretching the drawn artwork,
   so their diamonds stay undistorted however long or short the span is.
   Everything is measured in --u (see #app) and sits behind the stage, so
   petals fall in front of it. */

.frame {
  position: absolute;
  inset: var(--frame-inset);
  z-index: 1;
  pointer-events: none;
  opacity: 0.5;
}

.frame-corner {
  position: absolute;
  display: block;
  width: var(--corner);
  height: auto;
  aspect-ratio: 218 / 216;
}

/* The artwork is drawn for the bottom-right corner. */
.frame-corner--br { right: 0; bottom: 0; }
.frame-corner--bl { left: 0; bottom: 0; transform: scaleX(-1); }
.frame-corner--tr { right: 0; top: 0; transform: scaleY(-1); }
.frame-corner--tl { left: 0; top: 0; transform: scale(-1, -1); }

/* A segment is a thick rule and a thin rule (thick on the outer side, to
   match the corners) running between two diamonds. The rules are two
   background layers; the diamonds are the pseudo-elements. */
.frame-edge {
  position: absolute;
  background-image: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff);
  background-repeat: no-repeat;
}

.frame-edge::before,
.frame-edge::after {
  content: "";
  position: absolute;
  width: calc(16 * var(--u));
  height: calc(16 * var(--u));
  border-radius: calc(2.2 * var(--u));
  background: #fff;
  transform: rotate(45deg);
}

.frame-edge--top,
.frame-edge--bottom {
  left: calc(var(--corner) + 120 * var(--u));
  right: calc(var(--corner) + 120 * var(--u));
  height: calc(22 * var(--u));
  background-size:
    calc(100% - 21.5 * var(--u)) max(1.25px, calc(3 * var(--u))),
    calc(100% - 21.5 * var(--u)) max(1px, var(--u));
}

.frame-edge--top { top: calc(4.3 * var(--u)); }
.frame-edge--bottom { bottom: calc(4.3 * var(--u)); }

.frame-edge--top {
  background-position:
    calc(10.75 * var(--u)) calc(9.243 * var(--u)),
    calc(10.75 * var(--u)) calc(16.243 * var(--u));
}

.frame-edge--bottom {
  background-position:
    calc(10.75 * var(--u)) calc(9.757 * var(--u)),
    calc(10.75 * var(--u)) calc(4.757 * var(--u));
}

.frame-edge--top::before,
.frame-edge--bottom::before { left: calc(2.75 * var(--u)); top: calc(2.75 * var(--u)); }
.frame-edge--top::after,
.frame-edge--bottom::after { right: calc(2.75 * var(--u)); top: calc(2.75 * var(--u)); }

.frame-edge--left,
.frame-edge--right {
  top: calc(var(--corner) + 156 * var(--u));
  bottom: calc(var(--corner) + 156 * var(--u));
  width: calc(22 * var(--u));
  background-size:
    max(1.25px, calc(3 * var(--u))) calc(100% - 21.5 * var(--u)),
    max(1px, var(--u)) calc(100% - 21.5 * var(--u));
}

.frame-edge--left { left: calc(4.5 * var(--u)); }
.frame-edge--right { right: calc(4.5 * var(--u)); }

.frame-edge--left {
  background-position:
    calc(9.243 * var(--u)) calc(10.75 * var(--u)),
    calc(16.243 * var(--u)) calc(10.75 * var(--u));
}

.frame-edge--right {
  background-position:
    calc(9.757 * var(--u)) calc(10.75 * var(--u)),
    calc(4.757 * var(--u)) calc(10.75 * var(--u));
}

.frame-edge--left::before,
.frame-edge--right::before { left: calc(2.75 * var(--u)); top: calc(2.75 * var(--u)); }
.frame-edge--left::after,
.frame-edge--right::after { left: calc(2.75 * var(--u)); bottom: calc(2.75 * var(--u)); }

/* Too short for the side segments to fit between the corners. */
@media (max-height: 560px) {
  .frame-edge--left,
  .frame-edge--right { display: none; }
}

/* ---------- Top bar ----------
   Positioned as a transparent overlay above the stage (not squeezing it
   into a shorter box) so petals thrown upward stay visible passing behind
   it. It sits inside the frame, inset from the corner brackets; the bar
   itself lets clicks through, and only the real buttons (re-enabled below)
   stay on top and clickable. */

.topbar {
  position: absolute;
  top: calc(var(--frame-inset) + 70 * var(--u));
  left: calc(var(--frame-inset) + 64 * var(--u));
  right: calc(var(--frame-inset) + 64 * var(--u));
  z-index: 10;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.gender-selector {
  --gender-w: 60px;
  --gender-h: 46px;

  position: relative;
  display: inline-flex;
  pointer-events: auto;
}

/* One shared highlight that slides (with a bounce) to whichever option is
   active, instead of each button crossfading its own copy in place. */
.gender-active-pill {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--gender-w);
  height: var(--gender-h);
  border-radius: 6px;
  background: var(--pill-active);
  transition: transform 550ms cubic-bezier(0.34, 1.56, 0.64, 1);
  z-index: 0;
}

.gender-btn {
  position: relative;
  z-index: 1;
  width: var(--gender-w);
  height: var(--gender-h);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: #fff;
  cursor: pointer;
  padding: 0;
  transition: transform 150ms ease;
  -webkit-tap-highlight-color: transparent;
}

.gender-btn:hover:not(.active) { transform: translateY(-1px); }
.gender-btn:active { transform: scale(0.94); }

.topbar-right {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  pointer-events: auto;
}

.icon-btn {
  position: relative;
  width: 46px;
  height: 46px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: #fff;
  cursor: pointer;
  padding: 0;
  transition: transform 150ms ease, opacity 250ms ease;
  -webkit-tap-highlight-color: transparent;
}

.icon-btn:hover { transform: translateY(-1px); }
.icon-btn:active { transform: scale(0.92); }
.icon-btn.is-muted { opacity: 0.7; }

.gender-icon,
.icon-btn svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
}

.gender-icon { width: 26px; height: 26px; }
.icon-btn svg { width: 30px; height: 30px; }

.icon-btn .speaker-body { fill: currentColor; }

.icon-muted { display: none; }
.icon-btn.is-muted .icon-unmuted { display: none; }
.icon-btn.is-muted .icon-muted { display: block; }

.gender-btn:focus-visible,
.icon-btn:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.9);
  outline-offset: 2px;
}

@media (max-width: 640px) {
  .gender-selector { --gender-w: 52px; --gender-h: 44px; }
  .icon-btn { width: 44px; height: 44px; }
  .gender-icon { width: 24px; height: 24px; }
  .icon-btn svg { width: 28px; height: 28px; }

  /* dvh accounts for mobile browser chrome (address/tab bars) shrinking
     and growing the viewport, so the page never becomes tall enough to
     scroll as that chrome shows or hides. */
  html, body { overflow: hidden; }
  #app { height: 100dvh; }
}

/* ---------- Stage ---------- */

#stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 1;
}

#flowerSvg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  /* Applied here on the outer <svg> (not on the individual petals nested
     several transformed <g> levels deep inside it) because WebKit/Safari
     silently fails to render filter: drop-shadow() on SVG elements that
     sit inside transformed ancestor groups — confirmed missing on iPhone
     even though it renders fine on desktop. A filter directly on a
     top-level <svg> (no SVG ancestor) is unaffected.
     This is a soft ambient shadow under the flower as a whole — it's the
     only one iOS can render, so it's kept light/diffuse on purpose. The
     per-petal shadows below are the "real" ones and still do the visible
     work on every browser that supports them. */
  filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.16));
}

.petal-anchor {
  touch-action: none;
  transition: none;
  /* Without these, iOS Safari can hijack a slow or held drag with its own
     text-selection/callout gesture, aborting the pointer sequence mid-pull
     — the drag then feels like it randomly "doesn't take". */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.petal-anchor.snapping {
  transition: transform 380ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.petal-shape {
  cursor: grab;
  /* The thin outline below is off (width 0) by default — it only shows up
     at mobile widths, once the shadow is switched off there, see the
     media query further down. vector-effect keeps it a crisp, constant
     screen-pixel thickness no matter how the viewBox is scaled. */
  stroke: var(--outline-color, transparent);
  stroke-width: 0;
  vector-effect: non-scaling-stroke;
  /* Per-petal shadow — renders on desktop and Android but is silently
     dropped by iOS Safari (WebKit bug: filter on an SVG element nested
     inside transformed <g> ancestors). The #flowerSvg ambient shadow
     above is what keeps the flower from looking flat on iPhone; this one
     is the extra layered depth for browsers that support it. */
  filter: drop-shadow(0 4px 5px rgba(0, 0, 0, 0.14));
  transition: filter 150ms ease;
}

.petal-shape:active { cursor: grabbing; }

.petal-anchor.is-falling .petal-shape {
  filter: drop-shadow(0 10px 12px rgba(0, 0, 0, 0.2));
  cursor: default;
}

/* Invisible twin of .petal-shape, padded out with a wide stroke, that
   exists only to catch pointer/touch input — see createPetal() in app.js.
   Keeping it separate means .petal-shape's own stroke is free to be the
   visible color outline instead of doing double duty. */
.petal-hit-area {
  cursor: grab;
  fill: rgba(0, 0, 0, 0.006);
  stroke: rgba(0, 0, 0, 0.006);
  stroke-width: 18;
  pointer-events: all;
}

.center-circle {
  pointer-events: none;
  stroke: var(--outline-color, transparent);
  stroke-width: 0;
  vector-effect: non-scaling-stroke;
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.18));
}

/* At mobile sizes the shadow is dropped in favor of a thin outline on
   each shape instead — flatter, but crisper and cheaper to render, and
   it sidesteps the iOS drop-shadow limitations entirely rather than
   working around them. Placed after the base rules above (not folded
   into the earlier 640px block) so it actually wins the cascade — same
   selector specificity, and source order is the tiebreaker. */
@media (max-width: 640px) {
  #flowerSvg,
  .petal-shape,
  .petal-anchor.is-falling .petal-shape,
  .center-circle {
    filter: none;
  }
  .petal-shape,
  .center-circle {
    stroke-width: 1.5px;
  }
}

/* ---------- Messages ----------
   Two fixed, reserved spots — "loves me" always bottom-left, "loves me
   not" always bottom-right, each held well clear of the screen edges —
   and the text glides into its spot along a real quadratic-bezier hook
   (a start point + a control point, both set from JS), not a straight
   line: it rises first, then curves inward late, mirrored per side.
   --sx/--sy is the start offset, --cx/--cy the curve's control point,
   --tilt an occasional resting rotation. A faint sway continues while
   it lingers. All custom properties are set per-message from JS. */

.message-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
}

.message-text {
  position: absolute;
  max-width: 38%;
  font-family: "Caveat", "Segoe Script", "Bradley Hand", cursive;
  font-size: clamp(28px, 4.4vw, 50px);
  font-weight: 700;
  color: #ffffff;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.22);
  margin: 0;
  opacity: 0;
  animation: messageInOut 2.3s linear forwards;
}

/* At mobile widths 38% isn't enough room for "She loves me not" etc. to
   fit on one line — nowrap alone would just let it overflow past its box
   toward the far screen edge, so the box is also allowed to grow wider
   (still short of the viewport, matching the margin the JS side already
   keeps clear on the anchored edge in messageZone()). */
@media (max-width: 640px) {
  .message-text {
    max-width: 86vw;
    white-space: nowrap;
  }
}

/* Quadratic bezier B(t) = (1-t)²·P0 + 2(1-t)t·Pc, sampled at 11 evenly
   time-spaced points so it can live in a static @keyframes block; only
   the per-message --sx/--sy/--cx/--cy values change per message. The
   animation's own timing-function is linear on purpose — the curve
   shape already lives in these pre-eased position samples, so a linear
   interpolation between them reconstructs smooth bezier motion. Layering
   a second easing curve on top (the previous version's mistake) made
   every segment re-accelerate, which read as a jerky, un-fluid motion.
   Rotation eases in lock-step with the same (1-(1-t)²) progress so the
   tilt settles exactly as the curve does. The hold/sway/fade portion
   past 38% isn't bezier-sampled, so it gets its own gentle ease there. */
@keyframes messageInOut {
  0% {
    opacity: 0;
    transform: translate(calc(var(--sx) * 1px), calc(var(--sy) * 1px)) rotate(0deg);
  }
  4% {
    opacity: 0.55;
    transform: translate(
        calc(var(--sx) * 0.81px + var(--cx) * 0.18px),
        calc(var(--sy) * 0.81px + var(--cy) * 0.18px)
      )
      rotate(calc(var(--tilt) * 0.19deg));
  }
  8% {
    opacity: 1;
    transform: translate(
        calc(var(--sx) * 0.64px + var(--cx) * 0.32px),
        calc(var(--sy) * 0.64px + var(--cy) * 0.32px)
      )
      rotate(calc(var(--tilt) * 0.36deg));
  }
  11% {
    transform: translate(
        calc(var(--sx) * 0.49px + var(--cx) * 0.42px),
        calc(var(--sy) * 0.49px + var(--cy) * 0.42px)
      )
      rotate(calc(var(--tilt) * 0.51deg));
  }
  15% {
    transform: translate(
        calc(var(--sx) * 0.36px + var(--cx) * 0.48px),
        calc(var(--sy) * 0.36px + var(--cy) * 0.48px)
      )
      rotate(calc(var(--tilt) * 0.64deg));
  }
  19% {
    transform: translate(
        calc(var(--sx) * 0.25px + var(--cx) * 0.5px),
        calc(var(--sy) * 0.25px + var(--cy) * 0.5px)
      )
      rotate(calc(var(--tilt) * 0.75deg));
  }
  23% {
    transform: translate(
        calc(var(--sx) * 0.16px + var(--cx) * 0.48px),
        calc(var(--sy) * 0.16px + var(--cy) * 0.48px)
      )
      rotate(calc(var(--tilt) * 0.84deg));
  }
  27% {
    transform: translate(
        calc(var(--sx) * 0.09px + var(--cx) * 0.42px),
        calc(var(--sy) * 0.09px + var(--cy) * 0.42px)
      )
      rotate(calc(var(--tilt) * 0.91deg));
  }
  30% {
    transform: translate(
        calc(var(--sx) * 0.04px + var(--cx) * 0.32px),
        calc(var(--sy) * 0.04px + var(--cy) * 0.32px)
      )
      rotate(calc(var(--tilt) * 0.96deg));
  }
  34% {
    transform: translate(
        calc(var(--sx) * 0.01px + var(--cx) * 0.18px),
        calc(var(--sy) * 0.01px + var(--cy) * 0.18px)
      )
      rotate(calc(var(--tilt) * 0.99deg));
  }
  38% {
    transform: translate(0, 0) rotate(calc(var(--tilt) * 1deg));
    animation-timing-function: ease-in-out;
  }
  62% {
    transform: translate(calc(var(--tilt) * 0.6px), -3px) rotate(calc(var(--tilt) * 1.05deg));
    animation-timing-function: ease-in-out;
  }
  85% {
    opacity: 1;
    transform: translate(0, 0) rotate(calc(var(--tilt) * 1deg));
  }
  100% {
    opacity: 0;
    transform: translate(0, 0) rotate(calc(var(--tilt) * 1deg));
  }
}

/* If a new pluck interrupts a message that hasn't finished fading yet,
   it drops out of view instead of just vanishing. JS freezes the
   in-flight transform/opacity as inline styles right before adding this
   class, so the fall continues smoothly from wherever it currently was
   — no jump back to the start. */
.message-text.is-dismissed {
  animation: messageFallAway 380ms cubic-bezier(0.4, 0, 1, 1) forwards;
}

@keyframes messageFallAway {
  to {
    opacity: 0;
    transform: translateY(45vh) rotate(calc(var(--tilt, 0) * 1deg));
  }
}

/* Sits above the flower in the zero state, no pill/background — just a
   soft handwritten nudge. A bright band sweeps once left-to-right across
   the letters, then rests, on a loop, until the first petal is plucked
   (that's when app.js adds .hidden). Built with a background-clip:text
   gradient rather than animating color, so the "light" travels smoothly
   through the letters instead of the whole line dimming/brightening at
   once. */
.hint {
  position: absolute;
  top: 76px;
  left: 50%;
  transform: translateX(-50%);
  font-family: "Caveat", "Segoe Script", "Bradley Hand", cursive;
  font-weight: 600;
  font-size: clamp(19px, 3vw, 25px);
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 400ms ease;

  color: transparent;
  background-image: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.8) 0%,
    rgba(255, 255, 255, 0.8) 42%,
    #ffffff 50%,
    rgba(255, 255, 255, 0.8) 58%,
    rgba(255, 255, 255, 0.8) 100%
  );
  background-size: 260% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: hintShimmer 14.4s ease-in-out infinite;
}

@keyframes hintShimmer {
  0%, 12% { background-position: 180% 0; }
  55%, 100% { background-position: -80% 0; }
}

.hint.hidden { opacity: 0; }

/* ---------- End screen ----------
   A tan panel carrying the same corner-bracket frame as the main screen,
   with a white line heart beside the verdict, that drops in once the flower
   is bare. Sits above the flower/message layer but below the topbar
   (z-index), so the nav buttons stay visible and clickable even while it's
   up. */

.end-screen {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
}

.end-screen.hidden {
  display: none;
}

.end-card {
  position: relative;
  width: min(88vw, 620px);
  aspect-ratio: 1.56;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  /* The hairline edge is an inset shadow, not a border: a real border would
     make the frame's percentage offsets (see .end-card-corner) measure
     against a box 2px smaller than the card and shift the joint. */
  background: linear-gradient(180deg, rgba(219, 198, 180, 0.95) 0%, rgba(171, 148, 132, 0.95) 100%);
  box-shadow:
    inset 0 0 0 1px rgba(120, 90, 70, 0.32),
    0 16px 34px rgba(60, 40, 30, 0.28);
  animation: cardDrop 900ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

@keyframes cardDrop {
  0% { transform: translateY(-130vh) rotate(-7deg); opacity: 0; }
  50% { opacity: 1; }
  72% { transform: translateY(16px) rotate(2.5deg); }
  86% { transform: translateY(-7px) rotate(-1.2deg); }
  100% { transform: translateY(0) rotate(0deg); opacity: 1; }
}

/* The same corner artwork as the main frame. The numbers are not eyeballed:
   with the card at a 1.56 aspect ratio, a corner exactly half the card's
   height wide makes one artwork unit (u) = height / 436, and an 11u margin
   from the top and bottom edge puts the top and bottom corners 198u apart —
   the distance at which the end diamonds of their vertical arms land on
   exactly the same point, so the two arms join into one continuous line
   with a single diamond where they meet. If the card's aspect ratio or
   these offsets change, that joint stops lining up. */
.end-card-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.end-card-corner {
  position: absolute;
  display: block;
  width: 32.0513%;
  height: auto;
  aspect-ratio: 218 / 216;
}

.end-card-corner--br { right: 1.62%; bottom: 2.523%; }
.end-card-corner--bl { left: 1.62%; bottom: 2.523%; transform: scaleX(-1); }
.end-card-corner--tr { right: 1.62%; top: 2.523%; transform: scaleY(-1); }
.end-card-corner--tl { left: 1.62%; top: 2.523%; transform: scale(-1, -1); }

.end-card-body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: 83%;
}

.end-card-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(8px, 2.4vw, 18px);
}

.end-sign-icon {
  flex: none;
  width: clamp(52px, 13vw, 92px);
  height: auto;
  display: none;
  color: #fff;
}

/* Only the happy heart floats — the broken one stays put. It starts once
   the card has landed (its drop takes 900ms) and then drifts up and down
   forever, about 14px of travel at most. */
.end-sign-icon--happy {
  animation: heartFloat 4s ease-in-out 900ms infinite alternate;
}

@keyframes heartFloat {
  from { transform: translateY(0) rotate(0deg); }
  to { transform: translateY(-14px) rotate(2.5deg); }
}

@media (prefers-reduced-motion: reduce) {
  .end-sign-icon--happy { animation: none; }
}

/* The broken heart's drawing spends a third of its width on the little
   dizzy face beside the heart, so at the same box width its heart reads
   much smaller than the happy one — it gets a wider box to even them out. */
.end-sign-icon--sad { width: clamp(70px, 17.5vw, 124px); }

.end-screen.outcome-yes .end-sign-icon--happy { display: block; }
.end-screen.outcome-no .end-sign-icon--sad { display: block; }

.end-card-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.end-sign-text {
  margin: 0;
  font-family: "Caveat", "Segoe Script", "Bradley Hand", cursive;
  font-weight: 700;
  color: #fff;
  font-size: clamp(28px, 7.4vw, 50px);
  line-height: 1.05;
  text-shadow: 0 1px 4px rgba(90, 60, 40, 0.25);
}

/* A little nudge, only on the "loves me" outcome — one of a few random
   lines, so it doesn't repeat the same way every win. */
.end-sign-subtext {
  margin: 2px 0 0;
  display: none;
  font-family: "Caveat", "Segoe Script", "Bradley Hand", cursive;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.88);
  font-size: clamp(18px, 4.2vw, 26px);
}

.end-screen.outcome-yes .end-sign-subtext { display: block; }

.end-sign-retry {
  display: none;
  align-items: center;
  justify-content: center;
  align-self: center;
  margin-top: 8px;
  padding: 8px 24px;
  border: 1.5px solid rgba(255, 255, 255, 0.8);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.2);
  color: #fff;
  font: 600 15px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  cursor: pointer;
  transition: transform 150ms ease, background 150ms ease;
  -webkit-tap-highlight-color: transparent;
}

.end-screen.outcome-no .end-sign-retry { display: inline-flex; }

.end-sign-retry:hover { transform: translateY(-1px); background: rgba(255, 255, 255, 0.3); }
.end-sign-retry:active { transform: scale(0.96); }
