/* ============================================================================
   Birthday Connections Card
   Machinery. You shouldn't need to edit this to change content — see puzzle.js
   ========================================================================= */

:root {
  /* NYT Connections difficulty colors */
  --lvl-0: #f9df6d;   /* yellow  */
  --lvl-1: #a0c35a;   /* green   */
  --lvl-2: #b0c4ef;   /* blue    */
  --lvl-3: #ba81c5;   /* purple  */

  --tile-bg:       #efefe6;
  --tile-fg:       #121212;
  --tile-selected: #5a594e;
  --ink:           #121212;
  --muted:         #767676;
  --hairline:      #d3d3d3;
  --paper:         #ffffff;

  --ink-red:   #d23b3b;     /* the red pen used for corrections */
  --accent:    #8a4fb0;     /* italic purple for ~accented~ words */
  --card-ink:  #2c2a4a;
  --card-gold: #b08d57;
  --linen:     #f4ece0;     /* the surface the card sits on */
  --card-face: #fffdfa;     /* the paper of the card itself */

  /* Three stacked shadows: a tight contact edge, a mid lift, and a wide soft
     pool underneath. Together they read as hovering rather than lying flat. */
  --card-lift:
    0 2px 4px rgba(44, 42, 74, .07),
    0 12px 22px -8px rgba(44, 42, 74, .16),
    0 34px 60px -22px rgba(44, 42, 74, .38);

  --gap: 8px;
  --radius: 6px;
  --pad: 12px;          /* gap between the card and the edge of the screen */

  /* Built for a phone: nothing gets wider than a phone-sized column. */
  --col: 430px;
  --card-w: 430px;      /* the card is this size on every single page */

  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);
  --sal: env(safe-area-inset-left, 0px);
  --sar: env(safe-area-inset-right, 0px);

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --turn: 620ms;              /* how long one page takes to turn */
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background: var(--linen);
  color: var(--ink);
  font-family: "Libre Franklin", "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;      /* stop iOS inflating text in landscape */
  overscroll-behavior-y: none;         /* no rubber-band on a card */
  overflow-x: clip;                    /* screen transitions must not pan sideways */
}

button {
  font: inherit;
  color: inherit;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
  touch-action: manipulation;          /* kills the 300ms double-tap-zoom delay */
  -webkit-tap-highlight-color: transparent;
}

[hidden] { display: none !important; }

/* ---------------------------------------------------------------- screens -- */

/* All four pages stack in here. The perspective is what makes a page turn read
   as paper rotating away rather than a flat wipe. */
.pages {
  position: fixed;
  inset: 0;
  perspective: 1700px;
}

.screen {
  position: absolute;
  inset: 0;
  padding:
    calc(var(--sat) + var(--pad))
    calc(var(--sar) + var(--pad))
    calc(var(--sab) + var(--pad))
    calc(var(--sal) + var(--pad));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

/* ---------------------------------------------------------------- the card --
   Once the cover is open, everything stays inside a piece of card stock:
   warm paper, a gold keyline and a soft shadow, sitting on the linen page. */

.card-stage,
.card-page {
  position: relative;
  z-index: 1;
  width: min(84%, var(--card-w));
  height: 82%;                  /* identical box on every page */
  flex: none;                   /* never stretch or shrink to fit content */
  border-radius: 10px;

  /* The page that turns rotates about its own spine, like a real card. */
  transform-origin: left center;
  will-change: transform;
}

.card-page {
  background: var(--card-face);
  box-shadow: var(--card-lift);
  padding: 12px;
}

/* The gold keyline, matching the cover's */
.card-page::after {
  content: "";
  position: absolute;
  inset: 8px;
  border: 1px solid rgba(176, 141, 87, .38);
  border-radius: 6px;
  pointer-events: none;
}

/* Content is centred within the fixed card, and scrolls inside it if a page
   ever runs longer than the card rather than stretching the card itself. */
.card-page__inner {
  position: relative;
  z-index: 1;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  justify-content: safe center;
  padding: 8px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

/* The note is prose, so it earns more breathing room than the game board. */
.screen--note .card-page { padding: 20px; padding-bottom: 12px; }
.screen--note .card-page__inner { padding: 12px 10px; padding-bottom: 8px; }

/* Page turns. Going forward, the page you are leaving swings away on its spine
   and reveals the next one underneath. Going back, the previous page swings in
   over the top. Same motion the cover has always had, now on every page. */

.screen.is-turning-out .card-stage,
.screen.is-turning-out .card-page {
  animation: page-turn-away var(--turn) var(--ease-out) forwards;
}

.screen.is-turning-in .card-stage,
.screen.is-turning-in .card-page {
  animation: page-turn-back var(--turn) var(--ease-out) forwards;
}

@keyframes page-turn-away {
  0%   { transform: rotateY(0deg);    opacity: 1; }
  55%  { opacity: 1; }
  100% { transform: rotateY(-118deg); opacity: 0; }
}

@keyframes page-turn-back {
  0%   { transform: rotateY(-118deg); opacity: 0; }
  45%  { opacity: 1; }
  100% { transform: rotateY(0deg);    opacity: 1; }
}

/* ------------------------------------------------------------ navigation -- */

/* Bottom corners, not the middle of the screen: centred arrows would sit on
   top of the puzzle tiles and swallow taps meant for the game. Lifted clear of
   the very bottom edge, which is where home indicators, browser chrome and
   injected widgets all tend to land. */
.nav {
  position: fixed;
  bottom: calc(var(--sab) + 74px);
  z-index: 5;

  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;

  font-size: 26px;
  line-height: 1;
  color: var(--card-ink);
  background: rgba(255, 253, 250, .92);
  border: 1px solid rgba(176, 141, 87, .35);
  box-shadow: 0 2px 10px rgba(44, 42, 74, .14);
  opacity: .62;
  transition: opacity 160ms, transform 120ms var(--ease-out);
}

.nav:active { opacity: 1; transform: scale(.92); }

.nav--prev { left: calc(var(--sal) + 10px); padding-right: 3px; }
.nav--next { right: calc(var(--sar) + 10px); padding-left: 3px; }

@media (max-height: 460px) {
  .nav { width: 40px; height: 40px; font-size: 22px; bottom: calc(var(--sab) + 10px); }
}

/* ================================================================== COVER == */

.card-cover {
  position: absolute;
  inset: 0;
  width: 100%;
  border-radius: 10px;
  background: linear-gradient(160deg, #fffdf8 0%, #f6efe2 100%);
  box-shadow: var(--card-lift);

  display: flex;
  flex-direction: column;
  align-items: center;
  /* Bottom padding matches .card-page + .card-page__inner so the hint lands at
     exactly the same height as it does on every other page. */
  padding: 20px;
  text-align: center;

  transform-origin: left center;
  transform-style: preserve-3d;
  will-change: transform;
  overflow: hidden;              /* keeps confetti inside the card edge */
}

/* ---- balloons ---- */

.balloons {
  width: min(88%, 300px);
  height: auto;
  margin-bottom: 2px;
  overflow: visible;
}

.balloon { transform-origin: 50% 100%; }

/* Each drifts at its own pace so the cluster never looks mechanical. */
.balloon--1 { animation: bob 4.2s ease-in-out infinite; }
.balloon--2 { animation: bob 5.1s ease-in-out infinite .4s; }
.balloon--3 { animation: bob 3.8s ease-in-out infinite .9s; }
.balloon--4 { animation: bob 4.8s ease-in-out infinite .2s; }
.balloon--5 { animation: bob 5.6s ease-in-out infinite .7s; }

@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-7px) rotate(1.2deg); }
}

/* ---- confetti falling on the cover ----
   The host lives inside .card-cover, which is overflow:hidden, so every piece
   is clipped at the card's edge and none of it lands on the surface behind. */

.cover-confetti {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.cover-confetti i {
  position: absolute;
  display: block;
  width: 7px;
  height: 10px;
  opacity: 0;
  animation: cover-fall linear infinite;
}

/* `top` is animated rather than transform so the distance is a percentage of
   the card, which keeps the fall the right length whatever size the card is. */
@keyframes cover-fall {
  0%   { top: -12%; opacity: 0;  transform: rotate(var(--spin, 0deg)); }
  7%   { opacity: .8; }
  88%  { opacity: .8; }
  100% { top: 106%; opacity: 0;  transform: rotate(calc(var(--spin, 0deg) + 620deg)); }
}

/* Gold inner keyline, like a letterpress card */
.card-cover::after {
  content: "";
  position: absolute;
  inset: 8px;
  border: 1px solid rgba(176, 141, 87, .38);
  border-radius: 6px;
  pointer-events: none;
}

.card-cover__body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  width: 100%;
}

.card-cover__deco {
  color: var(--card-gold);
  font-size: 18px;
  line-height: 1;
}

/* "To: Laura" with "From: Tate" right underneath it. */
.card-cover__eyebrow { display: flex; flex-direction: column; align-items: center; gap: 6px; }

.card-cover__eyebrow {
  font-size: 12px;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--card-gold);
}

.card-cover__title {
  font-size: clamp(30px, 10vw, 44px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -.02em;
  color: var(--card-ink);
  max-width: 8ch;
}

.card-cover__rule {
  width: 44px;
  height: 1px;
  background: var(--card-gold);
}



@keyframes breathe {
  0%, 100% { opacity: .55; }
  50%      { opacity: 1; }
}

/* ================================================================ EFFECTS == */

/* Full-page layers. Above the card on purpose - these are meant to play over
   the whole screen, not be boxed in by the card the way the cover's are. */
.fx {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  overflow: hidden;
}

/* Behind the card rather than over it, for things that should read as
   background: the balloons and the confetti on the last message page. */
.fx--behind { z-index: 0; }

/* The solved-puzzle celebration: over the whole screen, outside the pages. */
.fx--global { position: fixed; }

.fx i {
  position: absolute;
  display: block;
}

/* confetti pieces. `top` is animated rather than transform so the fall is a
   percentage of the layer, and it loops rather than running out. */
.fx--piece {
  width: 9px;
  height: 14px;
  opacity: 0;
  animation: fx-fall linear infinite;
}

@keyframes fx-fall {
  0%   { top: -14%; opacity: 0; transform: rotate(var(--spin, 0deg)); }
  6%   { opacity: 1; }
  92%  { opacity: 1; }
  100% { top: 108%; opacity: 0; transform: rotate(calc(var(--spin, 0deg) + 680deg)); }
}

/* light hearts drifting up the note page */
.fx--heart {
  font-size: 18px;
  line-height: 1;
  opacity: 0;
  animation: heart-rise linear infinite;
}

@keyframes heart-rise {
  0%   { top: 104%; opacity: 0;   transform: translateX(0) rotate(-6deg); }
  10%  { opacity: .62; }
  50%  { transform: translateX(16px) rotate(6deg); }
  88%  { opacity: .62; }
  100% { top: -12%; opacity: 0;   transform: translateX(0) rotate(-4deg); }
}

/* balloons that a firework bursts into, which then float away */
.fx--pop-balloon {
  width: 26px;
  height: 34px;
  border-radius: 50% 50% 48% 48% / 55% 55% 45% 45%;
  opacity: 0;
  animation: balloon-pop var(--rise-ms, 9000ms) cubic-bezier(.25, .55, .4, 1) forwards;
}

.fx--pop-balloon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  width: 1px;
  height: 22px;
  background: rgba(176, 141, 87, .5);
  transform: translateX(-50%);
}

/* Thrown out to (--bx, --by) by the burst, then floats up from wherever it
   landed, drifting sideways a little on the way. */
@keyframes balloon-pop {
  0%   { opacity: 0; transform: translate(0, 0) scale(.15); }
  5%   { opacity: 1; transform: translate(calc(var(--bx) * .8), calc(var(--by) * .8)) scale(1.12); }
  10%  { transform: translate(var(--bx), var(--by)) scale(1); }
  90%  { opacity: 1; }
  100% { opacity: 0; transform: translate(calc(var(--bx) + var(--drift)), calc(var(--by) + var(--rise))) scale(1); }
}

/* balloons drifting up behind the card */
.fx--balloon {
  width: 30px;
  height: 39px;
  border-radius: 50% 50% 48% 48% / 55% 55% 45% 45%;
  opacity: 0;
  animation: balloon-rise linear infinite;
}

.fx--balloon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  width: 1px;
  height: 26px;
  background: rgba(176, 141, 87, .5);
  transform: translateX(-50%);
}

/* The note's balloons: small and pale, so they stay in the background. */
.fx--balloon-small { animation-name: balloon-rise-soft; }
.fx--balloon-small::after { height: 16px; background: rgba(176, 141, 87, .35); }

@keyframes balloon-rise-soft {
  0%   { top: 106%; opacity: 0;   transform: translateX(0); }
  12%  { opacity: .55; }
  50%  { transform: translateX(-12px); }
  88%  { opacity: .55; }
  100% { top: -14%; opacity: 0;   transform: translateX(0); }
}

@keyframes balloon-rise {
  0%   { top: 112%; opacity: 0;   transform: translateX(0); }
  12%  { opacity: .9; }
  50%  { transform: translateX(14px); }
  88%  { opacity: .9; }
  100% { top: -22%; opacity: 0;   transform: translateX(0); }
}

/* firework sparks. The glow is what makes them read against a pale page;
   plain dots this size just look like specks. */
.fx--spark {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  opacity: 0;
  animation: spark-fly 1250ms ease-out forwards;
}

@keyframes spark-fly {
  0%   { opacity: 1; transform: translate(0, 0) scale(1); }
  70%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) scale(.35); }
}

/* the shell, climbing from the bottom before it bursts */
.fx--shell {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  opacity: 0;
  animation: shell-climb 620ms cubic-bezier(.2, .55, .35, 1) forwards;
}

@keyframes shell-climb {
  0%   { opacity: 0;  transform: translateY(0) scale(.7); }
  12%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { opacity: .9; transform: translateY(var(--climb)) scale(1); }
}

/* the end page's shell: bigger, so it can be followed up from the bottom */
.fx--shell-big { width: 8px; height: 8px; }
.fx--shell-big::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 60%;
  width: 4px;
  height: 70px;
  border-radius: 2px;
  transform: translateX(-50%);
  background: linear-gradient(rgba(255, 150, 40, .9), rgba(255, 200, 90, 0));
}

/* the flash at the centre of a burst */
.fx--flash {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  opacity: 0;
  animation: spark-flash 420ms ease-out forwards;
}

@keyframes spark-flash {
  0%   { opacity: 1; transform: scale(.25); }
  100% { opacity: 0; transform: scale(4.2); }
}

/* ============================================================ PHOTO PAGE == */

/* The photo pages give the picture as much of the card as possible. */
.screen--photo .card-page { padding: 10px; }

/* The picture fills everything above the hint line. The 10px foot plus the
   card's own 10px puts the hint exactly where it sits on every other page. */
.card-page__inner--photo {
  padding: 0 0 10px;
  justify-content: flex-start;
}

/* Holds the picture, and clips picture two's zoom so it can never spread
   down over the hint. */
.photo-frame {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* Mounted, not full-bleed: these photos carry scores and names right out to
   their edges, so filling the card would crop away the point of them. */
.photo {
  display: block;
  max-width: 100%;
  max-height: 100%;
  min-height: 0;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 2px 12px rgba(44, 42, 74, .22);
  opacity: 0;
  transition: opacity 320ms var(--ease-out);
}

.photo.is-loaded { opacity: 1; }

.cake__photo.is-fading { animation: photo-fade 2000ms ease-in-out both; }

/* Picture one drifts in slowly. */
.photo.is-fading  { animation: photo-fade 2000ms ease-in-out both; }

/* Picture two arrives all at once. */
.photo.is-jumping { animation: photo-jump 620ms cubic-bezier(.2, 1.5, .4, 1) both; }

/* Picture three holds back while the fireworks get going, then drives in from
   the far corner, parks, and shakes itself still. `both` holds the first frame
   through the delay, so it is genuinely absent rather than just transparent. */
.photo.is-driving {
  animation: photo-drive 1700ms cubic-bezier(.22, .9, .3, 1) 1000ms both;
}

@keyframes photo-drive {
  0%   { opacity: 0; transform: translate(-62%, 46%) scale(.10) rotate(-28deg); }
  12%  { opacity: 1; }
  58%  { transform: translate(2%, -2%)  scale(1.07) rotate(4deg); }
  70%  { transform: translate(-1%, 1%)  scale(.97)  rotate(-3deg); }
  80%  { transform: translate(.6%, -.6%) scale(1.03) rotate(2deg); }
  88%  { transform: translate(-.4%, .4%) scale(.99)  rotate(-1.2deg); }
  94%  { transform: translate(.2%, 0)   scale(1.01) rotate(.6deg); }
  100% { opacity: 1; transform: translate(0, 0) scale(1) rotate(0deg); }
}

@keyframes photo-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes photo-jump {
  0%   { opacity: 0; transform: scale(.4) rotate(-4deg); }
  60%  { opacity: 1; transform: scale(1.06) rotate(1.5deg); }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}

/* Picture two eases in on the two of them at the podiums. The origin is what
   decides the framing: the point under it is the one that stays put. */
.photo.is-zooming {
  transform-origin: var(--zx, 93%) var(--zy, 75%);
  animation: photo-zoom var(--zt, 5000ms) ease-in-out forwards;
}

@keyframes photo-zoom {
  from { transform: scale(1); }
  to   { transform: scale(var(--zs, 1.5)); }
}

/* The spinning TV screen on the clue page. The layer matches the picture's
   box; the hole matches the screen within it and is dark, so while the sign
   is edge-on you see the back of the set, not the old screen behind it. */
.screen-flip {
  position: absolute;
  pointer-events: none;
}
.screen-flip__hole {
  position: absolute;
  background: #060a1c;
  perspective: 900px;
}
.screen-flip__card {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}
.screen-flip__face {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.screen-flip__face--back { transform: rotateY(180deg); }

/* Three and a half turns, winding up slow, spinning fast, settling on the
   clue. The duration must match FLIP.spin in app.js. */
.screen-flip__card.is-flipping { animation: screen-spin 2600ms cubic-bezier(.55, 0, .25, 1) forwards; }
.screen-flip__card.is-flipped  { transform: rotateY(1260deg); }

@keyframes screen-spin {
  0%   { transform: rotateY(0deg) scale(1); }
  50%  { transform: rotateY(630deg) scale(1.06); }
  100% { transform: rotateY(1260deg) scale(1); }
}

/* Then a soft glow round the screen, twice, so the eye goes to it. */
.screen-flip__hole.is-glowing { animation: screen-glow 1300ms ease-in-out 2; }
@keyframes screen-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(140, 180, 255, 0); }
  50%      { box-shadow: 0 0 18px 6px rgba(140, 180, 255, .85); }
}

/* The zoom needs somewhere to be clipped. */
.card-page__inner--photo { overflow: hidden; }

/* ======================================================= CORRECTION MARK == */

/* A word crossed out with the replacement written in above it, the way you
   would correct something in red pen. */

.edit {
  position: relative;
  display: inline-block;
  margin-right: .1em;
}

.edit__struck {
  position: relative;
  display: inline-block;
  color: var(--card-ink);
  transition: color 500ms ease-out;
}

.edit__struck.is-struck { color: var(--muted); }

/* The pen stroke itself, drawn left to right. */
.strike {
  position: absolute;
  left: -3%;
  right: -3%;
  top: 52%;
  height: 2.5px;
  border-radius: 2px;
  background: var(--ink-red);
  transform: scaleX(0);
  transform-origin: left center;
}

.strike.is-drawn { transform: scaleX(1); transition: transform 520ms cubic-bezier(.3,.8,.35,1); }

/* Letters of a handwritten insertion, revealed one at a time. */
.type__ch {
  display: inline-block;
  opacity: 0;
  animation: type-in 170ms ease-out forwards;
  white-space: pre;
}

@keyframes type-in {
  from { opacity: 0; transform: translateY(3px) rotate(-5deg); }
  to   { opacity: 1; transform: none; }
}

.edit__ins {
  opacity: 0;
  position: absolute;
  left: 50%;
  bottom: 118%;
  transform: translateX(-48%) rotate(-6deg);
  font-family: "Caveat", "Segoe Script", cursive;
  font-weight: 700;
  font-size: 1.15em;
  line-height: 1;
  color: var(--ink-red);
  white-space: nowrap;
  pointer-events: none;
}

.edit__ins.is-writing { opacity: 1; }

/* The caret that points into the gap where the new word belongs. */
.edit__caret {
  position: absolute;
  left: 50%;
  bottom: 88%;
  transform: translateX(-50%) scaleX(1.5);
  font-family: "Caveat", cursive;
  font-weight: 700;
  font-size: .8em;
  line-height: 1;
  color: var(--ink-red);
  pointer-events: none;
  opacity: 0;
}

.edit__caret.is-in { animation: caret-pop 260ms cubic-bezier(.2,1.6,.4,1) forwards; }

@keyframes caret-pop {
  from { opacity: 0; transform: translateX(-50%) scaleX(1.5) scale(.4); }
  to   { opacity: 1; transform: translateX(-50%) scaleX(1.5) scale(1); }
}

/* ================================================================== CAKE == */

/* Cake and words at the top, the photo filling everything below them. The
   spacer keeps the top where it was when the page was centred. */
.card-page__inner--cake { justify-content: flex-start; }
.card-page__inner--cake::before { content: ""; flex: 0 1 11%; }

.cake {
  width: min(62%, 205px);
  height: auto;
  overflow: visible;
  flex: none;
}

.cake__line {
  margin: 12px 0 0;
  text-align: center;
  font-size: clamp(19px, 5.6vw, 24px);
  font-weight: 800;
  letter-spacing: -.01em;
  color: var(--card-ink);
}

.cake__aside {
  /* Holds its line whether or not anything is written in it yet, so the
     picture below does not jump down when the writing starts. */
  min-height: 1.15em;
  margin: 2px 0 0;
  text-align: center;
  font-family: "Caveat", "Segoe Script", cursive;
  font-weight: 700;
  font-size: clamp(22px, 6.6vw, 28px);
  line-height: 1.1;
  color: var(--ink-red);
  transform: rotate(-3deg);
}

.flame {
  transform-origin: 50% 100%;
  filter: drop-shadow(0 0 6px rgba(246, 181, 60, .85));
}

.flame--1 { animation: flicker 1.9s ease-in-out infinite; }
.flame--2 { animation: flicker 2.3s ease-in-out infinite .35s; }
.flame--3 { animation: flicker 1.7s ease-in-out infinite .7s; }

@keyframes flicker {
  0%, 100% { transform: scaleY(1)    scaleX(1)    translateY(0); opacity: 1; }
  35%      { transform: scaleY(1.16) scaleX(.92)  translateY(-2px); opacity: .92; }
  65%      { transform: scaleY(.92)  scaleX(1.06) translateY(1px); opacity: 1; }
}

/* As big as the space below the words allows. Cropped, not squashed: the
   crop keeps their faces and the cake and lets their legs go first. */
.cake__photo {
  display: block;
  flex: 1 1 0;
  min-height: 120px;
  width: 100%;
  object-fit: cover;
  object-position: 50% 14%;
  margin: 26px 0 6px;
  border-radius: 4px;
  box-shadow: 0 2px 10px rgba(44, 42, 74, .2);
}

/* ================================================================== END == */

.card-page__inner--end { justify-content: center; }

/* A heart sitting in the lower half of the last page. */
.card-page__inner--end { position: relative; }
.end-heart {
  position: absolute;
  left: 50%;
  top: 74%;
  width: 74px;
  height: auto;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 4px 8px rgba(210, 59, 90, .3));
  animation: heart-in 1200ms ease-out 600ms both;
}

@keyframes heart-in {
  from { opacity: 0; transform: translate(-50%, -40%) scale(.85); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* "Start over" sits at the foot like every other page's hint. The matching
   auto margin on the words keeps them in the middle of the space above. */
.start-over { flex: none; }
.card-page__inner--end .the-end { margin-top: auto; }

.the-end {
  margin: 0;
  text-align: center;
  font-size: clamp(30px, 9vw, 42px);
  font-weight: 800;
  letter-spacing: .02em;
  color: var(--card-ink);
}

/* ================================================================= INTRO == */

/* Fills the space above the hint so the line sits in the middle of the card. */
.intro__body {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
}

.intro__heading {
  margin: 0;
  text-align: center;
  font-size: clamp(24px, 7vw, 32px);
  font-weight: 800;
  line-height: 1.22;
  letter-spacing: -.02em;
  color: var(--card-ink);
  text-wrap: balance;
}

/* "special", written in by hand in the card's gold so it stands out. */
.intro__heading em {
  display: inline-block;
  font-family: "Caveat", "Segoe Script", cursive;
  font-style: normal;
  font-weight: 700;
  font-size: 1.35em;
  line-height: 1;
  letter-spacing: 0;
  color: var(--card-gold);
  margin: 0 .14em;
  transform: rotate(-3deg);
}

/* ~Accented~ words: italic, in the purple from the puzzle. */
.intro__heading .w--accent {
  font-style: italic;
  color: var(--accent);
}

/* One word at a time, each coming forward out of the page. Only while
   .is-playing is on; without it the words simply sit there. */
.intro__heading .w { display: inline-block; }

.intro__heading.is-playing .w {
  animation: word-in 520ms cubic-bezier(.2, .8, .3, 1) both;
  animation-delay: calc(400ms + var(--at, 0ms));
}
/* Dots are quicker, and app.js starts each one halfway through the last. */
.intro__heading.is-playing .w--dot {
  animation-duration: 230ms;
}

/* A red pen line drawn under a word once the whole line has come in. */
.intro__heading .w--under { position: relative; }
.intro__heading .w--under::after {
  content: "";
  position: absolute;
  left: -4%;
  right: -4%;
  bottom: -.06em;
  height: .12em;
  min-height: 3px;
  border-radius: 3px;
  background: var(--ink-red);
  transform: rotate(-1.5deg) scaleX(0);
  transform-origin: left center;
}
.intro__heading.is-playing .w--under::after {
  animation: underline-draw 620ms cubic-bezier(.3, .8, .35, 1) calc(400ms + var(--u, 0ms)) forwards;
}
@keyframes underline-draw {
  to { transform: rotate(-1.5deg) scaleX(1); }
}
@keyframes word-in {
  0%   { opacity: 0; transform: scale(.3); filter: blur(6px); }
  60%  { opacity: 1; filter: blur(0); }
  100% { opacity: 1; transform: scale(1); }
}

/* "special" lands with a small bounce, keeping its tilt. */
.intro__heading.is-playing em.w {
  animation-name: word-in-bounce;
  animation-duration: 900ms;
}
@keyframes word-in-bounce {
  0%   { opacity: 0; transform: scale(.3) rotate(-3deg); filter: blur(6px); }
  35%  { opacity: 1; transform: scale(1.25) rotate(-3deg); filter: blur(0); }
  55%  { transform: scale(.9) rotate(-3deg); }
  75%  { transform: scale(1.06) rotate(-3deg); }
  100% { opacity: 1; transform: scale(1) rotate(-3deg); }
}

/* =================================================================== NOTE == */

.screen--note { cursor: pointer; }

.note__body, .note__heading, .note__signoff { align-self: stretch; }

/* Handwritten, in a light pen: Kalam at its thinnest. */
.note__heading {
  font-family: "Kalam", "Segoe Script", cursive;
  font-size: clamp(21px, 5.9vw, 28px);
  font-weight: 400;
  line-height: 1.15;
  margin: 0 0 8px;
  color: var(--card-ink);
}

.note__body p {
  font-family: "Kalam", "Segoe Script", cursive;
  font-size: clamp(14px, 4.2vw, 18px);
  font-weight: 300;
  line-height: 1.32;
  margin: 0 0 9px;
  color: #2f2d52;
}

/* While the note is coming in, each word waits hidden until app.js gives
   it its time, then fades slowly up. */
.nw-word { display: inline-block; }
.screen--note.is-writing .nw-word {
  animation: nw-in 900ms ease-in-out 99s both;
}
@keyframes nw-in {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: none; }
}

/* Taller phones have the room for bigger writing. */
@media (min-height: 850px) {
  .note__heading { font-size: clamp(22px, 6.2vw, 30px); margin-bottom: 10px; }
  .note__body p { font-size: clamp(15.5px, 4.6vw, 19.5px); line-height: 1.36; margin-bottom: 11px; }
}

.note__signoff {
  font-size: 17px;
  font-style: italic;
  color: var(--muted);
  margin: 28px 0 0;
}

/* Tucked into the top right of the second paragraph, with the text wrapping
   down its left side. It takes its space from the start, invisible, so the
   words do not jump when it fades in. */
.note__photo {
  float: right;
  width: 44%;
  height: auto;
  margin: 4px 0 6px 12px;
  border-radius: 4px;
  box-shadow: 0 2px 10px rgba(44, 42, 74, .2);
  opacity: 0;
  transition: opacity 2600ms ease-in-out;
}

.note__photo.is-shown { opacity: 1; }

/* Sits at the foot of the card. `margin-top: auto` is what pins it there:
   it soaks up the spare space above it. */

/* Kept out of sight until the page's animation has finished (app.js). */
.page-hint.is-held { visibility: hidden; }
.page-hint {
  margin: 18px 0 0;
  margin-top: auto;
  padding-top: 12px;
  align-self: stretch;
  text-align: center;
  font-size: 13px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
  animation: breathe 2.6s ease-in-out infinite;
}

/* ================================================================= PUZZLE == */

.puzzle__header { text-align: center; margin: 4px 0 14px; }

.puzzle__title {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -.01em;
  margin: 0 0 14px;
}

.puzzle__instruction {
  font-size: 16px;
  margin: 0;
}

/* ---- toast ---- */
.toast {
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.toast__pill {
  background: var(--ink);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  padding: 9px 14px;
  border-radius: 4px;
  animation: toast-in 180ms var(--ease-out);
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* ---- board ---- */
.board {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--gap);
}

.tile {
  position: relative;
  aspect-ratio: 4 / 3;
  min-height: 44px;              /* tap-target floor; see note below */
  border-radius: var(--radius);
  background: var(--tile-bg);
  color: var(--tile-fg);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: .01em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4px 6px;
  overflow: hidden;
  /* Grid items default to min-width:auto, so the nowrap label's intrinsic width
     would widen the column and swallow the gaps before app.js can shrink it. */
  min-width: 0;
  user-select: none;
  -webkit-user-select: none;
  transition: background-color 120ms linear, color 120ms linear, transform 120ms var(--ease-out);
}

.tile:active { transform: scale(.96); }

.tile.is-selected {
  background: var(--tile-selected);
  color: #fff;
}

/* The label is measured and scaled by app.js so any word the user writes fits
   its tile. inline-block is what makes the measurement meaningful — a block
   would always report the tile's own width. */
.tile__label {
  display: inline-block;
  white-space: nowrap;
  line-height: 1.1;
}

/* Fallback for a word too long to fit even at the minimum size. */
.tile.is-wrapped .tile__label {
  display: block;
  width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.05;
}

.tile.is-bouncing { animation: tile-bounce 300ms var(--ease-out); }
@keyframes tile-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-11px); }
}

.tile.is-wrong { animation: tile-shake 420ms ease-in-out; }
@keyframes tile-shake {
  0%, 100%      { transform: translateX(0); }
  15%, 45%, 75% { transform: translateX(-6px); }
  30%, 60%, 90% { transform: translateX(6px); }
}

/* Tiles sliding into position after a correct guess (FLIP). Slow enough to
   watch the four of them travel up to the top row. */
.tile.is-flipping { transition: transform 800ms cubic-bezier(.45, 0, .25, 1); }

/* Tiles spinning in when the puzzle opens. `both` keeps each one hidden until
   its own turn comes. */
.tile.is-dealing {
  animation: tile-deal 1000ms cubic-bezier(.2, .7, .3, 1) var(--deal-delay, 0ms) both;
}
@keyframes tile-deal {
  0%   { opacity: 0; transform: scale(.2) rotate(-900deg); }
  45%  { opacity: 1; }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}

/* ---- solved group band ---- */
.band {
  grid-column: 1 / -1;
  border-radius: var(--radius);
  min-height: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 8px;
  text-align: center;
  animation: band-in 320ms var(--ease-out);
}
@keyframes band-in {
  from { opacity: 0; transform: scale(.96); }
  to   { opacity: 1; transform: none; }
}

.band[data-level="0"] { background: var(--lvl-0); }
.band[data-level="1"] { background: var(--lvl-1); }
.band[data-level="2"] { background: var(--lvl-2); }
.band[data-level="3"] { background: var(--lvl-3); }

.band__title {
  font-size: 15px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .01em;
  line-height: 1.15;
  text-wrap: balance;        /* avoids a lonely last word on a wrapped title */
  overflow-wrap: anywhere;
}

/* A word written in {braces} in puzzle.js keeps its own capitalisation. */
.band__keep { text-transform: none; }

.band__words {
  font-size: 15px;
  font-weight: 400;
  line-height: 1.2;
}

/* ---- mistakes ---- */
.mistakes {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 18px 0 16px;
  font-size: 15px;
  min-height: 20px;
}
.mistakes__dots { display: flex; gap: 8px; align-items: center; }

.dot {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--tile-selected);
  transition: transform 220ms var(--ease-out), opacity 220ms;
}
.dot.is-spent { transform: scale(0); opacity: 0; }

/* ---- controls ---- */
.controls {
  display: flex;
  gap: 6px;
  justify-content: center;
  flex-wrap: wrap;
  width: 100%;
}

.btn {
  min-height: 48px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--ink);
  background: var(--paper);
  font-size: 14.5px;
  font-weight: 500;
  white-space: nowrap;
  transition: transform 120ms var(--ease-out), opacity 120ms;
}
.btn:active { transform: scale(.96); }

.btn:disabled {
  border-color: var(--hairline);
  color: var(--hairline);
  cursor: default;
}
.btn:disabled:active { transform: none; }

.btn--primary:not(:disabled) {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

/* Styled as a .page-hint so it matches the line at the foot of every other
   page. The header's auto margin splits the spare space with the hint's, so
   the board stays centred rather than riding up to the top. */
.restart { padding: 12px 0 0; }
.screen--puzzle .puzzle__header { margin-top: auto; }

/* Out of mistakes: everything but the title fades away and the message
   takes over the middle of the card. */
.screen--puzzle .board,
.screen--puzzle .mistakes,
.screen--puzzle .controls,
.screen--puzzle .restart { transition: opacity 550ms ease; }
.screen--puzzle.is-redo .board,
.screen--puzzle.is-redo .mistakes,
.screen--puzzle.is-redo .controls,
.screen--puzzle.is-redo .restart { opacity: 0; pointer-events: none; }
.screen--puzzle.is-redo .restart { visibility: hidden; }   /* its breathing would bring it back */

.redo {
  position: absolute;
  left: 8%;
  right: 8%;
  top: 50%;
  margin: 0;
  transform: translateY(-50%);
  text-align: center;
  font-size: clamp(26px, 8vw, 36px);
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -.02em;
  color: var(--ink-red);
  text-wrap: balance;
}
.redo.is-in { animation: redo-in 520ms cubic-bezier(.2, 1.5, .4, 1) both; }
@keyframes redo-in {
  from { opacity: 0; transform: translateY(-50%) scale(.6) rotate(-4deg); }
  to   { opacity: 1; transform: translateY(-50%) scale(1) rotate(0); }
}

/* Solved: "Restart puzzle" rises to about halfway between the board and the
   foot of the card, and "swipe to the next page" takes its place at the foot. */
.screen--puzzle.is-won .restart { flex: none; }

/* ================================================================= FINALE == */

.screen--finale {
  overflow: hidden;
}

.finale__inner {
  position: relative;
  z-index: 1;
  text-align: center;
  width: 100%;
}

/* Fills the page so the note sits centred and the hint sits at the foot. */
.finale__content {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
}

.finale__eyebrow {
  font-size: 12px;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--card-gold);
  margin: 0 0 14px;
}

.finale__heading {
  font-size: clamp(34px, 11vw, 56px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -.03em;
  color: var(--card-ink);
  margin: 0 0 22px;
}

.finale__body p {
  font-size: 17px;
  line-height: 1.6;
  margin: 0 0 14px;
}

/* ---- confetti ---- */
.confetti { position: absolute; inset: 0; pointer-events: none; }

.confetti i {
  position: absolute;
  top: -12vh;
  width: 9px;
  height: 14px;
  opacity: 0;
  animation: confetti-fall linear forwards;
}

@keyframes confetti-fall {
  0%   { opacity: 1; transform: translateY(0) rotate(0deg); }
  100% { opacity: 0; transform: translateY(115vh) rotate(720deg); }
}

/* ========================================================= WRAPPING PAPER == */

/* Covers the whole puzzle card until she has tapped it open. */
.gift {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: 10px;
  cursor: pointer;
  touch-action: none;          /* every finger movement here is ours, never a scroll */
  -webkit-tap-highlight-color: transparent;
  /* A small shake every few seconds, to say "open me". */
  animation: gift-wiggle 3.2s ease-in-out 1.2s infinite;
}

.gift.is-started { animation: none; }
.gift.is-jolted  { animation: gift-jolt 380ms ease-out; }

@keyframes gift-wiggle {
  0%, 82%, 100% { transform: none; }
  86% { transform: rotate(-1.4deg); }
  90% { transform: rotate(1.2deg); }
  94% { transform: rotate(-.7deg); }
}

@keyframes gift-jolt {
  0%   { transform: none; }
  30%  { transform: translateX(calc(var(--dir, -1) * 7px)) rotate(calc(var(--dir, -1) * 1deg)); }
  65%  { transform: translateX(calc(var(--dir, -1) * -3px)); }
  100% { transform: none; }
}

/* The paper itself. Striped and spotted in the puzzle's own four colours. */
.gift {
  --paper:
    radial-gradient(circle at 14px 14px, #f9df6d 0 4.5px, transparent 5px),
    radial-gradient(circle at 42px 42px, #ffffff 0 3.5px, transparent 4px),
    repeating-linear-gradient(45deg,
      rgba(255, 255, 255, .13) 0 10px, transparent 10px 28px),
    linear-gradient(160deg, #b86fc8 0%, #9657b0 100%);
}

/* Under the pieces while the paper is whole, so no hairline shows at a join.
   It goes as soon as the first piece is torn away. */
.gift {
  background: var(--paper);
  background-size: 56px 56px, 56px 56px, auto, auto;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .06);
}
.gift.is-tearing { background: none; box-shadow: none; }

.gift__strips { position: absolute; inset: 0; }

/* Every piece is the whole sheet, cut down to its own shape by clip-path (set
   in app.js), so the pattern runs unbroken across all of them. */
.gift__piece {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  background: var(--paper);
  background-size: 56px 56px, 56px 56px, auto, auto;
}

/* Torn off: a little hop, then flung away from her finger, tumbling down. */
.gift__piece.is-torn {
  animation: piece-fly 900ms cubic-bezier(.35, .1, .7, 1) both;
  z-index: 1;
}

@keyframes piece-fly {
  0%   { transform: none; opacity: 1; }
  20%  { transform: translate(calc(var(--tx) * .18), calc(var(--ty) * -.08)) rotate(calc(var(--rot) * .15)) scale(1.05); }
  80%  { opacity: 1; }
  100% { transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(.7); opacity: 0; }
}

/* The ribbon, crossing in the middle. Clipped to the present, so it slides
   off the paper rather than out across the table. */
.gift__ribbons {
  position: absolute;
  inset: 0;
  border-radius: 10px;
  overflow: hidden;
  z-index: 1;
}

.gift__ribbon {
  position: absolute;
  background:
    linear-gradient(90deg, rgba(0,0,0,.12), transparent 22%, transparent 78%, rgba(0,0,0,.12)),
    #d23b3b;
  transition: transform 560ms cubic-bezier(.5, 0, .7, .4), opacity 560ms;
}
.gift__ribbon--v { top: 0; bottom: 0; left: 50%; width: 28px; margin-left: -14px; }
.gift__ribbon--h {
  left: 0; right: 0; top: 42%; height: 28px; margin-top: -14px;
  background:
    linear-gradient(180deg, rgba(0,0,0,.12), transparent 22%, transparent 78%, rgba(0,0,0,.12)),
    #d23b3b;
}

.gift__bow {
  position: absolute;
  z-index: 1;
  left: 50%;
  top: 42%;
  width: 124px;
  height: auto;
  transform: translate(-50%, -52%);
  filter: drop-shadow(0 3px 4px rgba(0, 0, 0, .25));
  transition: transform 620ms cubic-bezier(.3, -.3, .6, 1), opacity 620ms;
}

/* The bow's parts move on their own as it comes undone. */
.gift__bow g {
  transform-box: fill-box;
  transition: transform 460ms cubic-bezier(.3, 1.4, .5, 1), opacity 460ms;
}
.bow__loop--l { transform-origin: 100% 60%; }
.bow__loop--r { transform-origin: 0 60%; }
.bow__tail--l { transform-origin: 100% 0; }
.bow__tail--r { transform-origin: 0 0; }
.bow__knot    { transform-origin: 50% 50%; }

/* Tap 1: a tug on the ends. The loops pull in tight. */
.gift.is-bow-1 .bow__loop--l { transform: rotate(-8deg) scale(.9, .96); }
.gift.is-bow-1 .bow__loop--r { transform: rotate(8deg) scale(.9, .96); }
.gift.is-bow-1 .bow__tail--l { transform: rotate(10deg); }
.gift.is-bow-1 .bow__tail--r { transform: rotate(-10deg); }

/* Tap 2: the knot gives. The loops slip out and go slack, the ends drop. */
.gift.is-bow-2 .bow__loop--l { transform: translate(-14px, 6px) rotate(-38deg) scale(.8); }
.gift.is-bow-2 .bow__loop--r { transform: translate(14px, 6px) rotate(38deg) scale(.8); }
.gift.is-bow-2 .bow__knot    { transform: scale(.55); opacity: .7; }
.gift.is-bow-2 .bow__tail--l { transform: rotate(28deg) translateY(6px); }
.gift.is-bow-2 .bow__tail--r { transform: rotate(-28deg) translateY(6px); }

/* Tap 3: the bow falls away, and the ribbon round the middle slides off. */
.gift.is-bow-3 .gift__bow,
.gift.is-bow-4 .gift__bow {
  transform: translate(-50%, 180%) rotate(calc(var(--dir, 1) * 30deg));
  opacity: 0;
  transition: transform 700ms cubic-bezier(.5, 0, .75, .5), opacity 700ms;
}
.gift.is-bow-3 .gift__ribbon--h,
.gift.is-bow-4 .gift__ribbon--h { transform: translateX(calc(var(--dir, 1) * 105%)); opacity: 0; }

/* Tap 4: the last of the ribbon slides off the bottom. */
.gift.is-bow-4 .gift__ribbon--v { transform: translateY(105%); opacity: 0; }

/* The little instruction label near the foot of the present. */
.gift__tag {
  position: absolute;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  margin: 0;
  padding: 7px 14px;
  z-index: 2;
  white-space: nowrap;
  font-size: 11.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--card-ink);
  background: var(--card-face);
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(44, 42, 74, .25);
  animation: breathe 2.6s ease-in-out infinite;
  transition: opacity 300ms;
}
.gift.is-done .gift__tag { opacity: 0; animation: none; }

/* The tiles wait, hidden and untappable, until it is their turn to spin in. */
.board.is-waiting { pointer-events: none; }
.board.is-waiting .tile { opacity: 0; }

/* ======================================================== CONGRATULATIONS == */

.congrats__heading {
  margin: 0;
  text-align: center;
  /* Sized so the whole word fits on one line of the card. */
  font-size: clamp(22px, 7.4vw, 38px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.02em;
  color: var(--card-ink);
  white-space: nowrap;
}

.congrats__heading .l { display: inline-block; }

/* Letters bounce in one after another in the puzzle colours, then the word
   keeps jumping every so often, a wave running along it. */
.congrats__heading.is-playing .l {
  animation:
    letter-pop 700ms cubic-bezier(.2, 1.6, .4, 1) calc(250ms + var(--n) * 70ms) both,
    letter-colour 2.8s linear calc(250ms + var(--n) * 70ms) infinite,
    letter-wave 2.4s ease-in-out calc(2000ms + var(--n) * 60ms) infinite;
}

@keyframes letter-pop {
  0%   { opacity: 0; transform: translateY(-60px) scale(.3) rotate(-25deg); }
  60%  { opacity: 1; transform: translateY(6px) scale(1.25) rotate(6deg); }
  100% { opacity: 1; transform: none; }
}

@keyframes letter-colour {
  0%, 100% { color: #d23b3b; }
  20%      { color: #e0a800; }
  40%      { color: #6c9a2c; }
  60%      { color: #4f74c7; }
  80%      { color: #9657b0; }
}

@keyframes letter-wave {
  0%, 30%, 100% { translate: 0 0; }
  12%           { translate: 0 -12px; }
}

/* ================================================================ CLOUDS == */

/* Covers the first picture, then parts to reveal it. */
.clouds {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: 10px;
  overflow: hidden;
  pointer-events: none;
}

.clouds i { position: absolute; display: block; }

.clouds__fog {
  inset: 0;
  background: radial-gradient(ellipse at 50% 45%, #f7f9fc 0%, #e9eef5 60%, #d8e0ea 100%);
  animation: fog-clear 2800ms ease-in-out both;
}

@keyframes fog-clear {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* "A year from now in 2027", written across the clouds, cut in two halves
   that part with them. */
.clouds__words {
  position: absolute;
  left: 0;
  right: 0;
  top: 46%;
  margin: 0;
  padding: 0 11%;
  text-align: center;
  font-size: clamp(28px, 8.6vw, 40px);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -.02em;
  font-style: italic;              /* same italic purple as "next year's" */
  color: var(--accent);
  text-wrap: balance;
  z-index: 1;
  transform: translateY(-50%);
  animation: 3800ms cubic-bezier(.45, 0, .35, 1) both;
}
/* Each letter fades in on its own, in random order (timings from app.js). */
.clouds__letter { opacity: 0; animation: cloud-letter 450ms ease-out both; }
@keyframes cloud-letter {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.clouds__words--l { clip-path: inset(0 50% 0 0); animation-name: words-part-l; }
.clouds__words--r { clip-path: inset(0 0 0 50%); animation-name: words-part-r; }

@keyframes words-part-l {
  0%   { transform: translate(0, -50%); opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: translate(-62%, -50%); opacity: 0; }
}
@keyframes words-part-r {
  0%   { transform: translate(0, -50%); opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: translate(62%, -50%); opacity: 0; }
}

.clouds__puff {
  border-radius: 50%;
  background:
    radial-gradient(closest-side at 40% 38%, #ffffff 0 55%, rgba(255,255,255,0) 100%),
    radial-gradient(closest-side at 62% 64%, #d6deea 0 50%, rgba(214,222,234,0) 100%);
  transform: translate(-50%, -50%);
  animation: cloud-part 4800ms cubic-bezier(.45, 0, .35, 1) both;
}

@keyframes cloud-part {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(1.15); }
  55%  { opacity: 1; }
  100% { opacity: 0; transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(1.6); }
}

/* ============================================================ responsive == */

@media (max-width: 380px) {
  :root { --gap: 6px; --pad: 8px; }
  .card-page { padding: 10px; }
  .card-page__inner { padding: 4px; }
  .screen--note .card-page { padding: 14px; padding-bottom: 10px; }
  .screen--note .card-page__inner { padding: 8px 6px; padding-bottom: 4px; }
  .card-cover { padding: 14px; }
  /* Keep Shuffle / Deselect All / Submit on one row, as the real game does. */
  .controls { gap: 5px; }
  .btn { padding: 0 9px; font-size: 13.5px; }
}

/* Landscape on a phone: keep the whole board on screen */
@media (max-height: 560px) and (orientation: landscape) {
  .screen { justify-content: flex-start; }
  .tile { aspect-ratio: 5 / 2; }
  .band { min-height: 46px; }
  .puzzle__title { margin-bottom: 6px; }
}

/* Very narrow phones (iPhone SE 1st gen and similar) need one more step down
   to keep the three game buttons on a single row. */
@media (max-width: 340px) {
  :root { --gap: 4px; }
  .card-page { padding: 8px; }
  .card-page__inner { padding: 2px; }
  /* Same 10px foot as the other pages, so the bottom line lines up. */
  .card-cover { padding-bottom: 10px; }
  .screen--note .card-page { padding-bottom: 8px; }
  .screen--note .card-page__inner { padding-bottom: 2px; }
  .finale__heading { font-size: 30px; margin-bottom: 14px; }
  .finale__body p { font-size: 15px; line-height: 1.5; margin-bottom: 10px; }
  .controls { gap: 4px; }
  .btn { padding: 0 7px; font-size: 12.5px; }

  /* 44px tall x a 4:3 ratio implies a 59px minimum WIDTH, and four of those no
     longer fit the narrower card here. A squarer tile keeps the 44px tap
     target without forcing the board wider than its container. */
  .tile { aspect-ratio: 6 / 5; }
}

@media (max-height: 700px) {
  .puzzle__header { margin: 0 0 6px; }
  .puzzle__title { font-size: 20px; margin-bottom: 6px; }
  .puzzle__instruction { font-size: 14px; }
  .toast { height: 24px; }
  .toast__pill { font-size: 12.5px; padding: 6px 10px; }
  .mistakes { margin: 10px 0 10px; font-size: 14px; }
  .dot { width: 13px; height: 13px; }
  .btn { min-height: 44px; }
}

/* Long category names: on shorter phones keep the solved bars compact so the
   board still fits on the card. */
@media (max-height: 760px) {
  .band { min-height: 50px; padding: 5px 8px; gap: 1px; }
  .band__title, .band__words { font-size: 13px; }
}

@media (max-height: 600px) {
  .band { min-height: 44px; padding: 4px 6px; }
  .band__title, .band__words { font-size: 11px; line-height: 1.12; }
}

/* The note is long: on shorter phones tighten it so the photo keeps its room. */
@media (max-height: 760px) {
  .note__heading { font-size: 23px; margin-bottom: 6px; }
  .note__body p { font-size: 15.5px; line-height: 1.28; margin-bottom: 6px; }
  .note__photo { margin: 3px 0 4px 10px; }
}

@media (max-height: 600px) {
  .note__heading { font-size: 22px; margin-bottom: 4px; }
  .note__body p { font-size: 13.5px; line-height: 1.25; margin-bottom: 4px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .card-stage.is-opening .card-cover { opacity: 0; }
}
