/* The cover: a sealed envelope that opens onto the card.

   Sizes come from geometry.css (generated): the envelope's aspect ratio, the
   seal's size and place, and one box + clip per flap. Everything on the cover
   is sized in fractions of the envelope, so it is the same composition on a
   phone as on a laptop.
*/

:root {
  --wine: #3a0a12;
  --wine-deep: #1c0409;
  --ivory: #f4ead9;
}

html.is-sealed,
html.is-sealed body {
  overflow: hidden;
  overscroll-behavior: none;
}

/* Belt and braces: `overflow: hidden` on the root does not stop every mobile
   browser from scrolling what is under a fixed layer, so while the envelope
   is shut the card is only ever one screen tall. */
html.is-sealed .card {
  height: 100vh;
  height: 100svh;
  overflow: hidden;
}

/* ------------------------------------------------------------ the gate */

.gate {
  /* Phones and anything portrait: the envelope covers the screen. */
  --env-h: max(100vh, calc(100vw / var(--env-r)));
  --env-w: max(100vw, calc(100vh * var(--env-r)));
  /* How far the camera draws back as the envelope opens, so the flaps are
     seen folding out rather than leaving the screen at once. */
  --pull: .94;
  position: fixed;
  inset: 0;
  z-index: 50;
  overflow: hidden;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  cursor: pointer;
}

@supports (height: 100svh) {
  .gate {
    --env-h: max(100svh, calc(100vw / var(--env-r)));
    --env-w: max(100vw, calc(100svh * var(--env-r)));
  }
}

/* The first frame, before the real envelope has decoded: a 24px copy of it,
   inlined, blown up and soft. */
html.is-loading .gate,
html.is-arriving .gate { background: var(--wine-deep) var(--env-tiny) center / cover no-repeat; }

/* Laptops and desktops wider than 3:4, where the card is a column with the
   table showing either side: the envelope is an object lying on that table
   rather than the screen. Everywhere else (phones, tablets either way up, a
   narrow window) it covers the screen, drawn for its shape: geometry.css,
   geometry-tab.css, geometry-land.css. */
@media (min-aspect-ratio: 751/1000) {
  html:not(.is-tablet) .gate {
    --env-h: min(92vh, 1100px);
    --env-w: calc(var(--env-h) * var(--env-r));
    --pull: .9;
  }
  @supports (height: 100svh) {
    html:not(.is-tablet) .gate { --env-h: min(92svh, 1100px); }
  }
}

.gate[hidden] { display: none; }

.gate-backdrop {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 60% at 50% 45%, rgba(120, 24, 40, .55), transparent 70%),
    radial-gradient(ellipse at 50% 50%, #4a0f19 0%, #26060c 62%, #120206 100%);
}

.gate-backdrop::after {
  /* The envelope itself, enormous and out of focus: the room it is lying in. */
  content: "";
  position: absolute;
  inset: -10%;
  background: url(../assets/cover/envelope.webp) center / cover no-repeat;
  filter: blur(40px) brightness(.55) saturate(1.1);
  opacity: .5;
}

.gate-inner {
  position: absolute;
  inset: 0;
}

.stage {
  position: absolute;
  inset: 0;
  perspective: calc(var(--env-h) * 2);
  perspective-origin: 50% 50%;
}

.env-box {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--env-w);
  height: var(--env-h);
  transform: translate(-50%, -50%);
}

.env { transform-style: preserve-3d; }

/* ------------------------------------------------------------ the paper */

/* The inside of the envelope: the panel the flaps fold away from, lined in
   the same ivory lace as their backs. */
.env-inside {
  position: absolute;
  inset: 0;
  background: #efe5d3 url(../assets/cover/liner.webp) center / cover no-repeat;
  box-shadow: 0 calc(var(--env-h) * .03) calc(var(--env-h) * .09) rgba(8, 0, 3, .5);
}

.inside-shade,
.inside-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Shadow from the flaps still folded over it. */
.inside-shade {
  background:
    radial-gradient(ellipse 60% 45% at 50% 48%, rgba(60, 12, 16, .35), rgba(40, 6, 10, .85) 100%);
}

.inside-glow {
  background: radial-gradient(ellipse 55% 40% at 50% 50%, rgba(255, 246, 228, .95), rgba(255, 236, 205, .45) 45%, rgba(255, 236, 205, 0) 75%);
  opacity: 0;
}

.env-base {
  position: absolute;
  inset: 0;
  background: url(../assets/cover/envelope.webp) 0 0 / 100% 100% no-repeat;
}

.flap {
  position: absolute;
  transform-style: preserve-3d;
}

/* At rest the envelope is the single unbroken base image: pieces cut from it
   would show hairlines where they meet. The top flap sits on top of the base
   (same pixels, so no seam); the other three wait underneath until the
   opening needs them. */
.gate:not(.is-lower-free) .flap-left,
.gate:not(.is-lower-free) .flap-right,
.gate:not(.is-lower-free) .flap-bottom { visibility: hidden; }
.gate.is-lower-free .env-base { visibility: hidden; }

.face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.face-front .art {
  position: absolute;
  background: url(../assets/cover/envelope.webp) 0 0 / 100% 100% no-repeat;
}

.face-back {
  transform: rotateX(180deg);
  background: #efe5d3 url(../assets/cover/liner.webp) center / cover no-repeat;
}

.shade {
  position: absolute;
  inset: 0;
  background: #1a0307;
  opacity: 0;
  pointer-events: none;
}

.face-back .shade { background: #3b1a14; opacity: .55; }

/* ------------------------------------------------------------ light and air */

.env-overlay { pointer-events: none; }
.env-overlay .seal { pointer-events: auto; }

.env-light {
  position: absolute;
  inset: 0;
  overflow: hidden;
  mix-blend-mode: soft-light;
}

.env-light i {
  /* A soft pool of window light drifting slowly across the paper. */
  position: absolute;
  left: -30%;
  top: -10%;
  width: 90%;
  height: 60%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 232, 214, .4), rgba(255, 232, 214, 0));
  animation: env-drift 16s ease-in-out infinite alternate;
}

@keyframes env-drift {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  50%  { transform: translate3d(55%, 35%, 0) scale(1.15); }
  100% { transform: translate3d(95%, 70%, 0) scale(.95); }
}

.env-vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 75% 60% at 50% 48%, transparent 55%, rgba(12, 0, 4, .38) 100%);
}

/* ------------------------------------------------------------ the seal */

.seal {
  position: absolute;
  left: var(--seal-x);
  top: var(--seal-y);
  width: var(--seal-size);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  outline: none;
}

.seal:focus-visible .seal-ring { opacity: 1; animation: none; transform: scale(1.12); }

.seal-body {
  position: absolute;
  inset: 0;
  transition: transform 160ms cubic-bezier(.3, .7, .4, 1);
  animation: seal-breathe 3.6s ease-in-out infinite;
}

.gate.is-pressed .seal-body { transform: scale(.955); }

@keyframes seal-breathe {
  0%, 100% { scale: 1; }
  50%      { scale: 1.018; }
}

.seal-whole,
.seal-half {
  position: absolute;
  inset: 0;
}

.seal img {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  filter: drop-shadow(0 calc(var(--env-w) * .006) calc(var(--env-w) * .009) rgba(30, 0, 6, .55));
}

/* The two halves are the same seal cut along the crack. Hidden until it breaks. */
.seal-half { opacity: 0; }
.seal-half-l { clip-path: polygon(0 0, 47% 0, 47% 3%, 50% 14%, 45% 24%, 51% 33%, 48% 44%, 55% 52%, 49% 61%, 53% 70%, 47% 80%, 52% 89%, 54% 97%, 54% 100%, 0 100%); }
.seal-half-r { clip-path: polygon(47% 0, 100% 0, 100% 100%, 54% 100%, 54% 97%, 52% 89%, 47% 80%, 53% 70%, 49% 61%, 55% 52%, 48% 44%, 51% 33%, 45% 24%, 50% 14%, 47% 3%); }

.seal-crack {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.seal-crack path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 110;
  stroke-dashoffset: 110;
}

.crack-dark  { stroke: rgba(88, 58, 40, .8); stroke-width: 1.5; }
.crack-light { stroke: rgba(255, 250, 240, .75); stroke-width: .6; transform: translate(.7px, .5px); }

/* Light catching the wax now and then, and only the wax. */
.seal-sheen {
  position: absolute;
  inset: 0;
  -webkit-mask: url(../assets/cover/seal.webp) center / 100% 100% no-repeat;
          mask: url(../assets/cover/seal.webp) center / 100% 100% no-repeat;
  background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, .55) 50%, transparent 62%) -150% 0 / 250% 100% no-repeat;
  mix-blend-mode: soft-light;
  animation: seal-sheen 5.2s ease-in-out 1.6s infinite;
}

@keyframes seal-sheen {
  0%, 62% { background-position: -150% 0; }
  100%    { background-position: 250% 0; }
}

/* An invitation to press: a thin ring leaving the seal, like a ripple. */
.seal-ring {
  position: absolute;
  inset: 4%;
  border-radius: 50%;
  border: 1px solid rgba(244, 234, 217, .6);
  opacity: 0;
  animation: seal-ring 2.8s cubic-bezier(.2, .6, .3, 1) 1.2s infinite;
}

@keyframes seal-ring {
  0%   { transform: scale(1);    opacity: 0; }
  12%  { opacity: .75; }
  100% { transform: scale(1.55); opacity: 0; }
}

.seal-glow {
  position: absolute;
  inset: -60%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 236, 205, .9), rgba(255, 214, 170, .35) 45%, rgba(255, 214, 170, 0));
  opacity: 0;
  transform: scale(.4);
  pointer-events: none;
}

/* Chips of wax thrown off the crack: small, angular, lit from the top left. */
.wax-crumb {
  position: absolute;
  left: var(--x0);
  top: var(--y0);
  width: var(--s);
  height: var(--s);
  margin: calc(var(--s) / -2);
  background: linear-gradient(135deg, #fbf5e9 0%, #e6d7bd 55%, #b9a282 100%);
  pointer-events: none;
}

.wax-crumb:nth-child(3n)   { clip-path: polygon(10% 20%, 90% 0, 70% 100%); }
.wax-crumb:nth-child(3n+1) { clip-path: polygon(0 40%, 60% 0, 100% 70%, 30% 100%); }
.wax-crumb:nth-child(3n+2) { clip-path: polygon(20% 0, 100% 30%, 80% 90%, 0 100%); }

/* ------------------------------------------------------------ words */

/* Just under the seal. Each envelope says where that is (--hint-y) and how
   large to write (--hint-k, a share of its width); these are the phone's. */
.gate-hint {
  position: absolute;
  left: 0;
  right: 0;
  top: var(--hint-y, 59%);
  margin: 0;
  text-align: center;
  font-family: "Pinyon Script", "Great Vibes", cursive;
  font-size: calc(var(--env-w) * var(--hint-k, .085));
  line-height: 1;
  color: var(--ivory);
  text-shadow: 0 1px calc(var(--env-w) * .02) rgba(20, 0, 5, .55);
  animation: hint-pulse 3.2s ease-in-out 1.4s infinite;
}

/* When the envelope covers the screen its bottom can fall off a short one;
   keep the hint where it can be read (no higher than 86% of the screen). */
.gate-hint { top: min(var(--hint-y, 59%), calc(50% + 36vh)); }
@supports (height: 100svh) {
  .gate-hint { top: min(var(--hint-y, 59%), calc(50% + 36svh)); }
}

@keyframes hint-pulse {
  0%, 100% { opacity: .62; }
  50%      { opacity: 1; }
}

.sound-toggle {
  position: absolute;
  top: max(14px, env(safe-area-inset-top));
  right: max(14px, env(safe-area-inset-right));
  z-index: 3;
  width: 40px;
  height: 40px;
  padding: 9px;
  border: 1px solid rgba(244, 234, 217, .35);
  border-radius: 50%;
  background: rgba(28, 4, 9, .35);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
  color: var(--ivory);
  cursor: pointer;
}

.sound-toggle svg { width: 100%; height: 100%; display: block; }
.sound-toggle path { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.sound-toggle .spk { fill: currentColor; }
.sound-toggle[aria-pressed="true"] .mute,
.sound-toggle[aria-pressed="false"] .wave { display: none; }

/* ------------------------------------------------------------ arriving */

/* The first frame is the tiny blurred envelope inlined in geometry.css; the
   real one fades in over it once decoded, the seal settles, then the words. */
html.is-loading .stage,
html.is-loading .env-overlay,
html.is-loading .sound-toggle { opacity: 0; }

html.is-arriving .stage       { animation: arrive-env 1100ms cubic-bezier(.2, .7, .2, 1) both; }
html.is-arriving .env-light,
html.is-arriving .env-vignette { animation: arrive-fade 1200ms ease both; }
html.is-arriving .seal         { animation: arrive-seal 900ms cubic-bezier(.2, 1.35, .35, 1) 350ms both; }
html.is-arriving .gate-hint    { animation: arrive-fade 900ms ease 900ms both, hint-pulse 3.2s ease-in-out 1.8s infinite; }
html.is-arriving .sound-toggle { animation: arrive-fade 700ms ease 1100ms both; }

@keyframes arrive-env {
  from { opacity: 0; transform: scale(1.04); filter: blur(10px); }
  to   { opacity: 1; transform: none; filter: none; }
}

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

@keyframes arrive-seal {
  from { opacity: 0; transform: translate(-50%, -50%) scale(1.25); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* ------------------------------------------------------------ opening */

.gate.is-opening { cursor: default; }
/* Every idle loop stops outright. A looping opacity animation left running on
   the compositor can draw over the script's fade even when it has lost. */
.gate.is-opening .seal-body,
.gate.is-opening .seal-ring,
.gate.is-opening .seal-sheen,
.gate.is-opening .gate-hint,
.gate.is-opening .env-light i { animation: none; }
.gate.is-opening .seal-ring { opacity: 0; }

/* The page underneath, waiting in the dark until the paper is out of the way. */
.card { transform-origin: 50% 45%; }

@media (prefers-reduced-motion: reduce) {
  .env-light i,
  .seal-body,
  .seal-ring,
  .seal-sheen,
  .gate-hint { animation: none !important; }
}

