/* Halloween 2026 — "It's Halloweeeeen!"
   Figma: iPhone 13 & 14 - 13 (node 64:946), 390 x 5468

   A fixed-coordinate collage. Every element is pinned at the coordinate it was
   drawn at on the 390-wide artboard, and the whole artboard is scaled to fit
   the phone (factor computed in script.js). Nothing reflows, so the
   composition on a Pro Max is identical to the one on an SE — only bigger.
   That is deliberate: the layout is a poster, not a document. */

:root {
  /* palette, straight off the artboard */
  --cream:     #fff8b7;
  --orange:    #ff6600;
  --dark:      #23142b;
  --blue:      #3467ff;
  --pink:      #ff89d6;
  --green:     #30ad75;
  --yellow:    #fae20e;
  --lavender:  #d8d1ff;
  --olive:     #515e48;

  --gasoek: "Gasoek One", system-ui, sans-serif;
  --staat:  "Staatliches", Impact, system-ui, sans-serif;
  --mono:   "Chivo Mono", ui-monospace, SFMono-Regular, monospace;

  /* The artboard in play. script.js reads these to work out --scale. The
     desktop media query at the bottom of this file swaps them for its own. */
  --design-w: 390;
  --design-h: 5468;

  /* Where the colour bands sit on that artboard (see .bleed). */
  --orange-top: 1146;
  --orange-h:   2605;
  --dark-top:   2717;
  --dark-h:     3546;
  /* Drawn for a phone. On anything wider it stays a phone-width column rather
     than stretching 128px type across a desktop window. */
  --max-scale: 1.12;
  --scale: 1;

  /* Photo viewer container. Both viewer frames on the artboard place the
     photo at y=107, 629 tall (≈1:1.61) — but 4:5 is the standard portrait
     photo ratio, and it is what these were shot and cropped for. Photos fill
     the box. Change this one value to re-proportion both viewers. */
  --viewer-ratio: 4 / 5;

  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

/* Artwork bleeding past the edges must never add a horizontal scrollbar. This
   has to sit on <html>: overflow on <body> gets propagated to the viewport
   instead of clipping anything. */
html {
  overflow-x: hidden;
  overflow-x: clip;
  background: var(--dark);
}

body {
  position: relative;
  background: var(--dark);
  -webkit-text-size-adjust: 100%;
}

/* The colour bands, drawn the full width of the window at the artboard's y
   positions scaled to match. They sit behind .stage and outside it on purpose.

   Inside .stage they were clipped to the column, so on any window wider than
   the column the sides showed bare page. And they have to stay real rectangles
   rather than a gradient's colour stops: hard stops get antialiased, which
   softens the horizontal boundary where cream meets orange.

   Positions come from --orange-top / --dark-top etc. on :root, which each
   layout sets to its own artboard's values, multiplied by the same --scale the
   artboard uses — so the bands stay locked to the artwork at every size. */
.bleed {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: calc(var(--design-h) * var(--scale) * 1px);
  background: var(--cream);
  overflow: hidden;
  z-index: 0;
}
.bleed > div {
  position: absolute;
  left: 0;
  width: 100%;
}
.bleed__orange {
  top: calc(var(--orange-top) * var(--scale) * 1px);
  height: calc(var(--orange-h) * var(--scale) * 1px);
  background: var(--orange);
}
.bleed__dark {
  top: calc(var(--dark-top) * var(--scale) * 1px);
  height: calc(var(--dark-h) * var(--scale) * 1px);
  background: var(--dark);
}

/* Holds the scaled artboard and gives the document its real height — .stage is
   scaled with a transform, which doesn't affect layout. */
.page {
  position: relative;
  z-index: 1;
  width: calc(var(--design-w) * var(--scale) * 1px);
  height: calc(var(--design-h) * var(--scale) * 1px);
  margin: 0 auto;
  overflow: hidden;
}

.stage {
  position: relative;
  width: calc(var(--design-w) * 1px);
  height: calc(var(--design-h) * 1px);
  transform: scale(var(--scale));
  transform-origin: 0 0;
  /* The Figma frame clips its contents; so does this. */
  overflow: hidden;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------- helpers -- */

/* Plain pinned artwork. */
.art {
  position: absolute;
  display: block;
  max-width: none;
}

/* Figma rotates a layer about its own centre inside a box sized to the
   rotated result. .rot is that box; .rot__i is the thing that turns. */
.rot {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rot__i { flex: none; }
.rot img { display: block; max-width: none; }

/* ---------------------------------------------------------------- hero -- */

.title-burst {
  /* Drawn 278.412 x 415.252 and turned -110.47deg. Positioned by the centre of
     its rotated bounding box (-309.8, -143.8, 486.4 x 405.7) so the turn lands
     where the artboard has it: mostly off the top-left corner, with one arm
     reaching in behind the H. */
  left: -205.798px;
  top: -148.575px;
  width: 278.412px;
  height: 415.252px;
  transform: rotate(-110.47deg);
}

.its-ellipse { left: 83.031px; top: 136.901px; width: 224.757px; height: 74.257px; }

.its {
  position: absolute;
  left: 36px; top: 156px; width: 318px;
  margin: 0;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 35px;
  line-height: 34px;
  color: #000;
  text-align: center;
}

.ltr {
  display: block;
  margin: 0;
  font-family: var(--gasoek);
  font-size: 128px;
  line-height: normal;
  text-align: center;
  white-space: nowrap;
}

.ltr--title { color: var(--blue); }
.ltr--boo   { color: var(--olive); }
.rsvp .ltr  { color: var(--pink); }

/* Letters pinned by their centre. */
.ltr--h,    .ltr--e1, .ltr--e2, .ltr--e3, .ltr--e4,
.ltr--bang, .ltr--w,  .ltr--l1, .ltr--l2,
.ltr--r,    .ltr--s,  .ltr--v,
.ltr--b,    .ltr--o1, .ltr--o2 {
  position: absolute;
  transform: translateX(-50%);
}

.ltr--h    { left: 66px;    top: 162px; }
.ltr--e1   { left: 264.5px; top: 283px; }
.ltr--e2   { left: 21.5px;  top: 396px; }
.ltr--e3   { left: 111.5px; top: 439px; }
.ltr--e4   { left: 202.5px; top: 424px; }
.ltr--bang { left: 379.5px; top: 456px; }
.ltr--w    { left: 152px;   top: 310px; }
.ltr--l1   { left: 251.5px; top: 147px; }
.ltr--l2   { left: 335.5px; top: 167px; }

.ltr-box--o   { left: -39.835px; top: 263.34px; width: 143.67px;  height: 204.32px;  }
.ltr-box--a   { left: 92.65px;   top: 177.22px; width: 130.701px; height: 198.564px; }
.ltr-box--n   { left: 240.70px;  top: 408.3px;  width: 126.599px; height: 198.402px; }
.ltr-box--e5  { left: 305.10px;  top: 292.89px; width: 104.796px; height: 193.213px; }

.ltr-box--o  .rot__i { transform: rotate(14.7deg);  }
.ltr-box--a  .rot__i { transform: rotate(-8.67deg); }
.ltr-box--n  .rot__i { transform: rotate(9.3deg);   }
.ltr-box--e5 .rot__i { transform: rotate(-6.3deg);  }

.orange-edge  { left: -7.06px;  top: 1117px;    width: 410.074px; height: 40.772px;  }
.orange-burst { left: 9.984px;  top: 1689px;    width: 310.741px; height: 308.295px; }

/* ------------------------------------------------------------ monsters -- */

/* Each creature is stacked from layers that share one viewBox (body / each eye
   / each pupil / mouth), so they register exactly however they are scaled and
   every piece can be moved on its own. A face that shifts against its own body
   is what separates "a creature" from "a picture being rotated".

   NO CYCLE HERE RESTS AT ITS OWN LOOP POINT except the two that are meant to:
   a blink is mostly hold, and the ghost's pupils are asked to pause at centre.
   Everywhere else the first and last keyframe are the same value reached from
   opposite directions, so the loop seam is invisible and the motion never
   stalls at the reset.

   PUPILS ARE TIED TO THEIR WHITES. Each pupil reads the same clock as the eye
   it sits in, so they move as one piece. To unhook one, give that monster's
   .mon__pupil-l / -r its own --eye-dur and --eye-delay. */

/* Bodies. Back and forth is the whole gesture; amplitude and timing vary just
   enough that it doesn't tick. */
/* The stop TIMES are spaced in proportion to how far the rotation travels
   between them, which holds the angular speed constant all the way round.
   Get that wrong and the slowest segment reads as a stall: the previous
   version spent its last 21% covering 0.2 of amplitude — a ninth of the speed
   of every other segment — and that showed up as a pause at every reset. */
@keyframes rock {
  0%     { rotate: calc(var(--amp) * -1);   }
  27.1%  { rotate: calc(var(--amp) *  0.9); }
  48.6%  { rotate: calc(var(--amp) * -0.6); }
  71.4%  { rotate: calc(var(--amp) *  1);   }
  100%   { rotate: calc(var(--amp) * -1);   }
}

/* A couple of pixels of drift on a different clock, so the rock doesn't read
   as a hinge. Deliberately tiny — seasoning, not motion. */
@keyframes drift {
  0%     { translate: var(--dx) 0; }
  31.9%  { translate: calc(var(--dx) * -0.4) calc(var(--dy) * -1); }
  61.8%  { translate: calc(var(--dx) * -1)   calc(var(--dy) * 0.5); }
  100%   { translate: var(--dx) 0; }
}

/* Eyes: a continuous bob. Top of the travel at 0% and 100%, bottom at 50%,
   with the horizontal component a quarter-cycle out of phase — so the path is
   a flattened loop that never stops moving and never stalls at the seam.
   --ey is larger than --ex, which is what keeps it a bounce rather than a
   circle. Left and right eyes run the same clock half a cycle apart, so one is
   rising while the other falls. */
@keyframes bounce {
  0%   { translate: 0                     calc(var(--ey) * -1); }
  25%  { translate: var(--ex)             0;                    }
  50%  { translate: 0                     var(--ey);            }
  75%  { translate: calc(var(--ex) * -1)  0;                    }
  100% { translate: 0                     calc(var(--ey) * -1); }
}

/* A blink is a lid, not a squash: shapes in the body's own colour close over
   the eye and open again. Two of them meet — the upper covering three quarters
   of the gap and the lower the last quarter — so the seam lands below centre
   the way a real lid does, rather than splitting the eye in half.

   Each lid layer is clipped to a window around the eyes, so a lid can only
   ever appear across them; it can't spill onto the page or over the rest of
   the face. Being body-coloured, the parts of that window which aren't eye are
   invisible, and all you see is the edge crossing the whites. Each lid travels
   exactly its own height, so it clears the window completely when open.

   The lids are shut and open again inside the last 8% of the cycle — about
   0.1s down and 0.12s back up. A blink is a snap, not a fade; stretch it and
   it stops reading as a blink and starts reading as something falling. How
   OFTEN it happens is the cycle length below, which is a separate dial. */
@keyframes blink-down {
  0%,  92%  { translate: 0 calc(var(--lid-h) * -1); }
  95.5%     { translate: 0 0; }
  100%      { translate: 0 calc(var(--lid-h) * -1); }
}
@keyframes blink-up {
  0%,  92%  { translate: 0 var(--lid-h); }
  95.5%     { translate: 0 0; }
  100%      { translate: 0 var(--lid-h); }
}

/* Look left, look right, rest in the middle. The holds are the point here:
   the sweep reads as tracking something, the pause as deciding. */
/* Look left, look right, rest in the middle. Sweeps take 9% of the cycle each
   and the side holds 12%, but the two centre holds meet across the loop seam
   and come to 49% together — so he spends about half his time looking straight
   at you. The sweep reads as tracking something, the long rest as deciding. */
@keyframes track {
  0%,  22%  { translate: 0 0; }
  31%, 43%  { translate: calc(var(--px) * -1) 0; }
  52%, 64%  { translate: var(--px) 0; }
  73%, 100% { translate: 0 0; }
}

/* Layer stack. Every layer fills the wrapper, so the pieces line up exactly at
   any scale and only their own animation moves them apart. */
.mon { position: absolute; }
.mon > img,
.mon__lid,
.mon__lid > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  max-width: none;
}

.monster {
  animation:
    rock  var(--rot-dur) linear infinite var(--rot-delay),
    drift var(--pos-dur) linear infinite var(--pos-delay);
}

/* The body layer takes nothing of its own — it IS the creature, and the
   wrapper already moves it. Each pupil shares its own eye's clock; the right
   side is offset half a cycle so the two alternate. */
.mon__eye-l, .mon__pupil-l, .card__eye-l, .card__pupil-l {
  animation: bounce var(--eye-dur) linear infinite var(--eye-delay);
}
.mon__eye-r, .mon__pupil-r, .card__eye-r, .card__pupil-r {
  animation: bounce var(--eye-dur) linear infinite
             calc(var(--eye-delay) - var(--eye-dur) / 2);
}
.mon__mouth, .card__mouth {
  animation: bounce var(--mouth-dur) linear infinite var(--mouth-delay);
}

/* ---- the ones whose bodies rock ---- */

.sun {
  --amp: 2deg; --dx: 1.5px; --dy: 1px;
  --rot-dur: 4.7s; --pos-dur: 7.3s;
  --rot-delay: -0.7s; --pos-delay: -2.9s;
  --ex: 1.2px; --ey: 2.6px; --eye-dur: 2.9s; --eye-delay: -1.1s;
  --mouth-dur: 3.4s; --mouth-delay: -1.8s;
  transform-origin: 50% 55%;
}
.sun .mon__mouth { --ex: 0.7px; --ey: 1.2px; }

.card {
  --amp: 1.5deg; --dx: 1.2px; --dy: 1.4px;
  --rot-dur: 5.9s; --pos-dur: 8.3s;
  --rot-delay: -3.1s; --pos-delay: -1.1s;
  --ex: 1px; --ey: 2.2px; --eye-dur: 3.3s; --eye-delay: -0.5s;
  --mouth-dur: 3.9s; --mouth-delay: -1.6s;
  transform-origin: 50% 90%;
}
.card__mouth { --ex: 0.6px; --ey: 1.1px; }

.cloud {
  --amp: 2.2deg; --dx: 1.5px; --dy: 1.2px;
  --rot-dur: 4.1s; --pos-dur: 6.1s;
  --rot-delay: -1.9s; --pos-delay: -4.3s;
  --ex: 1.3px; --ey: 2.8px; --eye-dur: 2.6s; --eye-delay: -0.9s;
  transform-origin: 30% 60%;
}

.burst {
  --amp: 2.4deg; --dx: 1.4px; --dy: 1.5px;
  --rot-dur: 5.3s; --pos-dur: 7.9s;
  --rot-delay: -2.3s; --pos-delay: -0.8s;
  --ex: 1.1px; --ey: 2.4px; --eye-dur: 3.1s; --eye-delay: -0.6s;
  transform-origin: 55% 45%;
}

/* ---- the ones that only blink ---- */

/* Bodies and eyes still; the lid does all the work. The clip window and
   --lid-h are measured off the eye geometry in the artwork — the window is the
   eyes' bounding box plus a few px, and the lid travels exactly its own height
   so it clears the window completely when open. */
.green .mon__eyes, .green2 .mon__eyes { animation: none; }

.green .mon__lid  { clip-path: inset(17.230% 28.328% 73.253% 27.222%); }
.green2 .mon__lid { clip-path: inset(25.056% 19.938% 56.736% 39.190%); }

/* They are never on screen together, so both run the same clock. */
.mon__lid--top > img { animation: blink-down 2.75s linear infinite -1.2s; }
.mon__lid--bot > img { animation: blink-up   2.75s linear infinite -1.2s; }

.green  .mon__lid--top > img { --lid-h: 41.11px; }
.green  .mon__lid--bot > img { --lid-h: 13.70px; }
.green2 .mon__lid--top > img { --lid-h: 47.17px; }
.green2 .mon__lid--bot > img { --lid-h: 15.72px; }

/* ---- the ghost: nothing but the pupils ---- */

/* His eyes are big enough for a real sweep, and he is the only one whose
   pupils are unhooked from their whites. 24px takes the pupil right out to the
   white's edge: the tightest of the four directions has 30px of clearance, so
   this leaves a few px of white showing rather than running off the shape. */
.boo__ghost .mon__eyes { animation: none; }
.boo__ghost .mon__pupils {
  --px: 24px;
  animation: track 1.9s linear infinite;
}

/* ---- RSVP ---- */

/* Each glyph on its own fast clock so the word buzzes rather than waves. The
   P, the arrow and its head already carry a fixed rotation from the artboard;
   the `rotate` property composes on top of that `transform` rather than
   replacing it. */
@keyframes sway {
  from { rotate: var(--a); }
  to   { rotate: var(--b); }
}

.rsvp .ltr,
.ltr-box--p .rot__i,
.arrow-box .rot__i,
.head-box .rot__i {
  animation: sway var(--dur) linear infinite alternate;
  animation-delay: var(--delay, 0s);
}

.ltr--r             { --a: -2.0deg; --b:  2.0deg; --dur: 0.42s; --delay: -0.11s; }
.ltr--s             { --a:  2.2deg; --b: -2.2deg; --dur: 0.37s; --delay: -0.25s; }
.ltr--v             { --a: -1.8deg; --b:  1.8deg; --dur: 0.45s; --delay: -0.06s; }
.ltr-box--p .rot__i { --a:  1.6deg; --b: -1.6deg; --dur: 0.40s; --delay: -0.19s; }
.arrow-box  .rot__i { --a: -1.2deg; --b:  1.2deg; --dur: 0.50s; --delay: -0.33s; }
.head-box   .rot__i { --a: -1.5deg; --b:  1.5deg; --dur: 0.46s; --delay: -0.02s; }

@media (prefers-reduced-motion: reduce) {
  .monster,
  .mon > img,
  .mon__lid > img,
  .card__eye-l, .card__eye-r, .card__pupil-l, .card__pupil-r, .card__mouth,
  .rsvp .ltr,
  .ltr-box--p .rot__i,
  .arrow-box .rot__i,
  .head-box .rot__i { animation: none; }
}

.sun    { left: 157.204px; top: -78.295px; width: 310.741px; height: 308.295px; }
.cloud  { left: 129.88px;  top: 1240px;    width: 392.12px;  height: 370.391px; }
.green  { left: 139px;     top: 2159px;    width: 276px;     height: 576px;     }
.burst  { left: -111px;    top: 2454px;    width: 369.763px; height: 377.552px; }
.green2 { left: 173px;     top: 3768px;    width: 231.603px; height: 345.436px; }

/* The yellow card: a monster with the invitation in its belly. Body, face and
   type are one element so they turn together. */
.card {
  position: absolute;
  left: -38px; top: 609px;
  width: 408px; height: 401px;
}
.card > * { position: absolute; display: block; max-width: none; }
.card__body    { left: 0;        top: 0;    width: 408px; height: 401px; }
.card__eye-l   { left: 147.082px; top: 103px; width: 35px; height: 32px; }
.card__pupil-l { left: 157.082px; top: 110px; width: 18px; height: 18px; }
.card__eye-r   { left: 193.082px; top: 109px; width: 35px; height: 36px; }
.card__pupil-r { left: 203.082px; top: 118px; width: 18px; height: 17px; }
.card__mouth   { left: 144.082px; top: 148px; width: 78px; height: 13px; }

/* Sits on the card but outside it, so the creature sways and the words don't. */
.card__text {
  position: absolute;
  left: 48px; top: 832px; width: 277px;
  margin: 0;
  font-family: var(--gasoek);
  font-size: 42px;
  line-height: 40px;
  color: #000;
}

/* --------------------------------------------------------- when + where -- */

.join {
  position: absolute;
  left: 0; top: 1050px; width: 390px;
  margin: 0;
  font-family: var(--staat);
  font-size: 50px;
  line-height: 36px;
  color: #000;
  text-align: center;
  text-transform: uppercase;
}

.when {
  position: absolute;
  left: 36px; top: 1166px; width: 318px;
  font-family: var(--gasoek);
  font-size: 69px;
  color: #000;
  text-align: center;
}
.when p { margin: 0; line-height: 60px; }

.where {
  position: absolute;
  left: 53px; top: 1432px; width: 223px;
  margin: 0;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 35px;
  line-height: 36px;
  color: #000;
}

/* ------------------------------------------------------ pills and tags -- */

.pill { border-radius: 40px; }
.pill--rain {
  position: absolute;
  left: 26px; top: 1665.748px;
  width: 306px; height: 71px;
  background: var(--green);
}
.pill--art { width: 306px; height: 71px; background: var(--pink); }
.pill-box--art { left: 33.3px; top: 2124.12px; width: 311.086px; height: 97.823px; }
.pill-box--art .rot__i { transform: rotate(-5.08deg); }

.tag {
  margin: 0;
  font-family: var(--staat);
  font-size: 50px;
  line-height: 74px;
  color: #000;
  text-align: center;
  width: 387px;
}

.tag--rain {
  position: absolute;
  left: -15.99px; top: 1663.079px;
  font-family: var(--gasoek);
  font-size: 31px;
}
.tag--photobooth {
  position: absolute;
  left: 13.01px; top: 2048px;
  font-family: var(--gasoek);
  font-size: 38px;
}
.tag--baby    { font-family: var(--gasoek); font-size: 33px; }
.tag--bounce  { width: 220.638px; line-height: 44px; }
.tag--costume { width: 220.638px; line-height: 44px; }

.tag-box--byob    { left: 93.997px;  top: 1580.748px; width: 391.686px; height: 104.783px; }
.tag-box--bounce  { left: 149.885px; top: 1907.011px; width: 233.29px;  height: 128.101px; }
.tag-box--art     { left: -8.497px;  top: 2121px;     width: 392.033px; height: 107.985px; }
.tag-box--baby    { left: -73.003px; top: 2326.594px; width: 391.686px; height: 104.783px; }
.tag-box--costume { left: 176.86px;  top: 2420.49px;  width: 227.679px; height: 107.552px; }

.tag-box--byob    .rot__i { transform: rotate(4.6deg);   }
.tag-box--bounce  .rot__i { transform: rotate(10.89deg); }
.tag-box--art     .rot__i { transform: rotate(-5.08deg); }
.tag-box--baby    .rot__i { transform: rotate(4.6deg);   }
.tag-box--costume .rot__i { transform: rotate(-5.18deg); }

.ell-box--byob { left: 196.588px; top: 1576.155px; width: 182.144px; height: 110.324px; }
.ell-box--baby { left: -22.947px; top: 2319.821px; width: 287.214px; height: 118.683px; }
.ell-box--byob .rot__i { transform: rotate(4.55deg); }
.ell-box--baby .rot__i { transform: rotate(4.55deg); }
.ell-box--byob img { width: 175.023px; height: 96.75px; }
.ell-box--baby img { width: 280.425px; height: 96.75px; }

.bounce-shape     { left: 159px;  top: 1894px; width: 220px; height: 151px; }
.photobooth-shape { left: -17px;  top: 2015px; width: 423px; height: 144px; }
.costume-shape    { left: 160px;  top: 2382px; width: 269px; height: 188px; }

/* ----------------------------------------------------------- body copy -- */

.body {
  position: absolute;
  left: 34px; width: 326px;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 31px;
  line-height: 36px;
  color: #fff;
  overflow-wrap: break-word;
}
.body        { top: 2857px; }
.body--outro { top: 4641px; }
.body p      { margin: 0; }
.body__gap   { height: 36px; }

/* The mirror ball (U+1FAA9) only reached emoji fonts in 2022, so a system with
   an older one renders tofu. The system fonts get first refusal; the subsetted
   Noto face loaded in <head> is the backstop. Naming them at all keeps the
   glyph out of Chivo Mono, which has no colour emoji. */
.emoji {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}

/* Inline links and photo triggers. They have to sit on the text baseline
   exactly like plain text, or the monospace line-up breaks and the highlight
   bars below stop lining up with their words.

   display:inline is load-bearing. Two of these are <button> elements, which
   default to inline-block — and on an inline-BLOCK box vertical padding grows
   the line box, which pushed every line below it down and slid the whole
   paragraph out from under the highlighter bars pinned to its line positions.
   Forced back to inline, the padding costs no layout at all.

   That padding is the tap target: on an inline box it grows the hit area
   without touching layout. It reaches down over the highlighter bar, so the
   whole marked word — stroke included — is tappable, roughly 50px tall rather
   than the ~30px of bare glyphs.

   Horizontal padding would be a different story: it pushes the following text
   along, and these paragraphs are monospaced with the bars pinned to character
   positions, so the whole line-up would drift. Keep it vertical only. */
.ink {
  display: inline;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  margin: 0;
  padding: 8px 0 12px;
  cursor: pointer;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.ink--potluck  { color: var(--orange);   }
.ink--bringing { color: var(--blue);     }
.ink--inspo    { color: var(--green);    }
.ink--costume  { color: var(--yellow);   }
.ink--movie    { color: var(--lavender); }

/* Highlighter strokes. Pinned to the line positions from the artboard rather
   than drawn on the inline box, which is how the design has them — they run
   past the ends of the words the way a real marker would.

   pointer-events:none is load-bearing. These sit ON TOP of the words they
   mark, so without it every tap aimed at a link hits the bar instead and
   nothing happens — which is exactly as broken as it sounds. */
.bar {
  position: absolute;
  height: 11px;
  opacity: 0.5;
  pointer-events: none;
}
.bar--potluck  { left: 29px;  top: 3132px; width: 140px; background: var(--orange);   }
.bar--what     { left: 251px; top: 3168px; width: 88px;  background: var(--blue);     }
.bar--bringing { left: 29px;  top: 3204px; width: 289px; background: var(--blue);     }
.bar--inspo    { left: 29px;  top: 3276px; width: 102px; background: var(--green);    }
.bar--costume  { left: 29px;  top: 3384px; width: 289px; background: var(--yellow);   }
.bar--movie    { left: 29px;  top: 4700px; width: 309px; background: var(--lavender); }

/* ---------------------------------------------------------------- RSVP -- */

/* One link covering the letters and the arrow. The box itself doesn't take
   taps — only the glyphs inside it do — so it can be generously sized without
   swallowing the artwork around it. */
.rsvp {
  position: absolute;
  left: 0; top: 4010px;
  width: 390px; height: 380px;
  display: block;
  pointer-events: none;
  -webkit-tap-highlight-color: transparent;
}
.rsvp > span { pointer-events: none; }
.rsvp .ltr, .rsvp .rot { pointer-events: auto; }

.ltr--r { left: 57.5px; top: 51px; }
.ltr--s { left: 152px;  top: 31px; }
.ltr--v { left: 246px;  top: 10px; }
.ltr-box--p { left: 276.493px; top: 65.5px; width: 114.014px; height: 194.002px; }
.ltr-box--p .rot__i { transform: rotate(6.06deg); }

.arrow-box { left: 27px; top: 213px; width: 282.804px; height: 115.094px; }
.arrow-box .rot__i { transform: rotate(11.35deg); }
.arrow-box__i { position: relative; display: block; width: 276px; height: 62px; }
.arrow-box__i img { position: absolute; top: -49.98%; right: -0.44%; bottom: -49.17%; left: -2.41%; width: auto; height: auto; }

.head-box { left: 264.84px; top: 220.35px; width: 115.607px; height: 149.934px; }
.head-box .rot__i { transform: rotate(103.8deg); }
.head-box__i { position: relative; display: block; width: 133.187px; height: 86.333px; }
.head-box__i img { position: absolute; top: 0; bottom: 25%; left: 6.7%; right: 6.7%; width: auto; height: auto; }

/* -------------------------------------------------------- new this year -- */

.star-new { left: 75px; top: 4356px; width: 248px; height: 261px; }

.new-box { left: 90.206px; top: 4440.67px; width: 220.868px; height: 106.6px; }
.new-box .rot__i { transform: rotate(-5.08deg); }
.new {
  margin: 0;
  width: 213.914px;
  font-family: var(--staat);
  font-size: 50px;
  line-height: 44px;
  color: #000;
  text-align: center;
}

/* ----------------------------------------------------------------- BOO -- */

.boo__ghost { left: 36px; top: 5167px; width: 319px; height: 347px; }
.ltr--b  { left: 67.5px; top: 5036px; }
.ltr--o1 { left: 166px;  top: 5002px; }
.ltr--o2 { left: 271px;  top: 4994px; }
.ltr-box--boobang { left: 311.98px; top: 5034px; width: 78.414px; height: 190.13px; }
.ltr-box--boobang .rot__i { transform: rotate(10.65deg); }

/* -------------------------------------------------------- photo viewer -- */

.viewer {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
}
.viewer[hidden] { display: none; }

/* Dims the invitation behind without hiding it. */
.viewer__scrim {
  position: absolute;
  inset: 0;
  background: rgba(18, 9, 23, 0.78);
}

/* Fixed size and ratio: every photo crops to fill this, so the frame never
   moves as the reel cycles. */
.viewer__frame {
  position: relative;
  outline: none;
  width: min(100vw, calc(var(--design-w) * var(--max-scale) * 1px));
  aspect-ratio: var(--viewer-ratio);
  background: #000;
}

.viewer__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  visibility: hidden;
}
.viewer__img.is-on { visibility: visible; }

/* The hand-drawn X from the artboard. Sized and placed in percentages of the
   photo frame so it tracks the frame at any phone width. On the artboard it
   runs a little past the right edge and gets clipped by the phone; here it is
   pulled in far enough to sit whole. */
.viewer__close {
  position: absolute;
  left: 87%;
  top: 1.9%;
  width: 23.4%;
  aspect-ratio: 91.2735 / 103.95;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transform: translate(-50%, -50%) rotate(15deg);
  -webkit-tap-highlight-color: transparent;
}
.viewer__close img {
  display: block;
  width: 100%;
  height: 100%;
}


/* =====================================================================
   DESKTOP
   Figma: MacBook Pro 16" - 4 (node 81:2398), 1728 x 4948

   Its own artboard rather than the phone one moved about: the paint order,
   the set of elements and the line lengths all differ. Both artboards sit in
   the page and this switches which one shows, at the same 860px breakpoint
   as the Luke's 3rd invite.

   Where a desktop element is the same kind of thing as a phone one, it keeps
   the phone class name and this block only overrides its position under
   .stage--d. That way the motion — every keyframe and every per-creature
   clock tuned above — is shared automatically, and the two layouts can't
   drift apart in how they move. Elements that exist only on desktop get a
   d- prefix.
   ===================================================================== */

.stage--d { display: none; }

@media (min-width: 861px) {
  :root {
    --design-w: 1728;
    --design-h: 4948;
    /* Drawn at 1728; keeps growing with the window up to ~2600 rather than
       sitting at 1:1 in a sea of colour on a big display. */
    --max-scale: 1.5;

    --orange-top: 1257;
    --orange-h:   2473;
    --dark-top:   2400;
    --dark-h:     3842;
  }

  .stage--m { display: none; }
  .stage--d { display: block; }

  /* The phone viewer is sized off the window WIDTH, which is right on a
     phone and absurd on a laptop — at 4:5, a full-width frame is taller than
     the screen. Here it sizes off the height instead and centres. */
  .viewer__frame {
    width: auto;
    height: min(86vh, 960px);
  }
}

/* ---------------------------------------------------------- background -- */

.d-orange-edge { left: -19px;  top: 1185px; width: 1771px; height: 109px; }
.d-dark-edge   { left: -69px;  top: 2293px; width: 1928px; height: 123px; }

/* The seven background blobs. Figma sizes these with container-query hypot()
   maths; each is placed here by the centre of its rotated bounding box, with
   its unrotated size taken from the SVG itself — the two agree to within
   0.3px for every blob, which is the check that this geometry is right. */
.d-blob-yellow-tl    { left:    -6.342px; top:  -264.651px; width:  534.022px; height:  796.494px; transform: rotate(-110.47deg); }
.d-blob-lavender-tr  { left:  1213.436px; top:  -181.879px; width:  797.576px; height: 1189.580px; transform: rotate(-170.20deg); }
.d-blob-pink-l       { left:  -557.508px; top:   612.176px; width: 1040.540px; height: 1551.970px; transform: rotate(-170.20deg); }
.d-blob-yellow-br    { left:  1387.418px; top:  3684.223px; width:  829.773px; height: 1237.610px; transform: rotate(2.01deg); }
.d-blob-orange-bl    { left: -1107.387px; top:  4224.153px; width: 1728.860px; height: 2101.820px; transform: rotate(-170.76deg) scaleX(-1); }
.d-blob-blue-l       { left:  -264.567px; top:  2640.635px; width:  681.890px; height: 1017.040px; transform: rotate(18.41deg); }
.d-blob-green-r      { left:  1319.466px; top:  2493.765px; width:  777.843px; height:  681.393px; transform: rotate(-71.40deg); }

/* ------------------------------------------------------------ monsters -- */

/* Same creatures as the phone layout, bigger. Rotation is in degrees so it
   carries over as-is, but drift and eye travel are in pixels of the artwork —
   and these are drawn larger, so the same pixel counts would read as a
   fraction of the movement. They're scaled by each creature's size relative
   to its phone version, so the motion looks the same on both. */

.stage--d .sun {
  left: 758px; top: -131.44px; width: 508.69px; height: 391.44px;
  /* the group also spans the "it's" pill, so the body's centre sits right
     of middle — pivot on the body, not the box */
  transform-origin: 61% 50%;
  --dx: 1.9px; --dy: 1.27px; --ex: 1.52px; --ey: 3.3px;
}
.stage--d .sun .mon__mouth { --ex: 0.89px; --ey: 1.52px; }
.d-its { left: 758px; top: -131.44px; width: 508.69px; height: 391.44px; }

.d-card {
  left: 610px; top: 922px; width: 508px; height: 499.284px;
  --amp: 1.5deg; --dx: 1.56px; --dy: 1.82px;
  --rot-dur: 5.9s; --pos-dur: 8.3s;
  --rot-delay: -3.1s; --pos-delay: -1.1s;
  --ex: 1.3px; --ey: 2.86px; --eye-dur: 3.3s; --eye-delay: -0.5s;
  --mouth-dur: 3.9s; --mouth-delay: -1.6s;
  transform-origin: 50% 90%;
}
.d-card .mon__mouth { --ex: 0.78px; --ey: 1.43px; }
.d-card-text { left: 610px; top: 922px; width: 508px; height: 499.284px; }

.stage--d .cloud {
  left: 1278px; top: 1608px; width: 758.748px; height: 716.705px;
  --dx: 2.9px; --dy: 2.3px; --ex: 2.5px; --ey: 5.4px;
}

.stage--d .burst {
  left: 932px; top: 2044px; width: 414.814px; height: 423.552px;
  --dx: 1.57px; --dy: 1.68px; --ex: 1.23px; --ey: 2.69px;
}

/* The kelly-green one is drawn with different proportions from its phone
   version, so it gets its own lid window, measured the same way. */
.stage--d .green { left: 1103px; top: 1760px; width: 308px; height: 600px; }
.stage--d .green .mon__lid { clip-path: inset(18.492% 28.477% 71.440% 27.503%); }
.stage--d .green .mon__lid--top > img { --lid-h: 45.31px; }
.stage--d .green .mon__lid--bot > img { --lid-h: 15.10px; }

/* The dark-green monster and the ghost are the phone artwork at the phone
   size, so their lid and pupil settings above apply untouched. */
.stage--d .green2     { left: 841.775px; top: 3128px; width: 231.603px; height: 345.436px; }
.stage--d .boo__ghost { left: 704.775px; top: 4645px; }

/* ------------------------------------------------------------- lettering -- */

.d-title { left: 493.96px; top: 226px; width: 725.042px; height: 688px; }

.d-join {
  position: absolute;
  left: 669px; top: 1536px; width: 390px;
  margin: 0;
  font-family: var(--staat);
  font-size: 70px;
  line-height: 36px;
  color: #000;
  text-align: center;
  text-transform: uppercase;
}

.d-when {
  position: absolute;
  left: 373px; top: 1602px; width: 982px;
  margin: 0;
  font-family: var(--gasoek);
  font-size: 109px;
  line-height: 60px;
  color: #000;
  text-align: center;
  white-space: nowrap;
}

.d-where {
  position: absolute;
  left: 584px; top: 1739px; width: 561px;
  font-family: var(--mono);
  font-weight: 600;
  font-size: 45px;
  color: #000;
  text-align: center;
}
.d-where p { margin: 0; line-height: 56px; }

/* ------------------------------------------------------------------ tags -- */

.d-costume-text { left: 387.889px; top: 2081.728px; width: 320.24px; height: 150.839px; }
.d-byob         { left: 331px;     top: 1795px;     width: 247.548px; height: 140.459px; }

.d-photobooth    { left: 5px;       top: 1835px;     width: 607.653px; height: 255.632px; }
.d-bouncehouse   { left: -52px;     top: 1956px;     width: 354.517px; height: 289.008px; }
.d-costume-shape { left: 306px;     top: 1970px;     width: 510.255px; height: 369.626px; }
.d-rain-pill     { left: 26.13px;   top: 2229.95px;  width: 349.666px; height: 95.104px;  }
.d-rain-text     { left: 60.05px;   top: 2226.51px;  width: 283.352px; height: 101.98px;  }
.d-babyzone      { left: 436px;     top: 2213px;     width: 322.627px; height: 171.433px; }
.d-art-pill      { left: 555.32px;  top: 1988.98px;  width: 341.4px;   height: 98.026px;  }
.d-art-text      { left: 603.127px; top: 2005.51px;  width: 240.546px; height: 66.229px;  }

.d-photobooth    .rot__i { transform: rotate(5.35deg);   }
.d-bouncehouse   .rot__i { transform: rotate(-16.31deg); }
.d-costume-shape .rot__i { transform: rotate(15.14deg);  }
.d-rain-pill     .rot__i { transform: rotate(-0.73deg);  }
.d-rain-text     .rot__i { transform: rotate(6.47deg);   }
.d-babyzone      .rot__i { transform: rotate(-8.61deg);  }
.d-art-pill      .rot__i { transform: rotate(-3.38deg);  }
.d-art-text      .rot__i { transform: rotate(1.7deg);    }

.d-photobooth    img { width: 591.452px; height: 201.345px; }
.d-bouncehouse   img { width: 307.611px; height: 211.133px; }
.d-costume-shape img { width: 458.568px; height: 258.844px; }
.d-rain-text     img { width: 277.094px; height: 71.224px;  }
.d-babyzone      img { width: 307.093px; height: 126.897px; }
.d-art-text      img { width: 238.894px; height: 59.157px;  }

.d-pill { display: block; }
.d-pill--rain { width: 348.538px; height: 90.665px; border-radius: 60px; background: var(--olive); }
.d-pill--art  { width: 337.373px; height: 78.279px; border-radius: 40px; background: var(--green); }

/* ------------------------------------------------------------- body copy -- */

/* 539 wide instead of 326: the lines break in different places, so every bar
   has its own coordinates here. Chivo Mono is monospaced, so these land the
   same way the phone ones do — the block is 648px, exactly 18 lines of 36. */
.stage--d .body        { left: 595px; top: 2559px; width: 539px; }
.stage--d .body--outro { left: 845px; top: 3915px; width: 326px; }

.stage--d .bar--potluck  { left: 887px; top: 2727px; width: 140px; }
.stage--d .bar--what     { left: 942px; top: 2763px; width: 88px;  }
.stage--d .bar--bringing { left: 591px; top: 2799px; width: 289px; }
.stage--d .bar--inspo    { left: 591px; top: 2834px; width: 102px; }
.stage--d .bar--costume  { left: 832px; top: 2907px; width: 289px; }
.stage--d .bar--movie    { left: 840px; top: 3974px; width: 309px; }

.stage--d .star-new { left: 557px;     top: 3957px; }
.stage--d .new-box  { left: 572.206px; top: 4041.67px; }

/* ------------------------------------------------------------------ RSVP -- */

/* Children are placed relative to the link's box, which starts at y=3450. */
.stage--d .rsvp { top: 3450px; width: 1728px; height: 400px; }

.stage--d .ltr--r { left: 726.28px; top: 64px; }
.stage--d .ltr--s { left: 820.78px; top: 44px; }
.stage--d .ltr--v { left: 914.78px; top: 23px; }
.stage--d .ltr-box--p { left: 945.273px; top: 78.5px; }

/* The desktop arrow is a different drawing from the phone one. */
.stage--d .arrow-box    { left: 648.5px; top: 216.83px; width: 383.598px; height: 142.887px; }
.stage--d .arrow-box__i { width: 377.19px; height: 70.039px; }
.stage--d .arrow-box__i img { top: -44.24%; right: -3.25%; bottom: -43.52%; left: -1.5%; }

.stage--d .head-box { left: 996px; top: 229px; width: 95.645px; height: 139.025px; }
.stage--d .head-box .rot__i { transform: rotate(85.9deg); }

/* ------------------------------------------------------------------- BOO -- */

.stage--d .ltr--b  { left: 736.28px; top: 4514px; }
.stage--d .ltr--o1 { left: 834.78px; top: 4480px; }
.stage--d .ltr--o2 { left: 939.78px; top: 4472px; }
.stage--d .ltr-box--boobang { left: 980.773px; top: 4512px; }
