/* ============================================================
   Level 10 — Разбитая ваза
   A broken celadon vessel on a walnut table. Assemble the
   fragments; a light then runs each crack and closes it, leaving
   the glaze unmarked, which is the whole point of the level.
   ============================================================ */

/* Seen from above, onto a walnut table. An earlier version of this
   level drew a wall and a table divided by a horizon, but the
   horizon had to be expressed in viewport units while the fragments
   are in stage units — so on some window shapes the pieces ended up
   lying on the wall. One surface, seen square on, is both truer to
   the rest of the campaign and impossible to get wrong.

   Light comes from the upper left, which is the convention every
   object in the game is shaded for. */
.scene {
  position: fixed;
  inset: 0;
  background-color: #7c6244;
  /* Grain in three passes at low contrast and slightly different
     spacings. One repeating pass at high contrast reads as a
     barcode; several faint ones out of phase read as wood. */
  background-image: radial-gradient(
      ellipse 76% 68% at 36% 18%,
      rgba(255, 236, 206, 0.15) 0%,
      rgba(255, 236, 206, 0.04) 46%,
      transparent 74%
    ),
    repeating-linear-gradient(
      92deg,
      rgba(46, 32, 18, 0.07) 0 3px,
      transparent 3px 19px,
      rgba(46, 32, 18, 0.045) 19px 21px,
      transparent 21px 47px
    ),
    repeating-linear-gradient(
      89deg,
      rgba(255, 236, 206, 0.035) 0 2px,
      transparent 2px 31px,
      rgba(46, 32, 18, 0.05) 31px 33px,
      transparent 33px 73px
    ),
    repeating-linear-gradient(90.6deg, rgba(46, 32, 18, 0.035) 0 5px, transparent 5px 26px),
    linear-gradient(158deg, #8b7150 0%, #7a6041 52%, #5c4830 100%);
}

/* ============================================================
   The cloth

   Plum linen, and the reason this level still reads as the one cool
   room in the campaign. It is inset from the stage so a border of
   walnut frames it, which also means the letterboxed margins on an
   odd window shape read as more table rather than as dead space.
   ============================================================ */

.cloth {
  position: absolute;
  inset: 46px 62px;
  border-radius: 3px;
  background-color: #625a6b;
  background-image:
    /* The crease left by having been put away. Off-centre, and
       spread over a tenth of the cloth: any narrower and it stops
       reading as a soft fold in linen and starts reading as a line
       someone drew. */
    linear-gradient(
      90deg,
      transparent 32%,
      rgba(38, 30, 44, 0.035) 37.5%,
      rgba(255, 240, 218, 0.022) 39.5%,
      rgba(38, 30, 44, 0.03) 41.5%,
      transparent 47%
    ),
    /* Slow mottling, so the dye is uneven the way dyed linen is. */
      radial-gradient(ellipse 44% 36% at 76% 30%, rgba(255, 240, 218, 0.05), transparent 70%),
    radial-gradient(ellipse 52% 40% at 18% 74%, rgba(34, 26, 40, 0.09), transparent 72%),
    radial-gradient(ellipse 30% 46% at 62% 88%, rgba(255, 240, 218, 0.035), transparent 70%),
    /* Weave: two faint passes crossed, never one strong one. */
      repeating-linear-gradient(90deg, rgba(255, 240, 218, 0.028) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(0deg, rgba(34, 26, 40, 0.05) 0 1px, transparent 1px 4px),
    radial-gradient(ellipse 74% 62% at 33% 14%, #7b7185 0%, #675e73 44%, #534b60 100%);
  box-shadow: 0 2px 3px rgba(30, 20, 10, 0.3), 0 22px 44px -14px rgba(30, 20, 10, 0.5),
    inset 0 1px 0 rgba(255, 240, 218, 0.12);
  transform: rotate(-0.5deg);
}

/* A hand-stitched hem, one of the few places in the game where a
   dashed line is literal rather than decorative. */
.cloth__hem {
  position: absolute;
  inset: 15px;
  border: 1.5px dashed rgba(255, 240, 218, 0.17);
  border-radius: 2px;
}

/* The cloth is not perfectly flat: the corners lift a little. */
.cloth::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 3px;
  background: radial-gradient(ellipse 120% 80% at 50% 50%, transparent 58%, rgba(30, 20, 10, 0.2) 100%);
}

.stage-wrap {
  position: fixed;
  inset: 0;
  z-index: 5;
}

.stage {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: center center;
}

.defs {
  position: absolute;
  width: 0;
  height: 0;
}

/* ============================================================
   The vessel's footprint
   ============================================================ */

.ghost,
.seams {
  position: absolute;
  top: var(--vy);
  left: var(--vx);
  width: 400px;
  height: 560px;
  overflow: visible;
  pointer-events: none;
}

.ghost {
  opacity: 0.85;
  filter: blur(0.4px);
  transition: opacity var(--dur-scene) var(--ease-soft);
}

.is-solved .ghost {
  opacity: 0;
}

.seams {
  z-index: 4;
}

.seam {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The crack as it sits on a vessel that has been put back together
   but not yet mended: a hairline, hidden until the last fragment
   lands so that a half-built vessel is not covered in lines. */
.seam--rest {
  stroke: rgba(44, 38, 28, 0.42);
  opacity: 0;
}

/* The two light strokes are a short dash that travels the crack.
   dasharray is "dash, everything else", so exactly one segment is
   ever on the path, and dashoffset walks it from before the start
   to past the end. */
.seam--bloom,
.seam--core {
  stroke-dasharray: var(--dash) calc(var(--len) + var(--dash) * 2);
  stroke-dashoffset: var(--dash);
  opacity: 0;
}

.seam--bloom {
  stroke: url(#healLight);
}

.seam--core {
  stroke: rgba(255, 253, 244, 0.95);
}

/* The sweep. Each crack starts a beat after the one before, so the
   mending travels around the vessel instead of flashing at once. */
.is-solved .seam--bloom,
.is-solved .seam--core {
  animation: heal-sweep 760ms var(--ease-inout) both;
  animation-delay: calc(300ms + var(--si, 0) * 120ms);
}

/* The crack shows itself just long enough to be seen closing, then
   is gone for good — the payoff is a vessel with no history. */
.is-solved .seam--rest {
  animation: heal-close 900ms var(--ease-soft) both;
  animation-delay: calc(180ms + var(--si, 0) * 120ms);
}

@keyframes heal-sweep {
  0% {
    stroke-dashoffset: var(--dash);
    opacity: 0;
  }
  14% {
    opacity: 1;
  }
  82% {
    opacity: 1;
  }
  100% {
    stroke-dashoffset: calc(var(--len) * -1);
    opacity: 0;
  }
}

@keyframes heal-close {
  0% {
    opacity: 0;
  }
  22% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

.glow {
  position: absolute;
  top: var(--vy);
  left: var(--vx);
  z-index: 3;
  width: 400px;
  height: 560px;
  pointer-events: none;
  background: radial-gradient(
    ellipse 62% 54% at 50% 52%,
    rgba(255, 250, 232, 0.5),
    transparent 70%
  );
  opacity: 0;
}

/* Held back until the last crack has closed: the bloom is the vessel
   settling into being whole, not a firework over a work in progress. */
.is-solved .glow {
  animation: bloom 1600ms var(--ease-soft) 1080ms both;
}

@keyframes bloom {
  0% {
    opacity: 0;
    transform: scale(0.9);
  }
  40% {
    opacity: 1;
  }
  100% {
    opacity: 0.5;
    transform: scale(1.04);
  }
}

/* ============================================================
   Fragments

   Each shard element spans the whole vessel box; only its clipped
   ceramic is painted. pointer-events therefore has to be scoped
   to the painted path, or the invisible boxes would shadow one
   another.
   ============================================================ */

.shards {
  position: absolute;
  z-index: 2;
  inset: 0;
}

.shard {
  position: absolute;
  /* Stacking is done with z-index rather than DOM order. Reordering
     the children to raise a fragment would re-parent it, and
     re-parenting a focused element blurs it — which silently breaks
     keyboard play the moment anything is picked up or placed. */
  z-index: 2;
  /* The shard box is the vessel box, so a seated fragment is
     simply an offset of zero. */
  top: var(--vy);
  left: var(--vx);
  width: 400px;
  height: 560px;
  pointer-events: none;
  transform: translate(calc(var(--sx) * 1px), calc(var(--sy) * 1px)) rotate(var(--rot, 0deg));
  transform-origin: calc(var(--cx) * 1px) calc(var(--cy) * 1px);
  outline: none;
}

.shard svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

/* Only the ceramic itself is grabbable. */
.shard__body {
  pointer-events: auto;
  cursor: grab;
  transition: filter var(--dur-lift) var(--ease-soft);
  filter: drop-shadow(0 2px 2px rgba(30, 18, 8, 0.24)) drop-shadow(0 8px 14px rgba(30, 18, 8, 0.28));
}

.shard.is-animated {
  transition: transform var(--dur-drop) var(--ease-snap);
}

.shard.is-held {
  z-index: 30;
}

.shard.is-held .shard__body {
  cursor: grabbing;
  filter: drop-shadow(0 8px 8px rgba(30, 18, 8, 0.26)) drop-shadow(0 22px 34px rgba(30, 18, 8, 0.4));
}

.shard:focus-visible .shard__body {
  filter: drop-shadow(0 0 0 2px rgba(212, 165, 60, 0.9))
    drop-shadow(0 8px 14px rgba(30, 18, 8, 0.28));
}

/* A seated fragment stops casting its own big shadow — it is part
   of the vessel now, not an object lying next to it. */
/* Seated fragments sit behind the loose ones, so a piece dropped
   over the vessel is always the one you can still grab. */
.shard.is-seated {
  z-index: 1;
  pointer-events: none;
}

.shard.is-seated .shard__body {
  cursor: default;
  filter: drop-shadow(0 1px 1px rgba(30, 18, 8, 0.2));
}

/* Loose fragments show a bright raw edge; seated ones let it sink
   almost away, so the vessel reads as assembled rather than as a
   pile of pieces that happen to touch. */
.shard__break {
  opacity: 1;
  transition: opacity var(--dur-drop) var(--ease-soft);
}

.shard.is-seated .shard__break {
  opacity: 0.28;
}

/* The raw edges dissolve across the whole sweep rather than snapping
   off, so the glaze closes behind the light instead of the breaks
   vanishing a moment before it arrives.

   `.shard.is-seated` above is two classes on the same element, so it
   outranks a plain `.is-solved` ancestor selector; the seated state
   has to be named here as well or the breaks never reach zero. */
.is-solved .shard.is-seated .shard__break {
  opacity: 0;
  transition-delay: 320ms;
  transition-duration: 1200ms;
}

@keyframes shard-seat {
  0% {
    transform: scale(1.035);
  }
  55% {
    transform: scale(0.992);
  }
  100% {
    transform: scale(1);
  }
}

.shard.is-seating .shard__body {
  animation: shard-seat var(--dur-snap) var(--ease-snap);
}

.shard.is-rejected .shard__body {
  animation: reject-shake 180ms var(--ease-reject);
}

/* ============================================================
   The mended vessel: one layer, no seams to leak antialiasing.
   ============================================================ */

.whole {
  position: absolute;
  z-index: 3;
  top: var(--vy);
  left: var(--vx);
  width: 400px;
  height: 560px;
  pointer-events: none;
  opacity: 0;
}

.whole svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* One shadow for one object, matching what a seated fragment casts so
   the exchange is invisible. */
.whole__body {
  filter: drop-shadow(0 2px 3px rgba(30, 18, 8, 0.26))
    drop-shadow(0 12px 20px rgba(30, 18, 8, 0.24));
}

/* The exchange happens under the travelling light, which runs at
   z-index 4 and so stays visible over it. By the time the light
   leaves the last crack there is nothing underneath but glaze. */
.is-solved .whole {
  animation: whole-in 900ms var(--ease-soft) 340ms both;
}

.is-solved .shard.is-seated .shard__body {
  animation: fragments-out 900ms var(--ease-soft) 340ms both;
}

@keyframes whole-in {
  to {
    opacity: 1;
  }
}

@keyframes fragments-out {
  to {
    opacity: 0;
  }
}

/* ============================================================
   Completion: the vessel lifts a little and breathes once.
   ============================================================ */

.is-solved .shards {
  animation: vessel-rise var(--dur-reward) var(--ease-snap) both;
}

@keyframes vessel-rise {
  0% {
    transform: translateY(0);
  }
  45% {
    transform: translateY(-10px);
  }
  100% {
    transform: translateY(0);
  }
}
