/* ==========================================================================
   phone-mock — 3D iPhone mockup rendered entirely in CSS.
   The screen is live DOM, so any HTML, image or video can be dropped into it.

   Markup (the parts marked * are injected by phone-mock.js):

     <div class="pm" data-preset="opal-a">
       <div class="pm__stage">
         <div class="pm__device">
           <div class="pm__extrude"> <i>…</i> </div>   *
           <div class="pm__back"></div>                *
           <div class="pm__front">
             <div class="pm__screen">  …your content…  </div>
             <div class="pm__island"></div>
             <div class="pm__gloss"></div>
           </div>
           <span class="pm__btn pm__btn--seam"></span>   *
           <span class="pm__btn pm__btn--action"></span> *
           …
         </div>
       </div>
     </div>

   Everything is driven by custom properties, so a preset is a camera plus a
   set of greys. See the [data-preset] blocks at the bottom.

   Every number in the `opal-a` / `opal-b` presets and in the metrics below is
   measured off the reference renders (transparent PNGs, 1240x1456, screen
   content baked in) — on the alpha channel for the silhouette, the edge angles
   and the foot fade, and on brightness profiles taken across the edge for the
   chamfer, the flank, the bezel and the button positions. Details in README.md.
   ========================================================================== */

.pm {
  /* --- device metrics (px == iOS points, iPhone 16 Pro) --------------- */
  --pm-screen-w: 393px;
  --pm-screen-h: 852px;
  /* The ring around the screen: 9.2pt metal chamfer, 3.4pt glass edge,
     5.2pt black bezel — 18pt all told. */
  --pm-bezel: 18px;
  --pm-glass-edge: 3px;
  --pm-black-bezel: 5px;
  /* 8.25 of 71.5 mm, so 0.1154 of the body width. */
  --pm-depth: 50px;
  /* 69pt, measured over the sagitta of the corner rather than as a circle fit:
     a circle fit across the whole arc overestimates Apple's squircle badly
     (80pt+), because the curvature drops off towards the straight edges. */
  --pm-radius-body: 69px;
  --pm-radius-screen: calc(var(--pm-radius-body) - var(--pm-bezel));

  /* --- camera --------------------------------------------------------- */
  --pm-perspective: 8000px;
  --pm-perspective-x: 50%;
  --pm-perspective-y: 50%;
  --pm-rx: 6deg;             /* pitch  */
  --pm-ry: -22deg;           /* yaw    */
  --pm-rz: 14deg;            /* roll   */
  --pm-scale: 1;
  --pm-shift-x: 0px;
  --pm-shift-y: 0px;

  /* --- look -----------------------------------------------------------
     The body is dark metal, not glass. These are brightness readings off the
     reference, not a style choice: the flank sits at a flat 88 of 255, the
     chamfer between 60 and 100, and it only gets brighter in two places — a
     1 to 2 px glint on the edge facing away (162) and the front edge of the
     buttons (180). A translucent white reads as a white plastic casing.

     The glint has to be over and done with inside the first percent of the
     gradient. The gradient runs across the whole front face, but the ring is
     only 4 % of that wide, so a stop at 1.5 % does not colour an edge — it
     colours half the ring, and the hairline turns into a broad band. */
  --pm-rail-angle: 100deg;
  --pm-rail-glint: #aeafb1;
  --pm-rail-near: #5f6165;
  --pm-rail: linear-gradient(
    var(--pm-rail-angle),
    var(--pm-rail-glint) 0%,
    #55565a 0.7%,
    #3c3d41 3%,
    #383a3e 55%,
    color-mix(in srgb, var(--pm-rail-near) 82%, #000) 90%,
    var(--pm-rail-near) 100%
  );
  --pm-flank: #58595b;
  --pm-flank-front: #6b6c6e;   /* the sliver where the flank meets the chamfer */
  --pm-btn-back: #6e7074;
  --pm-btn-front: #b4b5b7;
  --pm-glass: #8b8d91;         /* the bright edge of the cover glass */
  --pm-back: #14151a;

  --pm-glow: transparent;      /* ambient halo colour behind the device */
  --pm-glow-size: 70%;
  /* Screen sheen. Off by default: the reference screens are screenshots and
     carry no overlay at all. Raise it for a staged, glassier look. */
  --pm-gloss: 0;
  --pm-gloss-angle: 112deg;

  /* Where the device dissolves towards its foot, as a share of the BODY
     LENGTH — 100 %/100 % is off. In the reference it is fully opaque to 47 %
     and gone by 74 %, the same in both poses. */
  --pm-fade-start: 100%;
  --pm-fade-end: 100%;

  --pm-body-w: calc(var(--pm-screen-w) + 2 * var(--pm-bezel));
  --pm-body-h: calc(var(--pm-screen-h) + 2 * var(--pm-bezel));

  position: relative;
  /* The layout box is the *scaled* device, so a mockup takes up exactly as
     much room as it appears to. Rotation still overflows this box on purpose —
     presets are meant to be cropped by the surrounding section, like Opal's. */
  width: calc(var(--pm-body-w) * var(--pm-scale));
  height: calc(var(--pm-body-h) * var(--pm-scale));
}

/* Ambient halo — the coloured bloom Opal renders behind the phone. */
.pm::before {
  content: "";
  position: absolute;
  inset: -20%;
  background: radial-gradient(closest-side circle at 50% 45%, var(--pm-glow), transparent var(--pm-glow-size));
  filter: blur(8px);
  pointer-events: none;
}

/* Camera. The box is deliberately larger than .pm so the rotated device is not
   clipped.

   The contact shadow lives here and NOT on .pm__device: a filter forces its own
   element to transform-style: flat, and on the device that silently collapses
   the extrusion and the side buttons into the screen plane. On an element that
   only establishes `perspective` it is harmless. */
.pm__stage {
  position: absolute;
  inset: -30%;
  display: grid;
  place-items: center;
  /* Scaled along with the device, so the amount of foreshortening stays the
     same whatever --pm-scale is. */
  perspective: calc(var(--pm-perspective) * var(--pm-scale));
  perspective-origin: var(--pm-perspective-x) var(--pm-perspective-y);
  filter: drop-shadow(0 40px 60px rgba(0, 0, 0, .55))
          drop-shadow(0 8px 16px rgba(0, 0, 0, .4));
}

.pm__device {
  position: relative;
  width: var(--pm-body-w);
  height: var(--pm-body-h);
  transform-style: preserve-3d;
  transform:
    translate(var(--pm-shift-x), var(--pm-shift-y))
    scale(var(--pm-scale))
    rotateX(var(--pm-rx)) rotateY(var(--pm-ry)) rotateZ(var(--pm-rz));
}

/* --- the three faces ------------------------------------------------- */

.pm__front,
.pm__back,
.pm__extrude > i {
  position: absolute;
  inset: 0;
  border-radius: var(--pm-radius-body);
  backface-visibility: hidden;
  /* The foot fade sits on every layer of the device separately, because it has
     to run in DEVICE space: across the body, both long edges ending at the
     same point, exactly as the reference render does it. A vertical fade in
     screen space dissolves one edge before the other at 31 degrees of tilt, and
     the device falls apart diagonally. One mask on .pm__device instead is not
     an option — it would flatten the 3D. */
  -webkit-mask-image: linear-gradient(#000 var(--pm-fade-start), transparent var(--pm-fade-end));
          mask-image: linear-gradient(#000 var(--pm-fade-start), transparent var(--pm-fade-end));
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

.pm__front {
  transform: translateZ(calc(var(--pm-depth) / 2));
  padding: var(--pm-bezel);
  background: var(--pm-rail);
  overflow: hidden;
}

.pm__back {
  transform: translateZ(calc(var(--pm-depth) / -2)) rotateY(180deg);
  background-color: var(--pm-back);
}

/* Real extrusion: a stack of identical silhouettes 2px apart. Seen from an
   angle their outer slivers add up to a curved side wall, and the rounded
   corners follow along. A single rotated rectangle cannot do that, because it
   does not follow the corners. Slices are injected by JS. */
.pm__extrude {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

.pm__extrude > i {
  background-color: var(--pm-flank);
}

/* In the reference the flank is equally bright over its whole depth — the only
   difference is a slight lift on the front edge, where it meets the chamfer. */
.pm__extrude > i:first-child {
  background-color: var(--pm-flank-front);
}

/* --- screen ---------------------------------------------------------- */

.pm__screen {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: var(--pm-radius-screen);
  overflow: hidden;
  background: #000;
  isolation: isolate;
  /* The two rings the reference shows around the screen: the cover glass edge
     as the brightest line on the front (137 of 255, 3.4pt wide), then the black
     bezel of 5.2pt. Together they are why the screen there looks like it runs
     into the casing and is still framed.
     Content is authored at 393x852 and never re-rasterised, so text stays crisp
     however far the mockup is scaled up. */
  box-shadow:
    0 0 0 var(--pm-glass-edge) var(--pm-glass),
    inset 0 0 0 var(--pm-black-bezel) #000;
}

.pm__screen > img,
.pm__screen > video,
.pm__screen > iframe {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}

/* Dynamic Island: 125 x 37pt, 11pt below the bezel. The camera lens to the
   right of it is clearly visible in the reference and the difference between a
   pill and a part. */
.pm__island {
  position: absolute;
  top: calc(var(--pm-bezel) + 11px);
  left: 50%;
  width: 125px;
  height: 37px;
  border-radius: 19px;
  background: #000;
  transform: translateX(-50%);
  z-index: 2;
}

.pm__island::after {
  content: "";
  position: absolute;
  right: 11px;
  top: 50%;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  transform: translateY(-50%);
  background:
    radial-gradient(circle at 35% 30%, rgba(120, 150, 190, .55), transparent 60%),
    #0a0d12;
}

/* Optional glass sheen across the front face — see --pm-gloss. */
.pm__gloss {
  position: absolute;
  inset: 0;
  border-radius: var(--pm-radius-body);
  pointer-events: none;
  opacity: var(--pm-gloss);
  background: linear-gradient(
    var(--pm-gloss-angle),
    rgba(255, 255, 255, .16) 0%,
    rgba(255, 255, 255, .03) 14%,
    rgba(255, 255, 255, 0) 30%,
    rgba(255, 255, 255, 0) 62%,
    rgba(255, 255, 255, .07) 79%,
    rgba(255, 255, 255, 0) 100%
  );
  mix-blend-mode: screen;
}

@supports (mix-blend-mode: plus-lighter) {
  .pm__gloss { mix-blend-mode: plus-lighter; }
}

/* --- side buttons ----------------------------------------------------
   Two quarter turns about the vertical axis, each pinned to its edge, then
   pushed back by half the depth. backface-visibility culls whichever rail
   faces away, so both rails may carry parts at the same time — the pose
   decides which one you see, not a second rule per row.

   Positions and lengths are read off the reference as a share of the body
   length, with the real layout: action and volume on the left, only the side
   button on the right. So the default pose shows ONE button and the mirrored
   one shows THREE. */

.pm__btn {
  position: absolute;
  width: var(--pm-depth);
  border-radius: 3px;
  backface-visibility: hidden;
  pointer-events: none;
}

/* The front edge of a button is the brightest spot on the whole casing. After
   the rotation that is the element's left edge on the right rail and its right
   edge on the left rail. */
.pm__btn--power {
  right: 0;
  top: 23.1%;
  height: 9%;
  transform-origin: right center;
  /* translateX lifts the button half a point off the flank. Without it the
     button shares a plane with the silhouette stack and the depth test lets
     them win in turns — the button comes out striped. */
  transform: translateX(0.5px) translateZ(calc(var(--pm-depth) / -2)) rotateY(90deg);
  background-image: linear-gradient(to left, var(--pm-btn-back), var(--pm-btn-front));
}

.pm__btn--seam,
.pm__btn--action,
.pm__btn--vol-up,
.pm__btn--vol-down {
  left: 0;
  transform-origin: left center;
  transform: translateX(-0.5px) translateZ(calc(var(--pm-depth) / -2)) rotateY(-90deg);
  background-image: linear-gradient(to right, var(--pm-btn-back), var(--pm-btn-front));
}

.pm__btn--action   { top: 19.9%; height: 3.2%; }
.pm__btn--vol-up   { top: 26.8%; height: 7.2%; }
.pm__btn--vol-down { top: 35.8%; height: 7.4%; }

/* The antenna seam is not a button but a dark joint. In the reference it sits
   just below the top edge and is the reason the flank has a step there. */
.pm__btn--seam {
  top: 10.8%;
  height: 1.2%;
  border-radius: 0;
  background-image: none;
  background-color: #3a3b3e;
}

/* ==========================================================================
   Presets — a camera and a set of greys. Copy a block to make your own.
   ========================================================================== */

/* The two poses the reference actually contains. They are NOT mirror images of
   one another: B is tilted flatter, has a longer lens and is lit differently.
   The reference uses A for its first and third feature row and B for the
   mirrored second one.

   The three-axis split is not uniquely determined by the edge angles — the same
   two angles can be produced with very different rotateY. The third measurement
   settles it: the WIDTH OF THE FLANK, which at a known body depth is a direct
   read on rotateY. The fourth is the difference between the two long edges,
   which depends on focal length alone; in the reference they run almost
   parallel (1 degree apart), so the focal length is very long.

     Pose A            reference   rebuild
       top edge          13.48      13.47 deg
       long edge left    31.65      31.44 deg from vertical
       long edge right   30.69      30.90 deg
       flank / width     0.0564     0.0562

     Pose B
       top edge         -17.76     -17.73 deg
       long edge left    21.32      21.53 deg
       long edge right   22.14      21.96 deg
       flank / width     0.0392     0.0389

   The angles belong to the rotation order in .pm__device — rotateX, then
   rotateY, then rotateZ. Fed into a different order they give a different
   pose. */
.pm[data-preset="opal-a"] {
  --pm-rz: 31.7deg;
  --pm-ry: -23.4deg;
  --pm-rx: 38.2deg;
  --pm-perspective: 13600px;
  --pm-rail-angle: 118deg;
  --pm-fade-start: 47%;
  --pm-fade-end: 74%;
  /* Pose A stands in a darker light: flank 73, side button 90. */
  --pm-rail-glint: #9fa0a2;
  --pm-rail-near: #4c4e52;
  --pm-flank: #494a4d;
  --pm-flank-front: #5a5c5f;
  --pm-btn-back: #55565a;
  --pm-btn-front: #7b7d81;
  --pm-glass: #74767a;
}

.pm[data-preset="opal-b"] {
  --pm-rz: -22.6deg;
  --pm-ry: 16.6deg;
  --pm-rx: 14.1deg;
  --pm-perspective: 6300px;
  --pm-rail-angle: 242deg;
  --pm-fade-start: 47%;
  --pm-fade-end: 74%;
  /* Pose B is the brighter one: flank 88, front edge of the buttons 186. */
  --pm-rail-glint: #aeafb1;
  --pm-rail-near: #5f6165;
  --pm-flank: #58595b;
  --pm-flank-front: #6b6c6e;
  --pm-btn-back: #6e7074;
  --pm-btn-front: #b4b5b7;
  --pm-glass: #8b8d91;
}

/* Free poses, not measured off anything — a whole device, tilted. */
.pm[data-preset="tilt-right"] {
  --pm-rx: 6deg;
  --pm-ry: -22deg;
  --pm-rz: 14deg;
  --pm-perspective: 4000px;
  --pm-rail-angle: 112deg;
}

.pm[data-preset="tilt-left"] {
  --pm-rx: 6deg;
  --pm-ry: 22deg;
  --pm-rz: -14deg;
  --pm-perspective: 4000px;
  --pm-rail-angle: 248deg;
}

/* Camera close to the top-left corner, body running off the bottom. */
.pm[data-preset="closeup"] {
  --pm-rx: 14deg;
  --pm-ry: 18deg;
  --pm-rz: -12deg;
  --pm-perspective: 900px;
  --pm-perspective-x: 55%;
  --pm-perspective-y: 12%;
  --pm-shift-y: 6%;
  --pm-fade-start: 42%;
  --pm-fade-end: 70%;
  --pm-rail-angle: 300deg;
}

/* Straight-on, for mobile breakpoints — Opal ships a separate flat PNG here. */
.pm[data-preset="flat"] {
  --pm-rx: 0deg;
  --pm-ry: 0deg;
  --pm-rz: 0deg;
  --pm-perspective: 20000px;
  --pm-rail-angle: 160deg;
}

@media (max-width: 767px) {
  .pm[data-preset-mobile="flat"] {
    --pm-rx: 0deg;
    --pm-ry: 0deg;
    --pm-rz: 0deg;
    --pm-shift-y: 0px;
    --pm-fade-start: 100%;
    --pm-fade-end: 100%;
  }
}
