/* ==========================================================================
   ACT 5 — kutu. The object.
   Ground #151009 (drifted by the engine). One pinned stage, span 1.9:
   the box owns the whole frame, the copy sits over it on the side the box
   leaves free. The box is a real 3D render (three.js) with a real 3D CSS
   fallback, so the stage is never empty.

   Both paths wear the same design, in the page's own palette: near-black
   printed stock, the deck's real album covers under a scrim, the wordmark in
   Archivo, one sleeve-orange rule, and a deep-orange base tray. The client's
   early box mockups in assets/box/ are untouched on disk and unused: they are
   flat purple and green and belong to a different page.

   THE SAFE RECT. The canvas is the whole stage, edge to edge, but the object
   is not: it is framed into #box-safe, the free zone the copy leaves. Four
   custom properties define that zone, once per breakpoint, and BOTH paths read
   them: .box-css is laid out into it, and js/05-box.js measures the same
   element in pixels and solves the camera against it. Change them here and the
   WebGL box and the CSS box move together. Nothing else may set them.

   Prefix: .box-  /  ids: box-title, box-object, box-gl, box-safe, box-css
   ========================================================================== */

.box {
  /* the ground this act drifts to, restated so the stage lighting agrees with
     it even before the drift has finished interpolating */
  --box-ground: #151009;
  --box-warm: #E9C089;
  --box-cool: #6C86AE;

  /* --- the safe rect: where the object is framed inside the full-bleed frame.
     Desktop holds the copy on the left, so the object takes the right. The rem
     floors on top and bottom are clearances, not taste: the top one clears the
     fixed header at any height, the bottom one clears the corner disc, which
     rises as the window gets shorter (CONTRACT appendix A.8). */
  --box-safe-t: max(5.5rem, 10%);
  --box-safe-r: 3%;
  --box-safe-b: max(6.5rem, 11%);
  --box-safe-l: 44%;
}

/* .box-stage carries [data-sc-stage], so the engine adds .sc-stage and owns
   its position/height. Never set position or height here. */
.box-stage {
  display: grid;
  align-content: center;
  justify-items: center;
}

/* --- the shelf: a pool of raking light, not a box shadow ----------------- */
.box-shelf {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(112% 58% at 66% 38%,
      color-mix(in oklab, var(--box-warm) 13%, transparent) 0%, transparent 64%),
    radial-gradient(68% 42% at 20% 76%,
      color-mix(in oklab, var(--box-cool) 9%, transparent) 0%, transparent 72%);
}
.box-shelf::after {
  content: "";
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 20%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--tt-line), transparent);
}

/* --- the object layer: the whole stage ----------------------------------
   Absolute, so it is out of the stage's grid flow and cannot be squeezed by
   the copy. isolation keeps the two 3D paths in one stacking context of their
   own; the scrim and the copy are later siblings and paint over it in document
   order, no z-index anywhere in this act. */
.box-object {
  position: absolute;
  inset: 0;
  isolation: isolate;
  pointer-events: none;
}

.box-gl {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* [hidden] is the UA default; this only takes effect once JS clears it */
  touch-action: pan-y;
}
.box-gl:not([hidden]) { display: block; }

/* The frame the object is composed into. #box-safe paints nothing and is the
   measurable copy of this rectangle; .box-css is laid out into it. */
.box-safe,
.box-css {
  position: absolute;
  inset: var(--box-safe-t) var(--box-safe-r) var(--box-safe-b) var(--box-safe-l);
}
.box-safe { pointer-events: none; }

/* the CSS box steps aside the moment WebGL is actually running */
.box-object.is-gl .box-css { display: none; }

/* --- frame: the copy, over the object ------------------------------------ */
.box-frame { display: grid; justify-items: start; }

.box-copy {
  display: grid;
  /* more space above the heading than below it */
  gap: clamp(1.5rem, 2.6vw, 2.4rem);
  /* kept just inside the safe rect's left edge at every width, so the text and
     the object never contend for the same column */
  max-width: min(32rem, 40%);
}
.box-title {
  font-variation-settings: "wght" 830, "wdth" 116;
  color: var(--sc-ink);
}

/* the two lines occupy the same cell and hand over mid-act */
.box-swap { display: grid; min-height: 5.6em; }
.box-line {
  grid-area: 1 / 1;
  align-self: start;
  margin: 0;
  font-family: var(--sc-font-text);
  font-size: clamp(1.08rem, 1.5vw, 1.34rem);
  line-height: 1.5;
  max-width: 34ch;
  color: color-mix(in oklab, var(--sc-ink) 88%, var(--box-ground));
  text-wrap: pretty;
}
.box-n {
  font-size: 1.34em;
  font-variation-settings: "wght" 820, "wdth" 82;
  color: var(--sc-ink);
  margin-inline-end: 0.06em;
}

/* --- the copy's ground ----------------------------------------------------
   A band under the text, not a sheet over the frame. It holds the page ground
   at 92% across the column the copy occupies and is fully transparent again
   at 47%, three points inside the safe rect's left edge, so the object it is
   protecting the text from is not itself dimmed. Measured on the composited
   page, the brightest ground under the copy is rgb(40,33,20): 13.4:1 for the
   title and the 400, 10.0:1 for the two lines, before this layer. */
.box-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--box-ground) 92%, transparent) 0%,
    color-mix(in oklab, var(--box-ground) 88%, transparent) 32%,
    color-mix(in oklab, var(--box-ground) 30%, transparent) 40%,
    transparent 47%);
}

/* ==========================================================================
   The CSS-only box. Real 3D.
   Geometry mirrors the three.js placeholder exactly, so the two paths read as
   the same object:
     lid   1.000 x 0.075 x 1.000   (the "1 unit" longest axis)
     base  0.986 x 0.186 x 0.986
     total silhouette height 0.222  ==  6cm on a 27cm box
   --lw is the lid footprint; every other dimension derives from it.
   ========================================================================== */
.box-css {
  /* the safe rect is the query container, so --lw below is a share of the
     rectangle the object is framed into and not of the whole stage */
  container-type: size;
}

.box-css__pose {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;

  /* --lw is one length shared by every face of the box. It has to resolve to
     the SAME number on all of them, so it cannot be a percentage: a percentage
     inside a custom property is re-resolved against each element that uses it,
     and the faces live inside a box that is itself sized by --lw, so it
     compounded and the box came out roughly 40% too small. Query units are
     resolved against the safe rect once, for everyone.
     66cqw matches what the WebGL camera does: at 45 degrees of yaw the box
     diagonal is 1.414 lid widths, so 1.414 x 0.66 = 0.94 of the safe rect,
     which is exactly the fill fraction js/05-box.js solves for. The height
     term keeps it inside the rect when the rect is wide and short. */
  --lw: min(66cqw, 92cqh, 34rem);
  --bw: calc(var(--lw) * 0.986);
  --hb: calc(var(--lw) * 0.186);
  --hl: calc(var(--lw) * 0.075);

  /* the three.js camera sits ~3 box-widths out on a 24 degree lens; matching
     that keeps the foreshortening identical between the two paths at any size */
  perspective: calc(var(--lw) * 3.05);
  perspective-origin: 50% 42%;
}
/* poses B and C exist for reduced motion only */
.box-css__pose--b,
.box-css__pose--c { display: none; }

/* the scrolling pose starts turned away and resolves square, exactly as the
   WebGL path does */
.box-css__pose--a { --pose-a: 118deg; --pose-l: 0; }

.box-css__body {
  position: relative;
  width: var(--lw);
  height: var(--lw);
  transform-style: preserve-3d;
  /* one continuous turn, paced by the scroll, the same 140 degrees the WebGL
     path uses, and resolving on the same frame */
  --box-spin: calc(var(--sc-p, 0) * -140deg);
  --box-lift: calc(var(--lw) * 0.035 * clamp(0, calc((var(--sc-p, 0) - 0.66) * 3), 1));
  /* these read --box-lift, so they have to be computed on this element */
  --z-base-mid: calc(var(--lw) * 0.093);
  --z-lid-mid: calc(var(--lw) * 0.1845 + var(--box-lift));
  --z-lid-top: calc(var(--lw) * 0.222 + var(--box-lift));
  transform:
    translateY(calc(var(--lw) * -0.05))
    rotateX(64deg)
    rotateZ(calc(var(--pose-a, -26deg) + var(--box-spin)));
}

.box-css__body > * {
  position: absolute;
  inset: 0;
  margin: auto;
  box-sizing: border-box;
  font-style: normal;
}

/* contact shadow, on the floor plane */
.box-css__sh {
  width: calc(var(--lw) * 1.5);
  height: calc(var(--lw) * 1.5);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    hsl(var(--sc-shadow-color) / 0.75) 0%,
    hsl(var(--sc-shadow-color) / 0.36) 46%,
    transparent 74%);
  transform: translate3d(0, 0, calc(var(--lw) * -0.012));
}

/* --- base tray: the one accent, second stop, as a solid ------------------ */
.box-css__w {
  width: var(--bw);
  height: var(--hb);
  background-color: var(--tt-accent-deep);
  background-image: linear-gradient(180deg,
    rgb(255 255 255 / 0.16) 0%,
    rgb(0 0 0 / 0.06) 46%,
    rgb(0 0 0 / 0.36) 100%);
}
.box-css__w--n { transform: translate3d(0, calc(var(--bw) / -2), var(--z-base-mid)) rotateX(-90deg); }
.box-css__w--s { transform: translate3d(0, calc(var(--bw) /  2), var(--z-base-mid)) rotateX(-90deg); }
.box-css__w--e { transform: translate3d(calc(var(--bw) /  2), 0, var(--z-base-mid)) rotateX(-90deg) rotateY(90deg); }
.box-css__w--w { transform: translate3d(calc(var(--bw) / -2), 0, var(--z-base-mid)) rotateX(-90deg) rotateY(90deg); }

/* --- lid rim: printed stock, one step off the page ground ---------------- */
.box-css__r {
  width: var(--lw);
  height: var(--hl);
  background-color: #14100B;
  background-image: linear-gradient(180deg,
    rgb(255 255 255 / 0.15) 0%,
    rgb(0 0 0 / 0.04) 52%,
    rgb(0 0 0 / 0.30) 100%);
}
.box-css__r--n { transform: translate3d(0, calc(var(--lw) / -2), var(--z-lid-mid)) rotateX(-90deg); }
.box-css__r--s { transform: translate3d(0, calc(var(--lw) /  2), var(--z-lid-mid)) rotateX(-90deg); }
.box-css__r--e { transform: translate3d(calc(var(--lw) /  2), 0, var(--z-lid-mid)) rotateX(-90deg) rotateY(90deg); }
.box-css__r--w { transform: translate3d(calc(var(--lw) / -2), 0, var(--z-lid-mid)) rotateX(-90deg) rotateY(90deg); }

/* --- lid top -------------------------------------------------------------
   Nine real album covers out of the deck under the page ground, the wordmark,
   one orange rule. This is the no-JavaScript composition; js/05-box.js swaps
   the whole thing for the generated 1024px canvas (sixteen covers, the same
   layout) as soon as it can, via --box-lid.
   ------------------------------------------------------------------------- */
.box-css__t {
  width: var(--lw);
  height: var(--lw);
  display: grid;
  align-content: start;
  justify-items: start;
  padding: 8%;
  transform: translate3d(0, 0, var(--z-lid-top));
  background-color: var(--sc-canvas);
  background-image:
    linear-gradient(180deg,
      rgb(16 12 9 / 0.95) 0%,
      rgb(16 12 9 / 0.44) 56%,
      rgb(16 12 9 / 0.84) 100%),
    linear-gradient(135deg,
      rgb(233 192 137 / 0.11) 0%,
      rgb(233 192 137 / 0.02) 34%,
      rgb(0 0 0 / 0.24) 100%),
    url("../assets/covers/sm/6A97BA938F.jpg"),
    url("../assets/covers/sm/E4D59BE406.jpg"),
    url("../assets/covers/sm/65964D8B10.jpg"),
    url("../assets/covers/sm/9B05D976D9.jpg"),
    url("../assets/covers/sm/B51F6EEB39.jpg"),
    url("../assets/covers/sm/EDA502CE26.jpg"),
    url("../assets/covers/sm/34A93A141A.jpg"),
    url("../assets/covers/sm/8BF0657B62.jpg"),
    url("../assets/covers/sm/9FF052FC5E.jpg");
  background-size:
    auto, auto,
    33.34% 33.34%, 33.34% 33.34%, 33.34% 33.34%,
    33.34% 33.34%, 33.34% 33.34%, 33.34% 33.34%,
    33.34% 33.34%, 33.34% 33.34%, 33.34% 33.34%;
  background-position:
    0 0, 0 0,
    0% 0%, 50% 0%, 100% 0%,
    0% 50%, 50% 50%, 100% 50%,
    0% 100%, 50% 100%, 100% 100%;
  background-repeat: no-repeat;
  box-shadow: inset 0 0 0 1px rgb(243 234 220 / 0.14);
}

.box-css__mark {
  font-family: var(--sc-font-display);
  font-variation-settings: "wght" 840, "wdth" 122;
  font-weight: 800;
  font-size: calc(var(--lw) * 0.153);
  line-height: 0.88;
  letter-spacing: -0.03em;
  color: var(--sc-ink);
}
.box-css__rule {
  width: 30%;
  height: calc(var(--lw) * 0.0095);
  margin-block-start: 7%;
  background: var(--sc-accent);
}
.box-css__tag {
  margin-block-start: 4.5%;
  font-family: var(--sc-font-text);
  font-size: calc(var(--lw) * 0.031);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sc-ink-soft);
}

/* JS has generated the real lid canvas: use it and drop the DOM stand-in */
.box-css.is-painted .box-css__t {
  background-image: var(--box-lid);
  background-size: 100% 100%;
  background-position: 0 0;
}
.box-css.is-painted .box-css__t > * { display: none; }
.box-css.is-painted .box-css__r {
  background-image: var(--box-rim);
  background-size: 100% 100%;
  background-position: 0 0;
}

/* --- reduced motion: nothing moves, three fixed angles cross-fade -------- */
@media (prefers-reduced-motion: reduce) {
  .box-css__pose--b,
  .box-css__pose--c { display: grid; }
  .box-css__body {
    --box-spin: 0deg;
    --box-lift: calc(var(--lw) * 0.035 * var(--pose-l, 0));
  }
  /* three fixed angles, ending on the same square frame the turn resolves to */
  .box-css__pose--a { --pose-a: 74deg;  --pose-l: 0; }
  .box-css__pose--b { --pose-a: 26deg;  --pose-l: 0.4; }
  .box-css__pose--c { --pose-a: -22deg; --pose-l: 1; }
  /* pose A is the floor and never leaves; B and C wash in over it and hold */
  /* A real cross-fade: the pose that is leaving has to leave, or all three sit
     on top of each other and the act reads as a pile of boxes. A goes out as B
     comes in, B goes out as C comes in, and C holds to the end. Opacity only:
     under reduced motion nothing on this element moves. */
  .box-css__pose--a {
    opacity: clamp(0, calc(1 - (var(--sc-p, 0) - 0.30) * 7), 1);
  }
  .box-css__pose--b {
    opacity: min(clamp(0, calc((var(--sc-p, 0) - 0.30) * 7), 1),
                 clamp(0, calc(1 - (var(--sc-p, 0) - 0.62) * 7), 1));
  }
  .box-css__pose--c { opacity: clamp(0, calc((var(--sc-p, 0) - 0.62) * 7), 1); }
}

/* ==========================================================================
   Mobile. Portrait is designed here, not patched here.
   The stage is still one full-bleed frame; what changes is which side of it
   the copy takes. In portrait it takes the bottom, so the safe rect becomes
   the band above it. --box-copy-pad is how far the copy sits off the bottom
   edge (clear of the corner disc), and the safe rect's bottom inset is that
   plus the copy's own block, so the two can never overlap.
   ========================================================================== */
@media (max-width: 860px) {
  .box {
    --box-copy-pad: clamp(5rem, 23svh, 12rem);
    --box-safe-t: max(5.5rem, 8%);
    --box-safe-r: 4%;
    --box-safe-b: calc(var(--box-copy-pad) + 10rem);
    --box-safe-l: 4%;
  }
  .box-stage { align-content: end; }
  .box-frame {
    justify-items: center;
    padding-block-end: var(--box-copy-pad);
  }
  .box-copy {
    max-width: none;
    justify-items: center;
    text-align: center;
    gap: clamp(0.9rem, 2.4vw, 1.5rem);
  }
  .box-line { max-width: 30ch; }
  .box-shelf::after { bottom: 42%; }

  /* the band turns with the copy: bottom-up instead of left-in, transparent
     again well above the object */
  .box-scrim {
    background: linear-gradient(0deg,
      color-mix(in oklab, var(--box-ground) 92%, transparent) 0%,
      color-mix(in oklab, var(--box-ground) 88%, transparent) 42%,
      color-mix(in oklab, var(--box-ground) 30%, transparent) 50%,
      transparent 58%);
  }
}

@media (max-width: 620px) {
  .box-title { font-size: clamp(2rem, 10vw, 2.9rem); }
  .box-swap { min-height: 6.4em; }
  .box-line { font-size: 1.02rem; max-width: 28ch; }
}

/* A short viewport, phone landscape included: stacking the copy under the box
   leaves neither of them room, so both go back to sharing the width. This has
   to come after the portrait block to win at 844x390. */
@media (max-height: 560px) {
  .box {
    --box-safe-t: max(4.5rem, 9%);
    --box-safe-r: 3%;
    --box-safe-b: max(5rem, 10%);
    --box-safe-l: 44%;
  }
  .box-stage { align-content: center; justify-items: center; }
  .box-frame { justify-items: start; padding-block-end: 0; }
  .box-copy {
    max-width: min(32rem, 40%);
    justify-items: start;
    text-align: start;
  }
  .box-shelf::after { bottom: 14%; }
  .box-scrim {
    background: linear-gradient(90deg,
      color-mix(in oklab, var(--box-ground) 92%, transparent) 0%,
      color-mix(in oklab, var(--box-ground) 88%, transparent) 32%,
      color-mix(in oklab, var(--box-ground) 30%, transparent) 40%,
      transparent 47%);
  }
}
