/* ============================================================================
   Limb from Limb: launch hero

   The page opens on the game's own main menu, restaged for a browser. Sky,
   canopy, tree, moon and the hanging sign are the exact files MainMenu.unity
   loads, and every colour is sampled from the game's source rather than
   eyeballed. Sources are cited inline so they can be re-checked when the menu
   changes.

   The copy is written for someone who has never heard of the game. Most people
   arriving here will not have played the demo, so the hero sells what the game
   is and the demo gets one conditional line near the bottom.

   Single theme by intent, not omission: this is a night scene with a moon in
   it. There is no light-mode reading of that.
   ========================================================================== */

/*
   The game's two relevant faces, used the way GameFont splits them:
     Horror: MrHorror, the main-menu sign and the headline. Ragged capitals.
     Chunky: Free Cheese, the campaign dialogs. Labels and small caps here.
   MrHorror has no em dash and draws only display sizes well, so it is confined
   to the headline and the sign, exactly as in game. Free Cheese draws lowercase
   as small caps, so it never carries running text. Licences ship beside both.
*/
@font-face {
  font-family: 'MrHorror';
  src: url('/fonts/MrHorror-Regular.otf') format('opentype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Free Cheese';
  src: url('/fonts/FreeCheese-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Night sky: SkyGradient.GetPalette, TimeOfDay default (Assets/Scripts/Sandbox/SkyGradient.cs) */
  --night-top:     #003462;
  --night-mid:     #084E7E;   /* Color.Lerp(top, horizon, 0.56f) */
  --night-horizon: #0F6394;

  /* StarField.baseColor / .warmColor */
  --star-cool: #DBE8FF;
  --star-warm: #FFEBD1;

  /* MainMenuController.SignLabelColor, and GameFont.Ink */
  --eggshell: #F0EAD6;
  --ink:      #301608;

  /* GameFont.Gold, and DemoAnnouncementDialog's border */
  --gold: #F5D45C;

  /* DemoAnnouncementDialog: PanelFill, ButtonFill, BodyCream */
  --panel:     #061318;
  --panel-btn: #0E2B30;
  --cream:     #FFEDC2;

  --display: 'MrHorror', 'Impact', sans-serif;
  --dialog:  'Free Cheese', 'Impact', sans-serif;
  --body: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
          'Helvetica Neue', Arial, sans-serif;

  --step--1: clamp(0.8rem, 0.77rem + 0.16vw, 0.9rem);
  --step-0:  clamp(0.98rem, 0.94rem + 0.22vw, 1.1rem);
  --step-1:  clamp(1.12rem, 1.03rem + 0.42vw, 1.38rem);
  --step-3:  clamp(1.9rem, 1.4rem + 2.4vw, 3.2rem);

  /* The headline gets its own ramp: MrHorror is a display face and this is the
     one place on the page allowed to shout. */
  --headline: clamp(2.4rem, 1.5rem + 4.2vw, 4.6rem);

  --wrap: 1140px;
}

/* --- reset ---------------------------------------------------------------- */

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

html { -webkit-text-size-adjust: 100%; }

/* Stops the rubber-band overscroll on iOS, which drags the page off its own
   background and flashes the white canvas above the canopy and below the grass. */
html { overscroll-behavior-y: none; }

body {
  margin: 0;
  min-height: 100svh;
  overscroll-behavior-y: none;
  /* Carries the sky to the privacy and 404 pages too, which have no .sky layer
     of their own and no JS. On the front page .sky paints the same gradient over
     the top, so the two never disagree.

     Deliberately NOT background-attachment:fixed. A fixed attachment forces a
     full repaint of the gradient on every scroll frame, and on iOS it tears
     against the URL bar animation, which is half of what made this page feel
     broken while scrolling. */
  background: linear-gradient(180deg,
    var(--night-top) 0%,
    var(--night-mid) 56%,
    var(--night-horizon) 100%) no-repeat;
  background-size: 100% 100%;
  color: var(--cream);
  font-family: var(--body);
  font-size: var(--step-0);
  line-height: 1.6;
  overflow-x: hidden;
}

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

h1 { margin: 0; line-height: 1.02; font-weight: 400; }

p { margin: 0 0 1em; }

ul { margin: 0; padding: 0; list-style: none; }

a { color: var(--gold); text-underline-offset: 3px; }
a:hover { color: var(--cream); }

:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 3px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--gold);
  color: var(--ink);
  padding: 0.7em 1.2em;
  font-family: var(--dialog);
}
.skip-link:focus { left: 0.5rem; top: 0.5rem; }

.wrap {
  width: min(100% - 2.5rem, var(--wrap));
  margin-inline: auto;
}

/* --- sky ------------------------------------------------------------------ */

/* Fixed, so the scene behaves like the menu's camera rather than a scrolling
   backdrop when the content runs past one viewport. */
/*
   Sized in lvh, not svh and not inset:0.

   Mobile browsers grow the viewport as the URL bar retracts. svh is the SMALL
   viewport (bar showing), so a fixed layer sized that way is short by the height
   of the bar the moment you scroll: you get bare canvas below the grass and,
   because the vignette is bottom-anchored, sky above the tree on the way back up.
   lvh is the LARGE viewport, so these always cover the tallest state and simply
   overscan by the bar's height when it is showing. The overscan lands on the
   bottom of the ground art, where nothing is happening.
*/
.sky {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;    /* fallback; on mobile vh already resolves to the large viewport */
  height: 100lvh;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
    var(--night-top) 0%,
    var(--night-mid) 56%,
    var(--night-horizon) 100%);
}

#starfield { position: absolute; inset: 0; width: 100%; height: 100%; }

/*
   MenuMoonCornerAnchor pins the moon to a screen corner. The left corner is
   where main_menu_foreground_v2 puts its tree, so on the web it goes right:
   the canopy is thinner there and the moon actually reads instead of sitting
   behind a trunk.
*/
.moon {
  position: absolute;
  top: clamp(1rem, 8vh, 4.5rem);
  right: clamp(1rem, 6vw, 7rem);
  width: clamp(76px, 11vw, 150px);
  aspect-ratio: 1;
}
.moon__disc,
.moon__glow { position: absolute; inset: 0; width: 100%; height: auto; }
.moon__glow {
  scale: 2.1;
  opacity: 0.5;
  animation: moonbreathe 7s ease-in-out infinite;
}

@keyframes moonbreathe {
  0%, 100% { opacity: 0.42; scale: 2.0; }
  50%      { opacity: 0.6;  scale: 2.2; }
}

.clouds { position: absolute; inset: 0; overflow: hidden; }
/* Positioned and animated by js/scene.js, which mirrors CloudField's drift. */
.cloud {
  position: absolute;
  will-change: transform;
  opacity: 0.5;
  filter: brightness(0.8);
}

/* --- vignette ------------------------------------------------------------- */

/*
   main_menu_foreground_v2 is a full-screen frame, not a ground strip: a canopy
   across the top, a gnarled tree down the left, hills and grass along the
   bottom, transparent through the middle.

   The frame box reproduces object-fit:cover by hand at the art's own 16:9.
   Doing it that way, rather than object-fit on the img, is what lets the eyes be
   pinned to the tree hollow in percentages and stay in the hollow at every
   viewport size.
*/
.vignette {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;    /* see .sky: lvh so the URL bar cannot open a gap */
  height: 100lvh;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

/*
   Anchored left, not centred. The art is 16:9 and almost no browser window is,
   so the frame is always wider than the viewport and something gets cropped.
   Centring takes it off both edges, and both edges is where the tree is: at
   1440x900 the frame is 1600px wide, so 5% comes off each side and the hollow
   with the eyes in it ends up half off-screen. Anchoring left keeps the tree,
   the hollow and the eyes whole at every size and crops the hills on the right,
   which are the least distinctive part of the plate. In portrait it is the
   difference between seeing the tree and seeing only middle-distance grass.
*/
.vignette__frame {
  position: absolute;
  bottom: 0;
  left: 0;
  width: max(100vw, calc(100vh * 16 / 9));
  height: max(100vh, calc(100vw * 9 / 16));
  width: max(100vw, calc(100lvh * 16 / 9));
  height: max(100lvh, calc(100vw * 9 / 16));
}

.vignette__art { width: 100%; height: 100%; }

/*
   Not eyeballed. These are the EyesInTheHollow RectTransform values out of
   MainMenu.unity, where the eyes and the foreground are siblings under the same
   stretched LayeredEnvironment rect, so the anchors map straight to percentages
   of the frame:

     anchor x 0.0875            -> left 8.75%
     anchor y 0.35185 (bottom)  -> top 64.815%
     sizeDelta 188 of a 1920 reference width -> 9.792%

   The earlier hand-placed values (10.9% / 60.5% / 3.2%) put them in roughly the
   right hole at a third of the right size.
*/
.vignette__eyes {
  position: absolute;
  left: 8.75%;
  top: 64.815%;
  width: 9.792%;
  transform: translate(-50%, -50%);
  animation: blink 6.5s steps(1, end) infinite;
}

@keyframes blink {
  0%, 92%, 96%, 100% { opacity: 1; }
  94%                { opacity: 0; }
}

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

.hero {
  position: relative;
  z-index: 2;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: clamp(1.25rem, 4vh, 2.5rem) 0 clamp(1.25rem, 3.5vh, 2rem);
}

.hero__grid {
  width: min(100% - 2.5rem, 1120px);
  margin-inline: auto;
  display: grid;
  gap: clamp(1.5rem, 4vw, 3.5rem);
}

@media (min-width: 900px) {
  .hero__grid {
    grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
    align-items: center;
  }
}

.hero__copy { max-width: 36rem; }

.logo {
  width: min(62vw, 340px);
  margin-bottom: clamp(1rem, 2.5vh, 1.6rem);
  filter: drop-shadow(0 10px 22px rgba(0, 0, 0, 0.55));
}

/*
   Deliberately not the dialog face, and deliberately not uppercased. Free Cheese
   draws lowercase as small caps, so either one turns "iOS" into "IOS" and gets
   the platform's own name wrong in the first line on the page.
*/
.eyebrow {
  margin: 0 0 0.55rem;
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.16em;
  color: var(--gold);
}

/*
   The one place the page shouts, in the game's own sign face. Eggshell on the
   chocolate ink outline that MainMenuController sets at runtime
   (SignLabelColor + GameFont.Ink at SignLabelOutlineWidth 0.2), emulated as an
   8-way text-shadow, which survives everywhere paint-order does not.
*/
.headline {
  font-family: var(--display);
  font-size: var(--headline);
  color: var(--eggshell);
  text-transform: uppercase;
  text-wrap: balance;
  text-shadow:
     0.038em 0 0 var(--ink), -0.038em 0 0 var(--ink),
     0 0.038em 0 var(--ink),  0 -0.038em 0 var(--ink),
     0.027em 0.027em 0 var(--ink), -0.027em 0.027em 0 var(--ink),
     0.027em -0.027em 0 var(--ink), -0.027em -0.027em 0 var(--ink),
     0 0.08em 0.16em rgba(0, 0, 0, 0.6);
}

.lede {
  margin: clamp(0.9rem, 2vh, 1.3rem) 0 0;
  /* Wide enough to keep the punchline on the same line as what it undercuts;
     at 46ch "them." fell to a line of its own and killed the timing. */
  max-width: 34rem;
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--cream);
  text-shadow: 0 2px 8px rgba(0, 6, 18, 0.75);
}

.aside {
  margin: 1.1rem 0 0;
  padding-top: 1rem;
  max-width: 46ch;
  border-top: 1px solid rgba(245, 212, 92, 0.24);
  font-size: var(--step--1);
  line-height: 1.5;
  color: rgba(255, 237, 194, 0.68);
}

.foot {
  position: relative;
  z-index: 2;
  width: min(100% - 2.5rem, 1120px);
  margin: clamp(0.9rem, 2.5vh, 1.5rem) auto 0;
  font-size: var(--step--1);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 237, 194, 0.55);
}

/* --- the hanging sign ----------------------------------------------------- */

/*
   The rig exists so the sign has something to hang from. Left to itself the
   sprite's ropes stop in mid air and the whole thing floats; here they continue
   past the top of the viewport into the canopy, and the sway pivots about the
   far end of that rope rather than about the planks, so the assembly swings as
   one rigid body instead of pinwheeling.
*/
.rig {
  position: relative;
  display: flex;
  justify-content: center;
}

.sign {
  container-type: inline-size;
  position: relative;
  width: min(84%, 330px);
  transform-origin: 50% -100vh;   /* the far end of the rope, above the frame */
  animation: sway 7s ease-in-out infinite;
}
.sign:hover { animation-play-state: paused; }

/*
   The rope, continued upward out of the frame using the sign's OWN pixels
   (img/rope.webp, cut from this sprite by tools/make_web_assets.py and
   cross-faded so it tiles without a seam). A hand-drawn CSS gradient was tried
   first and was wrong three ways at once: far too pale against the sprite's
   near-black #2C312C, too thin, and starting above where the sprite's rope
   actually begins, which left a visible gap at the join.

   Every number below is measured off sign.png (570x735):
     left rope  x 46..63,  centre 54.5  ->  9.561%
     right rope x 503..519, centre 511  -> 89.649%
     width 18px                         ->  3.158%
     first opaque rope row y=27         ->  3.673% down, so bottom: 96.327%
     tile height 35px                   ->  6.140cqw of the sign's width

   background-position:bottom anchors the tiling at the join rather than at the
   far end, so the twist lines up with the sprite no matter how tall the rope
   has to be. The canopy sits in a layer below the content, so the top is masked
   out and the rope fades into the foliage instead of crossing in front of it.
*/
.sign::before,
.sign::after {
  content: '';
  position: absolute;
  bottom: 96.327%;
  width: 3.158%;
  height: 100vh;
  transform: translateX(-50%);
  background-image: url('/img/rope.webp');
  background-repeat: repeat-y;
  background-position: bottom center;
  background-size: 100% 6.14cqw;
  -webkit-mask-image: linear-gradient(to top, #000 0 55%, transparent 80%);
  mask-image: linear-gradient(to top, #000 0 55%, transparent 80%);
}
.sign::before { left: 9.561%; }
.sign::after  { left: 89.649%; }

@keyframes sway {
  0%, 100% { transform: rotate(-0.35deg); }
  50%      { transform: rotate(0.35deg); }
}

.sign__wood {
  width: 100%;
  filter: drop-shadow(0 12px 20px rgba(0, 0, 0, 0.5));
}

/*
   Slat geometry from MainMenuController (Assets/Scripts/UI/MainMenuController.cs),
   measured off this same 570x735 sprite:

     SignRowCenters  = 241.5, 433.5, 629.5   (px from the top)
     SignLabelSize.x = 455 of 570 wide
     SignLabelCapsDrop = 14: MrHorror's ink centre sits about 0.372em above the
                             baseline while the line box centres, so the caps
                             ride high until the label is dropped.

   Each centre below is (row + 14) / 735 as a percentage, so the web labels land
   exactly where the game's do. Sizes are in cqw against the sign's own width, so
   the lettering scales with the plank rather than with the viewport. 10cqw puts
   the widest string ("1 HILARIOUS GAME", 6.59em measured off MrHorror's hmtx
   table) at 82.6% of the label box, which is the same fill the game itself
   allows: MainMenuController caps labels at 116px, where its own longest label,
   OPTIONS, sits at about 83% of the usable wood.
*/
.slat {
  position: absolute;
  left: 50%;
  width: 79.8%;              /* 455 / 570 */
  translate: -50% -50%;
  text-align: center;
  font-family: var(--display);
  font-size: 10cqw;
  line-height: 1;
  color: var(--eggshell);
  text-transform: uppercase;
  white-space: nowrap;
  text-shadow:
     0.045em 0 0 var(--ink), -0.045em 0 0 var(--ink),
     0 0.045em 0 var(--ink),  0 -0.045em 0 var(--ink),
     0.032em 0.032em 0 var(--ink), -0.032em 0.032em 0 var(--ink),
     0.032em -0.032em 0 var(--ink), -0.032em -0.032em 0 var(--ink),
     0 0.09em 0.14em rgba(0, 0, 0, 0.55);
}

.slat--one   { top: 34.76%; }
.slat--two   { top: 60.88%; }
.slat--three { top: 87.55%; }

/*
   Phones get the facts without the plank. Stacked into one column the sign sits
   below the copy, and a rope drawn from there to the top of the frame runs the
   whole length of the page straight across the headline. The sign only reads as
   hung when it is beside the copy with the canopy directly above it, so below
   900px the wood and the ropes come off and the three slats become a plain
   strip. Same markup, same content, no floating plank.
*/
@media (max-width: 899px) {
  /*
     The eyes keep their scene-accurate spot, which on a phone puts them directly
     behind the email field: they peek out of the gap between the input and the
     button and read as a rendering fault rather than as something watching. The
     tree still carries the hollow, so the detail is dropped rather than moved
     somewhere the game never puts it.
  */
  .vignette__eyes { display: none; }

  .rig { justify-content: flex-start; }

  .sign {
    width: 100%;
    animation: none;
    container-type: normal;
  }
  .sign::before,
  .sign::after,
  .sign__wood { display: none; }

  .sign__stats {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.6rem;
  }

  .slat {
    position: static;
    translate: none;
    width: auto;
    font-size: 1.2rem;
    color: var(--gold);
    text-shadow: 0 2px 6px rgba(0, 6, 18, 0.8);
  }
}

/* --- the form ------------------------------------------------------------- */

#signup { margin-top: clamp(1.25rem, 3vh, 1.9rem); }

/* Input and button ride together as one control until there is no room. */
.signup {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: flex-end;
}

.field { flex: 1 1 15rem; min-width: 0; }

.field__label {
  display: block;
  margin-bottom: 0.3rem;
  font-family: var(--dialog);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 237, 194, 0.7);
}

.field__input {
  width: 100%;
  font: inherit;
  font-size: max(16px, var(--step-0));   /* 16px stops iOS zooming on focus */
  color: var(--cream);
  background: rgba(6, 19, 24, 0.92);
  border: 2px solid rgba(0, 0, 0, 0.6);
  border-radius: 3px;
  padding: 0.66em 0.75em;
}
.field__input::placeholder { color: rgba(255, 237, 194, 0.4); }
.field__input:focus-visible { outline-offset: 1px; }

/* The page's one loud element. Gold on ink is the game's own lettering pairing,
   inverted into a fill so the single action on the page reads first. */
.btn {
  flex: 0 0 auto;
  font-family: var(--dialog);
  font-size: var(--step-1);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink);
  background: linear-gradient(180deg, #FBE07A 0%, var(--gold) 55%, #D9B23F 100%);
  border: 2px solid var(--ink);
  border-radius: 3px;
  padding: 0.52em 1.15em;
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.75);
  cursor: pointer;
  transition: transform 0.08s ease, box-shadow 0.08s ease, filter 0.15s ease;
}
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: translateY(3px); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.75); }
.btn[disabled] { filter: grayscale(0.55) brightness(0.82); cursor: default; }

@media (max-width: 27rem) {
  .btn { width: 100%; }
}

.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__lead {
  margin: 0.7rem 0 0;
  font-size: var(--step--1);
  color: rgba(255, 237, 194, 0.62);
}

.form__status {
  margin: 0.4rem 0 0;
  min-height: 1.2em;
  font-size: var(--step--1);
  line-height: 1.4;
  color: rgba(255, 237, 194, 0.72);
}
.form__status--warn  { color: var(--gold); }
.form__status--error { color: #F2837F; }
.form__status a { color: var(--gold); }

/* Success: the form is replaced by the dialog's cleared state. */
.won {
  display: grid;
  gap: 0.2rem;
  justify-items: start;
  padding: 0.9rem 1.1rem;
  background: rgba(6, 19, 24, 0.94);
  border: 2px solid rgba(245, 212, 92, 0.92);
  border-radius: 4px;
  animation: slam 0.42s cubic-bezier(0.2, 1.5, 0.4, 1) 1;
}
.won__head {
  margin: 0;
  font-family: var(--dialog);
  font-size: var(--step-1);
  text-transform: uppercase;
  color: var(--gold);
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
}
.won__line {
  margin: 0;
  font-family: var(--dialog);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold);
}
.won__note {
  margin: 0.35rem 0 0;
  font-size: var(--step--1);
  line-height: 1.45;
  color: rgba(255, 237, 194, 0.75);
}

@keyframes slam {
  0%   { transform: translateY(-18px) scale(1.04); }
  60%  { transform: translateY(3px) scale(0.99); }
  100% { transform: translateY(0) scale(1); }
}

/* --- privacy page --------------------------------------------------------- */

.prose {
  position: relative;
  z-index: 2;
  padding-block: clamp(3rem, 8vw, 5rem);
}
.prose h1 {
  font-family: var(--dialog);
  font-size: var(--step-3);
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 0.6em;
}
.prose h2 {
  margin: 2rem 0 0.5rem;
  font-family: var(--dialog);
  font-size: var(--step-1);
  font-weight: 400;
  text-transform: uppercase;
  color: var(--gold);
}
.prose p, .prose li { color: rgba(255, 237, 194, 0.82); max-width: 68ch; }

/* --- 404 ------------------------------------------------------------------ */

.gone {
  position: relative;
  z-index: 2;
  min-height: 100svh;
  display: grid;
  place-content: center;
  text-align: center;
  gap: 0.4rem;
}
.gone img { width: min(34vw, 150px); margin: 0 auto 1.5rem; }
.gone h1 {
  font-family: var(--display);
  font-size: var(--step-3);
  color: var(--eggshell);
  text-shadow: 0.04em 0 0 var(--ink), -0.04em 0 0 var(--ink),
               0 0.04em 0 var(--ink), 0 -0.04em 0 var(--ink);
}
.gone p { color: rgba(255, 237, 194, 0.72); }

/* --- motion --------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .sign,
  .moon__glow,
  .vignette__eyes,
  .won { animation: none; }
  .cloud { transition: none !important; }
  *, *::before, *::after { transition-duration: 0.01ms !important; }
}
