/* ============================================================
   Chrome for the puzzle engine.

   Positioning, stacking, grab feedback and the seat/reject
   animations — the parts every data-driven level needs and none
   should re-describe. A level's own stylesheet is then only its
   scene: wall, surface, container, props.

   Coordinates come from the engine as unitless CSS custom
   properties in virtual stage units, so nothing here needs to
   know the screen size.
   ============================================================ */

/* --- the stage ---------------------------------------------- */

/* Stage.create writes `translate(-50%, -50%) scale(n)` onto the
   stage, which only centres it if the element is anchored to the
   middle of its container first. Every engine-driven level needs
   exactly this, so it lives here rather than being restated in
   each level's stylesheet. */
.stage-wrap {
  position: fixed;
  inset: 0;
  z-index: 5;
}

.stage {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: center center;
}

.places,
.objects {
  position: absolute;
  inset: 0;
}

.places {
  z-index: 1;
}

.objects {
  z-index: 2;
}

/* --- places ------------------------------------------------ */

/* A place is a point, so it is centred on its coordinate and the
   level draws whatever marks it: a slot, a hook, a well, or
   nothing at all. */
.place {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  transform: translate(calc(var(--px) * 1px - 50%), calc(var(--py) * 1px - 50%));
}

/* --- objects ----------------------------------------------- */

.obj {
  position: absolute;
  top: 0;
  left: 0;
  /* Stacking is done with z-index, never by reordering the DOM:
     re-parenting a focused element blurs it, which silently breaks
     keyboard play the moment anything is picked up. */
  z-index: 2;
  width: calc(var(--w) * 1px);
  height: calc(var(--h) * 1px);
  outline: none;
  --rot: 0;
  /* Rotation is part of the same transform as the position so the two
     never fight: a tilted pencil still travels to its slot in a
     straight line. Translate first, then spin about the centre. */
  transform: translate(calc(var(--ox) * 1px - 50%), calc(var(--oy) * 1px - 50%))
    rotate(calc(var(--rot) * 1deg));
}

/* A frame the player turns rather than carries. The whole tile is
   grabbable because there is no meaningful "outside" to click past,
   and the cursor says turn, not drag. */
.obj.is-turnable .obj__body {
  cursor: grab;
}

.obj.is-turnable.is-held .obj__body {
  cursor: grabbing;
}

/* Reaching the target angle is the entire feedback in a rotate
   level, so it gets a moment of its own. */
.obj.is-aligned .obj__body {
  animation: obj-align var(--dur-snap) var(--ease-snap);
}

@keyframes obj-align {
  0% {
    filter: drop-shadow(0 0 0 rgba(212, 165, 60, 0));
  }
  40% {
    filter: drop-shadow(0 0 10px rgba(212, 165, 60, 0.75));
  }
  100% {
    filter: drop-shadow(0 0 0 rgba(212, 165, 60, 0));
  }
}

.obj svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

/* Only the painted silhouette is grabbable, so a click in the
   transparent corner of a bounding box falls through to whatever
   is actually underneath. */
.obj__body {
  pointer-events: auto;
  cursor: grab;
  transition: filter var(--dur-lift) var(--ease-soft);
  filter: drop-shadow(0 2px 2px rgba(40, 30, 18, 0.2))
    drop-shadow(0 7px 12px rgba(40, 30, 18, 0.22));
}

/* Placed objects stop casting a big shadow — they belong to the
   arrangement now, not to the pile beside it — and drop behind the
   loose ones, so a piece dropped over a full rack is always the
   one you can still grab. */
.obj.is-placed {
  z-index: 1;
}

.obj.is-placed .obj__body {
  filter: drop-shadow(0 1px 1px rgba(40, 30, 18, 0.22));
}

/* Stuck to the table: no grab cursor, almost no lift shadow.
   Stack depth among glued pieces is set inline from object index. */
.obj.is-glued .obj__body {
  cursor: default;
  filter: drop-shadow(0 0.5px 0.5px rgba(40, 30, 18, 0.18));
}

.obj.is-animated {
  transition: transform var(--dur-drop) var(--ease-snap);
}

.obj.is-held {
  z-index: 30;
}

.obj.is-held .obj__body {
  cursor: grabbing;
  filter: drop-shadow(0 7px 7px rgba(40, 30, 18, 0.24))
    drop-shadow(0 20px 30px rgba(40, 30, 18, 0.36));
}

.obj:focus-visible .obj__body {
  filter: drop-shadow(0 0 0 2px rgba(212, 165, 60, 0.9))
    drop-shadow(0 7px 12px rgba(40, 30, 18, 0.24));
}

/* --- feedback ---------------------------------------------- */

/* Landing squashes once. Without it a snap reads as teleporting,
   with it the object reads as having weight. */
.obj.is-seating .obj__body {
  animation: obj-seat var(--dur-snap) var(--ease-snap);
}

@keyframes obj-seat {
  0% {
    transform: scale(1.06);
  }
  55% {
    transform: scale(0.965);
  }
  100% {
    transform: scale(1);
  }
}

.obj.is-rejected .obj__body {
  animation: obj-reject 200ms var(--ease-reject);
}

@keyframes obj-reject {
  0%,
  100% {
    transform: translateX(0);
  }
  25% {
    transform: translateX(-5px);
  }
  75% {
    transform: translateX(5px);
  }
}

/* Nothing is grabbable once the puzzle is solved: the arrangement
   is the reward and must not be disturbed by a stray click. */
.is-solved .obj__body {
  pointer-events: none;
  cursor: default;
}
