/* ============================================================
   CHOMP — the landing page: one continuous vertical world.
   Every scene is a full-bleed plate with the content floating on
   top; scenes bleed into each other with soft fades so the page
   reads as one descent rather than eight boxes.

   Performance rule, learned the hard way in the first build:
   transforms and opacity only. No filters on big surfaces, no
   per-frame layout reads, one throttled scroll handler.
   ============================================================ */

:root{
  --sky:      #9ccdf0;
  --navy:     #17204a;
  --navy-2:   #0d1330;
  --ink:      #17204a;
  --ink-dim:  #5d6788;
  --pink:     #ee5f96;
  --pink-lt:  #ff92ba;
  --pink-dk:  #c93a70;
  --cream:    #f7f3ea;
  --paper:    #eef4fb;

  --wrap: 1180px;
  --gut: clamp(20px, 5vw, 60px);
  --display: "Nunito", ui-rounded, "SF Pro Rounded", system-ui, sans-serif;
  --body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --ease: cubic-bezier(.22,.68,.24,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
}

*,*::before,*::after{ box-sizing:border-box; }
/* Das `overflow-x:hidden` muss hier oben stehen, nicht nur am Body. Die
   Platten ragen absichtlich 2% über beide Seiten hinaus (die Parallaxe
   braucht den Vorrat), und das verbreitert das Layout-Viewport: bei 390px
   sichtbarer Breite rechnet der Browser mit 424px. Eine `position:fixed`
   Leiste misst sich an diesem breiteren Viewport — ihr rechter Rand lag
   damit 34px außerhalb des Bildes, und alles was dort sitzt (der
   Contact-Knopf) war angeschnitten. Am Body allein greift die Regel für
   fixierte Elemente nicht. */
html{ scroll-behavior:smooth; -webkit-text-size-adjust:100%; overflow-x:hidden; }
.w-body{
  margin:0; background:var(--paper); color:var(--ink);
  font-family:var(--body); font-size:17px; line-height:1.6;
  overflow-x:hidden; -webkit-font-smoothing:antialiased;
}
img{ max-width:100%; height:auto; display:block; }
a{ color:inherit; }
.wrap{ position:relative; z-index:3; width:min(100% - var(--gut)*2, var(--wrap)); margin-inline:auto; }
.wrap.center{ text-align:center; }
.skip{ position:absolute; left:-9999px; }
.skip:focus{ left:8px; top:8px; z-index:99; background:var(--pink); color:#fff; padding:10px 16px; border-radius:10px; }

/* -------------------------- nav ----------------------------- */
.wnav{
  position:fixed; top:0; left:0; right:0; z-index:70;
  display:flex; align-items:center; justify-content:space-between;
  gap:20px; padding:18px clamp(18px,4vw,44px);
  transition:background .4s var(--ease), box-shadow .4s var(--ease);
}
.wnav.solid{
  background:#eef4fb;
  box-shadow:0 2px 0 rgba(23,32,74,.07);
}
.wnav-brand{ display:inline-flex; align-items:center; gap:10px; text-decoration:none;
  font-family:var(--display); font-weight:1000; font-size:1.35rem; letter-spacing:-.03em; color:var(--navy); }
/* The brand mark is the real goo, not a gradient circle: the same render
   the hero uses, with the same face drawn over it in SVG. The face is
   scaled up a touch against the hero's proportions — at 26px the hero's
   own eye sizes collapse into two grey dots. */
.goo-mark{ position:relative; display:inline-block; flex:none;
  width:26px; height:26px; vertical-align:middle; }
.goo-mark--lg{ width:34px; height:34px; }
.goo-mark img{ width:100%; height:100%; display:block; }
.goo-mark svg{ position:absolute; inset:0; width:100%; height:100%; }
.goo-mark .sc{ fill:#fff; }
.goo-mark .pu{ fill:#22091a; }
.goo-mark .mo{ fill:none; stroke:#22091a; stroke-width:4.2; stroke-linecap:round; }
.wnav-links{ display:flex; gap:clamp(16px,3vw,40px); }
.wnav-links a{ font-family:var(--display); font-weight:900; font-size:.92rem;
  text-decoration:none; color:var(--navy); opacity:.85; }
.wnav-links a:hover{ opacity:1; }
/* Die beiden Knöpfe rechts sind `.chunky.sm`, kein eigener Stil — siehe
   unten bei den Buttons. */
.wnav-actions{ display:flex; align-items:center; gap:10px; }
/* Ab etwa 500px wird es zu dritt zu eng: Marke, Store-Knopf und Contact
   brauchen zusammen ~486px. Der Store-Knopf geht, nicht Contact — der
   grüne CTA im Hero steht auf Handys ohnehin schon im ersten Bild, der
   Kontaktweg sonst nur im Footer.
   Zwei Klassen, nicht eine: `.chunky` setzt `display:inline-flex` und
   steht weiter unten im Stylesheet, würde ein einzelnes `.wnav-store`
   also bei gleicher Spezifität überstimmen. */
@media (max-width:560px){ .wnav-actions .wnav-store{ display:none; } }
@media (max-width:820px){ .wnav-links{ display:none; } }

/* ------------------------- scenes --------------------------- */
/* No box, no clip, no radius. A scene with overflow:hidden is a
   rectangle, and a rectangle always shows its edge — that is what made
   the joins visible and what cut the goo off at the bottom. Instead the
   scenes overlap deeply and each backdrop fades out at both ends, so one
   landscape dissolves into the next with nothing to see. */
.scene{ position:relative; }
.scene + .scene{ margin-top:-18vh; }
.plate{
  position:absolute; inset:-6% -2%; z-index:0;
  background-position:center; background-size:cover; background-repeat:no-repeat;
  will-change:transform;
  /* the dissolve lives here, not in an overlay */
  -webkit-mask-image:linear-gradient(180deg, transparent 0%, #000 15%, #000 84%, transparent 100%);
  mask-image:linear-gradient(180deg, transparent 0%, #000 15%, #000 84%, transparent 100%);
}
/* the hero starts hard at the top of the page, it has nothing above it */
.hero .plate{
  -webkit-mask-image:linear-gradient(180deg, #000 0%, #000 84%, transparent 100%);
  mask-image:linear-gradient(180deg, #000 0%, #000 84%, transparent 100%);
}
.plate-hero  { background-image:url("scene/hero.webp"); }
.plate-forest,
.plate-forest2,
.plate-forest3,
.plate-forest4{ background-image:url("scene/forest.webp"); }
.plate-candy { background-image:url("scene/candy.webp"); }
.plate-ice   { background-image:url("scene/ice.webp"); }
.plate-autumn{ background-image:url("scene/autumn.webp"); }
.plate-valley{ background-image:url("scene/valley.webp"); }
.plate-sunset{ background-image:url("scene/sunset.webp"); }

/* soft seams between scenes so nothing reads as a cut */
/* superseded by the plate mask */
.plate-fade{ display:none; }
/* No white wash over the artwork. The plates stay at full strength; only
   the very top and bottom get a soft tint so neighbouring scenes can melt
   into one another. Readability is bought with glass behind the text, not
   by bleaching the background. */
.plate-veil{ display:none; }

/* ============================================================
   SHEET — the game's own card, not a frosted panel.
   `ui/sheet.webp` is WorldGooBackgroundDark straight out of
   Assets.xcassets: deep navy, glossy goo pooling in the corners,
   four-point sparkles.
   ============================================================ */
.sheet{
  position:relative; display:inline-block;
  max-width:min(100%, 720px);
  padding:clamp(32px,3.8vw,54px) clamp(32px,4.2vw,60px);
  color:#fff;
  /* NOT border-image: the moment border-image is set, border-radius is
     ignored outright and the card renders with hard square corners.
     background-size:cover keeps the goo blobs round (it crops rather
     than stretches) and the radius then actually applies. */
  background:#16214a url("ui/sheet.webp") center / cover no-repeat;
  border-radius:clamp(26px,3vw,40px);
  box-shadow:0 26px 46px rgba(10,16,40,.34),
             inset 0 0 0 1px rgba(255,255,255,.10);
}

/* the sticker badge that floats over a sheet's top edge in the game */
.sheet-badge{
  position:absolute; top:0; left:50%;
  transform:translate(-50%,-58%) rotate(-3deg);
  padding:9px 22px; border-radius:999px;
  background:linear-gradient(180deg,var(--pink-lt),var(--pink) 55%,var(--pink-dk));
  box-shadow:0 5px 0 #8e2350, 0 10px 20px rgba(160,30,80,.4);
  font-family:var(--display); font-weight:900; font-size:.72rem;
  letter-spacing:.2em; text-transform:uppercase; color:#fff; white-space:nowrap;
}
.sheet .lede{ color:rgba(255,255,255,.72); }
.sheet .kicker{ color:var(--pink-lt); }
.sheet h2{ color:#fff; }

/* ============================================================
   CHUNKY BUTTON — the game's ChunkyButtonStyle: a gradient face
   sitting on a darker edge capsule, with a gloss cap. Pressing
   pushes the face down onto the edge. No blur, no glass.
   ============================================================ */
.chunky{
  --face-a:#ff92ba; --face-b:#ee5f96; --face-c:#d4477c; --edge:#8e2350;
  /* Der Schein unter dem Knopf als RGB-Tripel, damit die kleinen Größen
     ihn mit eigener Deckkraft aufbauen können. Die großen Knöpfe unten
     schreiben ihren Schatten weiterhin selbst aus — unangetastet. */
  --glow:140,25,70;
  position:relative; display:inline-flex; align-items:center; gap:12px;
  padding:17px 34px; border:0; border-radius:999px; cursor:pointer;
  text-decoration:none; color:#fff;
  font-family:var(--display); font-weight:1000; font-size:1.05rem;
  letter-spacing:.01em;
  background:linear-gradient(180deg,var(--face-a) -18%,var(--face-b) 46%,var(--face-c));
  box-shadow:0 6px 0 var(--edge), 0 12px 22px rgba(140,25,70,.34);
  transition:transform .09s ease, box-shadow .09s ease;
}
.chunky::after{           /* gloss cap over the top half of the face */
  content:""; position:absolute; inset:3px 3px 48% 3px; border-radius:999px;
  background:linear-gradient(180deg, rgba(255,255,255,.46), rgba(255,255,255,0));
  pointer-events:none;
}
.chunky:hover{
  transform:translateY(-1px);
  box-shadow:0 7px 0 var(--edge), 0 14px 26px rgba(140,25,70,.36);
}
/* Part of the entrance, not a hover effect: the button hops once after the
   three tagline words have finished, closing the sequence. Same single-arc
   shape and per-keyframe easing as `wordBounce`. */
@keyframes ctaBounce{
  0%   { transform:translateY(0) scale(1,1);
         animation-timing-function:cubic-bezier(.22,.66,.3,1); }
  40%  { transform:translateY(-10px) scale(1.024,.976);
         animation-timing-function:cubic-bezier(.58,0,.76,.42); }
  68%  { transform:translateY(0) scale(.988,1.016);
         animation-timing-function:cubic-bezier(.2,.75,.3,1); }
  100% { transform:translateY(0) scale(1,1); }
}
/* a press has to beat the bounce, or the button would not go down */
.chunky:active{
  animation:none;
  transform:translateY(6px); box-shadow:0 0 0 var(--edge), 0 4px 10px rgba(140,25,70,.28);
}
.chunky.navy{
  --face-a:#3d4d84; --face-b:#233163; --face-c:#182449; --edge:#080d22;
  --glow:8,13,34;
  box-shadow:0 6px 0 var(--edge), 0 12px 22px rgba(8,13,34,.4);
}
.chunky.navy:hover{ box-shadow:0 7px 0 var(--edge), 0 14px 26px rgba(8,13,34,.44); }
.chunky.navy:active{ box-shadow:0 0 0 var(--edge), 0 4px 10px rgba(8,13,34,.32); }
.chunky.green{
  --face-a:#8ec25e; --face-b:#6da33f; --face-c:#5b8b33; --edge:#3e6323;
  --glow:40,80,20;
  box-shadow:0 6px 0 var(--edge), 0 12px 22px rgba(40,80,20,.34);
}
.chunky.green:hover{ box-shadow:0 7px 0 var(--edge), 0 14px 26px rgba(40,80,20,.38); }
.chunky.green:active{ box-shadow:0 0 0 var(--edge), 0 4px 10px rgba(40,80,20,.30); }
/* Nav-Größe. Die Unterkante schrumpft von 6px auf 4px mit — ein 6px-Sockel
   unter einem 40px hohen Knopf sieht aus wie ein Fehler, nicht wie Tiefe.
   Der Weg beim Drücken ist derselbe Wert, sonst bleibt eine Fuge stehen.
   Der Schein läuft über `--glow`, damit die Regel für alle drei Farben
   gilt und nicht dreimal dasteht — sie steht nach `.navy` und `.green`,
   gewinnt also gegen deren große Schatten. */
.chunky.sm{
  padding:11px 22px; gap:8px; white-space:nowrap;
  font-size:.84rem; letter-spacing:.08em; text-transform:uppercase;
  box-shadow:0 4px 0 var(--edge), 0 8px 16px rgba(var(--glow),.30);
}
.chunky.sm:hover{ box-shadow:0 5px 0 var(--edge), 0 10px 19px rgba(var(--glow),.34); }
.chunky.sm:active{
  transform:translateY(4px);
  box-shadow:0 0 0 var(--edge), 0 3px 8px rgba(var(--glow),.26);
}
.chunky.sm .cta-apple{ width:16px; height:16px; }
/* Solange es keine Store-Seite gibt, ist der CTA ein Hinweis und kein
   Link: kein Zeigefinger, kein Klick der ins Leere geht. */
.chunky[role="note"]{ cursor:default; }

/* ------------------------ typography ------------------------ */
h1,h2{ font-family:var(--display); margin:0; letter-spacing:-.035em; line-height:.95; }
.h-xl { font-size:clamp(2.1rem,5vw,3.9rem); font-weight:1000; }
.h-xxl{ font-size:clamp(2.6rem,7vw,5.6rem); font-weight:1000; }
.kicker{ font-family:var(--display); font-weight:900; font-size:.74rem;
  letter-spacing:.26em; text-transform:uppercase; color:var(--pink); margin:0 0 1.1rem; }
.kicker.dark{ color:var(--pink-dk); }
.lede{ color:var(--ink-dim); font-size:clamp(1rem,1.4vw,1.14rem); max-width:46ch; margin:1.2rem 0 0; }
.wrap.center .lede{ margin-inline:auto; }
.lede.sm{ font-size:.95rem; }

/* --------------------------- hero --------------------------- */
/* Anchored to the top rather than centred: with `justify-content:center` a
   negative margin on the only flex child moves it up by just half its value,
   so the offset never means what it says. flex-start + one padding does. */
.hero{ min-height:100svh; display:flex; flex-direction:column; justify-content:flex-start;
  padding-top:clamp(96px,20vh,230px); }
.hero .plate-fade{ height:16%; }
.hero-mid{ position:relative; z-index:3; text-align:center; padding:0; }
.bigmark{
  display:flex; align-items:center; justify-content:center; gap:.055em;
  /* The glyphs span 4.17em including the tracking (measure the children, not
     the h1 — it is a block-level flex box and its own rect is the container).
     That puts the overflow ceiling at ~22vw; 19vw keeps a comfortable margin
     and the 12rem cap is what desktop actually sees. */
  font-size:clamp(3.6rem,19vw,12rem); font-weight:1000; color:var(--navy);
  letter-spacing:0; line-height:1;
  text-shadow:0 6px 0 rgba(255,255,255,.28), 0 18px 40px rgba(23,32,74,.28);
}
/* ===================== the entrance ==========================
   Each letter lands with an overshoot, then the goo drops into the
   O slot, squashes on impact, springs back and opens its eyes.
   All of it is CSS keyframes on transform/opacity — nothing here
   costs a layout. ============================================ */
.ltr{
  display:inline-block;
  animation:ltrIn .62s cubic-bezier(.2,1.5,.35,1) both;
  animation-delay:calc(var(--i) * .085s);
}
@keyframes ltrIn{
  0%   { opacity:0; transform:translateY(64px) scale(.55) rotate(-6deg); }
  55%  { opacity:1; transform:translateY(-14px) scale(1.12) rotate(2deg); }
  76%  { transform:translateY(6px) scale(.96) rotate(-1deg); }
  100% { opacity:1; transform:none; }
}

.mark-slot{
  position:relative; display:inline-block;
  width:.82em; height:.82em; margin:0 .03em;
  animation:gooHop 1.05s cubic-bezier(.28,1,.4,1) both;
  animation-delay:.42s;
}
@keyframes gooHop{
  0%  { opacity:0; transform:translateY(-190px) scale(.24, .24); }
  38% { opacity:1; transform:translateY(10px)  scale(1.30, .74); }  /* impact squash */
  56% {            transform:translateY(-26px) scale(.86, 1.20); }  /* springs up */
  72% {            transform:translateY(5px)   scale(1.10, .92); }
  86% {            transform:translateY(-6px)  scale(.97, 1.04); }
  100%{ opacity:1; transform:none; }
}
.mark-goo{ width:100%; height:100%;
  filter:drop-shadow(0 10px 22px rgba(200,50,110,.45)); }
.mark-face{ position:absolute; inset:0; width:100%; height:100%; }
.mark-face .sc{ fill:#fff; }
.mark-face .pu{ fill:#22091a; }
/* eyes stay shut until the goo has landed */
#mkEyeL, #mkEyeR{ transform-box:fill-box; transform-origin:center;
  animation:eyesOpen .5s ease-out both; animation-delay:1.16s; }
@keyframes eyesOpen{ 0%{ transform:scaleY(.06); } 60%{ transform:scaleY(1.14); } 100%{ transform:scaleY(1); } }
/* transform, not `d:path()` — path morphing in keyframes is not
   supported everywhere and would simply not run in some browsers */
#mkMouth{ transform-box:fill-box; transform-origin:center top;
  animation:mouthUp .55s cubic-bezier(.2,1.5,.35,1) both; animation-delay:1.26s; }
@keyframes mouthUp{
  0%  { transform:scaleY(.12) scaleX(.7); opacity:.4; }
  60% { transform:scaleY(1.3) scaleX(1.06); opacity:1; }
  100%{ transform:none; opacity:1; }
}
.tagline{ font-family:var(--display); font-weight:900;
  font-size:clamp(1.35rem,3.2vw,2.35rem); color:var(--navy); margin:1.2rem 0 0;
  text-shadow:0 2px 12px rgba(255,255,255,.6); }
.tagline b{ color:var(--pink-dk); }
/* The words are on screen from the first paint — they only bounce, they
   do not arrive. The bounce waits for the wordmark to finish: the last
   letter lands at .875s and the goo's hop ends at 1.47s (delay .42s +
   1.05s duration), so nothing moves before 1.5s, one word every .13s. */
.tw{ display:inline-block; margin-right:.28em;
  /* Same softening as the skin orbs: the old .62s with a 1.5 overshoot
     bezier snapped. Longer, lower peak, eased rather than sprung, and a
     slightly wider stagger so the three words read as a wave. */
  animation:wordBounce .9s linear both;
  animation-delay:calc(1.5s + var(--w) * .15s); }
.tw:last-child{ margin-right:0; }
/* ONE arc, not two. The old curve went -9px, back down past zero, then up
   again to -2px — two separate rises, which is exactly the double jump it
   looked like. Here the word only ever goes up once and comes down; the
   bounce afterwards is squash and recover, no second hop. Easing is per
   keyframe (decelerate into the apex, accelerate out of it), so the
   animation itself is `linear` to avoid easing the easing. */
@keyframes wordBounce{
  0%   { transform:translateY(0) scale(1,1);
         animation-timing-function:cubic-bezier(.22,.66,.3,1); }
  40%  { transform:translateY(-10px) scale(1.035,.968);
         animation-timing-function:cubic-bezier(.58,0,.76,.42); }
  68%  { transform:translateY(0) scale(.985,1.022);
         animation-timing-function:cubic-bezier(.2,.75,.3,1); }
  100% { transform:translateY(0) scale(1,1); }
}

.chunky.cta{ cursor:default; gap:12px; padding-inline:clamp(26px,3.4vw,42px);
  margin-top:clamp(28px,4.5vh,64px);
  /* the last word starts at 1.5s + 2 x .15s and runs .9s, so it is done at
     2.7s — this picks up right there */
  /* deliberately no fill-mode: `both` would pin the 100% keyframe on the
     element forever, and an animation's value outranks the `transform` in
     `.chunky:hover` — the button would stop reacting to hover entirely.
     0% is already identity, so there is nothing to hold before the delay. */
  animation:ctaBounce .8s linear;
  animation-delay:2.72s; }
.cta-apple{ width:22px; height:22px; flex:none; fill:#fff; opacity:.95; }


/* --------------------------- grow --------------------------- */
.grow{ text-align:center; }
.grow .kicker{ text-align:center; }
.grow{ display:flex; flex-direction:column; justify-content:flex-start;
  min-height:132svh; padding-top:clamp(80px,12vh,150px); text-align:center; }
/* rises out of the bottom edge; the copy sits above it, never on it */
/* fully inside the scene now — nothing clips it any more, so it must not
   hang past the edge either */
.huge-goo{ position:absolute; z-index:2; left:50%; bottom:8%;
  width:min(520px,60vw); transform:translateX(-50%); will-change:transform;
  filter:drop-shadow(0 26px 44px rgba(120,40,70,.28)); }
.huge-goo img{ width:100%; height:auto; display:block; }
/* the shared face is authored for the small wordmark goo; at 520px the
   same proportions read as bug-eyed, so pull it in to match the sculpt
   the old baked-in face had */
.huge-goo .goo-face{ transform:scale(.78); transform-origin:50% 54%; }
/* shared face overlay: the plain spheres carry no features of their own */
.goo-face{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
.goo-face .sc{ fill:#fff; }
.goo-face .pu{ fill:#22091a; }
.goo-face .mo{ fill:none; stroke:#22091a; stroke-width:3.1; stroke-linecap:round; }
.mults{ list-style:none; display:flex; justify-content:center; gap:clamp(16px,4vw,54px);
  margin:clamp(28px,4vw,46px) 0 0; padding:0; position:relative; z-index:5; }
.mults li{ font-family:var(--display); font-weight:1000;
  font-size:clamp(1.3rem,3.4vw,2.4rem); color:var(--navy); opacity:.28;
  transition:opacity .5s var(--ease), transform .5s var(--ease-out); }
.mults li.on{ opacity:1; color:var(--pink-dk); transform:translateY(-6px); }

/* ======================= THE RUN — one 360° turn ==============
   The phone is a two-sided card in 3D: screen and back are separate
   faces with `backface-visibility:hidden`, so the turn actually shows
   the back rather than a mirrored screen. It spins exactly once, when
   the section first comes into view, and then stays face on at full
   size — the animation is `forwards`, and the class that starts it is
   never removed, so scrolling back up cannot rewind it.
   ============================================================= */
/* Scenes deliberately overlap by 18vh so each one dissolves into the next,
   and a later section normally paints on top — right for backgrounds.
   But `perspective` makes this section a stacking context, so the phone's
   own z-index cannot lift it above a later sibling, and the worlds plate
   was painting across its lower third. Order the two explicitly: the
   autumn plate still fades in behind, the phone just stays in front of it.
   `.skins` needs the same rank so `.worlds` does not cover it in turn. */
.showcase, .skins{ z-index:2; }
.worlds{ z-index:1; }
.showcase{
  min-height:100svh;
  display:flex; align-items:center; justify-content:center;
  padding:clamp(70px,10vh,130px) var(--gut);
  perspective:1700px;
}
.dev3d{
  position:relative; z-index:3;
  /* Height is the real constraint, not width: the card is ~2.1x as tall
     as it is wide, so a width-only rule makes it overshoot the viewport on
     any normal screen. 34svh keeps it just under. The ratio is the real
     iPhone 16 Pro Max body, matching `.device`. */
  width:clamp(210px, min(52vw, 36svh), 430px);
  aspect-ratio:77.6 / 163;
  transform-style:preserve-3d;
  transform:rotateY(-180deg) scale(.62);   /* parked on its back */
}
.dev3d.spun{
  will-change:transform;
  animation:phoneTurn 1.5s cubic-bezier(.22,.72,.2,1) forwards;
}
@keyframes phoneTurn{
  0%   { transform:rotateY(-360deg) scale(.62); }
  78%  { transform:rotateY(6deg)    scale(1.03); }
  100% { transform:rotateY(0deg)    scale(1); }
}
/* both faces fill the card and hide their own reverse */
.dev3d > .device,
.dev3d > .dev-back{
  position:absolute; inset:0; width:100%; aspect-ratio:auto;
  -webkit-backface-visibility:hidden; backface-visibility:hidden;
}
.dev-back{ transform:rotateY(180deg); }
.dev-back img{
  width:100%; height:100%; object-fit:contain; display:block;
  filter:drop-shadow(0 26px 46px rgba(19,30,66,.40));
}

/* ========================== WORLDS FAN ========================
   Five phones stacked into an overlapping fan. Each card gets a
   fixed "at rest" offset from --n; the scroll handler writes one
   custom property, --spread, and every card multiplies it by its
   own distance from the centre. That is one style write per card
   per frame, transforms only, no layout reads.
   ============================================================= */
.worlds{
  min-height:290svh;
  /* `overflow:clip` and not `hidden`: hidden would make this a scroll
     container, which kills the sticky positioning inside it. Clip just
     trims the painting to the section box — which is what the plate needs,
     because a sticky element with a big negative bottom margin has a
     margin box only 220px tall, so sticky happily slides it until THAT
     reaches the section bottom and leaves a whole viewport of autumn
     painted over the scene below. */
  overflow:clip;
}
/* Both the plate and the content are pinned for the length of the section.
   Two things fall out of that. A 16:9 plate `cover`-ing a 230svh section
   would be scaled ~3x into a blurry smear — pinned, it is framed exactly
   like every other scene. And the fan stays on screen for the whole
   section, so "scroll further, they spread further" has room to happen;
   unpinned it had already left the viewport a fifth of the way in. */
.worlds .plate,
.worlds-stage{
  position:sticky; top:0; height:100svh;
  margin-bottom:-100svh;
}
/* `inset:auto` clears the base rule's absolute offsets — but it clears
   `top` with them, and sticky without a `top` never sticks. Re-set it.
   The parallax also slides this ±90px, so it needs vertical overscan or
   the drift exposes a bare strip at the top and bottom of the section. */
.worlds .plate{
  inset:auto; top:-110px; width:104%; margin-left:-2%;
  height:calc(100svh + 220px);
}
.worlds-stage{
  z-index:3; display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  gap:clamp(18px,2.6vh,34px); padding:0 var(--gut);
}
.worlds-head{ margin:0; }
.worlds-title{
  color:var(--navy); text-transform:uppercase; letter-spacing:.02em;
  text-shadow:0 3px 0 rgba(255,255,255,.55), 0 14px 30px rgba(120,70,20,.30);
}
.worlds-sub{
  margin:.9rem 0 0; font-family:var(--display); font-weight:900;
  font-size:clamp(.98rem,1.9vw,1.25rem); color:var(--navy);
  text-shadow:0 2px 10px rgba(255,255,255,.7);
}
.fan-names{
  position:relative; z-index:3; margin:clamp(26px,4vh,46px) 0 0;
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:.5rem clamp(14px,2.4vw,34px);
  font-family:var(--display); font-weight:900; font-size:.8rem;
  letter-spacing:.14em; text-transform:uppercase; color:var(--navy);
}
.fan-names span{ text-shadow:0 2px 8px rgba(255,255,255,.75); }
.fan{
  position:relative; z-index:3;
  width:100%; max-width:1180px;
  height:clamp(320px,52svh,560px);
  --spread:0;                       /* 0 = stacked, 1 = fanned out */
}
.fan-card{
  position:absolute; left:50%; top:0; margin:0;
  width:clamp(120px,min(17vw,23svh),210px);
  /* --d is the card's signed distance from the middle card */
  --d:calc(var(--n) - 2);
  /* The outer pair travels 2x this, so a flat 108px pushes the fan
     ~672px wide and off the edge of a narrow window. Cap it against the
     viewport as well. */
  /* the outer cards also rotate, and a ~290px-tall card turned 6deg grows
     its box by another ~15px each side, so leave room for that too */
  --step:min(108px, 11vw);
  transform:
    translateX(calc(-50% + var(--d) * (30px + var(--spread) * var(--step))))
    translateY(calc(var(--d) * var(--d) * (8px + var(--spread) * 16px)))
    rotate(calc(var(--d) * (2.5deg + var(--spread) * 3.5deg)));
  will-change:transform;
}
/* the middle card sits on top, the outer ones behind it */
.fan-card[style*="--n:2"]{ z-index:5; }
.fan-card[style*="--n:1"],
.fan-card[style*="--n:3"]{ z-index:4; }
.fan-card .device{ width:100%; }
.fan-card .device-screen > img{ width:100%; height:100%; object-fit:cover; display:block; }

/* -------------------------- skins --------------------------- */
/* ========================== SKINS FIELD =======================
   Ten finishes, each with a gold bloom behind it and a face drawn on
   top. Every orb drifts on its own loop — different duration, phase
   and direction — so they wander past one another instead of pulsing
   together. Transforms and opacity only; the glow is a static
   gradient, never a filter.
   ============================================================= */
/* The fan needs to finish opening before the next scene starts sliding
   over it, so this one join does not overlap. */
/* the overlap is back: the fan finishes opening at 66% of the worlds
   section, leaving ~60svh of clearance before this one starts to arrive */
.skins{
  min-height:100svh;
  /* Only the BOTTOM needs containing here. Clipping the top as well cut
     into the plate's own fade-in and left a hard line at the join — and
     this plate is supposed to bleed upward, because a scene dissolving in
     over the one before it is the whole design. clip-path lets me clip one
     edge; overflow:clip is all-or-nothing. */
  clip-path:inset(-150vh 0 0 0);
  padding:clamp(100px,14vh,170px) var(--gut) clamp(90px,13vh,160px);
  display:flex; flex-direction:column; align-items:center; justify-content:center;
}
/* Deliberately NOT a pinned plate. Sticking the backdrop made this the one
   join on the page that behaved differently: the orbs slid up and out while
   the valley stayed put, instead of the whole scene travelling together the
   way the run does into the worlds. A plain parallax plate — the same one
   every other scene uses — makes the two transitions match. */
.skins::before{ content:none; }
.skins-head{ position:relative; z-index:3; margin-bottom:clamp(34px,5vh,64px); }
.skins-title{
  color:var(--navy); text-transform:uppercase; letter-spacing:.02em;
  text-shadow:0 3px 0 rgba(255,255,255,.55), 0 14px 30px rgba(30,60,30,.28);
}
.skins-sub{
  margin:.9rem 0 0; font-family:var(--display); font-weight:900;
  font-size:clamp(.98rem,1.9vw,1.25rem); color:var(--navy);
  text-shadow:0 2px 10px rgba(255,255,255,.75);
}
/* An explicit column count rather than flex-wrap: with ten items, wrapping
   gave 6+4 at 1440px because the rotation on each orb inflates its box and
   pushes the break around. A grid puts the same skins in the same places at
   every width — and 5 and 2 both divide ten evenly. */
.orbit-field{
  position:relative; z-index:3;
  width:100%; max-width:900px;
  display:grid; grid-template-columns:repeat(5,1fr); justify-items:center;
  /* tight on purpose: the balls have to sit close enough that the wander
     actually brings neighbours into contact, otherwise the repulsion has
     nothing to do and they just float in their own lanes */
  gap:clamp(10px,1.8vh,22px) clamp(2px,0.6vw,10px);
}
@media (max-width:900px){ .orbit-field{ grid-template-columns:repeat(4,1fr); } }
@media (max-width:660px){ .orbit-field{ grid-template-columns:repeat(3,1fr); } }
@media (max-width:440px){ .orbit-field{ grid-template-columns:repeat(2,1fr); } }
.orb{
  position:relative;
  width:100%; max-width:clamp(96px,14vw,158px);
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:0; border:0; background:none; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  /* the wander is written by world.js, not a keyframe: two orbs on fixed
     keyframes cannot know about each other, and these have to push apart
     when they meet. See the orb field section there. */
  will-change:transform;
}
/* Every skin PNG is the same 300x420 canvas with the ball always at the
   same spot — 85.3% of the width across, centred at 50% / 66.67%. So the
   glow is placed off those constants and needs no per-skin correction at
   any viewport size. */
.orb-body{
  position:relative; display:block; width:100%;
  aspect-ratio:300 / 420;
  transition:transform .5s cubic-bezier(.22,.9,.26,1);
}
.orb-body img{ width:100%; height:auto; display:block;
  filter:drop-shadow(0 14px 20px rgba(30,50,30,.30)); }
.orb-glow{
  position:absolute; left:50%; top:66.67%;
  width:168%; aspect-ratio:1; transform:translate(-50%,-50%);
  border-radius:50%; pointer-events:none; z-index:-1;
  /* the field sits on a bright sky now, so the bloom needs a denser core
     than it did on navy or it simply disappears */
  background:radial-gradient(circle,
    rgba(255,214,110,.95) 0%, rgba(255,190,60,.80) 20%,
    rgba(255,170,34,.44) 38%, rgba(255,150,20,.16) 54%,
    rgba(255,150,20,0) 70%);
}

/* Bounce on hover, and on tap for touch. Softer than the first pass: the
   old one used a 1.5 overshoot bezier over .62s and read as a snap. This
   is nearly twice as long, peaks lower, and eases rather than springs. */
.orb:hover .orb-body,
.orb:focus-visible .orb-body{ animation:orbBounce .95s linear; }
.orb:active .orb-body{ transform:translateY(2px) scale(.972); }
@keyframes orbBounce{
  0%   { transform:translateY(0) scale(1,1);
         animation-timing-function:cubic-bezier(.22,.66,.3,1); }
  40%  { transform:translateY(-14px) scale(1.045,.960);
         animation-timing-function:cubic-bezier(.58,0,.76,.42); }
  68%  { transform:translateY(0) scale(.980,1.028);
         animation-timing-function:cubic-bezier(.2,.75,.3,1); }
  100% { transform:translateY(0) scale(1,1); }
}
.orb-name{
  font-family:var(--display); font-weight:900; font-size:.76rem;
  letter-spacing:.1em; text-transform:uppercase; color:var(--navy);
  text-align:center; line-height:1.3;
  text-shadow:0 2px 8px rgba(255,255,255,.85);
}
.orb:hover .orb-name{ color:var(--pink-dk); }

/* ---------------- transition between two scenes ----------------
   A soft cloud band straddling the seam, so one world dissolves into
   the next instead of butting against it. */
.seam{ display:none;
  position:absolute; left:0; right:0; z-index:4; height:clamp(140px,18vh,260px);
  pointer-events:none;
  /* haze rather than a hard edge: three soft blooms of cream light that
     sit across the join, so one scene dissolves into the next */
  background:
    radial-gradient(60% 100% at 18% 50%, rgba(255,252,246,.72), transparent 70%),
    radial-gradient(70% 100% at 54% 60%, rgba(255,252,246,.60), transparent 72%),
    radial-gradient(56% 100% at 88% 44%, rgba(255,252,246,.66), transparent 70%);
}
.seam.bottom{ bottom:calc(-1 * clamp(60px,8vh,120px)); }
.seam.top{ top:calc(-1 * clamp(60px,8vh,120px)); }

/* ===================== iPhone mockup ==========================
   Ported from `style.css`, which only the doc pages load now.
   Pure gradients and box-shadows — no filter, nothing that needs
   re-rasterising while the card is spinning.
   ============================================================= */
.device{
  display:block; position:relative;
  /* iPhone 16 Pro Max, not a generic 9/19.5 slab: the body is
     77.6 x 163.0 mm and the display 73.3 x 159.3 mm, so the side bezel is
     2.77% of the width and the top/bottom bezel 1.13% of the height. That
     lands the screen at 0.4601, which is what the clip was actually shot
     at — the old 9/19.5 box was noticeably too narrow for it. */
  aspect-ratio:77.6 / 163;
  border-radius:16% / 7.6%;
  padding:.85%;
  /* titanium rail: bright on the sides, dark top and bottom */
  background:
    linear-gradient(90deg,#2a2b33 0%,#8f93a3 3%,#e6e8f0 6%,#6e7280 12%,
                    #33353f 50%,#6e7280 88%,#e6e8f0 94%,#8f93a3 97%,#2a2b33 100%);
  box-shadow:0 26px 50px -12px rgba(19,30,66,.42), 0 6px 14px rgba(19,30,66,.16);
}
.device::before{           /* the black bezel under the glass */
  content:""; position:absolute; inset:.85%;
  border-radius:15.2% / 7.1%; background:#07070b;
}
.device-screen{
  position:absolute; inset:1.13% 2.77%;
  border-radius:13.8% / 6.4%;
  overflow:hidden; background:#0b0f1e; z-index:1;
}
.device-screen > img,
.device-screen > video{ width:100%; height:100%; object-fit:cover; display:block; }
/* The capture carries the iOS status bar — clock, battery and the red
   recording dot, all inside the top 5% of the frame.
   The old fix overscanned the clip and slid it up 7.5%, which did hide
   the status bar but also dragged the game's own HUD up with it: the
   pause button sits at 7.4% and ended up inside the screen's rounded
   top-right corner, visibly clipped. So the clip now sits square in the
   screen at 1:1 and a short opaque bar covers the status row instead —
   the game's background there is nearly black anyway, and the drawn
   Dynamic Island sits on top of it. */
.device-status{
  position:absolute; z-index:2; left:0; right:0; top:0; height:5.4%;
  background:#080f0d; pointer-events:none;
}
.device-island{            /* Dynamic Island */
  position:absolute; z-index:3; top:2.6%; left:50%; transform:translateX(-50%);
  width:30%; height:3.4%; min-height:14px; border-radius:99px; background:#000;
}
.device-gloss{             /* one diagonal sheen across the glass */
  position:absolute; z-index:2; inset:1.13% 2.77%; border-radius:13.8% / 6.4%;
  pointer-events:none;
  background:linear-gradient(114deg,
    rgba(255,255,255,.20) 0%, rgba(255,255,255,.06) 17%,
    transparent 34%, transparent 74%, rgba(255,255,255,.09) 100%);
}
.device--lg .device-btn{
  position:absolute; width:1.9%; border-radius:2px; z-index:0;
  background:linear-gradient(180deg,#9498a8,#4b4e5c);
}
.device--lg .b-mute{ left:-1.5%; top:15.5%; height:4.2%; }
.device--lg .b-up{   left:-1.7%; top:23%;   height:8.6%; }
.device--lg .b-dn{   left:-1.7%; top:33.5%; height:8.6%; }
.device--lg .b-pwr{  right:-1.7%; top:26%;  height:11.5%;
  background:linear-gradient(180deg,#4b4e5c,#9498a8); }
/* the black ease in / ease out over the clip, driven from world.js */
.device-fade{ position:absolute; z-index:2; inset:0; background:#000;
  opacity:0; pointer-events:none; }

/* --------- everything on this page has rounded corners --------- */
/* The footer never got styled for this page — only a border-radius. Its
   old rules live in `style.css`, which this page does not load, so it was
   rendering as bare links on the page background. */
.wfoot{
  position:relative; z-index:4;
  display:flex; flex-direction:column; align-items:center; gap:11px;
  padding:clamp(24px,3.4vh,38px) var(--gut) clamp(20px,2.8vh,30px);
  background:linear-gradient(180deg,#17204a,#0d1330);
  color:rgba(255,255,255,.72); text-align:center;
}
.wfoot nav{ display:flex; flex-wrap:wrap; justify-content:center; gap:clamp(18px,3vw,38px); }
.wfoot nav a{
  font-family:var(--display); font-weight:900; font-size:.92rem;
  color:#fff; text-decoration:none; opacity:.85;
}
.wfoot nav a:hover{ opacity:1; color:var(--pink-lt); }
.wfoot p{ margin:0; font-size:.85rem; color:rgba(255,255,255,.52); }
.mults li{ border-radius:14px; }

@media (prefers-reduced-motion:reduce){
  .ltr, .mark-slot, .tw, #mkEyeL, #mkEyeR, #mkMouth{ animation:none !important; opacity:1; transform:none; }
  /* no turn: the phone just sits there, face on */
  .dev3d, .dev3d.spun{ animation:none !important; transform:none !important; }
  .orb{ animation:none !important; }
  .orb:hover .orb-body, .orb:focus-visible .orb-body{ animation:none !important; }
  .chunky.cta{ animation:none !important; }
}
