:root {
  /* a festive evening sky: deep indigo turning violet, never black */
  --night: #1b1a45;
  --night-deep: #130f30;
  --night-lift: #2c2f6e;
  --gold: #e9c68b;
  --champagne: #f7ead2;
  --ink: #f3ece0;
  --ink-soft: rgba(243, 236, 224, 0.8);
  --line: rgba(233, 198, 139, 0.38);
  /* the message in a soft, warm serif with rounded strokes: festive and
     clear; everything the app itself says in a round, friendly face */
  --serif: "Fraunces", Georgia, "Times New Roman", serif;
  --sans: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --beat: 1.2s; /* one heartbeat; js/rhythm.js sets it, with --beat-delay */
  /* one way of moving for the whole card: every view glides to the next in this
     time, on this one soft curve (card.js: GLIDE, easeInOut); the sheets, the
     places and the note keep their playful bounce */
  --glide: calc(var(--beat) * 2);
  /* the sheets open just above the buttons, which always stay in sight */
  --sheet-bottom: calc(4.4rem + env(safe-area-inset-bottom) + 0.6rem);
  --soft: cubic-bezier(.65, 0, .35, 1);
  color-scheme: dark;
}

* { box-sizing: border-box; }

/* the card itself never scrolls, by any finger anywhere: only what is in front
   moves (a places list scrolls within itself, sheets.css). The folded
   message's resting words may reach below the screen; they never make the
   page longer than it is */
html { overflow: hidden; overflow: clip; -webkit-text-size-adjust: 100%; overscroll-behavior: none; }

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100svh;
  background: var(--night);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  overflow: clip;
  touch-action: none;
  /* a card, not a document: a finger drawn across it never marks its words,
     and no touch is answered with the browser's own tinted highlight */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* the evening sky behind everything, fixed without background-attachment
   (iOS): deep indigo with soft glows of rose, violet, teal and amber, joyful
   and never loud; card.js paints the very same sky in the scene */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(70% 45% at 50% 0%, rgba(255, 196, 130, 0.16), transparent 70%),
    radial-gradient(80% 50% at 0% 100%, rgba(255, 112, 150, 0.22), transparent 70%),
    radial-gradient(80% 50% at 100% 100%, rgba(150, 112, 255, 0.20), transparent 70%),
    radial-gradient(70% 45% at 100% 25%, rgba(70, 210, 200, 0.12), transparent 70%),
    radial-gradient(70% 45% at 0% 35%, rgba(255, 160, 90, 0.10), transparent 70%),
    radial-gradient(120% 80% at 50% 10%, var(--night-lift) 0%, var(--night) 55%, var(--night-deep) 100%);
}

/* one column on every screen: the photo as large as the room above the
   buttons allows, whole, with its dedication beneath, and the buttons at the
   bottom, where the thumb is; they never move. The photo's place is reckoned
   from the card's own size, so nothing that appears or leaves can push it;
   with the whole message open it rises (js/layout.js) */
.card {
  position: relative;
  max-width: 40rem;
  height: 100vh;
  height: 100svh;
  margin: 0 auto;
  padding: var(--pad-t) var(--gut) 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) var(--bar-h);
  container-type: size;
  --pad-t: max(0.6rem, calc(env(safe-area-inset-top) + 0.4rem));
  --gut: 0.75rem;
  --bar-h: calc(4.4rem + env(safe-area-inset-bottom));
  --gap: 0.8rem;
  /* the title above the sentence: the larger of the two, on every screen */
  --ded-size: clamp(1.1rem, 5vw, 1.5rem);
  --ded-h: calc(var(--ded-size) * 1.3 * 2);
}
/* the photo's height and top, from the room the card gives, in its own units */
.photo, .message {
  --stage-h: calc(100cqh - var(--bar-h));
  --ph: min(100cqw / 0.7505, var(--stage-h) - var(--gap) - var(--ded-h));
  --ptop: calc(var(--pad-t) + (var(--stage-h) - var(--ph) - var(--gap) - var(--ded-h)) / 2);
}

/* the 3D scene lies over the first screen, full width; transparent where nothing is */
#scene {
  position: absolute;
  top: 0;
  left: calc(50% - 50vw);
  width: 100vw;
  height: 100svh;
  z-index: 1;
  /* every finger is the card's own: the cube's, or two zooming into the photo */
  touch-action: none;
  opacity: 0;
  transition: opacity 0.25s ease;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}


/* the scene shows once it can draw the first photo itself, exactly where the plain one was */
.card.drawn #scene { opacity: 1; }

.greeting, .core { transition: opacity var(--glide) var(--soft); }

h1 { margin: 0; font-weight: 400; }

/* the buttons, in the card's last row and always in the same place: active
   users on the left, share and like on the right */
.bar {
  position: relative;
  z-index: 5;
  align-self: start;
  margin-top: calc((4.4rem - 2.75rem) / 2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 2.75rem;
}
.actions { display: flex; align-items: center; gap: 0.45rem; }
.like-wrap, .share-wrap, .here-wrap { position: relative; display: flex; }

.like {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  height: 2.75rem;
  padding: 0 0.85rem 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: 2rem;
  /* no blur of the moving scene behind it: it would be blurred anew on every
     frame, the phone's dearest work; a little more glass reads the same */
  background: rgba(24, 19, 56, 0.78);
  color: var(--gold);
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.like svg {
  width: 1.25rem;
  height: 1.25rem;
  fill: transparent;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
  transition: fill 0.35s ease, transform 0.35s ease;
  animation: breathe 4s ease-in-out infinite;
}

@keyframes breathe { 0%, 100% { transform: scale(0.92); opacity: 0.8; } 50% { transform: scale(1.06); opacity: 1; } }

.like.liked svg { fill: currentColor; filter: drop-shadow(0 0 6px rgba(233, 198, 139, 0.7)); }
.like b:empty { display: none; }

/* a little burst of light when a like is given, a ring spreading from the
   heart's middle; its rim counted inside its size, so its middle is the
   button's (a pseudo-element is not matched by the * rule above) */
.like::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1.1rem;
  height: 1.1rem;
  margin: -0.55rem 0 0 -0.55rem;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--gold);
  opacity: 0;
  pointer-events: none;
}

.like.burst::after { animation: ripple 0.8s ease-out; }

/* the photo, whole, never touching the screen's edges, so the golden light
   round its frame always has room; the postcard lies under the scene, so the
   cube floats in front of it */
.slot { min-height: 0; }

.photo {
  position: absolute;
  z-index: 0;
  top: var(--ptop);
  left: 50%;
  width: calc(var(--ph) * 0.7505);
  margin: 0 0 0 calc(var(--ph) * -0.37525);
  aspect-ratio: 1200 / 1599;
  transform-origin: 50% 0;
  transition: opacity 0.5s ease;
  border-radius: 1.25rem;
  overflow: hidden;
  box-shadow:
    0 0 0 1px var(--line),
    0 30px 60px -24px rgba(0, 0, 0, 0.75),
    0 0 90px -16px rgba(233, 198, 139, 0.3);
}

.photo img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  /* two fingers zoom into it, inside the frame (card.js) */
  transform-origin: 0 0;
}


/* the card steps out of the mist as it opens: a muted dark glass lies over
   all of it while it is made ready, the thin gold line above fills, then a
   soft light passes once across and the glass lifts, and the card comes
   forward, the words beginning to rise: the very opposite of play mode,
   where all but the cube falls back (js/message.js sets .revealed) */
.veil {
  position: fixed;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  overflow: hidden;
  /* deep and whole: nothing shows through while the card is made ready, so
     it never seems built up, piece by piece; it is simply there when the
     mist lifts, as a real card would be */
  background:
    radial-gradient(90% 60% at 50% 45%, rgba(60, 50, 120, 0.35), transparent 70%),
    radial-gradient(120% 80% at 50% 10%, #2a2c66 0%, #1b1a45 55%, #130f30 100%);
  /* it lifts by fading alone: calm, and light for any phone */
  transition: opacity calc(var(--beat) * 1.6) cubic-bezier(.45, 0, .55, 1);
  /* and should anything keep the page from saying it is ready, it lifts anyway */
  animation: lift 1s ease 3.5s forwards;
}
.veil i {
  position: absolute;
  inset: -10% -80%;
  background: linear-gradient(100deg, transparent 42%, rgba(255, 226, 170, 0.2) 50%, transparent 58%);
  transform: translateX(-62%);
}
.revealed .veil { opacity: 0; }
.revealed .veil i { transform: translateX(62%); transition: transform calc(var(--beat) * 1.3) cubic-bezier(.45, 0, .55, 1); }
.unveiled .veil { display: none; }
@keyframes lift { to { opacity: 0; visibility: hidden; } }
/* away in another tab or app, the mist covers the card again at once, so that
   coming back it steps out of it once more, the light passing across */
.away .veil { opacity: 1; transition: none; animation: none; }
.away .veil i { transform: translateX(-62%); transition: none; }

/* the card opens calmly: the photo appears once it is where it will stand
   while the message is read (js/message.js), and never later than this */
.card.unset .photo { visibility: hidden; animation: appear 0s 3.5s forwards; }
@keyframes appear { to { visibility: visible; } }

/* the lens's invitation: a touch of light in the middle of the photo, rings
   spreading out from it as from the active users' dot; the photo leans in
   with it (js/scene.js) */
.photo .touch {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1rem;
  height: 1rem;
  margin: -0.5rem 0 0 -0.5rem;
  pointer-events: none;
}
/* the touch of light itself, and two rings of its own that spread from it,
   each fading on its own (the active users' gold, with the faintest dark
   edge, so they show on the bright table as on the dark) */
.photo .touch::before, .photo .touch::after, .photo .touch i {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0;
}
.photo .touch::before {
  background: radial-gradient(circle, #fff6e4 0%, var(--gold) 45%, rgba(233, 198, 139, 0) 72%);
  box-shadow: 0 0 14px rgba(255, 210, 150, 0.75), 0 0 2px rgba(40, 24, 0, 0.35);
}
.photo .touch::after, .photo .touch i {
  border: 2px solid var(--gold);
  box-shadow: 0 0 6px rgba(255, 210, 150, 0.45), 0 0 1px rgba(40, 24, 0, 0.45), inset 0 0 1px rgba(40, 24, 0, 0.3);
}
/* clear enough to be seen at once, slow enough to stay calm */
.photo.touching .touch::before { animation: touch-dot 1.8s ease-out both; }
.photo.touching .touch::after { animation: touch-ring 2.1s cubic-bezier(.2, .6, .35, 1) 0.2s both; }
.photo.touching .touch i { animation: touch-ring 2.1s cubic-bezier(.2, .6, .35, 1) 0.65s both; }
@keyframes touch-dot {
  0% { opacity: 0; transform: scale(0.6); }
  14% { opacity: 1; transform: scale(1.1); }
  30% { transform: scale(0.85); }
  100% { opacity: 0; transform: scale(1); }
}
@keyframes touch-ring {
  0% { opacity: 0.95; transform: scale(1); }
  100% { opacity: 0; transform: scale(7); }
}

/* the small cube's invitation, in the lens's own language: a touch of light
   on the cube, rings spreading from it, while the cube comes a little way
   forward and goes back (js/scene.js places it on the cube) */
/* play mode's number above the large cube: the turns the most elegant
   solution needs from here (js/scene.js places it). It never jumps: a good
   turn lights it softly, a mistake dims it a moment */
.togo {
  position: absolute;
  z-index: 2;
  transform: translate(-50%, -100%);
  display: grid;
  justify-items: center;
  gap: 0.2rem;
  color: var(--gold);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--glide) var(--soft);
}
.card.stage .togo.on { opacity: 1; }
.togo b {
  font-family: var(--serif);
  font-size: 2.4rem;
  font-weight: 400;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 16px rgba(255, 200, 130, 0.3);
}
.togo span { font-family: var(--sans); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--gold); opacity: 0.8; }
/* on a phone the words under the number narrower, on two lines, so the
   players and the stopwatch fit beside them on one top line (the human) */
@media (max-width: 699.98px) { .togo span { text-align: center; line-height: 1.2; letter-spacing: 0.03em; } .togo span span { display: block; } }
.togo.down b { animation: togo-lit calc(var(--beat) * 1.2) ease-out; }
.togo.up b { animation: togo-dim calc(var(--beat) * 1.2) ease-out; }
@keyframes togo-lit {
  0% { text-shadow: 0 0 26px rgba(255, 215, 150, 0.95); }
  100% { text-shadow: 0 0 16px rgba(255, 200, 130, 0.3); }
}
@keyframes togo-dim { 0% { opacity: 0.45; } 100% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .togo b { animation: none !important; } }

.tap-me {
  position: absolute;
  z-index: 2;
  width: 1rem;
  height: 1rem;
  margin: -0.5rem 0 0 -0.5rem;
  pointer-events: none;
}
.tap-me::before, .tap-me::after, .tap-me i {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0;
}
.tap-me::before {
  background: radial-gradient(circle, #fff6e4 0%, var(--gold) 45%, rgba(233, 198, 139, 0) 72%);
  box-shadow: 0 0 14px rgba(255, 210, 150, 0.75), 0 0 2px rgba(40, 24, 0, 0.35);
}
.tap-me::after, .tap-me i {
  border: 2px solid var(--gold);
  box-shadow: 0 0 6px rgba(255, 210, 150, 0.45), 0 0 1px rgba(40, 24, 0, 0.45);
}
.tap-me.touching::before { animation: touch-dot 1.8s ease-out both; }
.tap-me.touching::after { animation: touch-ring 2.1s cubic-bezier(.2, .6, .35, 1) 0.2s both; }
.tap-me.touching i { animation: touch-ring 2.1s cubic-bezier(.2, .6, .35, 1) 0.65s both; }

/* while the cube wakes up: a heart beats where it will appear, and a thin
   gold line at the top fills; both fade once the cube is there */
.waking {
  position: absolute;
  top: 9%;
  right: 12%;
  width: 1.2rem;
  height: 1.2rem;
  border-radius: 50%;
  background: radial-gradient(circle, #fff6e4 0%, var(--gold) 45%, rgba(233, 198, 139, 0) 72%);
  box-shadow: 0 0 22px 6px rgba(255, 210, 150, 0.55);
  animation: glowpulse 1.8s ease-in-out infinite;
  transition: opacity 0.6s ease;
}

@keyframes glowpulse { 0%, 100% { transform: scale(0.8); opacity: 0.7; } 50% { transform: scale(1.15); opacity: 1; } }

.card.drawn .waking { opacity: 0; animation: none; }

.loading {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 8;
  pointer-events: none;
  transition: opacity 0.7s ease;
}

/* the thin gold line fills in one calm, even stroke from left to right, the
   same every time, and where it ends the light passes on across the card */
.loading i {
  display: block;
  height: 100%;
  transform-origin: 0 50%;
  transform: scaleX(0);
  background: linear-gradient(90deg, transparent, var(--gold) 25%, #fff3dc 70%, var(--gold));
  box-shadow: 0 0 8px rgba(233, 198, 139, 0.7);
  animation: fill 1.1s cubic-bezier(.45, 0, .55, 1) 0.05s forwards;
}
@keyframes fill { to { transform: scaleX(1); } }

/* how far the card has come, quietly under the line (js/dom.js progress) */
.loading b {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 0.7rem);
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--sans);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: var(--gold);
  opacity: 0.7;
  text-shadow: 0 0 8px rgba(233, 198, 139, 0.45);
}
.revealed .loading { opacity: 0; }

/* the message: its first line stays beneath the photo, a dedication; the
   answer below it unfolds word by word and folds back into it (js/message.js) */
.message {
  position: absolute;
  z-index: 3;
  left: var(--gut);
  right: var(--gut);
  top: calc(var(--ptop) + var(--ph) + var(--gap));
  display: grid;
  gap: 0.55rem;
  justify-items: center;
  text-align: center;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.greeting { max-width: 30rem; text-wrap: balance; pointer-events: auto; cursor: pointer; -webkit-tap-highlight-color: transparent; }
/* touched, the message shows no frame; reached by the keyboard, a faint gold one */
.greeting:focus, .core:focus { outline: none; }
.greeting:focus-visible, .core:focus-visible { outline: 1px solid rgba(233, 198, 139, 0.45); outline-offset: 0.4rem; border-radius: 0.5rem; }
.core { max-width: 30rem; }
.card:not(.folded) .core { pointer-events: auto; cursor: pointer; }

/* the words appear one by one, as if written by light, in a merry jumble, each
   with its own tilt (js/message.js) */
.message .w {
  position: relative;
  display: inline-block;
  --d: calc(var(--o) * 76ms + 0.3s);
  /* they land firmly and stay: no spring, no wobble, nothing left to settle */
  transition: opacity 1s ease, transform 1.1s cubic-bezier(.2, .8, .2, 1), filter 0.9s ease;
  transition-delay: var(--d);
}
/* found again, the answer comes at once, and a little quicker */
.card.again:not(.folded) .key .w {
  --d: calc(var(--k) * 45ms + 0.05s);
  transition-duration: 0.8s, 0.9s, 0.7s;
}
/* each word arrives as a little pearl of light that rises and opens into it,
   and folding, it becomes a pearl again and sinks home into the dedication */
.message .w::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 55%;
  width: 0.5em;
  height: 0.5em;
  margin: -0.25em 0 0 -0.25em;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fffaf0 0 18%, rgba(255, 226, 170, 0.75) 40%, rgba(255, 200, 140, 0) 72%);
  box-shadow: 0 0 6px rgba(255, 215, 150, 0.5);
  opacity: 0;
  pointer-events: none;
}
.card:not(.unwritten):not(.folded) .message .w::before { animation: pearl 1.1s cubic-bezier(.2, .7, .3, 1) var(--d) both; }
.card.again:not(.folded) .lead .w::before { animation: none; }
.card.folded .key .w::before { animation: pearl-home var(--fd) ease-in var(--f) both; }
@keyframes pearl {
  0% { opacity: 0; transform: translateY(1.4em) scale(0.3); }
  35% { opacity: 0.95; }
  70% { opacity: 0.8; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-0.1em) scale(2.2); }
}
@keyframes pearl-home {
  0% { opacity: 0; transform: scale(0.6); }
  30% { opacity: 0.9; transform: translateY(-0.2em) scale(1); }
  100% { opacity: 0; transform: translateY(-2.4em) scale(0.25); }
}
/* the words breathe: every four beats a soft light runs through the lines,
   the dedication always, the answer while it is open. The glow lies ready on
   each word (its own copy, data-t, js/message.js) and is only shown and
   hidden: a shadow painted anew on every frame cost the phone dearly */
.message .w::after {
  content: attr(data-t);
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
  color: transparent;
  text-shadow: 0 0 9px rgba(255, 222, 170, 0.32), 0 0 2px rgba(255, 240, 210, 0.28);
  opacity: 0;
  pointer-events: none;
  /* not there at all while the words are written: blurred as they come, it
     cost Safari so much that the last words stayed blurred (js/message.js
     sets .glint once they stand, .glint-key while the answer stands open) */
  display: none;
}
.card.glint .lead .w::after,
.card.glint-key:not(.folded) .key .w::after { display: block; }
.card:not(.unwritten) .lead .w::after,
.card:not(.unwritten):not(.folded) .key .w::after {
  animation: glint calc(var(--beat) * 4) ease-in-out calc(var(--beat-delay, 0s) + var(--i) * 45ms) infinite;
}
@keyframes glint {
  0%, 68%, 100% { opacity: 0; }
  80% { opacity: 1; }
}
/* and "Herzen" glows warmly with every heartbeat while the answer is open:
   a warm copy of it, shown on the beat */
.message .w.heart::after { color: #fcd6bf; text-shadow: none; }
.card:not(.unwritten):not(.folded) .key .w.heart::after { animation: heartglow var(--beat) ease-out var(--beat-delay, 0s) infinite; }
@keyframes heartglow {
  0%, 40%, 100% { opacity: 0; }
  7% { opacity: 1; }
  16% { opacity: 0.45; }
  25% { opacity: 0.8; }
}
.card.folded .key .w,
.card.unwritten .message .w {
  opacity: 0;
  transform: translate(var(--x, 0), var(--y, 0.7em)) scale(0.55) rotate(var(--t, -4deg));
  filter: blur(5px);
}

/* folding, the answer goes the way it came, the last word first: slowly at
   first, while it can still be read, then quicker (--f, --fd from js/message.js) */
.card.folded .key .w {
  transition-duration: var(--fd);
  transition-delay: var(--f);
  transition-timing-function: ease-in;
}
/* the answer waits, quietly: now and then the message draws a breath and
   begins to unfold, its first words rising a little out of the blur, a faint
   warm light beneath the line, the photo lifting a touch (js/scene.js); then
   it lets it be. Once folded, the answer's words rest, so that stirring and
   settling are quick and close */
.greeting { position: relative; touch-action: none; }
.core { touch-action: none; }
.greeting::after {
  content: "";
  position: absolute;
  left: 18%;
  right: 18%;
  top: 92%;
  height: 1.7em;
  background: radial-gradient(ellipse at 50% 0%, rgba(255, 205, 140, 0.34), rgba(255, 190, 120, 0) 70%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.7s ease;
}
.card.folded.peek .greeting::after { opacity: 1; transition-duration: 0.7s; }
.card.folded.rested .key .w { transition-duration: 0.7s; transition-delay: calc(var(--k) * 30ms); transition-timing-function: ease; }
.card.folded.rested .key .w::before { animation: none; }
.card.folded.peek .key .w.first {
  opacity: 0.6;
  transform: translate(var(--x, 0), calc(var(--y, 0.7em) * 0.45)) scale(0.86) rotate(calc(var(--t, -4deg) * 0.6));
  filter: blur(1px);
  transition-duration: 0.75s;
  transition-delay: calc(var(--k) * 55ms);
  transition-timing-function: cubic-bezier(.3, .7, .4, 1);
}
.card.folded.rested.peek .key .w.first::before { animation: pearl-peek 0.62s ease-out calc(var(--k) * 45ms) both; }
@keyframes pearl-peek {
  0% { opacity: 0; transform: translateY(1.2em) scale(0.3); }
  45% { opacity: 0.6; }
  100% { opacity: 0; transform: translateY(0.3em) scale(0.85); }
}
/* pulled by a finger, the answer follows it word by word, in the order it
   comes (js/message.js sets --pull from 0, folded, to 1, open) */
/* each word a little after the one before, and every one whole and sharp
   the moment the answer is fully open, the last ones too ("am", "liegen"):
   before, those never quite arrived and sharpened only after the finger */
.card.pulling .key .w {
  --p: clamp(0, calc((var(--pull, 0) - var(--kn, 0) * 0.45) / 0.55), 1);
  opacity: var(--p) !important;
  transform: translate(calc(var(--x, 0em) * (1 - var(--p))), calc(var(--y, 0.7em) * (1 - var(--p)))) scale(calc(0.55 + 0.45 * var(--p))) rotate(calc(var(--t, -4deg) * (1 - var(--p)))) !important;
  filter: blur(calc(5px * (1 - var(--p)))) !important;
  transition: none !important;
}
.card.pulling .key .w::before, .card.pulled .message .w::before { animation: none !important; }

/* golden sparks rising from the scroll as it opens */
.spark {
  position: fixed;
  left: 0;
  top: 0;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 7;
  opacity: 0;
  background: radial-gradient(circle, #fff8e2 0%, #ffd27a 45%, rgba(255, 190, 110, 0) 72%);
  animation: spark 1.3s cubic-bezier(.15, .7, .3, 1) var(--d) forwards;
}
@keyframes spark {
  0% { opacity: 0; transform: translate(var(--x), var(--y)) scale(0.4); }
  15% { opacity: 1; }
  100% { opacity: 0; transform: translate(calc(var(--x) + var(--dx)), calc(var(--y) + var(--dy))) scale(1.3); }
}

.core p { margin: 0; text-wrap: balance; }

/* the title in a warm peach rose, the sentence in champagne: friendly on the night */
.lead {
  margin: 0;
  font-family: var(--serif);
  font-variation-settings: "SOFT" 100, "WONK" 1;
  font-size: var(--ded-size);
  font-style: italic;
  font-weight: 600;
  line-height: 1.3;
  color: #ffb49c;
}

.key {
  font-family: var(--serif);
  font-variation-settings: "SOFT" 100;
  font-size: clamp(1rem, 4.4vw, 1.3rem);
  font-weight: 500;
  line-height: 1.38;
  margin: 0;
  text-wrap: balance;
  color: var(--champagne);
}

/* how many hearts are here right now; a tap shows where, anonymously */
.here {
  position: relative;
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* no blur of the moving scene behind it: it would be blurred anew on every
     frame, the phone's dearest work; a little more glass reads the same */
  background: rgba(24, 19, 56, 0.78);
  color: var(--gold);
  font: inherit;
  line-height: 1;
  cursor: pointer;
  z-index: 5;
  -webkit-tap-highlight-color: transparent;
}

/* in its place from the start; it gives a little hop once the count is real */
.here.live { animation: popin 0.75s cubic-bezier(.3, 1.6, .5, 1) both; }
@keyframes popin { from { transform: scale(0.82); } }
/* active users: a person, so one knows at once who is found there, slowly
   sending out rings, like a signal (the human, 2026-10-19; a dot before) */
.signal {
  position: relative;
  display: inline-block;
  width: 1.05rem;
  height: 1.05rem;
  vertical-align: middle;
}
.signal svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: var(--gold);
  filter: drop-shadow(0 0 4px rgba(233, 198, 139, 0.7));
}

.signal::before, .signal::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1.5px solid var(--gold);
  animation: signal-ripple 3.6s ease-out infinite;
}

.signal::after { animation-delay: 1.8s; }

@keyframes ripple { from { transform: scale(1); opacity: 0.85; } to { transform: scale(3.4); opacity: 0; } }
@keyframes signal-ripple { from { transform: scale(0.7); opacity: 0.7; } to { transform: scale(2); opacity: 0; } }

/* the number sits on the circle's edge, like a badge on an app */
.here b {
  position: absolute;
  top: -0.3rem;
  right: -0.35rem;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.28rem;
  box-sizing: border-box;
  border-radius: 0.6rem;
  background: var(--gold);
  color: var(--night);
  font-family: var(--sans);
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1.15rem;
  text-align: center;
  font-variant-numeric: lining-nums tabular-nums;
  box-shadow: 0 0 0 2px rgba(24, 19, 56, 0.85);
}

.here i { display: none; }

/* someone new is here: a ring of light spreads from the button, and its
   number springs up (js/bar.js); a heart given anywhere: the heart is glad,
   and its number springs up too */
.here::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1.1rem;
  height: 1.1rem;
  margin: -0.55rem 0 0 -0.55rem;
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--gold);
  opacity: 0;
  pointer-events: none;
}
.here.arrive::after { animation: ripple 1.2s ease-out; }
.here.arrive b, .like b.up { animation: count-pop 0.7s cubic-bezier(.3, 1.6, .5, 1); }
@keyframes count-pop { 0% { transform: scale(1); } 35% { transform: scale(1.4); } 100% { transform: scale(1); } }

/* the status line: very small, nearly hidden, halfway between the buttons
   and the bottom edge (card.js --version-top), across the whole bar and the
   same on the card and in play mode (the human, 2026-10-19): the frames, the
   effects step and the card's work on the left, from where the active users
   start; the last commit's date and time and the version on the right,
   ending where the heart ends. deploy.sh stamps both, locally the server */
.status {
  position: absolute;
  top: calc(100% + var(--version-top, 0.2rem));
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  line-height: 1;
  white-space: nowrap;
  font-family: var(--sans);
  font-size: 0.5rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  pointer-events: none;
}
.status .load, .status .stamp .built, .status .stamp .version, .status .stamp .dot { opacity: 0.28; } /* the words quiet, the buttons bright */
.status .load { min-width: 0; white-space: nowrap; } /* whole, never cut: there is room up to the date */
/* both answer a tap, their touch reaching well round the small words: the
   frames choose the effects step, the version shows what is new */
.status .load, .status button.version { padding: 0.45rem 0; margin: -0.45rem 0; pointer-events: auto; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.status button.version { border: 0; background: none; font: inherit; letter-spacing: inherit; color: inherit; }
.status button.version[aria-expanded="true"] { opacity: 0.7; }
/* the effects, a star with five lights in an arc, as many lit as the step is
   high; and what is new, a little scroll with a spark, a gold dot while
   something is new to this device (the human, 2026-10-19: cute) */
.fxline, .stamp { display: flex; align-items: center; min-width: 0; }
/* in play mode two round buttons like the play buttons, above the active
   users (the effects) and above the heart (what is new); on the card they
   rest, there is nothing to choose (the human, 2026-10-19) */
.bar button.fx, .bar button.newsbtn { display: none; }
.card.stage .bar button.fx, .card.stage .bar button.newsbtn {
  position: absolute;
  bottom: calc(100% + 0.7rem);
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 1px solid rgba(255, 210, 140, 0.6);
  border-radius: 50%;
  background: rgba(24, 19, 56, 0.78); /* as the play buttons: the same ground, ring and size */
  box-shadow: 0 0 18px rgba(255, 200, 130, 0.25);
  color: var(--gold);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  animation: popin 0.6s cubic-bezier(.3, 1.6, .5, 1) both;
}
.card.stage .bar button.fx { left: 0; }
.card.stage .bar button.newsbtn { right: 0; }
.bar button.fx:active, .bar button.newsbtn:active { transform: scale(0.86); transition-duration: 0.12s; }
.bar button.fx svg, .bar button.newsbtn svg { width: 1.2rem; height: 1.2rem; overflow: visible; filter: drop-shadow(0 0 4px rgba(233, 198, 139, 0.45)); }
.fx .light { fill: currentColor; opacity: 0.22; transform-box: fill-box; transform-origin: center; transition: opacity 0.4s ease; }
.fx .light.lit { opacity: 1; }
.fx .star { fill: currentColor; transform-box: fill-box; transform-origin: center; }
.fx .moon { fill: currentColor; opacity: 0; transform-box: fill-box; transform-origin: center; }
/* a tap: the star turns once and the lights pop one after another */
.fx.pop .star { animation: fx-turn 0.7s cubic-bezier(.3, 1.5, .5, 1); }
.fx.pop .light.lit { animation: fx-light 0.45s cubic-bezier(.3, 1.6, .5, 1) both; }
.fx.pop .light:nth-child(2) { animation-delay: 0.06s; }
.fx.pop .light:nth-child(3) { animation-delay: 0.12s; }
.fx.pop .light:nth-child(4) { animation-delay: 0.18s; }
.fx.pop .light:nth-child(5) { animation-delay: 0.24s; }
@keyframes fx-turn { 0% { transform: none; } 45% { transform: rotate(150deg) scale(1.35); } 100% { transform: rotate(180deg); } }
@keyframes fx-light { 0% { transform: scale(0.4); opacity: 0.2; } 60% { transform: scale(1.6); opacity: 1; } 100% { transform: none; opacity: 1; } }
/* slumbering: the star gives way to a small moon, breathing slowly */
.fx.sleeping .star, .fx.sleeping .light { opacity: 0; }
.fx.sleeping .moon { opacity: 1; animation: fx-moon 4.8s ease-in-out infinite; }
@keyframes fx-moon { 0%, 100% { transform: rotate(-8deg); } 50% { transform: rotate(8deg) scale(1.08); } }
.newsbtn .scroll { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.newsbtn .spark { fill: currentColor; transform-box: fill-box; transform-origin: center; }
.newsbtn .new { fill: #ffd79a; opacity: 0; transform-box: fill-box; transform-origin: center; }
/* something new: the dot glows, and now and then the scroll hops, a few times, then rests */
.newsbtn.fresh .new { opacity: 1; animation: news-dot 2.4s ease-in-out infinite; }
.newsbtn.fresh svg { animation: news-hop 1.2s cubic-bezier(.3, 1.5, .5, 1) 2s 3 both; }
.newsbtn.fresh .spark { animation: twinkle 2.4s ease-in-out infinite; }
@keyframes news-dot { 0%, 100% { transform: scale(0.85); } 50% { transform: scale(1.15); } }
@keyframes news-hop { 0%, 100% { transform: none; } 30% { transform: translateY(-4px) rotate(-8deg); } 55% { transform: translateY(0) rotate(4deg); } 75% { transform: translateY(-1.5px); } }
/* a tap: it unrolls with a little turn */
.newsbtn.open svg { animation: news-open 0.6s cubic-bezier(.3, 1.5, .5, 1); }
@keyframes news-open { 0% { transform: none; } 50% { transform: rotate(-16deg) scale(1.3); } 100% { transform: none; } }
@media (prefers-reduced-motion: reduce) { .fx *, .newsbtn, .newsbtn * { animation: none !important; } }

/* share */
.share {
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* no blur of the moving scene behind it: it would be blurred anew on every
     frame, the phone's dearest work; a little more glass reads the same */
  background: rgba(24, 19, 56, 0.78);
  color: var(--gold);
  cursor: pointer;
  opacity: 0.85;
  transition: opacity 0.3s, transform 0.3s;
  z-index: 5;
}

.share:hover { opacity: 1; transform: scale(1.05); }
.share svg { width: 1.25rem; height: 1.25rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; margin-top: -0.1rem; }

/* the buttons of the bar are one family: each a circle of the same size, with
   the same fine gold rim and dark glass; their icons breathe on the heartbeat,
   four beats a breath, one beat after another like a soft wave across the bar;
   a tap makes each give a springy little dip */
.here, .share, .like, .exit, .dial, .solve {
  position: relative;
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  gap: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* no blur of the moving scene behind it: it would be blurred anew on every
     frame, the phone's dearest work; a little more glass reads the same */
  background: rgba(24, 19, 56, 0.78);
  color: var(--gold);
  opacity: 1;
  font: inherit;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.5s cubic-bezier(.3, 1.6, .5, 1), border-color 0.6s ease, box-shadow 0.6s ease;
}
.here:active, .share:active, .like:active, .exit:active, .solve:active { transform: scale(0.86); transition-duration: 0.12s; }
/* play mode's own buttons, in the middle of the bar, bright in front while
   everything else has stepped back: the level and the dial, the pin, the wand */
.playset { display: none; }
.card.stage .playset { display: flex; align-items: center; gap: 0.45rem; }
.card.stage .dial, .card.stage .solve { border-color: rgba(255, 210, 140, 0.6); animation: popin 0.6s cubic-bezier(.3, 1.6, .5, 1) both; }
.card.stage .level { animation: popin 0.6s cubic-bezier(.3, 1.6, .5, 1) both; }
/* the level and the dial side by side: the level a small tab tucked under the
   dial's rim, five bars rising like steps, as many lit as the level; a tap,
   the next (js/scene.js). The dial's glass is whole here, so nothing of the
   tab shows through it */
.mixer { position: relative; display: flex; align-items: center; --lvl: 2; }
.level {
  position: relative;
  flex: none;
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 2.15rem;
  margin-right: -0.9rem;
  padding: 0 calc(0.9rem + 5px) 0.5rem 0.62rem;
  border: 1px solid rgba(255, 210, 140, 0.45);
  border-radius: 1.1rem 0 0 1.1rem;
  background: rgba(24, 19, 56, 0.78);
  color: var(--gold);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: scale 0.45s cubic-bezier(.3, 1.6, .5, 1);
}
.level:hover { border-color: rgba(255, 210, 140, 0.75); }
/* a thumb's reach, the bar's full height */
.level::before { content: ""; position: absolute; inset: -0.35rem 0 -0.35rem -0.4rem; }
.level i {
  display: block;
  width: 3px;
  height: calc(3px + var(--i) * 3px);
  border-radius: 1.5px;
  background: rgba(233, 198, 139, 0.24);
  transform-origin: 50% 100%;
  transition: background-color 0.3s ease var(--d, 0ms), box-shadow 0.3s ease var(--d, 0ms);
}
.level i:nth-child(1) { --i: 1; }
.level i:nth-child(2) { --i: 2; }
.level i:nth-child(3) { --i: 3; }
.level i:nth-child(4) { --i: 4; }
.level i:nth-child(5) { --i: 5; }
.level i.lit { background: var(--gold); box-shadow: 0 0 4px rgba(255, 210, 140, 0.75); }
/* set: the new top bar springs up, bars given back dip as they fade */
.level i.pop { animation: bar-pop 0.55s cubic-bezier(.3, 1.6, .5, 1) var(--d, 0ms); }
.level i.fall { animation: bar-fall 0.45s ease-out var(--d, 0ms); }
@keyframes bar-pop {
  0% { transform: scaleY(0.5); filter: brightness(1.6); }
  55% { transform: scaleY(1.3); filter: brightness(1.5); }
  100% { transform: none; filter: none; }
}
@keyframes bar-fall { 0%, 100% { transform: none; } 35% { transform: translateY(2px) scaleY(0.6); } }
/* "how hard may it be?": a light runs up the five bars and back */
.level.wackel i { animation: bar-wave 1.5s ease-in-out calc(var(--i) * 110ms); }
@keyframes bar-wave {
  0%, 44%, 100% { transform: none; }
  22% { transform: scaleY(1.3); background-color: #ffe2a8; box-shadow: 0 0 6px rgba(255, 215, 150, 0.95); }
}
/* the dial: its key lays out a chiffre at the level and sends it to all who
   play just now. The key glows the brighter the higher the level; a level
   set, the dial swells a moment and the key twists, a ring of light going
   out from it */
.mixer .dial {
  z-index: 1;
  background: rgb(22, 18, 51);
  transition: transform 0.5s cubic-bezier(.3, 1.6, .5, 1), scale 0.45s cubic-bezier(.3, 1.6, .5, 1), border-color 0.6s ease;
}
.dial .sign, .solve svg { width: 1.15rem; height: 1.15rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.dial .sign { width: 1.25rem; height: 1.25rem; filter: drop-shadow(0 0 calc(var(--lvl) * 1.1px) rgba(255, 205, 130, 0.8)); transition: filter 0.4s ease; }
.dial:active, .level:active, .level.pressed { scale: 0.88; transition-duration: 0.12s; }
.dial.swell { scale: calc(1.04 + var(--lvl) * 0.016); }
/* the light round the dial, the wand and the pin when one of them invites */
.dial::after, .solve::after, .exit::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1px solid rgba(255, 215, 150, 0.85);
  box-shadow: 0 0 16px 2px rgba(255, 205, 122, 0.4);
  opacity: 0;
  pointer-events: none;
}
/* and the dial's gleam, a spark running once round its rim */
.dial::before {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  background: conic-gradient(from 0deg, transparent 0 62%, rgba(255, 228, 175, 0.95) 96%, transparent 100%);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
  opacity: 0;
  pointer-events: none;
}
/* no puzzle laid out: the chiffre's ring breathes on the card's breath,
   swelling softly outwards, the gleam running once round it; clearer until
   one has sent a chiffre (.first); it rests while another button invites */
.playset { --breath: 0.6; --swell: 1.06; }
.playset.first { --breath: 1; --swell: 1.16; }
.playset.open .dial::after { animation: ring-breath calc(var(--beat) * 4) ease-in-out var(--beat-delay, 0s) infinite; }
.playset.open .dial::before { animation: gleam calc(var(--beat) * 4) cubic-bezier(.45, 0, .35, 1) var(--beat-delay, 0s) infinite; }
.playset.asking .dial::after, .playset.asking .dial::before, .playset .dial.busy::after, .playset .dial.busy::before { animation: none; }
@keyframes ring-breath {
  0%, 100% { opacity: 0; transform: none; }
  30% { opacity: var(--breath); transform: scale(var(--swell)); }
  62% { opacity: 0.12; transform: scale(1.02); }
}
@keyframes gleam {
  0% { transform: rotate(-90deg); opacity: 0; }
  8%, 40% { opacity: 1; }
  50%, 100% { transform: rotate(270deg); opacity: 0; }
}
/* "send a chiffre": the key lifts, slides into its lock and turns, twice,
   the ring glowing up and the gleam running round */
.dial .sign { transform-origin: 50% 50%; }
.playset .dial.wackel .sign { animation: key-turn 1.8s ease-in-out; }
.playset .dial.wackel::after { animation: ask-glow 1.8s ease-in-out; }
.playset .dial.wackel::before { animation: gleam 1.8s cubic-bezier(.45, 0, .35, 1); }
@keyframes key-turn {
  0%, 44%, 100% { transform: none; }
  10% { transform: translate(-1.5px, -1.5px) scale(1.16); }
  20% { transform: translate(1px, 1px) scale(1.1); }
  30% { transform: translate(1px, 1px) rotate(28deg) scale(1.1); }
  38% { transform: rotate(-4deg); }
  56% { transform: translate(-1.2px, -1.2px) scale(1.12); }
  64% { transform: translate(1px, 1px) scale(1.08); }
  74% { transform: translate(1px, 1px) rotate(24deg) scale(1.08); }
  84% { transform: rotate(-3deg); }
}
@keyframes ask-glow { 0%, 100% { opacity: 0; transform: none; } 25% { opacity: 1; transform: scale(1.1); } 60% { opacity: 0.55; transform: none; } }
/* the level set: the ring goes out from the dial, the key twists */
.playset .dial.charged .sign { animation: key-charge 0.7s cubic-bezier(.3, 1.4, .5, 1); }
.playset .dial.charged::after { animation: ring-out 0.8s ease-out; }
@keyframes key-charge { 0% { transform: rotate(-24deg) scale(0.82); } 60% { transform: rotate(6deg) scale(1.14); } 100% { transform: none; } }
@keyframes ring-out { 0% { opacity: 0.9; transform: none; } 100% { opacity: 0; transform: scale(1.45); } }
/* while the hands lay the chiffre out, the key turns to and fro with them */
.playset .dial.busy .sign { animation: key-jiggle 0.3s ease-in-out infinite alternate; filter: drop-shadow(0 0 6px rgba(255, 215, 150, 0.95)); }
@keyframes key-jiggle { from { transform: rotate(-12deg); } to { transform: rotate(12deg); } }
/* the level shown the first times: a fingertip of light on its bars, tapping
   them; a ring spreads with each tap (js/scene.js showLevelWay) */
.tip {
  position: absolute;
  left: calc(0.62rem + 13px);
  top: 70%;
  z-index: 2;
  width: 1rem;
  height: 1rem;
  margin: -0.5rem 0 0 -0.5rem;
  border-radius: 50%;
  background: radial-gradient(circle, #fff6e4 0%, var(--gold) 45%, rgba(233, 198, 139, 0) 72%);
  box-shadow: 0 0 14px rgba(255, 210, 150, 0.75), 0 0 2px rgba(40, 24, 0, 0.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease, transform 0.4s ease;
  transform: translate(6px, 8px) scale(0.6);
}
.tip.on { opacity: 0.95; transform: none; }
.tip i { position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--gold); box-shadow: 0 0 6px rgba(255, 210, 150, 0.45); opacity: 0; }
.tip.press { animation: tip-press 0.5s ease-out; }
.tip.press i { animation: touch-ring 1.1s cubic-bezier(.2, .6, .35, 1); }
@keyframes tip-press { 0%, 100% { scale: 1; } 30% { scale: 0.7; } }
/* the wand: "I can solve it", it swishes once and its three stars sparkle one
   after another; while the invisible hand solves, they twinkle on */
.solve svg { transform-origin: 19% 81%; }
.solve .twinkle { fill: currentColor; stroke: none; transform-box: fill-box; transform-origin: center; }
.solve .twinkle:nth-child(3) { --k: 2; }
.solve .twinkle:nth-child(4) { --k: 1; }
.solve.wackel svg { animation: wand-swish 1.4s cubic-bezier(.3, .7, .4, 1); }
.solve.wackel .twinkle { animation: twinkle 0.9s ease-out calc(0.3s + var(--k, 0) * 160ms); }
.solve.wackel::after { animation: ask-glow 1.4s ease-in-out; }
.solve.busy .twinkle { filter: drop-shadow(0 0 4px rgba(255, 215, 150, 0.95)); animation: twinkle 1.2s ease-in-out calc(var(--k, 0) * 240ms) infinite; }
@keyframes wand-swish { 0%, 100% { transform: none; } 22% { transform: rotate(-20deg); } 48% { transform: rotate(9deg) scale(1.08); } 70% { transform: rotate(-3deg); } }
@keyframes twinkle { 0%, 100% { transform: none; opacity: 1; } 35% { transform: scale(1.8) rotate(45deg); filter: drop-shadow(0 0 3px #fff3d6); } 65% { transform: scale(0.7); opacity: 0.7; } }
/* the exit: a circle like the dial and the wand, four arrows pointing
   inward, as one leaves a full screen; the only way out of play mode */
.exit { display: none; }
.card.stage .exit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  padding: 0;
  border-radius: 50%;
  border-color: rgba(255, 210, 140, 0.75);
  box-shadow: 0 0 18px rgba(255, 200, 130, 0.25);
  animation: popin 0.6s cubic-bezier(.3, 1.6, .5, 1) both;
}
.exit svg { width: 1.2rem; height: 1.2rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* once, after a while in play mode: the arrows draw inward twice, softly,
   its ring glowing up: this is the way back */
.exit.wackel svg { animation: exit-ask 1.4s cubic-bezier(.3, .7, .4, 1); }
.exit.wackel::after { animation: ask-glow 1.4s ease-in-out; }
@keyframes exit-ask { 0%, 100% { transform: none; } 20% { transform: scale(0.78); } 40% { transform: scale(1.04); } 62% { transform: scale(0.84); } 82% { transform: scale(1.02); } }
/* pressed: the arrows draw in as the cube glides back into its sky */
.exit.leaving svg { animation: exit-go 0.7s cubic-bezier(.3, .7, .4, 1); }
@keyframes exit-go { 0% { transform: none; } 45% { transform: scale(0.7); } 100% { transform: none; } }
/* a narrow phone: the play buttons a little smaller and closer together; on
   the narrowest, what has stepped back also grows smaller, to make room */
@media (max-width: 370px) { .card.stage .playset { gap: 0.3rem; } }
@media (max-width: 359px) {
  .card.stage .playset { gap: 0.25rem; }
  .card.stage .playset .dial, .card.stage .exit, .card.stage .solve, .card.stage .bar button.fx, .card.stage .bar button.newsbtn { width: 2.45rem; height: 2.45rem; }
  .level { gap: 1.5px; padding-left: 0.42rem; }
}
@media (max-width: 340px) {
  .bar .here, .bar .actions { transition: filter var(--glide) var(--soft), scale var(--glide) var(--soft); }
  .card.stage .bar .here { scale: 0.84; transform-origin: 0 50%; }
  .card.stage .bar .actions { scale: 0.84; transform-origin: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .dial::before, .dial::after, .solve::after, .exit::after,
  .dial .sign, .solve svg, .solve .twinkle, .exit svg, .level i, .tip, .tip i { animation: none !important; }
}
/* play mode: whatever is not the cube falls back behind a muted dark glass,
   the photo and the sky in the scene (js/backdrop.js), the words and the
   buttons here, in the same measure */
.message, .bar .here, .bar .actions { transition: filter var(--glide) var(--soft); }
.card.stage .message { filter: brightness(0.32) saturate(0.45); }
/* the active users, share and the heart stay usable in play mode, a step
   quieter than the play buttons, which keep their gold rings (e12a, 2026-10-19) */
.card.stage .bar .here, .card.stage .bar .actions { filter: brightness(0.75); }
/* the cube large: the dedication goes altogether, the picture calmer */
.message { transition: filter var(--glide) var(--soft), opacity var(--glide) var(--soft); }
.card.stage .message { opacity: 0; }
.card.staging .message { opacity: calc(1 - var(--stage, 0)); }
/* drawn by a finger, the card behind darkens exactly as far as the cube comes
   forward (js/scene.js sets --stage from 0 to 1) */
.card.staging .message {
  filter: brightness(calc(1 - 0.68 * var(--stage, 0))) saturate(calc(1 - 0.55 * var(--stage, 0)));
  transition: none;
}
.card.staging .bar .here, .card.staging .bar .actions { filter: brightness(calc(1 - 0.25 * var(--stage, 0))); transition: none; }
/* behind the glass the words rest: a tap there is a tap beside the cube */
.card.stage .message, .card.stage .greeting, .card.stage .core { pointer-events: none; }
/* the cube large, nothing works unseen (fluency first, 2026-10-19): the
   words' light rests where it is, and the photo, wholly under play mode's
   sky once the cube has come, is not composited at all; back at once when
   the cube goes. The bar's buttons breathe on as on the card (the human) */
.card.stage .message *, .card.stage .message *::after { animation-play-state: paused; }

.card.stage .photo { visibility: hidden; transition: opacity 0.5s ease, visibility 0s linear calc(var(--glide) + 0.2s); }
.here .signal, .share svg, .like svg { animation: breathe calc(var(--beat) * 4) ease-in-out var(--beat-delay, 0s) infinite; }
.share svg { animation-delay: calc(var(--beat-delay, 0s) + var(--beat)); }
/* the heart does not breathe, it beats: a strong double beat, lub-dub, every
   second heartbeat of the card, with a glow on each */
.like svg { animation: heartbeat calc(var(--beat) * 2) ease-in-out var(--beat-delay, 0s) infinite; filter: drop-shadow(0 0 3px rgba(255, 205, 122, 0.45)); }
@keyframes heartbeat {
  0%, 34%, 100% { transform: scale(1); }
  8% { transform: scale(1.26); }
  15% { transform: scale(1.04); }
  22% { transform: scale(1.18); }
}
.like.liked svg { filter: drop-shadow(0 0 8px rgba(255, 205, 122, 0.85)); }
/* given, it pops once and its ring spreads (js/bar.js), then beats on */
.like.burst svg, .like.liked.burst svg { animation: heart-pop 0.7s cubic-bezier(.3, 1.6, .5, 1); }
@keyframes heart-pop { 0% { transform: scale(1); } 35% { transform: scale(1.45); } 100% { transform: scale(1); } }
.share svg, .like svg { width: 1.25rem; height: 1.25rem; margin: 0; }
/* the share button longs to share: on its breath, every four heartbeats, the
   arrow rises out of its box a little further than it should, as if it would
   be off, and settles back, while the button's rim glows warm; copied, the
   arrow flies away upwards and comes back from below (js/bar.js .sent) */
.share svg { stroke-width: 1.85; overflow: visible; animation: share-breath calc(var(--beat) * 4) ease-in-out calc(var(--beat-delay, 0s) + var(--beat)) infinite; }
.share .arrow { animation: share-lift calc(var(--beat) * 4) cubic-bezier(.3, .7, .4, 1) calc(var(--beat-delay, 0s) + var(--beat)) infinite; }
.share::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1px solid rgba(255, 210, 140, 0.7);
  box-shadow: 0 0 18px 1px rgba(255, 205, 122, 0.38);
  opacity: 0;
  pointer-events: none;
  animation: share-glow calc(var(--beat) * 4) ease-in-out calc(var(--beat-delay, 0s) + var(--beat)) infinite;
}
@keyframes share-breath { 0%, 100% { transform: scale(0.94); } 22% { transform: scale(1.1); } 55% { transform: scale(1); } }
@keyframes share-lift {
  0%, 8%, 60%, 100% { transform: none; }
  24% { transform: translateY(-3.6px); }
  36% { transform: translateY(-2.4px); }
  46% { transform: translateY(-2.9px); }
}
@keyframes share-glow {
  0%, 62%, 100% { opacity: 0; }
  24% { opacity: 1; }
}
.share.sent .arrow { animation: share-fly 0.9s cubic-bezier(.5, 0, .3, 1); }
@keyframes share-fly {
  0% { transform: none; opacity: 1; }
  40% { transform: translateY(-14px); opacity: 0; }
  41% { transform: translateY(8px); opacity: 0; }
  100% { transform: none; opacity: 1; }
}
/* the like's count sits on its rim like the active users' number */
.like b {
  position: absolute;
  top: -0.3rem;
  right: -0.35rem;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.28rem;
  box-sizing: border-box;
  border-radius: 0.6rem;
  background: var(--gold);
  color: var(--night);
  font-family: var(--sans);
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1.15rem;
  text-align: center;
  font-variant-numeric: lining-nums tabular-nums;
  box-shadow: 0 0 0 2px rgba(24, 19, 56, 0.85);
}
@media (prefers-reduced-motion: reduce) {
  .veil i { display: none; }
  .loading i { animation-duration: 0.3s; }
  .here.arrive::after, .here.arrive b, .like b.up,
  .tap-me::before, .tap-me::after, .tap-me i,
  .share, .share::after, .share .arrow, .share.sent .arrow,
  .like svg, .like.liked svg, .like.burst svg, .waking, .signal::before, .signal::after, .here .signal, .share svg { animation: none; }
  .message .w, .card.folded .key .w, .card.unwritten .message .w { transition: opacity 0.4s ease; transition-delay: 0s; transform: none; filter: none; }
  .message .w, .message .w::before, .message .w::after { animation: none !important; }
  .card.folded.peek .key .w.first { opacity: 0; transform: none; }
  .here.live { animation: none; }
}
