/* =========================================================================
   TEN TEN — a birthday site
   Single committed theme: a room at midnight, lit by candles.
   Every colour is painted explicitly so the page holds on any host ground.
   ========================================================================= */

:root{
  --ink:      #09070F;   /* deepest night, plum-biased black          */
  --dusk:     #140F1E;   /* section ground                            */
  --veil:     #221A30;   /* raised surfaces, cards                    */
  --edge:     #362A48;   /* hairlines                                 */
  --cream:    #F5EDE2;   /* primary text                              */
  --mauve:    #9C8BAA;   /* the chosen neutral — plum-biased grey     */
  --candle:   #F2A93F;   /* the one bold thing: candlelight           */
  --candle-d: #C4761C;
  --rose:     #C97B96;   /* quiet secondary                           */

  --serif: "Instrument Serif", "Iowan Old Style", Georgia, serif;
  --sans:  "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
  --hand:  "Caveat", "Segoe Script", cursive;
  --hand-np: "Kalam", "Caveat", "Nirmala UI", sans-serif;   /* has Devanagari */

  --measure: 62ch;
  --ease: cubic-bezier(.22,1,.36,1);
}

/* Kalam — a handwriting face that actually has Devanagari glyphs. The site's
   other hand is Caveat, which is Latin only, so the letter would have fallen
   back to a flat system font for every Nepali word. Vendored rather than
   fetched, because the letter is the centre of the whole page. */
@font-face{
  font-family:"Kalam"; font-style:normal; font-weight:400; font-display:swap;
  src:url("../fonts/kalam-devanagari-400.woff2") format("woff2");
  unicode-range:U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FF;
}
@font-face{
  font-family:"Kalam"; font-style:normal; font-weight:700; font-display:swap;
  src:url("../fonts/kalam-devanagari-700.woff2") format("woff2");
  unicode-range:U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FF;
}
@font-face{
  font-family:"Kalam"; font-style:normal; font-weight:400; font-display:swap;
  src:url("../fonts/kalam-latin-400.woff2") format("woff2");
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
}

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

html{ scroll-behavior:smooth; overflow-x:hidden; background:#FBF4EE; }

body{
  margin:0;
  /* short pages (like /parcel before it is filled) do not end halfway down
     the screen with a seam under them */
  min-height:100vh;
  /* Paper, not midnight. The gate is cream and the opening is white; a
     black page after them belonged to an identity this site no longer has. */
  background:
    radial-gradient(60vw 50vw at 8% 0%,   rgba(255,226,232,.55), transparent 60%),
    radial-gradient(55vw 50vw at 96% 14%, rgba(255,235,214,.55), transparent 58%),
    #FBF4EE;
  color:#4A3540;
  font-family:var(--sans);
  font-size:clamp(15px,1.05vw,17px);
  line-height:1.65;
  overflow-x:hidden;
  -webkit-font-smoothing:antialiased;
}
body.is-locked{ overflow:hidden; height:100vh; }

img{ max-width:100%; display:block; }

::selection{ background:var(--candle); color:var(--ink); }

:focus-visible{
  outline:2px solid var(--candle);
  outline-offset:3px;
  border-radius:2px;
}

/* ---------- atmosphere ---------------------------------------------------- */

#grain{
  position:fixed; inset:-50%; z-index:60; pointer-events:none;
  opacity:.05; mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grain 5s steps(6) infinite;
}
@keyframes grain{
  0%,100%{ transform:translate(0,0) }
  20%{ transform:translate(-3%,2%) }
  40%{ transform:translate(2%,-3%) }
  60%{ transform:translate(-2%,-2%) }
  80%{ transform:translate(3%,1%) }
}

#glow{
  position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(60vw 60vw at 50% 12%, rgba(242,169,63,.14), transparent 62%),
    radial-gradient(70vw 50vw at 82% 88%, rgba(201,123,150,.10), transparent 60%);
  transition:opacity .8s linear;
}

#spark{
  position:fixed; width:14px; height:14px; z-index:59; pointer-events:none;
  border-radius:50%; left:0; top:0; opacity:0;
  background:radial-gradient(circle, rgba(242,169,63,.55), transparent 70%);
  transition:opacity .35s linear;
}
@media (pointer:coarse){ #spark{ display:none } }

/* ---------- shared type --------------------------------------------------- */

.eyebrow{
  font-size:.72rem; letter-spacing:.28em; text-transform:uppercase;
  color:var(--mauve); font-weight:500; margin:0;
}
.display{
  font-family:var(--serif); font-weight:400; line-height:1.02;
  letter-spacing:-.015em; text-wrap:balance; margin:0;
  font-size:clamp(2.6rem,8.5vw,6.4rem);
}
.display em{ font-style:italic; color:var(--candle); }
.lede{
  max-width:var(--measure); color:var(--mauve);
  font-size:clamp(1rem,1.6vw,1.16rem); margin:0;
}
.hand{
  font-family:var(--hand); font-size:1.35rem; color:var(--rose);
  line-height:1.3;
}

section{
  position:relative; z-index:1;
  padding:clamp(6rem,14vh,10rem) clamp(1.25rem,6vw,4rem);
}
.wrap{ max-width:1180px; margin-inline:auto; }
.stack{ display:flex; flex-direction:column; }

/* ---------- 0. the gate --------------------------------------------------- */

#gate{
  position:fixed; inset:0; z-index:80;
  background:radial-gradient(80vw 80vw at 50% 45%, #1B1327, var(--ink) 70%);
  display:grid; place-items:center; padding:2rem; text-align:center;
}
#gate .stack{ gap:1.75rem; align-items:center; max-width:34rem; }
#gate .display{ font-size:clamp(2rem,6vw,3.4rem); }

.gate-form{ display:flex; gap:.5rem; width:min(100%,26rem); }
.gate-input{
  flex:1; background:transparent; border:0; border-bottom:1px solid var(--edge);
  color:var(--cream); font-family:var(--sans); font-size:1.05rem;
  padding:.7rem .2rem; text-align:center; letter-spacing:.06em;
}
.gate-input::placeholder{ color:#5C4E6C; }
.gate-input:focus{ outline:none; border-bottom-color:var(--candle); }

.btn{
  font-family:var(--sans); font-size:.78rem; font-weight:600;
  letter-spacing:.16em; text-transform:uppercase;
  color:var(--ink); background:var(--candle);
  border:0; border-radius:999px; padding:.85rem 1.6rem; cursor:pointer;
  transition:transform .25s var(--ease), background .25s linear;
}
.btn:hover{ transform:translateY(-2px); background:#FFC15E; }
.btn-ghost{
  background:transparent; color:var(--cream);
  border:1px solid var(--edge);
}
.btn-ghost:hover{ background:var(--veil); border-color:var(--candle); }

.gate-hint{ min-height:1.4em; color:var(--rose); font-size:.9rem; margin:0; }

/* --- the gate riding on top of the site (her birthday) ---------------------
   The whole gate — photos, hearts, card — is one layer above everything, so
   that when she answers it can fade out as a piece and reveal the white
   opening already starting underneath, instead of cutting to it. While it is
   up, the site beneath is taken out entirely: it is not merely covered, it is
   not there to be scrolled to or glimpsed round the edges. */
#gateway{
  position:fixed; inset:0; z-index:150;
  transition:opacity .7s ease;
}
#gateway.is-gone{ opacity:0; pointer-events:none; }
body.is-gated #stage,
body.is-gated #soundBtn{ display:none; }

/* The speaker button is gone for good: the song starts on its own after her
   tap on "Let me in", steps aside for her film, and comes back at the map.
   The element stays in the page because music.js still keeps its state. */
#soundBtn{ display:none !important; }

/* a wrong answer gives a small shake rather than just a line of text */
.gate-input.is-wrong{ animation:gate-no .42s cubic-bezier(.36,.07,.19,.97); }
@keyframes gate-no{
  10%, 90%{ transform:translateX(-1px) }
  20%, 80%{ transform:translateX(3px) }
  30%, 50%, 70%{ transform:translateX(-6px) }
  40%, 60%{ transform:translateX(6px) }
}
@media (prefers-reduced-motion:reduce){ .gate-input.is-wrong{ animation:none; } }

/* ---------- 1. hero ------------------------------------------------------- */

#hero{
  min-height:100svh; display:grid; place-items:center; text-align:center;
  padding-block:clamp(4rem,10vh,8rem);
}
#hero .stack{ gap:2.25rem; align-items:center; }

.counter{
  display:flex; gap:clamp(1.25rem,4vw,3rem); flex-wrap:wrap; justify-content:center;
  padding-top:.5rem;
}
.counter div{ display:flex; flex-direction:column; gap:.35rem; }
.counter b{
  font-family:var(--serif); font-weight:400; font-size:clamp(1.9rem,4.4vw,3rem);
  color:var(--cream); font-variant-numeric:tabular-nums; line-height:1;
}
.counter span{
  font-size:.66rem; letter-spacing:.24em; text-transform:uppercase; color:var(--mauve);
}

/* ---------- states driven by JS (Motion) ---------------------------------- */

[data-reveal]{ will-change:transform,opacity; }
.no-js [data-reveal]{ opacity:1 !important; transform:none !important; }

/* ---------- motion opt-out ------------------------------------------------ */

@media (prefers-reduced-motion:reduce){
  *{ animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; }
  html{ scroll-behavior:auto; }
  #grain{ display:none; }
}

/* =========================================================================
   THE OPENING — a field of flowers, grown from the centre outward
   ========================================================================= */

#intro{
  position:fixed; inset:0; z-index:100;
  background:#FFFFFF;
  overflow:hidden;
  contain:strict;
}
#intro.done{ pointer-events:none; }

/* two nested stages so the turn and the push-in never fight over transform */
#field, #bed{
  position:absolute; left:50%; top:50%; width:0; height:0;
  transform-origin:50% 50%;
  will-change:transform;
}
#bed{ left:0; top:0; }

.fl{
  position:absolute; left:0; top:0;
  width:var(--s); height:var(--s);
  margin-left:calc(var(--s) / -2);
  margin-top:calc(var(--s) / -2);
  will-change:transform, opacity;
  transform:translate3d(0,0,0) scale(0);
}
/* the shadow is baked into the image itself — no runtime filter, so a phone
   can carry 150 of these at once */
.fl img{
  width:100%; height:100%; object-fit:contain; display:block;
  animation:fl-spin var(--spin, 26s) linear infinite;
  animation-direction:var(--dir, normal);
  transition:none;
}
/* while the whole field is turning, every flower spins harder too — and
   harder still in the seconds just before they break formation */
#bed.fast   .fl img{ animation-duration:var(--spin-fast, 3s); }
#bed.faster .fl img{ animation-duration:var(--spin-blur, 1.4s); }
@keyframes fl-spin{ from{ transform:rotate(0deg) } to{ transform:rotate(360deg) } }

#skipIntro{
  /* above the clip: it is the only way out of the opening, so nothing may
     ever be layered over it */
  position:absolute; right:1.25rem; bottom:1.25rem; z-index:6;
  background:rgba(255,255,255,.86); border:0; cursor:pointer; border-radius:999px;
  font-family:var(--sans); font-size:.7rem; letter-spacing:.24em;
  text-transform:uppercase; color:#7A6A88;
  padding:.55rem 1rem; opacity:0; transition:opacity .5s linear, color .2s linear;
  backdrop-filter:blur(6px);
  box-shadow:0 4px 16px rgba(38,20,50,.14);
}
#skipIntro.show{ opacity:1; }
#skipIntro:hover{ color:#241C2E; }

@media (prefers-reduced-motion:reduce){
  .fl img{ animation:none; }
}

/* ---------- the finale: the heart, the rose, the words ------------------- */

/* the heart beats once the flowers have formed it */
#field.beating{ animation:heart-beat 1.5s ease-in-out infinite; }
@keyframes heart-beat{
  0%,100%{ transform:scale(1) }
  16%{ transform:scale(1.05) }
  32%{ transform:scale(1) }
  48%{ transform:scale(1.028) }
  64%{ transform:scale(1) }
}

#finale{
  position:absolute; inset:0; z-index:3;
  display:none; pointer-events:none;
}
#finale.on{ display:block; }

/* The bouquet sits over the heart — deliberately larger than it, so the
   heart reads as a frame behind rather than a container around.
   Height-driven, because the bouquet is a portrait shape. */
#finale .bouquet{
  position:absolute; left:50%; top:var(--heart-y, 38%);
  translate:-50% -50%;
  opacity:0;
}
#finale .bouquet::before{
  content:""; position:absolute; inset:-24% -42%; z-index:-1;
  background:radial-gradient(circle, rgba(255,183,150,.24), rgba(255,183,150,0) 68%);
}
#finale .bouquet img{
  display:block; height:clamp(280px,50vh,560px); width:auto;
  /* a small permanent hop, like he cannot keep still */
  animation:kitty-idle 2.6s ease-in-out infinite;
  transform-origin:50% 92%;
}
@keyframes kitty-idle{
  0%,100%{ transform:translateY(0) rotate(-2deg) scaleY(1) }
  28%    { transform:translateY(-9px) rotate(1.5deg) scaleY(1.03) }
  55%    { transform:translateY(0) rotate(-.5deg) scaleY(.98) }
  78%    { transform:translateY(-4px) rotate(1deg) scaleY(1.015) }
}

#finale .say{
  position:absolute; left:0; right:0; bottom:clamp(2vh,4.5vh,6vh);
  display:flex; flex-direction:column; align-items:center;
  gap:.15rem; padding-inline:1.25rem; text-align:center;
}

#finale .love{
  margin:0; display:flex; gap:clamp(.45rem,1.6vw,1rem); flex-wrap:wrap;
  justify-content:center;
  font-family:var(--serif); font-weight:400;
  font-size:clamp(1.25rem,3.7vw,2.5rem); line-height:1;
  letter-spacing:.11em; color:#241C2E;
}
#finale .love span{ display:inline-block; opacity:0; }

#finale .hername{
  margin:.02em 0 0; opacity:0;
  font-family:var(--serif); font-style:italic; font-weight:400;
  font-size:clamp(2.1rem,7.2vw,4.8rem); line-height:.96;
  letter-spacing:-.01em; color:#C6203C;
}

#finale .pets{
  margin:.35em 0 0; display:flex; flex-wrap:wrap; justify-content:center;
  gap:.4rem clamp(.6rem,2vw,1.4rem);
  font-family:var(--hand); font-size:clamp(1.05rem,2.5vw,1.6rem);
  color:#8E2F4A; line-height:1;
}
#finale .pets span{ display:inline-block; opacity:0; }
#finale .pets span + span::before{
  content:"·"; margin-right:clamp(.6rem,2vw,1.4rem); color:#D7A9B8;
}

@media (prefers-reduced-motion:reduce){
  #field.beating{ animation:none; }
  #finale .bouquet img{ animation:none; }
  #finale .bouquet, #finale .love span, #finale .hername, #finale .pets span{ opacity:1; }
}

/* the words step aside for the clip rather than sitting under it */
#finale{ transition:opacity .55s ease; }
#finale.stepped-aside{ opacity:0; }

/* ---------- the clip that opens the page ---------------------------------
   Edge to edge and dark: the first thing she sees after she types the day we
   began, with nothing around it.

   It cannot simply fill the screen, though. The clip is a cinema strip,
   about two and a quarter times as wide as it is tall, and a phone held
   upright is half as wide as it is tall. Filling that shape would crop away
   four fifths of the picture — including the writing on the glass, which is
   the entire point of it. So the picture is allowed to fill any screen at
   least as wide as 1.32:1, and on anything narrower it keeps that shape and
   sits in the black. Her Mac and the iPad either way round are filled; her
   phone gets a wide band across the middle of a dark screen, with his face
   and the date both still in it.

   Nothing is drawn on it. There is no play button and no scrubber, and it
   cannot be tapped by accident; skip is the way out.
   ------------------------------------------------------------------------ */
#clip{
  position:absolute; inset:0; z-index:4;
  display:grid; place-items:center;
  background:#0B0809;
  pointer-events:none;
  opacity:0; visibility:hidden;
  transition:opacity .65s ease, visibility 0s linear .65s;
}
#clip.on{
  opacity:1; visibility:visible;
  transition:opacity .65s ease, visibility 0s linear 0s;
}
#clip.going{ opacity:0; }

.clip-frame{
  position:relative;
  width:100vw;
  height:min(100svh, calc(100vw / 1.32));
  overflow:hidden;
  background:#0B0809;
  transform:scale(1.05);
  transition:transform 1.1s cubic-bezier(.2,1,.36,1);
}
#clip.on .clip-frame{ transform:scale(1); }
#clip.going .clip-frame{ transform:scale(1.04); }

.clip-frame video{
  display:block; width:100%; height:100%; object-fit:cover;
}

@media (prefers-reduced-motion:reduce){
  .clip-frame{ transform:none; transition:none; }
  #clip.on .clip-frame, #clip.going .clip-frame{ transform:none; }
}

/* ---------- the cake ------------------------------------------------------
   Drawn rather than photographed, so it is sharp at any size on any of her
   screens and there is nothing to download. The whole sequence — the lighter
   coming in, each candle catching, the burn, the blow, the smoke — is CSS
   running off the `.on` class, and the beats are worked out in the template
   from CAKE in story.py. JavaScript only throws the confetti and says when
   it is over.

   Every animated piece of the drawing sets transform-box:fill-box. Without
   it an SVG element turns about the middle of the whole picture rather than
   about itself, which is how you get a flame that swings around the cake
   instead of flickering on its wick.
   ------------------------------------------------------------------------ */
#cake{
  position:absolute; inset:0; z-index:4;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:clamp(.4rem, 1.5vh, 1.2rem);
  padding:clamp(.75rem, 3vw, 2rem);
  pointer-events:none;
  opacity:0; visibility:hidden;
  transition:opacity .7s ease, visibility 0s linear .7s;
}
#cake.on{
  opacity:1; visibility:visible;
  transition:opacity .7s ease, visibility 0s linear 0s;
}
#cake.going{ opacity:0; transition:opacity .9s ease; }

.cake-art{
  /* whichever bites first: the width of the window, its height, or 48rem */
  width:min(90vw, calc(64svh * 448 / 414), 48rem);
}
.cake-art svg{ display:block; width:100%; height:auto; overflow:visible; }

#cake.on .cake-art{
  animation:ck-rise .95s cubic-bezier(.22,1.2,.34,1) both;
}
@keyframes ck-rise{
  from{ opacity:0; transform:translateY(42px) scale(.86) }
  to  { opacity:1; transform:none }
}

.cake-wish{
  margin:0; opacity:0;
  font-family:var(--hand); color:#A32040;
  font-size:clamp(1.2rem, 3vw, 2rem); line-height:1;
}

/* Everything below hangs off `#cake.on`. An animation with a delay starts
   counting the moment its element exists, and this one exists from the first
   paint — unscoped, the candles lit and blew themselves out during the
   flowers, and what reached her was a cake with two dead wicks. */

/* the lighter and its flame — the journey itself is generated per age */
#cake.on .ck-lighter{ transform-box:fill-box; transform-origin:50% 86%; opacity:0; }
#cake.on .ck-lflame{ transform-box:fill-box; transform-origin:50% 100%; opacity:0; }

/* --- a candle ------------------------------------------------------------
   Two animations on one flame, and neither may fill backwards: an animation
   waiting out its delay with fill-mode `both` applies its first frame from
   the very start, so the blow would flatten the flame before it was ever
   lit. `forwards` contributes nothing until it actually runs.
   ------------------------------------------------------------------------ */
#cake.on .ck-flame{
  transform-box:fill-box; transform-origin:50% 100%;
  opacity:0;
  animation:
    ck-ignite .38s cubic-bezier(.2,1.5,.4,1) var(--t-on, 1s) forwards,
    ck-blow   .62s cubic-bezier(.4,0,.75,.2) var(--t-off, 6s) forwards;
}
@keyframes ck-ignite{
  0%  { opacity:0; transform:scale(.15) }
  55% { opacity:1; transform:scale(1.28) }
  100%{ opacity:1; transform:scale(1) }
}
@keyframes ck-blow{
  0%  { opacity:1; transform:none }
  35% { opacity:1; transform:translateX(9px) skewX(38deg) scale(1.06, .78) }
  72% { opacity:.55; transform:translateX(19px) skewX(56deg) scale(.5, .34) }
  100%{ opacity:0; transform:translateX(26px) skewX(64deg) scale(.08, .06) }
}

/* the flicker lives on an inner group so it never fights the ignite and the
   blow for the same transform */
#cake.on .ck-flick{
  transform-box:fill-box; transform-origin:50% 100%;
  animation:ck-flick 1.15s ease-in-out infinite alternate;
}
@keyframes ck-flick{
  0%  { transform:scale(1, 1) skewX(-3deg) }
  35% { transform:scale(.95, 1.09) skewX(2deg) }
  70% { transform:scale(1.05, .94) skewX(-1deg) }
  100%{ transform:scale(.97, 1.05) skewX(3deg) }
}

#cake.on .ck-smoke{
  transform-box:fill-box; transform-origin:50% 100%;
  opacity:0;
  animation:ck-smoke 2.1s ease-out var(--t-off, 6s) forwards;
}
@keyframes ck-smoke{
  0%  { opacity:0; transform:translateY(6px) scale(.5) }
  18% { opacity:.6 }
  100%{ opacity:0; transform:translateY(-34px) scale(1.5) }
}

@media (prefers-reduced-motion:reduce){
  #cake.on .ck-flick{ animation:none; }
  #cake.on .cake-art{ animation:none; opacity:1; }
}

/* ---------- the burst: flowers raining, and a shockwave off the rose ------ */

#fall{
  position:absolute; inset:0; z-index:2;
  pointer-events:none; overflow:hidden;
  display:none;
}
#fall.on{ display:block; }

.pf{
  position:absolute; top:-20vh; left:var(--x);
  width:var(--w); height:var(--w);
  will-change:transform;
  animation:pf-fall var(--d) linear var(--delay) infinite;
}
.pf i{
  display:block; width:100%; height:100%;
  will-change:transform;
  animation:pf-sway var(--sway) ease-in-out var(--delay) infinite alternate;
}
.pf img{
  width:100%; height:100%; object-fit:contain; display:block;
  opacity:var(--o, .92);
  filter:blur(var(--blur, 0px));
}

/* thrown outward when the bouquet lands, then left to fall */
.pop-bit{
  position:absolute; left:50%; top:var(--heart-y, 38%);
  width:var(--w); height:var(--w);
  margin-left:calc(var(--w) / -2); margin-top:calc(var(--w) / -2);
  z-index:2; pointer-events:none; will-change:transform, opacity;
}
.pop-bit img{ width:100%; height:100%; object-fit:contain; display:block; }

@keyframes pf-fall{
  from{ transform:translate3d(0,0,0) }
  to  { transform:translate3d(0,132vh,0) }
}
@keyframes pf-sway{
  from{ transform:translateX(calc(var(--drift) * -1)) rotate(var(--r1)) }
  to  { transform:translateX(var(--drift)) rotate(var(--r2)) }
}

/* the shockwave the rose punches through the heart */
#burst{
  position:absolute; left:50%; top:var(--heart-y, 40%);
  translate:-50% -50%;
  width:1px; height:1px; z-index:2; pointer-events:none;
}
#burst i{
  position:absolute; left:50%; top:50%; translate:-50% -50%;
  width:24vh; height:24vh; border-radius:50%;
  border:2px solid rgba(198,32,60,.55);
  opacity:0; transform:scale(0);
}
#burst i:last-child{ border-color:rgba(242,169,63,.5); }

@media (prefers-reduced-motion:reduce){
  #fall{ display:none !important; }
  .pf, .pf i{ animation:none; }
}

/* =========================================================================
   THE WALL — her photographs behind the gate question
   Positions are deliberate, not random: they ring the centre so the
   question always sits in clear space.
   ========================================================================= */

#wall{
  position:fixed; inset:0; z-index:70;
  pointer-events:none;
  will-change:transform;
}

#wall .ph{
  position:absolute;
  left:var(--x); top:var(--y);
  width:var(--w);
  margin:0;
  transform:rotate(var(--r)) translateY(0);
  border-radius:3px;
  background:#F5EDE2;
  padding:6px 6px 6px;
  box-shadow:0 18px 44px rgba(0,0,0,.55), 0 2px 6px rgba(0,0,0,.4);
  opacity:0;
  animation:
    ph-in .9s cubic-bezier(.22,1,.36,1) forwards,
    ph-float var(--float, 9s) ease-in-out infinite;
  animation-delay: calc(.25s + var(--i) * .09s), calc(var(--i) * .7s);
}

/* One frame shape for every photo. The phone screenshots are 0.46 aspect —
   left alone they render 400px tall and fall off the screen. */
#wall .ph img{
  display:block; width:100%; height:auto; border-radius:1px;
  aspect-ratio:4 / 5; object-fit:cover; object-position:center 42%;
  filter:saturate(.95) brightness(.88);
}

@keyframes ph-in{
  from{ opacity:0; transform:rotate(var(--r)) translateY(26px) scale(.94) }
  to  { opacity:var(--o, .78); transform:rotate(var(--r)) translateY(0) scale(1) }
}
@keyframes ph-float{
  0%,100%{ translate:0 0 }
  50%    { translate:0 -12px }
}

/* the ring: nine places, none of them the middle */
#wall .ph:nth-child(1){ --x:1%;   --y:9%;   --w:15vw; --r:-6deg; --o:.80; --float:9s }
#wall .ph:nth-child(2){ --x:17%;  --y:59%;  --w:13vw; --r:4deg;  --o:.72; --float:11s }
#wall .ph:nth-child(3){ --x:3%;   --y:47%;  --w:11vw; --r:7deg;  --o:.60; --float:8s }
#wall .ph:nth-child(4){ --x:25%;  --y:4%;   --w:12vw; --r:5deg;  --o:.66; --float:12s }
#wall .ph:nth-child(5){ --x:62%;  --y:5%;   --w:13vw; --r:-5deg; --o:.74; --float:10s }
#wall .ph:nth-child(6){ --x:83%;  --y:31%;  --w:14vw; --r:6deg;  --o:.80; --float:9.5s }
#wall .ph:nth-child(7){ --x:70%;  --y:62%;  --w:12vw; --r:-4deg; --o:.70; --float:12.5s }
#wall .ph:nth-child(8){ --x:44%;  --y:71%;  --w:10vw; --r:3deg;  --o:.58; --float:10.5s }
#wall .ph:nth-child(9){ --x:86%;  --y:3%;   --w:9vw;  --r:-8deg; --o:.55; --float:13s }

/* darkness in the middle so the question always reads */
#wallveil{
  position:fixed; inset:0; z-index:71; pointer-events:none;
  background:
    radial-gradient(58% 52% at 50% 46%, rgba(9,7,15,.94) 0%, rgba(9,7,15,.86) 42%, rgba(9,7,15,0) 78%),
    linear-gradient(rgba(9,7,15,.55), rgba(9,7,15,.25) 40%, rgba(9,7,15,.7));
}

#gate{ z-index:80; background:transparent; }

@media (max-width:820px){
  /* fewer, smaller, hugging the edges — a phone has no room to spare */
  #wall .ph{ padding:4px; }
  #wall .ph:nth-child(1){ --x:0%;   --y:4%;   --w:33vw; --r:-7deg; --o:.78 }
  #wall .ph:nth-child(2){ --x:63%;  --y:69%;  --w:31vw; --r:5deg;  --o:.7 }
  #wall .ph:nth-child(3){ --x:-4%;  --y:63%;  --w:29vw; --r:6deg;  --o:.62 }
  #wall .ph:nth-child(4){ --x:60%;  --y:3%;   --w:32vw; --r:6deg;  --o:.72 }
  #wall .ph:nth-child(5){ --x:25%;  --y:78%;  --w:26vw; --r:-4deg; --o:.6 }
  #wall .ph:nth-child(6),
  #wall .ph:nth-child(7),
  #wall .ph:nth-child(8),
  #wall .ph:nth-child(9){ display:none }

  #wallveil{
    background:
      radial-gradient(70% 40% at 50% 45%, rgba(9,7,15,.95) 0%, rgba(9,7,15,.88) 45%, rgba(9,7,15,.35) 80%),
      linear-gradient(rgba(9,7,15,.6), rgba(9,7,15,.3) 40%, rgba(9,7,15,.75));
  }
}

@media (prefers-reduced-motion:reduce){
  #wall .ph{ animation:none; opacity:var(--o, .7); }
}

/* Per-photo framing. A 4:5 crop out of a 0.46-aspect phone screenshot throws
   away 42% of the height, so where that window sits decides whether you get
   her face or her forehead. Tuned one photo at a time. */
#wall .ph:nth-child(1) img{ object-position:center 62% }
#wall .ph:nth-child(2) img{ object-position:center 44% }
#wall .ph:nth-child(3) img{ object-position:center 58% }
#wall .ph:nth-child(4) img{ object-position:center 54% }
#wall .ph:nth-child(5) img{ object-position:center 65% }
#wall .ph:nth-child(6) img{ object-position:center 60% }
#wall .ph:nth-child(7) img{ object-position:center 46% }
#wall .ph:nth-child(8) img{ object-position:center 40% }
#wall .ph:nth-child(9) img{ object-position:center 42% }

/* =========================================================================
   THE GATE, SOFTENED
   Everything here is scoped to .gate-page, so the dark site and the white
   opening are untouched. Her photos come up to full brightness — on the
   dark version they were sitting at 60%.
   ========================================================================= */

.gate-page{
  --sugar:  #FFF7F3;   /* warm cream ground     */
  --blush:  #FFE6EC;   /* the pink that isn't loud */
  --rose:   #E4718F;   /* buttons, accents      */
  --deep:   #BE3E5E;   /* the one strong colour */
  --soft:   #5A4650;   /* body text, not black  */
  --faint:  #A3909A;

  background:
    radial-gradient(46vw 46vw at 12% 8%,  rgba(255,214,226,.85), transparent 62%),
    radial-gradient(50vw 50vw at 88% 22%, rgba(255,231,214,.80), transparent 60%),
    radial-gradient(60vw 60vw at 60% 96%, rgba(255,220,236,.75), transparent 62%),
    var(--sugar);
  color:var(--soft);
}

/* --- the photographs, bright and unashamed ----------------------------- */

.gate-page #wall .ph{
  padding:8px 8px 8px;
  border-radius:12px;
  background:#FFFFFF;
  box-shadow:0 16px 34px rgba(190,62,94,.16), 0 3px 8px rgba(190,62,94,.10);
}
.gate-page #wall .ph img{
  border-radius:7px;
  filter:saturate(1.04) brightness(1.02);
}
.gate-page #wall .ph:nth-child(1){ --o:1 }
.gate-page #wall .ph:nth-child(2){ --o:.96 }
.gate-page #wall .ph:nth-child(3){ --o:.9 }
.gate-page #wall .ph:nth-child(4){ --o:.94 }
.gate-page #wall .ph:nth-child(5){ --o:1 }
.gate-page #wall .ph:nth-child(6){ --o:1 }
.gate-page #wall .ph:nth-child(7){ --o:.94 }
.gate-page #wall .ph:nth-child(8){ --o:.88 }
.gate-page #wall .ph:nth-child(9){ --o:.86 }

/* a soft haze instead of the black hole, so the card still sits forward */
.gate-page #wallveil{
  background:
    radial-gradient(46% 42% at 50% 46%, rgba(255,247,243,.92) 0%,
                    rgba(255,247,243,.62) 46%, rgba(255,247,243,0) 76%);
}

/* --- little hearts drifting up ----------------------------------------- */

#hearts{ position:fixed; inset:0; z-index:72; pointer-events:none; overflow:hidden; }
#hearts i{
  position:absolute; bottom:-8vh; left:var(--x);
  width:var(--s); height:var(--s);
  background:var(--c);
  -webkit-mask:var(--m) center/contain no-repeat;
          mask:var(--m) center/contain no-repeat;
  opacity:0;
  animation:heart-rise var(--d) linear var(--delay) infinite;
}
@keyframes heart-rise{
  0%   { opacity:0;   transform:translateY(0) rotate(0deg) scale(.7) }
  12%  { opacity:.85 }
  80%  { opacity:.5 }
  100% { opacity:0;   transform:translateY(-112vh) rotate(var(--r)) scale(1) }
}

/* --- the card ----------------------------------------------------------- */

.gate-page #gate{ background:transparent; padding:1.5rem; }

.gate-page .card{
  position:relative;
  display:flex; flex-direction:column; align-items:center; gap:1.05rem;
  width:min(100%, 33rem);
  padding:clamp(2.2rem,5vw,3rem) clamp(1.4rem,4vw,2.6rem) clamp(1.8rem,4vw,2.4rem);
  text-align:center;
  background:rgba(255,255,255,.94);
  border:1px solid rgba(190,62,94,.10);
  border-radius:32px;
  box-shadow:0 26px 70px rgba(190,62,94,.18), 0 4px 14px rgba(190,62,94,.08);
  backdrop-filter:blur(10px);
  /* the card lands and then stays put — a form that drifts is a form that
     is hard to tap. The motion lives in the heart badge instead. */
  animation:card-in .9s cubic-bezier(.2,1.3,.4,1) .1s both;
}
@keyframes card-in{
  from{ opacity:0; transform:translateY(26px) scale(.94) }
  to  { opacity:1; transform:translateY(0) scale(1) }
}
.gate-page .card-heart{
  position:absolute; top:-19px; left:50%; translate:-50% 0;
  width:38px; height:38px; border-radius:50%;
  display:grid; place-items:center;
  background:linear-gradient(160deg,#FF8FAC,var(--deep));
  color:#fff; font-size:1rem; line-height:1;
  box-shadow:0 8px 18px rgba(190,62,94,.34);
  animation:heart-beat 1.9s ease-in-out infinite;
}
@keyframes heart-beat{
  0%,100%{ transform:translateX(-50%) scale(1) }
  22%    { transform:translateX(-50%) scale(1.16) }
  40%    { transform:translateX(-50%) scale(1) }
  58%    { transform:translateX(-50%) scale(1.09) }
}

.gate-page .eyebrow{
  font-family:"Quicksand", var(--sans); font-weight:700;
  font-size:.72rem; letter-spacing:.2em; text-transform:uppercase;
  color:var(--rose);
}
.gate-page .display{
  font-family:"Quicksand", var(--sans); font-weight:700;
  font-size:clamp(1.6rem,4.6vw,2.35rem); line-height:1.18;
  letter-spacing:-.01em; color:#3E2C35;
}
.gate-page .display em{
  font-family:var(--serif); font-style:italic; font-weight:400;
  color:var(--deep); font-size:1.12em;
}
.gate-page .lede{
  font-family:"Quicksand", var(--sans); font-weight:500;
  font-size:1rem; color:var(--faint); max-width:24rem;
}

.gate-page .gate-form{
  display:flex; flex-direction:column; gap:.75rem; width:100%; margin-top:.2rem;
}
.gate-page .gate-input{
  width:100%; border:1.5px solid rgba(190,62,94,.16); border-radius:999px;
  background:#FFF3F6; color:#3E2C35;
  font-family:"Quicksand", var(--sans); font-weight:600; font-size:1.02rem;
  padding:.9rem 1.2rem; text-align:center; letter-spacing:.08em;
  transition:border-color .2s, background .2s, box-shadow .2s;
}
.gate-page .gate-input::placeholder{ color:#D9AEBC; font-weight:500; letter-spacing:.1em; }
.gate-page .gate-input:focus{
  outline:none; background:#fff;
  border-color:var(--rose);
  box-shadow:0 0 0 5px rgba(228,113,143,.16);
}

.gate-page .btn{
  width:100%;
  font-family:"Quicksand", var(--sans); font-weight:700;
  font-size:.9rem; letter-spacing:.08em; text-transform:none;
  color:#fff; background:linear-gradient(150deg,#F2809E,var(--deep));
  border-radius:999px; padding:.95rem 1.4rem;
  box-shadow:0 12px 26px rgba(190,62,94,.30);
  transition:transform .22s cubic-bezier(.2,1.4,.4,1), box-shadow .22s;
}
.gate-page .btn:hover{
  transform:translateY(-3px) scale(1.02);
  background:linear-gradient(150deg,#FF93AE,#CE4869);
  box-shadow:0 16px 32px rgba(190,62,94,.38);
}
.gate-page .btn:active{ transform:translateY(-1px) scale(.99); }

.gate-page .gate-hint{
  font-family:var(--hand); font-size:1.3rem; color:var(--deep); margin:0;
}
.gate-page .gate-hint:empty{ display:none; }

.gate-page :focus-visible{ outline-color:var(--deep); }

@media (prefers-reduced-motion:reduce){
  .gate-page .card{ animation:none; }
  .gate-page .card-heart{ animation:none; }
  #hearts{ display:none; }
}

/* =========================================================================
   THE FLIGHT — Kathmandu to Lafayette, sent by air
   Dressed as an airmail envelope: striped border, a stamp, a written
   address. The map is a true equirectangular projection, so both cities
   are placed from their real coordinates.
   ========================================================================= */

/* The envelope is the whole screen: one viewport, edge to edge, with the
   heading and the address line sharing the height and the map taking
   everything that is left. */
#flight{
  --paper:#FFFFFF;
  --ink-2:#3E2C35;
  --muted:#93818B;
  --air-red:#C8102E;
  --air-blue:#1B4C8C;
  --seal:#BE3E5E;
  --sea:#FBEBEC;            /* the map's own ocean, sampled from the file */

  min-height:100svh;
  padding:clamp(.6rem,1.6vw,1.1rem);
  display:flex;
}
#flight .wrap{
  max-width:none; width:100%;
  display:flex; flex-direction:column;
}
.envelope-card{ flex:1 1 auto; display:flex; flex-direction:column; min-height:0; }

/* --- the envelope ------------------------------------------------------- */

.envelope-card{
  position:relative; margin:0;
  background:var(--paper);
  padding:clamp(.7rem,1.6vw,1.1rem);
  border-radius:3px;
  box-shadow:0 22px 50px rgba(84,52,64,.16), 0 2px 5px rgba(84,52,64,.10);
}

/* the red-and-blue airmail edge, drawn rather than imported */
.envelope-card::before{
  content:""; position:absolute; inset:0; border-radius:3px; pointer-events:none;
  padding:7px;
  background:repeating-linear-gradient(
    -45deg,
    var(--air-red)  0 9px,
    #FFFFFF        9px 18px,
    var(--air-blue) 18px 27px,
    #FFFFFF        27px 36px);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
  opacity:.85;
}

/* A stamp. The perforated edge is drawn with a repeating radial gradient
   on the background rather than a mask — mask-composite for a four-sided
   perforation is fragile and rendered as a plain grey box. */
.stamp{
  position:absolute; top:-16px; right:clamp(1rem,4vw,2.6rem); z-index:3;
  width:86px; height:82px;
  padding:6px;
  background:
    radial-gradient(circle 4px at 0 50%,   transparent 96%, #FDF3E4) left  /50% 11px repeat-y,
    radial-gradient(circle 4px at 100% 50%, transparent 96%, #FDF3E4) right /50% 11px repeat-y,
    radial-gradient(circle 4px at 50% 0,   transparent 96%, #FDF3E4) top   /11px 50% repeat-x,
    radial-gradient(circle 4px at 50% 100%, transparent 96%, #FDF3E4) bottom/11px 50% repeat-x,
    #FDF3E4;
  background-origin:border-box;
  filter:drop-shadow(0 5px 10px rgba(84,52,64,.20));
  transform:rotate(4deg);
}
.stamp-inner{
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  width:100%; height:100%;
  border:1px solid rgba(200,16,46,.45);
  color:var(--air-red); line-height:1;
}
.stamp-inner b{
  font-family:"Instrument Serif", var(--serif); font-weight:400; font-size:1.34rem;
  /* the count changes every frame — proportional digits make it wobble */
  font-variant-numeric:tabular-nums; line-height:1;
}
.stamp-inner i{
  font-style:normal; font-family:"Instrument Sans", var(--sans);
  font-size:.42rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--air-blue); margin-top:.26rem; white-space:nowrap;
}

/* --zoom is how much wider than the frame the map is drawn. On a phone the
   whole world in one strip is 150px tall and unreadable, so the map is drawn
   larger than the screen and the frame pans to follow the bird. */
.atlas{
  --zoom:1;
  position:relative;
  flex:1 1 auto; min-height:clamp(220px,46svh,620px);
  overflow:hidden;
  /* the same pink as the map's own sea, so the space above and below the
     strip reads as more ocean rather than as a letterbox */
  background:var(--sea);
}
.atlas-inner{
  position:absolute; left:0; top:0;
  width:calc(100% * var(--zoom));
  aspect-ratio:1024 / 392;
  /* Both the placement and the camera are set from flight.js, which knows
     where the bird is and where the route sits on the map. */
  will-change:transform;
}
.atlas-map{ width:100%; height:100%; object-fit:fill; display:block; }
.atlas-svg{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }

.route{
  stroke:rgba(27,76,140,.26); stroke-width:2.2;
  stroke-dasharray:6 8; stroke-linecap:round;
  vector-effect:non-scaling-stroke;
}
.route-lit{
  stroke:var(--air-red); stroke-width:2.6;
  stroke-dasharray:6 8; stroke-linecap:round;
  vector-effect:non-scaling-stroke;
}

/* --- the two ends -------------------------------------------------------- */

.pin{
  position:absolute; translate:-50% -50%;
  display:flex; flex-direction:column; align-items:center; gap:.28rem;
  opacity:0;
}
.pin i{
  width:9px; height:9px; border-radius:50%;
  background:var(--air-red); box-shadow:0 0 0 3px rgba(200,16,46,.18);
  position:relative;
}
.pin i::after{
  content:""; position:absolute; inset:-4px; border-radius:50%;
  border:1.5px solid rgba(200,16,46,.6);
  animation:pin-ping 2.6s ease-out infinite;
}
@keyframes pin-ping{
  0%  { transform:scale(.7); opacity:.9 }
  70% { transform:scale(2.6); opacity:0 }
  100%{ transform:scale(2.6); opacity:0 }
}
.pin.is-quiet b{ opacity:0; transition:opacity .4s linear; }
.pin b{
  font-family:"Instrument Sans", var(--sans); font-weight:600;
  font-size:.56rem; letter-spacing:.18em; text-transform:uppercase;
  color:#7A3040; background:rgba(255,255,255,.9);
  padding:.16rem .42rem; border-radius:2px; white-space:nowrap;
}

/* --- the pigeon ---------------------------------------------------------- */

.bird{
  position:absolute; left:0; top:0;
  width:clamp(50px,6.2vw,82px); aspect-ratio:64/52;
  translate:-50% -50%;
  opacity:0; will-change:transform;
}
.bird svg{ width:100%; height:100%; overflow:visible; }
.bird .body{ fill:#9E93B4; }
.bird .head{ fill:#ABA0BF; }
.bird .beak{ fill:#E8A33F; }
.bird .tail{ fill:#7B6E92; }
.bird .eye{ fill:#241D2E; }
.bird .wing path{ fill:#5B4E72; }
.bird .wing-far path{ fill:#453A58; }
.bird .thread{ stroke:#B99F76; stroke-width:1; }
.bird .letter{ fill:#FFFBF2; stroke:#B99F76; stroke-width:1; }
.bird .letter-fold{ fill:none; stroke:#B99F76; stroke-width:.9; }

.bird .wing{ transform-origin:30px 24px; }
/* --flap is set from JS every frame, from his actual speed along the path */
.bird .wing-near{ animation:flap-near var(--flap,.42s) ease-in-out infinite alternate; }
.bird .wing-far { animation:flap-far  var(--flap,.42s) ease-in-out infinite alternate; }
@keyframes flap-near{
  from{ transform:rotate(-34deg) }
  to  { transform:rotate(26deg) }
}
@keyframes flap-far{
  from{ transform:rotate(-18deg) }
  to  { transform:rotate(38deg) }
}

/* --- the address line ---------------------------------------------------- */

.address{
  flex:0 0 auto;
  display:flex; align-items:center; justify-content:center; flex-wrap:wrap;
  gap:.5rem .9rem;
  padding-block:clamp(.9rem,2.6vh,1.6rem);
  font-family:var(--hand); font-size:clamp(1.15rem,2.4vw,1.5rem);
  color:var(--ink-2);
}
.address .line{ border-bottom:1px solid rgba(62,44,53,.18); padding-bottom:.1rem; }
.address .arrow{ color:var(--air-red); font-family:var(--sans); font-size:1rem; }

@media (max-width:820px){
  /* --zoom is worked out from the real frame size in flight.js now */
  .bird{ width:clamp(46px,13vw,64px); }
  .stamp{ width:74px; height:70px; right:.8rem; }
  .stamp-inner b{ font-size:1.1rem; }
}

@media (prefers-reduced-motion:reduce){
  .bird .wing{ animation:none; }
  .pin i::after{ animation:none; }
}

/* ---------- the sound control ------------------------------------------- */

#soundBtn{
  position:fixed; left:1.1rem; bottom:1.1rem; z-index:120;
  width:44px; height:44px; padding:0;
  display:grid; place-items:center;
  border:1px solid rgba(245,237,226,.16);
  border-radius:999px;
  background:rgba(20,15,30,.72);
  backdrop-filter:blur(8px);
  color:var(--mauve);
  cursor:pointer;
  opacity:0; pointer-events:none;
  transition:opacity .5s linear, color .2s, border-color .2s, transform .2s;
}
#soundBtn.is-ready{ opacity:.85; pointer-events:auto; }
#soundBtn:hover{ color:var(--cream); border-color:rgba(242,169,63,.5); transform:translateY(-2px); }

#soundBtn svg{ width:20px; height:20px; fill:none; stroke:currentColor;
  stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
#soundBtn .spk{ fill:currentColor; stroke:none; }
#soundBtn .wv{ opacity:0; transition:opacity .2s; }
#soundBtn .off{ opacity:1; transition:opacity .2s; }
#soundBtn.is-on{ color:var(--candle); opacity:1; }
#soundBtn.is-on .wv{ opacity:1; }
#soundBtn.is-on .off{ opacity:0; }
#soundBtn.is-on .wv1{ animation:wv 1.6s ease-in-out infinite; }
#soundBtn.is-on .wv2{ animation:wv 1.6s ease-in-out .2s infinite; }
@keyframes wv{ 0%,100%{ opacity:.35 } 50%{ opacity:1 } }

/* if the browser blocked autoplay, make the button ask to be pressed */
#soundBtn.needs-tap{ opacity:1; animation:tap-me 1.8s ease-in-out infinite; }
@keyframes tap-me{
  0%,100%{ box-shadow:0 0 0 0 rgba(242,169,63,.45) }
  70%    { box-shadow:0 0 0 12px rgba(242,169,63,0) }
}

/* the opening sits above everything, so lift the button over it */
#soundBtn{ z-index:120; }

@media (prefers-reduced-motion:reduce){
  #soundBtn.needs-tap{ animation:none; }
  #soundBtn.is-on .wv1, #soundBtn.is-on .wv2{ animation:none; }
}

/* =========================================================================
   THE PARCEL — dropped on her city, opened from there
   ========================================================================= */

/* --- the marker sitting on Lafayette ------------------------------------ */

.drop{
  position:absolute; translate:-50% calc(-50% - 26px); z-index:4;
  display:flex; flex-direction:column; align-items:center; gap:.28rem;
  padding:0; border:0; background:none; cursor:pointer;
  text-decoration:none;
  opacity:0; pointer-events:none;
}
.drop.is-here{ pointer-events:auto; }
.drop.is-open{ opacity:0 !important; pointer-events:none; }

.drop-env{
  position:relative;
  width:clamp(34px,3.4vw,44px); aspect-ratio:28/20;
  display:grid; place-items:center;
  background:#FFFDF8; border-radius:3px;
  box-shadow:0 6px 16px rgba(84,52,64,.30);
  transition:transform .25s cubic-bezier(.2,1.4,.4,1);
}
.drop-env svg{ width:78%; height:78%; fill:none;
  stroke:#C8102E; stroke-width:1.6; stroke-linejoin:round; }
.drop:hover .drop-env{ transform:translateY(-3px) scale(1.06); }

/* a ring going out from it, so the eye finds it on a busy map */
.drop.is-here .drop-env::after{
  content:""; position:absolute; inset:-6px; border-radius:6px;
  border:2px solid rgba(200,16,46,.55);
  animation:drop-ping 2.2s ease-out infinite;
}
@keyframes drop-ping{
  0%  { transform:scale(.85); opacity:.9 }
  70% { transform:scale(1.9); opacity:0 }
  100%{ transform:scale(1.9); opacity:0 }
}

.drop-word{
  font-family:var(--hand); font-size:1.05rem; color:#8E2F4A;
  background:rgba(255,255,255,.9); border-radius:999px;
  padding:.02rem .5rem; white-space:nowrap;
}

/* =========================================================================
   THE FILM — between the flowers and the map
   ========================================================================= */

#film{
  position:relative; overflow:hidden;
  min-height:100svh;
  display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  gap:clamp(.9rem,2.4vh,1.6rem);
  padding:clamp(1.5rem,5vh,3rem) clamp(1rem,4vw,3rem);
}

/* --- everything drifting around it -----------------------------------------
   Photographs of her, the flowers from the opening, hearts, and three soft
   lights. All of it is blurred in the files rather than with filter:blur, and
   only transform and opacity animate — the compositor handles those without
   waking the main thread, which matters when it is also decoding 1080p. */
.film-drift{
  position:absolute; inset:0; z-index:0;
  pointer-events:none; overflow:hidden;
}
.film-drift > span{
  position:absolute; left:var(--x); top:var(--y);
  width:var(--s); height:var(--s); margin:calc(var(--s) / -2);
  will-change:transform;
  animation:film-wander var(--dur) ease-in-out var(--delay) infinite;
}
/* the fade-in sits on the child and the drift on the parent, so the two
   animations never fight over the same transform */
.film-drift img,
.d-heart i{
  width:100%; height:100%;
  animation:film-bloom 2.2s ease var(--fd) both;
}

.d-photo{ border-radius:50%; overflow:hidden; }
.d-photo img{ object-fit:cover; border-radius:50%; }

.d-bloom img{ object-fit:contain; filter:blur(1.3px) saturate(1.08); }

/* The heart drifts on the span and fades on the <i> inside it. Both on the
   one element meant two animations writing `transform`, the later one won,
   and the hearts sat at opacity 0 forever. */
.d-heart{ font-size:var(--s); line-height:1; }
.d-heart i{
  display:grid; place-items:center;
  font-style:normal;
  filter:drop-shadow(0 3px 8px rgba(190,70,110,.35));
}

/* Each thing wanders through three places of its own and comes home, so the
   loop closes on itself with no jump. The waypoints are whole transforms
   handed in from the template, which is what lets forty-four items share one
   set of keyframes and still never travel together. */
@keyframes film-wander{
  0%, 100%{ transform:translate(0,0) rotate(0deg) scale(1) }
  25%     { transform:var(--w1) }
  50%     { transform:var(--w2) }
  75%     { transform:var(--w3) }
}
@keyframes film-bloom{
  from{ opacity:0; transform:scale(.82) }
  to  { opacity:var(--o); transform:scale(1) }
}

/* the lights: red, blue and pink. Big, soft and slow — they set the colour of
   the room rather than being anything she looks at. */
.glow{
  position:absolute; border-radius:50%; pointer-events:none;
  width:min(70vw, 620px); aspect-ratio:1;
  animation:film-swim 26s ease-in-out infinite alternate;
}
.glow-red{
  left:-8%; top:6%;
  background:radial-gradient(circle, rgba(255,138,158,.46), transparent 66%);
  animation-duration:23s;
}
.glow-blue{
  right:-10%; top:34%;
  background:radial-gradient(circle, rgba(150,186,255,.40), transparent 66%);
  animation-duration:29s; animation-delay:-6s;
}
.glow-pink{
  left:50%; top:52%; translate:-50% 0;
  width:min(96vw, 860px);
  background:radial-gradient(circle, rgba(255,196,221,.55), transparent 64%);
  animation-duration:34s; animation-delay:-12s;
}
@keyframes film-swim{
  from{ transform:translate(0,0) scale(1) }
  to  { transform:translate(4%, -5%) scale(1.14) }
}

.film-stage{
  position:relative; z-index:1;
  display:flex; justify-content:center;
}

/* a little screen, the size of the phone it was filmed on */
.screen{
  position:relative;
  width:min(84vw, 330px);
  aspect-ratio:9/16; max-height:74svh;
  border-radius:16px; overflow:hidden;
  background:#2B2039;
  box-shadow:0 26px 60px rgba(84,52,64,.26), 0 3px 10px rgba(84,52,64,.14);
}
.screen video{
  display:block; width:100%; height:100%; object-fit:cover;
}

/* --- the letter ------------------------------------------------------------
   Sealed on the parcel page until she taps it. The envelope is blush with a
   cream flap and a wax heart; the paper it opens into is round-cornered, tied
   with a dashed rule, and ruled in the faintest pink.
   ------------------------------------------------------------------------ */
.note{
  position:relative;
  display:flex; flex-direction:column; align-items:center;
  justify-content:center;
  /* the rings are measured against the height of this box, so it has to be
     at least a screen tall or the top and bottom of every ellipse is cut off */
  min-height:100svh;
  padding-block:clamp(2rem,8vmin,5rem);
}
.note .env, .note .letter, .note .letter-skip{ position:relative; z-index:2; }

/* The float is on the artwork, not on the button. Animating the button
   itself meant the thing she is aiming at drifted under her thumb — and a
   moving hit area is exactly as annoying as it sounds. The box she taps
   never moves; only the envelope inside it does. */
.env{
  position:relative;
  display:flex; flex-direction:column; align-items:center; gap:.85rem;
  border:0; background:none; cursor:pointer; padding:1rem;
  transition:opacity .5s ease;
}
.is-open .env{ opacity:0; }
@keyframes env-float{
  from{ transform:translateY(0) rotate(-1.5deg) }
  to  { transform:translateY(-7px) rotate(1.5deg) }
}

.env-card{
  position:relative; display:block;
  width:min(74vw, 236px); aspect-ratio:3/2;
  border-radius:8px;
  background:linear-gradient(165deg, #FFD9E2, #FFC3D3 62%, #FFB7CA);
  box-shadow:0 18px 38px rgba(150,70,100,.26), 0 2px 5px rgba(150,70,100,.14);
  animation:env-float 4.4s ease-in-out infinite alternate;
  transition:box-shadow .35s ease, filter .35s ease;
}
/* hover lifts the light on it rather than moving it, so the target is still
   exactly where she aimed */
.env:hover .env-card, .env:focus-visible .env-card{
  box-shadow:0 26px 52px rgba(150,70,100,.34), 0 3px 7px rgba(150,70,100,.18);
  filter:brightness(1.04);
}
.env:hover .env-seal, .env:focus-visible .env-seal{
  transform:translate(-50%,-50%) scale(1.08);
}
.is-open .env-card{ animation:none; }
/* the flap, folded down over the front */
.env-flap{
  position:absolute; inset:0; border-radius:8px; overflow:hidden;
}
.env-flap::before{
  content:""; position:absolute; left:0; right:0; top:0; height:64%;
  background:linear-gradient(180deg, #FFF4F6, #FFE6EC);
  clip-path:polygon(0 0, 100% 0, 50% 100%);
  transform-origin:top center;
  transition:transform .55s cubic-bezier(.5,-0.25,.4,1.3);
}
.is-open .env-flap::before{ transform:rotateX(-168deg); }

.env-seal{
  position:absolute; left:50%; top:56%; translate:-50% -50%;
  width:46px; height:46px; border-radius:50%;
  display:grid; place-items:center;
  background:radial-gradient(circle at 35% 30%, #E2547C, #B92E56);
  box-shadow:0 4px 10px rgba(140,30,60,.4), inset 0 -2px 4px rgba(0,0,0,.18);
  transition:transform .4s ease, opacity .4s ease;
}
.env-seal svg{ width:24px; height:24px; fill:#FFE3EB; }
.is-open .env-seal{ transform:translate(-50%,-50%) scale(.4) rotate(-25deg); opacity:0; }

.env-say{
  font-family:var(--hand); font-size:1.45rem; color:#A32040; line-height:1;
}
.env-tap{
  font-family:"Instrument Sans", var(--sans); font-weight:600;
  font-size:.62rem; letter-spacing:.2em; text-transform:uppercase;
  color:#BFA3AE;
}

/* --- the question before the envelope -------------------------------------
   Two buttons that are not equally willing. The sizes are driven by one
   custom property each, so the script only ever writes a number and the
   easing, the shadow and the tilt are all decided here.

   It shrinks rather than dodging the pointer: dodging is a mouse trick, and
   on her phone there is no hover — a button that runs away is just a button
   she cannot press. Shrinking reads the same on every screen she owns.
   ------------------------------------------------------------------------ */
.ask{
  display:flex; flex-direction:column; align-items:center;
  gap:clamp(1.4rem, 5vh, 2.8rem);
  text-align:center; padding-inline:1rem;
  transition:opacity .5s ease, transform .5s ease;
}
.ask.is-answered{ opacity:0; transform:scale(.94); pointer-events:none; }
/* `display:flex` above outranks the browser's own rule for the hidden
   attribute, so setting ask.hidden did nothing and the answered question went
   on holding its place in the column — which is what pushed the letter down
   the page and opened a band of empty background above it. */
.ask[hidden]{ display:none; }
/* between one question and the next */
.ask.is-turning{ opacity:0; transform:translateY(-12px); pointer-events:none; }

.ask-q{
  margin:0; max-width:20ch;
  font-family:var(--hand-np);
  font-size:clamp(1.45rem, 4.2vw, 2.5rem);
  line-height:1.4; color:#A32040;
  transition:opacity .25s ease;
}

/* A fixed row height: "yes" grows by transform, which does not move anything
   around it, but the row still needs room for it at full size or the layout
   jumps the first time it is scaled. */
.ask-row{
  display:flex; align-items:center; justify-content:center;
  gap:clamp(.9rem, 4vw, 2.4rem);
  min-height:clamp(96px, 16vh, 150px);
}

.ask-yes, .ask-no{
  --s:1; --tilt:0deg;
  border:0; cursor:pointer; border-radius:999px;
  font-family:"Instrument Sans", var(--sans); font-weight:600;
  font-size:clamp(1rem, 2.1vw, 1.3rem); line-height:1;
  padding:.9em 1.9em;
  transform:scale(var(--s)) rotate(var(--tilt));
  transition:transform .45s cubic-bezier(.34,1.5,.64,1),
             opacity .35s ease, box-shadow .35s ease;
}
.ask-yes{
  background:linear-gradient(165deg, #E8607C, #C6203C);
  color:#FFF3F5;
  box-shadow:0 14px 34px rgba(198,32,60,.30), 0 2px 6px rgba(198,32,60,.20);
}
.ask-yes:hover, .ask-yes:focus-visible{
  box-shadow:0 18px 44px rgba(198,32,60,.38), 0 3px 8px rgba(198,32,60,.24);
}
.ask-no{
  background:#FFFEFB; color:#9C8894;
  box-shadow:0 8px 20px rgba(84,52,64,.14), 0 1px 3px rgba(84,52,64,.10);
}
.ask-no:hover, .ask-no:focus-visible{ color:#7A6875; }
/* too small to mean anything any more */
.ask-no.is-spent{ opacity:0; pointer-events:none; }

/* while she is being asked, the envelope waits out of sight */
.note.is-asking .env{
  opacity:0; pointer-events:none; transform:scale(.92);
}
.env{ transition:opacity .5s ease, transform .5s ease; }

@media (prefers-reduced-motion:reduce){
  .ask-yes, .ask-no{ transition:opacity .2s linear; }
}

/* --- the paper -------------------------------------------------------------
   Old cotton writing paper: ivory gone warm at the edges, a torn (deckle)
   edge, folded in three the way it went into the envelope. The sheet itself
   is one generated image, static/img/paper.webp (scripts/make_paper.py),
   laid on with border-image so its torn edge stays the same width whatever
   size the letter is, and only the middle stretches.

   It lives on ::before, not on the letter, so the shadow under it (::after)
   can follow the torn shape without a filter on the letter itself — a filter
   there would be redrawn on every word the pencil writes, which phones feel. */
.letter{
  --lh:2.2rem;
  --deckle:22px;
  position:relative; isolation:isolate;
  width:min(94vw, 42rem);
  padding:clamp(2.2rem,4.2vw,3rem) clamp(2rem,4.4vw,3.2rem);
  opacity:0; transform:translateY(18px) scale(.97);
  transition:opacity .7s ease, transform .7s cubic-bezier(.2,1,.36,1);
}
.letter.is-in{ opacity:1; transform:none; }
.letter::before{
  content:""; position:absolute; inset:0; z-index:-1;
  border:var(--deckle) solid transparent;
  border-image:url("../img/paper.webp") 44 fill / var(--deckle) stretch;
  pointer-events:none;
}
.letter::after{
  content:""; position:absolute; z-index:-2;
  inset:calc(var(--deckle) * .6) calc(var(--deckle) * .5) calc(var(--deckle) * .2);
  background:rgba(84,52,28,.30);
  filter:blur(18px); transform:translateY(14px);
  pointer-events:none;
}
/* the heart becomes a wax seal, pressed on the top edge */
.letter-heart{
  position:absolute; top:-8px; left:50%; translate:-50% 0; z-index:3;
  width:40px; height:40px;
  border-radius:47% 53% 51% 49% / 53% 47% 53% 47%;
  display:grid; place-items:center;
  background:
    radial-gradient(circle at 36% 30%, rgba(255,190,190,.45), transparent 42%),
    radial-gradient(circle at 50% 50%, #B32B3D 0 52%, #8C1A2B 64%, #6B1020 100%);
  color:rgba(255,214,214,.82); font-size:1.02rem; line-height:1;
  text-shadow:0 -1px 0 rgba(0,0,0,.35), 0 1px 0 rgba(255,170,170,.35);
  box-shadow:0 4px 10px rgba(84,30,30,.35), inset 0 -2px 4px rgba(0,0,0,.25);
}

.letter-page{
  position:relative;
  max-height:min(66svh, 36rem);
  overflow-y:auto; overscroll-behavior:contain;
  /* no scrollbar on the paper: a grey bar with arrows down the side of an
     old letter looked wrong, and the page follows the pencil anyway — she
     can still scroll it with a finger, the wheel or the trackpad */
  scrollbar-width:none;
  padding:.3rem .5rem 0 .3rem;
  /* faint pressed rules, one per line of writing. "local" makes them scroll
     with the words, so they stay under the lines they belong to. */
  background:repeating-linear-gradient(
      transparent 0 calc(var(--lh) - 1px),
      rgba(120,84,50,.14) calc(var(--lh) - 1px) var(--lh));
  background-attachment:local;
  background-position:0 calc(.3rem - .32rem);
}
.letter-page::-webkit-scrollbar{ display:none; width:0; height:0; }
.letter-page p{
  margin:0 0 var(--lh);
  font-family:var(--hand-np);
  font-size:clamp(1.06rem,1.45vw,1.22rem);
  line-height:var(--lh);
  color:#3A2A20;            /* iron-gall ink, brown-black */
}
.letter-page p:last-child{ margin-bottom:0; }
.letter-sign{ margin-top:var(--lh); text-align:right; }
.letter-sign p{ margin-bottom:calc(var(--lh) * .3); color:#7E2426; }

.letter-skip{
  margin:.9rem 0 0; text-align:center;
  font-family:"Instrument Sans", var(--sans); font-weight:500;
  font-size:.62rem; letter-spacing:.18em; text-transform:uppercase;
  color:#C9AEBA;
}


/* --- after the letter: his photo, then his video, on the same paper ------
   While the letter is being written the paper keeps its own size. When the
   photo comes, the paper reshapes itself around the photo — wide for the
   photo, tall for the video — so each fills the paper right up to its torn
   edge, with only a hair of paper showing. note.js works out the size; the
   paper glides from one shape to the next. */
.letter.is-sizing{
  transition:opacity .7s ease, transform .7s cubic-bezier(.2,1,.36,1),
             width 1s cubic-bezier(.4,0,.2,1), height 1s cubic-bezier(.4,0,.2,1);
}
.after{
  position:absolute; z-index:2;
  inset:0;
  pointer-events:none;
  /* the torn edge: the photo and the video are cut with the paper's own
     outline (the same image, used as a mask), so their edges are ragged
     exactly where the paper's are rather than straight */
  -webkit-mask-box-image:url("../img/paper.webp") 44 fill / var(--deckle) stretch;
  mask-border:url("../img/paper.webp") 44 fill / var(--deckle) stretch;
  mask-border-mode:alpha;
}
.after-photo, .after-video{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  opacity:0; visibility:hidden;
  transition:opacity 1s ease, visibility 0s linear 1s;
}
/* the video is not in the picture at all until its turn: a faded-out video
   layer can still show through on some browsers */
.after-video{ display:none; background:#000; }
.letter.shows-video .after-video{
  display:block; opacity:1; visibility:visible;
  animation:after-in 1s ease both;
}
@keyframes after-in{ from{ opacity:0 } to{ opacity:1 } }
.letter.shows-photo .after-photo{ opacity:1; visibility:visible; transition-delay:0s; }
.letter-page{ transition:opacity 1s ease; }
/* taken out of the flow while the pictures are up, so the paper can take
   their shape instead of the letter's */
.letter.is-after .letter-page{
  position:absolute; left:0; right:0; top:0;
  opacity:0; visibility:hidden; pointer-events:none;
}
.letter.is-after .pencil{ display:none; }
@media (prefers-reduced-motion:reduce){
  .after-photo, .after-video{ transition:opacity .4s linear; animation:none !important; }
  .letter.is-sizing{ transition:opacity .4s linear; }
}


/* graphite settling on the page */
.w{
  opacity:0; filter:blur(2px); transform:translateY(-2px);
  display:inline-block;
  transition:opacity .3s ease, filter .3s ease, transform .3s ease;
}
.w.is-written{ opacity:1; filter:none; transform:none; }
.no-js .w{ opacity:1; filter:none; transform:none; }

.pencil{
  position:absolute; left:0; top:0;
  width:32px; height:32px;
  opacity:0; pointer-events:none;
  transform:translate(-4px,-6px);
  transition:opacity .35s ease, left .16s linear, top .24s ease;
  filter:drop-shadow(0 3px 5px rgba(84,52,64,.3));
}
.letter.is-writing .pencil{
  opacity:1; animation:pencil-scratch .3s ease-in-out infinite alternate;
}
.pencil svg{ width:100%; height:100%; }
.pencil .body{ fill:#EFC074; }
.pencil .tip{ fill:#F8E6C6; }
.pencil .lead{ fill:#3A2E36; }
.pencil .cap{ fill:#EE9BB6; }
@keyframes pencil-scratch{
  from{ transform:translate(-4px,-6px) rotate(0deg) }
  to  { transform:translate(-3px,-7px) rotate(-3deg) }
}

/* Phones keep the smaller sheet — a card-sized letter in the middle of the
   screen with her photos circling round it. iPads and laptops get the big
   sheet above. 600px is the line: every phone held upright is narrower,
   every iPad is wider. */
@media (max-width:600px){
  .letter{ --lh:2rem; --deckle:16px; width:min(84vw, 30rem);
            padding:1.9rem 1.7rem; }
  .letter-page{ max-height:38svh; }
  .letter-page p{ font-size:1.02rem; }
  /* twelve prints to a ring now (24 photos): a touch smaller on a phone so
     neighbours on the same ring never overlap */
  .orb-r0{ --size:74px; }
  .orb-r1{ --size:82px; }
}

.film-hint{
  margin:0; display:flex; align-items:center; gap:.6rem;
  font-family:var(--hand); font-size:1.25rem; color:#A8929C;
  opacity:0; pointer-events:none; cursor:pointer;
  transition:opacity .8s ease, color .2s linear;
}
.film-hint.is-on{ opacity:1; pointer-events:auto; }
.film-hint:hover{ color:#A32040; }
.film-hint i{ font-style:normal; animation:film-nudge 1.9s ease-in-out infinite; }
@keyframes film-nudge{
  0%,100%{ transform:translateY(0) } 50%{ transform:translateY(5px) }
}

@media (prefers-reduced-motion:reduce){
  .film-hint i, .pencil, .env{ animation:none; }
  /* paused, not cancelled: cancelling the ring drops every print back to
     the centre of the screen in one heap */
  .orb{ animation-play-state:paused; }
  .film-drift > span, .film-drift img, .d-heart i, .glow{ animation:none; }
}

/* --- the page behind "open" -----------------------------------------------
   Not a letter and not a scrapbook: an empty room on the same paper as the
   rest of the site. Only the blocks that have something in them appear, so
   the layout is whatever he decides to put here.
   ------------------------------------------------------------------------ */

#parcel{ padding-block:clamp(3rem,8vh,6rem) clamp(6rem,14vh,10rem); }
#parcel .wrap{ max-width:64rem; }

/* these are <section> elements, and the global section rule was adding
   10rem of padding to each, leaving craters between the blocks */
#parcel .block{ padding:0; margin-bottom:clamp(2.5rem,6vh,4rem); }
.block-label{
  margin:0 0 1.3rem;
  font-family:"Instrument Sans", var(--sans); font-weight:600;
  font-size:.66rem; letter-spacing:.26em; text-transform:uppercase;
  color:#A32040;
  display:flex; align-items:center; gap:.9rem;
}
.block-label::after{
  content:""; flex:1; height:1px; background:rgba(163,32,64,.18);
}

/* --- her photos -----------------------------------------------------------
   Prints tipped out onto the page: taped down, a little crooked, drawn on
   by hand. The doodles are white ink over the photo, so they need a shadow
   under them to survive the pale bits of a picture.
   ------------------------------------------------------------------------ */
/* --- her photographs, circling the letter -----------------------------------
   Each print rides its own ring, and the ring is an ellipse rather than a
   circle: a screen is wider than it is tall, so a circle wide enough to
   reach the sides is taller than the window and a circle short enough to
   fit leaves the left and right thirds empty. The ellipse is sized in vw
   across and vh down, so it fills whatever shape the window happens to be —
   her phone standing up, the iPad either way round, the Mac.

   The path is written as translate only. A ring built out of
   `rotate(θ) translateX(r)` needs a second animation inside it turning the
   print back the other way, or the photographs tumble like a fairground
   wheel — and those two have to stay in step for ever. Stepping around the
   ellipse in plain x/y means nothing rotates in the first place, so there
   is nothing to keep in step. Twenty-four steps is smooth enough that the
   straight lines between them read as a curve.

   Negative delays put them at different points on the ring from the first
   frame rather than all setting off from the same place.
   ------------------------------------------------------------------------ */
.orbit{
  /* full-bleed: the rings are wider than the column the letter sits in, and
     they should run off the sides of the window, not stop at the margin */
  position:absolute; top:0; bottom:0;
  left:50%; width:100vw; margin-left:-50vw;
  overflow:hidden; z-index:0;
  pointer-events:none;
  opacity:0; transition:opacity 1.4s ease .5s;
}
.note.is-open .orbit{ opacity:1; }

.orb{
  position:absolute; left:50%; top:50%;
  width:0; height:0;
  animation:orb-ring var(--dur) linear var(--delay) infinite;
}
/* Each radius has a floor in pixels as well as a share of the window: on a
   narrow screen a percentage collapses and every print ends up hidden
   behind the paper or bunched above it. The ceilings stop the outermost
   ring from leaving the window on a very wide monitor. */
/* Two rings, not three. Three sat about seventy pixels apart vertically
   while a print is over two hundred tall, so on a laptop they could not help
   but stack on top of each other. Two rings put a real gap between them, and
   the inner one now runs close enough to the paper to pass behind its edges —
   which closes the ring of dead background that was sitting around the
   letter.

   Each radius has a floor in pixels as well as a share of the window: on a
   narrow screen a percentage collapses and every print ends up hidden behind
   the paper or bunched above it. The ceilings stop the outer ring from
   leaving the window on a very wide monitor. */
.orb-r0{
  --rx:clamp(190px, 26vw, 520px); --ry:clamp(215px, 22vh, 330px);
  --size:clamp(84px, 6.6vw + 2.6vh, 150px);
}
.orb-r1{
  --rx:clamp(268px, 40vw, 780px); --ry:clamp(322px, 38vh, 505px);
  --size:clamp(94px, 7.6vw + 3vh, 172px);
}
.orb-spin{ display:block; }
@keyframes orb-ring{
  0%{ transform:translate(calc(var(--rx) * 1.0000), calc(var(--ry) * 0.0000)) }
  4.1667%{ transform:translate(calc(var(--rx) * 0.9219), calc(var(--ry) * 0.3873)) }
  8.3333%{ transform:translate(calc(var(--rx) * 0.7643), calc(var(--ry) * 0.6449)) }
  12.5%{ transform:translate(calc(var(--rx) * 0.5827), calc(var(--ry) * 0.8127)) }
  16.6667%{ transform:translate(calc(var(--rx) * 0.3920), calc(var(--ry) * 0.9200)) }
  20.8333%{ transform:translate(calc(var(--rx) * 0.1966), calc(var(--ry) * 0.9805)) }
  25%{ transform:translate(calc(var(--rx) * -0.0003), calc(var(--ry) * 1.0000)) }
  29.1667%{ transform:translate(calc(var(--rx) * -0.1969), calc(var(--ry) * 0.9804)) }
  33.3333%{ transform:translate(calc(var(--rx) * -0.3922), calc(var(--ry) * 0.9199)) }
  37.5%{ transform:translate(calc(var(--rx) * -0.5829), calc(var(--ry) * 0.8125)) }
  41.6667%{ transform:translate(calc(var(--rx) * -0.7645), calc(var(--ry) * 0.6447)) }
  45.8333%{ transform:translate(calc(var(--rx) * -0.9221), calc(var(--ry) * 0.3870)) }
  50%{ transform:translate(calc(var(--rx) * -1.0000), calc(var(--ry) * -0.0003)) }
  54.1667%{ transform:translate(calc(var(--rx) * -0.9219), calc(var(--ry) * -0.3873)) }
  58.3333%{ transform:translate(calc(var(--rx) * -0.7643), calc(var(--ry) * -0.6449)) }
  62.5%{ transform:translate(calc(var(--rx) * -0.5827), calc(var(--ry) * -0.8127)) }
  66.6667%{ transform:translate(calc(var(--rx) * -0.3920), calc(var(--ry) * -0.9200)) }
  70.8333%{ transform:translate(calc(var(--rx) * -0.1966), calc(var(--ry) * -0.9805)) }
  75%{ transform:translate(calc(var(--rx) * 0.0003), calc(var(--ry) * -1.0000)) }
  79.1667%{ transform:translate(calc(var(--rx) * 0.1969), calc(var(--ry) * -0.9804)) }
  83.3333%{ transform:translate(calc(var(--rx) * 0.3922), calc(var(--ry) * -0.9199)) }
  87.5%{ transform:translate(calc(var(--rx) * 0.5829), calc(var(--ry) * -0.8125)) }
  91.6667%{ transform:translate(calc(var(--rx) * 0.7645), calc(var(--ry) * -0.6447)) }
  95.8333%{ transform:translate(calc(var(--rx) * 0.9221), calc(var(--ry) * -0.3870)) }
  100%{ transform:translate(calc(var(--rx) * 1.0000), calc(var(--ry) * -0.0000)) }
}


/* the print itself: a small polaroid, hanging crooked */
.orb-print{
  display:block;
  width:var(--size); margin:calc(var(--size) / -2) 0 0 calc(var(--size) / -2);
  padding:6px 6px 0;
  background:#FFFEFB; border-radius:3px;
  transform:rotate(var(--tilt));
  box-shadow:0 10px 24px rgba(84,52,64,.20), 0 2px 5px rgba(84,52,64,.12);
}
.orb-print img{
  display:block; width:100%; aspect-ratio:4/5; object-fit:cover;
  border-radius:2px;
}
.orb-print::after{
  content:""; display:block; height:14px;   /* the white lip under a polaroid */
}

.snap{
  position:relative; margin:0;
  background:#FFFEFB; padding:10px 10px 0;
  border-radius:3px;
  box-shadow:0 16px 34px rgba(84,52,64,.16), 0 2px 6px rgba(84,52,64,.10);
}

/* a strip of tape over one corner */
.tape{
  position:absolute; top:-11px; left:50%;
  width:74px; height:24px; margin-left:-46px;
  transform:rotate(-7deg);
  background:linear-gradient(180deg,
    rgba(255,236,222,.72), rgba(255,224,206,.60));
  border-left:1px solid rgba(255,255,255,.6);
  border-right:1px solid rgba(255,255,255,.6);
  box-shadow:0 1px 3px rgba(120,90,105,.14);
  pointer-events:none;
}
.snap:nth-child(even) .tape{ left:14px; margin-left:0; transform:rotate(-24deg); }
.snap:nth-child(3n) .tape{ left:auto; right:10px; margin-left:0; transform:rotate(21deg); }

.frame{ position:relative; line-height:0; overflow:hidden; border-radius:2px; }
.snap img{
  display:block; width:100%; aspect-ratio:4/5; object-fit:cover;
  border-radius:2px;
}

/* the pen */
.doodle{
  position:absolute; inset:0; width:100%; height:100%;
  pointer-events:none; overflow:visible;
}
.doodle path, .doodle circle{
  fill:none; stroke:#FFFFFF; stroke-width:4.2;
  stroke-linecap:round; stroke-linejoin:round;
  filter:drop-shadow(0 1px 3px rgba(40,20,30,.5));
}
.doodle .scribble{
  fill:#FFFFFF; stroke:none;
  font-family:var(--hand); font-size:36px; font-weight:600;
  filter:drop-shadow(0 2px 4px rgba(40,20,30,.55));
  transform:rotate(-5deg);
}

/* The pen draws rather than the doodle simply appearing. This is CSS and not
   Motion on purpose: an animation library that does not recognise an SVG
   property leaves the line hidden forever, and a doodle that never arrives
   is worse than one that never moved. Only hide it once the JS has said it
   is here to un-hide it. */
.has-js .doodle path,
.has-js .doodle circle{
  stroke-dasharray:1; stroke-dashoffset:1;
  transition:stroke-dashoffset .6s cubic-bezier(.4,0,.2,1);
}
.has-js .doodle .scribble{
  opacity:0;
  transition:opacity .4s ease, transform .5s cubic-bezier(.2,1.4,.4,1);
  transform:rotate(-5deg) scale(.8);
}
.has-js .snap.is-drawn .doodle path,
.has-js .snap.is-drawn .doodle circle{ stroke-dashoffset:0; }
.has-js .snap.is-drawn .doodle .scribble{ opacity:1; transform:rotate(-5deg) scale(1); }

/* one line after another, the way a hand works */
.doodle g > *:nth-child(2){ transition-delay:.20s; }
.doodle g > *:nth-child(3){ transition-delay:.36s; }
.doodle g > *:nth-child(4){ transition-delay:.50s; }
.doodle g > .scribble{ transition-delay:.52s, .52s; }

/* An emoji under the print instead of a sentence. The emoji fonts come
   first and carry only emoji glyphs, so a caption written as words falls
   straight through to the handwriting and still looks right. */
.snap figcaption{
  font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",
              var(--hand);
  font-size:1.5rem; letter-spacing:.12em; color:#5A4650;
  text-align:center; padding:.55rem .35rem .7rem; line-height:1.15;
}

/* film — the clips are shot on a phone, so they are tall. Left to fill the
   column a single one stood nearly two thousand pixels high; capped, it sits
   about the size of the phone it was filmed on. */
.grid-videos{
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:clamp(1.2rem,3vw,2rem);
}
.reel{ margin:0; flex:0 1 340px; max-width:340px; }
.reel video{
  display:block; width:100%; max-height:76vh; border-radius:4px;
  background:#2B2039;
  box-shadow:0 18px 40px rgba(84,52,64,.20);
}
.reel figcaption{
  font-family:var(--hand); font-size:1.12rem; color:#5A4650;
  text-align:center; padding-top:.6rem;
}

/* notes */
.grid-wishes{
  display:grid; gap:1rem;
  grid-template-columns:repeat(auto-fill, minmax(min(100%,250px), 1fr));
}
.wish-note{
  margin:0; background:#FFFDF8; border-radius:3px;
  border:1px solid rgba(120,90,105,.14);
  padding:1.3rem 1.4rem 1.1rem;
  box-shadow:0 10px 24px rgba(84,52,64,.10);
}
.wish-note p{
  margin:0 0 .9rem;
  font-family:var(--hand); font-size:1.2rem; line-height:1.45; color:#3E2C35;
}
.wish-note footer{
  font-family:"Instrument Sans", var(--sans);
  font-size:.62rem; letter-spacing:.18em; text-transform:uppercase; color:#A32040;
}

/* whatever he wants to say, as one card among the rest */
.note-card{
  background:#FFFDF8; border-radius:3px;
  border:1px solid rgba(120,90,105,.14);
  padding:clamp(1.6rem,4vw,2.6rem);
  box-shadow:0 16px 36px rgba(84,52,64,.12);
  max-width:44rem;
}
.note-card p{
  margin:0 0 1.05rem;
  font-family:var(--hand); font-size:clamp(1.2rem,2.2vw,1.45rem);
  line-height:1.6; color:#3E2C35;
}
.note-card p:first-child{ color:#A32040; }
.note-sign{ text-align:right; color:#A32040 !important; margin-top:1.6rem !important; }

@media (prefers-reduced-motion:reduce){
  .drop.is-here .drop-env::after{ animation:none; }
  .snap{ transition:none; }
}
