/*
 * ---------------------------------------------------------------------
 *  KUTIJA
 * ---------------------------------------------------------------------
 *
 *  One box, built from six faces pushed out of a single point, plus the
 *  ground shadow under it and the blurred cover behind it.
 *
 *  A face with no scan is black on purpose: nothing here draws cardboard
 *  or paper that is not in a file.
 */


.scene {
    width: var(--box-width);
    height: var(--box-height);

    perspective: calc(var(--perspective) * var(--box-scale));

    perspective-origin: var(--perspective-origin);

    /*
     * The box and the diskettes beside it have to share one 3D
     * space, not just one perspective. Flattened, they would be
     * painted in the order they appear in the markup, and a
     * diskette that the turn has carried round behind the box
     * would still be drawn over its cover. With this, what is
     * behind the box is hidden by the box.
     */
    transform-style: preserve-3d;
}

/*
 * The book itself.
 * Every face sits at the centre of this element and is then
 * pushed outwards with translateZ, so the six faces meet
 * at the edges of the box described by the size variables.
 */
.book {
    position: relative;

    width: var(--box-width);
    height: var(--box-height);

    transform-style: preserve-3d;

    /*
     * Every box on the page derives its own measurements from
     * its own --width / --height / --depth. The identical chain
     * exists on :root, but a custom property is resolved where
     * it is declared, so a value inherited from :root would
     * carry :root's numbers no matter what a box asked for.
     * Repeating it here is what lets a shelf of differently
     * sized boxes each come out right.
     */
    --box-width:  calc(var(--width)  * var(--box-scale));
    --box-height: calc(var(--height) * var(--box-scale));
    --box-depth:  calc(var(--depth)  * var(--box-scale));

    --half-width:  calc(var(--box-width)  / 2);
    --half-height: calc(var(--box-height) / 2);
    --half-depth:  calc(var(--box-depth)  / 2);

    --corner: calc(1px * var(--box-scale));

    /* the script overwrites this every frame */
    transform:
        rotateX(-8deg)
        rotateY(-28deg);
}

/*
 * A face is black until a scan says otherwise. Everything drawn
 * on below — the tooth, the sheen, the printed board — hangs off
 * .has-art, so a side with no file stays flat black rather than
 * being dressed up as a book it is not.
 */
.face {
    position: absolute;

    background-color: #000;

    border-radius: var(--corner);

    backface-visibility: hidden;

    /*
     * The script writes a lighting value into --shade on every
     * face each frame: 1 is facing the light, --ambient is
     * turned fully away from it.
     */
    --shade: 1;

    filter: brightness(var(--shade));
}

/* overlays have to follow the curve or they square the corner off */
.face::before,
.face::after {
    border-radius: inherit;
}

/* ---- covers ------------------------------------------------ */

.front,
.back {
    inset: 0;

    background-size: 100% 100%;
    background-position: center;

    /*
     * Where the highlight sits and how strong it is. Declared
     * here rather than on ::after so the values the script
     * writes onto the face are inherited by the overlay.
     */
    --spec-x: 50%;
    --spec-y: 30%;
    --spec-a: 0;
}

.front {
    background-image: var(--art-front);

    transform: translateZ(var(--half-depth));
}


/* ---- a cover that opens ------------------------------------- */

/*
 * Some big boxes are not sealed: the front is a flap hinged down the
 * spine, and under it there is more printing — a map, a spread, the
 * contents listed. This is that flap.
 *
 * It holds the cover and the cover's underside, so the two turn
 * together. Closed it changes nothing: the cover sits exactly where it
 * would if the flap were not there at all, which is why every box can
 * be built with one and only the boxes with the artwork ever open.
 */
.flap {
    position: absolute;
    inset: 0;

    transform-style: preserve-3d;

    /*
     * The hinge runs down the spine edge — but along the front of the
     * box, not through the middle of it.
     *
     * The third number is the one that matters. Without it the origin
     * sits at the centre of the box's depth, half the box behind the
     * cover it is turning, and the cover swings out on an arm that
     * long: it opens away from the box with a gap where the hinge
     * should be. Pushed out to the front plane, the axis is the edge
     * of the cover itself and the two halves stay touching.
     */
    transform-origin: 0 50% var(--half-depth);

    transition: transform .85s cubic-bezier(.33, .1, .2, 1);
}

.book.has-flap {
    cursor: pointer;
}

.book.is-open .flap {
    transform: rotateY(-124deg);
}

/*
 * The cover lifts a little under the pointer, and that is the whole of
 * the invitation: a box that answers the mouse is a box you try. An
 * icon stuck on the edge said the same thing louder and looked like a
 * sticker on a thing that is meant to look photographed.
 */
.book.has-flap:hover .flap {
    transform: rotateY(-9deg);
}

.book.has-flap.is-open:hover .flap {
    transform: rotateY(-124deg);
}


/*
 * The underside of the flap. Turned to face backwards, so it is the
 * side you see once the flap has swung past you, and set a hair inside
 * the cover so the two do not fight over the same plane.
 *
 * Its shading is fixed rather than lit like the outer faces: it spends
 * its life either hidden or square to a viewer who has just opened it,
 * and lighting a face that is halfway through a hinge is more bother
 * than the difference is worth.
 */
.lining {
    inset: 0;

    background-image: var(--art-lining);
    background-size: 100% 100%;

    --shade: .88;

    transform:
        translateZ(calc(var(--half-depth) - 1px))
        rotateY(180deg);
}

/*
 * What the flap covers: printed on the box itself, just behind the
 * cover, and in shadow until the cover is off it.
 */
.inside {
    inset: 0;

    background-image: var(--art-inside);
    background-size: 100% 100%;

    --shade: .8;

    transform: translateZ(calc(var(--half-depth) - 3px));
}

/*
 * One scan of the whole inside, laid across the two halves it was
 * printed on: the cover gets the left half, the box the right. Drawn
 * at twice the width and slid to one side or the other, so the file is
 * used whole and neither half is ever stretched.
 *
 * The cover reads the right way round despite being a mirrored face:
 * its own 180° and the swing of the flap cancel out, which is how the
 * inside of any book cover works.
 */
.book.is-spread .lining {
    background-size: 200% 100%;
    background-position: left center;
}

.book.is-spread .inside {
    background-size: 200% 100%;
    background-position: right center;
}

.back {
    background-image: var(--art-back);

    filter:
        brightness(calc(var(--shade) * var(--back-brightness)))
        saturate(var(--back-saturation));

    transform:
        rotateY(180deg)
        translateZ(var(--half-depth));

    /* keeps .seam's outline from poking past the rounded corner */
    overflow: hidden;
}

/*
 * The seam of a two-piece box: the base is its own shallow tray,
 * walled on all four sides, and the lid fits over it like a lid on
 * a shoebox. Where the lid's wall meets the base's, a groove runs
 * right round the back panel, a little in from the true edge —
 * not a mark across the middle of it.
 *
 * Hidden by default — only .book.has-lidbase draws it. Built once
 * in buildBox() rather than as a pseudo-element, because .back
 * already spends its ::before/::after on the grain and the moving
 * highlight.
 */
.seam {
    display: none;
}

.book.has-lidbase .back .seam {
    display: block;

    position: absolute;
    inset: calc(0.5px * var(--box-scale));

    border: calc(1px * var(--box-scale)) solid rgba(0,0,0,1);

    pointer-events: none;
}

/*
 * The moving highlight.
 *
 * The script parks this blob wherever the light is reflecting
 * off the cover and fades it out as the cover turns away, so
 * the sheen slides across the art as you rotate the box
 * instead of being painted on in one spot.
 */
.front.has-art::after,
.back.has-art::after {
    content: "";

    position: absolute;
    inset: 0;

    background:
        radial-gradient(
            ellipse 75% 55% at var(--spec-x) var(--spec-y),
            rgba(255,255,255, var(--spec-a)),
            rgba(255,255,255, 0) 70%
        );

    pointer-events: none;
}

/*
 * Cardboard tooth.
 *
 * Flat colour lit flatly looks like plastic, so this lays a
 * fine noise over the print. It is a bump map in spirit: the
 * overlay blend brightens the peaks and darkens the pits, and
 * because it rides on top of the shading it gets stronger in
 * the light and disappears in shadow.
 *
 * Only where there is print to give tooth to: on a black face
 * it would be grey speckle on nothing.
 */
.front.has-art::before,
.back.has-art::before,
.spine.has-art::before {
    content: "";

    position: absolute;
    inset: 0;

    opacity: var(--grain);

    mix-blend-mode: overlay;

    pointer-events: none;

    background-image:
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='180' height='180' filter='url(%23n)' opacity='.55'/></svg>");

    background-size: calc(180px * var(--box-scale));
}

/* ---- spine, on the left edge ------------------------------- */

.spine {
    width: var(--box-depth);
    height: 100%;

    /* the face is laid out flat, then swung into place */
    left: calc(var(--half-width) - var(--half-depth));

    background-image: var(--art-spine);

    background-size: 100% 100%;
    background-position: center;

    transform:
        rotateY(-90deg)
        translateZ(var(--half-width));
}

/* ---- page block, on the right edge -------------------------- */

.pages {
    width: var(--box-depth);
    height: 100%;

    left: calc(var(--half-width) - var(--half-depth));

    background-image: var(--art-pages);

    background-size: 100% 100%;
    background-position: center;

    transform:
        rotateY(90deg)
        translateZ(var(--half-width));
}

/* ---- head and tail ------------------------------------------ */

.top,
.bottom {
    width: 100%;
    height: var(--box-depth);

    top: calc(var(--half-height) - var(--half-depth));

    background-size: 100% 100%;
    background-position: center;
}

.top {
    background-image: var(--art-top);

    transform:
        rotateX(90deg)
        translateZ(var(--half-height));
}

.bottom {
    background-image: var(--art-bottom);

    /* the underside never catches the light, scan or no scan */
    box-shadow: inset 0 0 40px rgba(0,0,0,.6);

    transform:
        rotateX(-90deg)
        translateZ(var(--half-height));
}

/* ---- ground shadow ------------------------------------------ */

.shadow {
    position: absolute;

    width: var(--box-width);
    height: calc(60px * var(--box-scale));

    left: 50%;
    top: 50%;

    margin-left: calc(var(--half-width) * -1);
    margin-top: calc(var(--half-height) - 15px * var(--box-scale));

    background: rgba(0,0,0,.7);

    filter: blur(calc(24px * var(--box-scale)));

    border-radius: 50%;
}

/*
 * The front cover, blown up behind the box as a backdrop: the
 * box is lit by the colours of its own artwork.
 *
 * Blurred hard on purpose. Anything sharp enough to read as a
 * second cover competes with the real one standing in front of
 * it; at this radius only the colour survives.
 *
 * Deliberately free of --box-scale: it is scenery, not part of
 * the box, so zooming the box in and out slides it past a
 * backdrop that stays put.
 */
.game-art-back {
    position: absolute;
    inset: 0;

    width: 100%;
    height: 100%;

    /* fills the stage whatever shape the scan happens to be */
    object-fit: cover;

    /*
     * Scaled past the edges because a blur that wide pulls
     * transparency in from outside the image and would draw a
     * pale frame around the stage.
     */
    transform: scale(1);

    filter: blur(24px) saturate(1.2);

    opacity: 0;

    transition: opacity .35s ease;

    pointer-events: none;

    z-index: 0;
}

/*
 * Faint enough that it never competes with the box. A cover
 * carries far more ink than the logo that used to sit here, so
 * this is where to turn it down if a bright one gets loud.
 */
.game-art-back.is-on {
    opacity: .22;
}

/* the box and its shadow sit in front of the backdrop */
.shadow {
    z-index: 1;
}

.scene {
    position: relative;
    z-index: 2;
}

/* the shelf is about the boxes, not one game's artwork */
body.view-shelf .game-art-back {
    display: none;
}


/* ---- phones ------------------------------------------------ */

/*
 * Two things come off on a phone, both for the same reason: they
 * are cheap to describe and expensive to draw.
 *
 * The cardboard tooth blends with what is underneath it, and a
 * blend mode cannot be done by the compositor alone -- the layer
 * behind has to be read back for every pixel, every frame the box
 * turns. On a desktop that is absorbed; on a phone it is most of
 * the frame budget, and it is worst exactly when a side swings
 * into view and a new blended surface appears.
 *
 * The backdrop is a full-screen image under a 24px blur. It never
 * moves, so it costs nothing to keep still -- but it is held in
 * memory at screen size alongside six scans, and that is what
 * pushes an older phone into re-rasterising the box as it turns.
 *
 * Both are worth having on a desktop and neither is worth a
 * stutter. To put them back, delete this block.
 */
@media (max-width: 700px) {

    .front.has-art::before,
    .back.has-art::before,
    .spine.has-art::before {
        display: none;
    }

    .game-art-back {
        filter: none;

        /*
         * Without the blur it would read as a second cover behind
         * the real one, so it drops to a colour wash instead.
         */
        transform: scale(1.6);
        opacity: 0;
    }

    .game-art-back.is-on {
        opacity: .12;
    }
}
