/* 3D product viewer (viewer3d.js): the CSS-3D model of the customer's keychain
   (scene3d.js + model-album.js / model-slab.js) and the step layout that puts
   it beside the editing surface. Shared by the customer flow (index.html) and
   the POC page (poc-3d.html). Component looks (tabs, crop stage, controls)
   come from styles.css. */

/* --- step layout: viewer + editing surface --- */

/* Phones: one column in normal flow (a sticky child needs the whole layout
   as its containing block, which a grid item would not get). The viewer
   sticks to the top of the viewport while the editor scrolls under it, so the
   model stays in view while a photo is dragged or a song is typed. The page
   background hides what scrolls past the rounded corners. */
.step-layout {
  display: block;
  margin-top: 12px;
}

.step-viewer {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bg);
  padding: 4px 0 8px;
  transition: margin-bottom 0.2s ease;
}

/* Padding, not margin: a margin would collapse with the slot's collapse
   margin below and shift the editor by the gap. */
.step-editor {
  padding-top: 14px;
}

.step-editor > :first-child {
  margin-top: 0;
}

/* Collapsed while the customer works in the editor (viewer3d.js sets
   `is-compact` once the slot is stuck): a short stage, no caption or shadow,
   smaller pills. The slot keeps its full in-flow footprint by trading stage
   height for bottom margin, so nothing under the finger jumps when it
   collapses or expands, and the page shows through the band it frees. Both
   ease over the same time so the footprint holds throughout. */
@media (max-width: 899px) {
  .step-viewer.is-compact {
    margin-bottom: 114px; /* 264 - 150 */
  }
  .step-viewer.is-compact:has(.scene3d-tall) {
    margin-bottom: 150px; /* 300 - 150 */
  }
  .step-viewer.is-compact .scene-caption,
  .step-viewer.is-compact .scene3d-shadow {
    display: none;
  }
  .step-viewer.is-compact .scene-hint,
  .step-viewer.is-compact .scene-reset {
    top: 8px;
    padding: 5px 9px;
    font-size: 0.72rem;
  }
}

@media (min-width: 900px) {
  .step-layout {
    display: grid;
    grid-template-columns: minmax(340px, 5fr) minmax(380px, 6fr);
    gap: 24px;
    align-items: start;
  }
  .step-viewer {
    top: 12px;
    padding: 0;
  }
  .step-editor {
    padding-top: 0;
  }
}

/* Preview: nothing to edit, so the viewer sits above the print faces at
   full width and scrolls with the page. */
.step-layout.is-stacked {
  grid-template-columns: 1fr;
}
.step-layout.is-stacked .step-viewer {
  position: static;
  padding: 0;
}

/* --- viewer frame --- */

.scene3d-frame {
  position: relative;
}

.scene3d {
  position: relative;
  border-radius: 14px;
  background: linear-gradient(180deg, #f7f5fc 0%, #e9e5f5 100%);
  overflow: hidden;
}

/* The stage's height is its `--stage-h`, which the scene (scene3d.js) also
   reads as its fit target, so a stage mid-way through the height transition
   is fitted to where it ends up. `--stage-reserve` is the band kept clear at
   the bottom for the caption and shadow. */
.scene3d-stage {
  --stage-h: 380px;
  --stage-reserve: 64px;
  height: var(--stage-h);
  transition: height 0.2s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  perspective: 1100px;
  /* The whole stage is the orbit handle. Touch turns the model sideways only
     (pan-y): vertical swipes scroll the page, so a phone screen filled by the
     model and the crop editor never traps the customer. No text selection
     while dragging. */
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  outline: none;
}

/* The keychain composite is portrait (ring on top), so its stage is taller. */
.scene3d-tall .scene3d-stage {
  --stage-h: 440px;
}

/* Shorter while stuck to the top of a phone screen, shorter still once the
   customer scrolls into the editor (is-compact); the preview (stacked) keeps
   room to turn the product around. */
@media (max-width: 899px) {
  .step-viewer .scene3d-stage {
    --stage-h: 264px;
  }
  .step-viewer .scene3d-tall .scene3d-stage {
    --stage-h: 300px;
  }
  .step-viewer.is-compact .scene3d-stage,
  .step-viewer.is-compact .scene3d-tall .scene3d-stage {
    --stage-h: 150px;
    --stage-reserve: 24px; /* no caption; just clearance for the tilt's perspective */
  }
  .is-stacked .step-viewer .scene3d-stage {
    --stage-h: 340px;
  }
  .is-stacked .step-viewer .scene3d-tall .scene3d-stage {
    --stage-h: 400px;
  }
}

.scene3d-stage:focus-visible {
  box-shadow: inset 0 0 0 3px #b9a6ec;
}

.scene3d-stage.is-dragging {
  cursor: grabbing;
}

/* Drag follows the pointer 1:1; the easing is only for preset-to-preset.
   `is-instant` is the scene committing a page-driven refit (a slot change or
   the phone collapse) without the easing. */
.scene3d-stage.is-dragging .model3d,
.scene3d-stage.is-dragging .face3d,
.scene3d-stage.is-dragging .tab3d,
.scene3d-stage.is-dragging .hub3d,
.scene3d-stage.is-dragging .ring3d,
.scene3d-stage.is-dragging .cd3d-front,
.scene3d-stage.is-dragging .cd3d-back,
.scene3d-stage.is-instant .model3d,
.scene3d-stage.is-instant .face3d,
.scene3d-stage.is-instant .tab3d,
.scene3d-stage.is-instant .hub3d,
.scene3d-stage.is-instant .ring3d,
.scene3d-stage.is-instant .cd3d-front,
.scene3d-stage.is-instant .cd3d-back {
  transition: none;
}

/* Overlay controls: a "drag to rotate" nudge at rest, "Reset view" once the
   user has turned the product away from the section's preset. */
.scene-hint,
.scene-reset {
  position: absolute;
  top: 12px;
  font: inherit;
  font-size: 0.8rem;
  line-height: 1;
  padding: 7px 11px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.82);
  color: #5d5a73;
  border: 1px solid rgba(26, 26, 38, 0.1);
  backdrop-filter: blur(4px);
}

.scene-hint {
  left: 12px;
  margin: 0;
  pointer-events: none;
}

.scene-reset {
  right: 12px;
  min-height: 0;
  cursor: pointer;
  color: #6f4cc7;
  font-weight: 600;
}

.scene-reset:hover {
  background: #fff;
}

.scene-reset:focus-visible {
  outline: 3px solid #b9a6ec;
}

/* The section caption lies along the bottom of the scene, over the shadow
   area the fit keeps clear, so it costs no height on a phone. */
.scene-caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 18px 14px 9px;
  font-size: 12px;
  line-height: 1.35;
  color: #5d5a73;
  text-align: center;
  background: linear-gradient(180deg, rgba(233, 229, 245, 0) 0%, rgba(233, 229, 245, 0.95) 45%);
  pointer-events: none;
}

.scene-caption:empty {
  display: none;
}

/* Section buttons under the model (preview step): the crop tabs' look. */
.viewer3d-views {
  margin-top: 10px;
  justify-content: center;
  padding-bottom: 0;
}

.scene3d-shadow {
  position: absolute;
  left: 50%;
  bottom: 22px;
  width: 46%;
  height: 26px;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at center, rgba(26, 26, 38, 0.18), transparent 70%);
  pointer-events: none;
}

/* --- the models --- */

/* The rotating model root (album case or keychain slab). */
.model3d {
  position: relative;
  transform-style: preserve-3d;
  transition: transform 0.9s cubic-bezier(0.33, 0.9, 0.35, 1);
}

.tray3d,
.lid3d,
.lid3d-pivot {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

.lid3d-pivot {
  transform-origin: 0% 50%;
  transition: transform 0.9s cubic-bezier(0.33, 0.9, 0.35, 1);
}

@property --lit {
  syntax: '<number>';
  inherits: false;
  initial-value: 0.8;
}

/* Clear plastic: only the prints are opaque. A faint tint and edge line keep
   the slab legible without hiding what's behind it - the hinge column beside
   the front cover shows the tray inside, exactly like the real case. */
.face3d {
  position: absolute;
  left: 50%;
  top: 50%;
  backface-visibility: hidden;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(26, 26, 38, 0.16);
  box-sizing: border-box;
}

.face3d canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Thick clear edges read frostier than the faces (light piping). */
.face3d-edge {
  background: rgba(255, 255, 255, 0.42);
}

/* Lighting: a dark overlay whose opacity follows the part's `--lit`, set per
   frame from its world-space normal. On print faces it covers only the print
   rect (--px/--py/--pw/--ph), so the clear margins stay clear. An overlay,
   not filter:, so preserve-3d is never flattened. Eased between presets,
   instant while dragging (see .is-dragging). */
.face3d,
.tab3d,
.hub3d,
.ring3d,
.cd3d-front,
.cd3d-back {
  transition: --lit 0.9s cubic-bezier(0.33, 0.9, 0.35, 1);
}

.face3d::after,
.tab3d::after,
.hub3d::after,
.ring3d::after,
.cd3d-front::after,
.cd3d-back::after {
  content: '';
  position: absolute;
  left: var(--px, 0);
  top: var(--py, 0);
  width: var(--pw, 100%);
  height: var(--ph, 100%);
  background: #1a1a26;
  opacity: calc((1 - var(--lit, 0.8)) * 0.32);
  pointer-events: none;
}

.hub3d::after,
.ring3d::after,
.cd3d-front::after,
.cd3d-back::after {
  border-radius: 50%;
}

/* --- keychain slab (acrylic + Spotify) --- */

/* The slab's big glass faces sit in front of the print; they keep the tint
   and edge line but skip the dark overlay, so turning the slab never dims the
   print seen through them. The print faces at the core carry the lighting. */
.face3d-glass::after {
  content: none;
}

.face3d-print {
  background: none;
  border: 0;
}

/* The slab's tab is four stacked slices across its depth, so each slice is
   lighter than the album's single flat tab, and only the outer two slices
   draw the edge line. */
.slab3d .tab3d {
  background: rgba(255, 255, 255, 0.3);
}

.slab3d .tab3d-inner {
  border-color: transparent;
}

/* Split ring: brushed steel, punched to a ring by a mask (the mask also clips
   the lighting overlay), stacked in three slices for wire thickness. */
.ring3d {
  position: absolute;
  left: 50%;
  top: 50%;
  border-radius: 50%;
  background: conic-gradient(
    from 210deg,
    #b8b9c2,
    #f2f3f6 18%,
    #9a9ba6 38%,
    #e4e5ea 58%,
    #a9aab4 78%,
    #b8b9c2
  );
  -webkit-mask: radial-gradient(
    circle closest-side,
    transparent calc(100% - var(--wire)),
    #000 calc(100% - var(--wire) + 0.5px)
  );
  mask: radial-gradient(
    circle closest-side,
    transparent calc(100% - var(--wire)),
    #000 calc(100% - var(--wire) + 0.5px)
  );
}

/* Seam of the snap-in back panel, on the back glass face. */
.seam3d {
  position: absolute;
  background: rgba(26, 26, 38, 0.2);
  pointer-events: none;
}

/* --- mini album --- */

.cd3d-front::after {
  -webkit-mask: radial-gradient(circle, transparent var(--hole), #000 calc(var(--hole) + 0.5px));
  mask: radial-gradient(circle, transparent var(--hole), #000 calc(var(--hole) + 0.5px));
}

/* Booklet corner clips inside the lid. */
.clip3d {
  position: absolute;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.55);
  border: 1px solid rgba(26, 26, 38, 0.14);
  border-radius: 2px;
  pointer-events: none;
}

/* Keychain loop tab: a flat clear piece with a hole, seen from both sides. */
.tab3d {
  position: absolute;
  left: 50%;
  top: 50%;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid rgba(26, 26, 38, 0.18);
  border-radius: 45% 45% 0 0 / 60% 60% 0 0;
  -webkit-mask: radial-gradient(circle at 50% 44%, transparent var(--tab-hole), #000 calc(var(--tab-hole) + 0.5px));
  mask: radial-gradient(circle at 50% 44%, transparent var(--tab-hole), #000 calc(var(--tab-hole) + 0.5px));
}

/* Raised rim of the disc well on the tray card. */
.well3d {
  position: absolute;
  left: 50%;
  top: 50%;
  box-sizing: border-box;
  border-radius: 50%;
  border: 1.5px solid rgba(26, 26, 38, 0.14);
  background: rgba(255, 255, 255, 0.08);
  backface-visibility: hidden;
}

/* Spindle hub: concentric moulded rings, shows through the disc's hole. */
.hub3d {
  position: absolute;
  left: 50%;
  top: 50%;
  box-sizing: border-box;
  border-radius: 50%;
  border: 1px solid rgba(26, 26, 38, 0.2);
  background: radial-gradient(
    circle,
    #f6f6fa 0 28%,
    #cfd0da 30% 42%,
    #eeeef4 44% 68%,
    #c8c9d4 70% 78%,
    #e9e9f0 80%
  );
  backface-visibility: hidden;
}

.cd3d {
  position: absolute;
  left: 50%;
  top: 50%;
  transition: transform 0.9s cubic-bezier(0.33, 0.9, 0.35, 1);
  transform-style: preserve-3d;
}

.cd3d-front {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  backface-visibility: hidden;
}

.cd3d-front canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Data side of the disc: brushed silver with the faint rainbow of a real CD,
   hub ring, and the same punched centre hole as the printed face. */
.cd3d-back {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  transform: rotateY(180deg);
  backface-visibility: hidden;
  background:
    radial-gradient(
      circle,
      rgba(255, 255, 255, 0.85) 0 19%,
      rgba(150, 150, 160, 0.55) 19.5% 22%,
      rgba(255, 255, 255, 0.35) 22.5% 24%,
      transparent 25%
    ),
    conic-gradient(
      from 200deg,
      #c6c7cd,
      #e6e7ec 10%,
      #b7c3d8 22%,
      #d8cfe6 34%,
      #c6c7cd 46%,
      #e1e6ea 58%,
      #bcc5d3 70%,
      #d2cbd9 82%,
      #c6c7cd
    );
  -webkit-mask: radial-gradient(circle, transparent var(--hole), #000 calc(var(--hole) + 0.5px));
  mask: radial-gradient(circle, transparent var(--hole), #000 calc(var(--hole) + 0.5px));
}

@media (prefers-reduced-motion: reduce) {
  .scene3d-stage,
  .step-viewer,
  .model3d,
  .lid3d-pivot,
  .cd3d,
  .face3d,
  .tab3d,
  .hub3d,
  .ring3d,
  .cd3d-front,
  .cd3d-back {
    transition: none;
  }
}
