/**
 * phone_frame.css — the Chippie phone-bezel video composite.
 *
 * SSOT for the bezel geometry. The crop ratios below are measured against the
 * demo clip's own padding, so they and the clip travel together: re-cut the film
 * and every number here has to be re-measured. That coupling is why this is one
 * shared file rather than a copy per page — the landing hero and the QR-scan
 * promo page must never drift apart on it.
 *
 * Pairs with templates/components/phone_video.html (markup + frame-sync
 * constants) and static/js/controllers/esm/video_controller.js (behaviour).
 * Depends on --ease-decel from the public shell's tokens. The greens here are
 * literals on purpose — see the fill rule below for why they cannot be tokens.
 *
 * .phone-col is NOT here: that is the landing hero's grid cell, not the composite.
 * Pages own their own layout around .phone and may override its width.
 */

/* phone — frame.png bezel with video showing through the transparent screen cutout */
.phone {
  /* min() so the promo page's wider container does not blow it up past this cap;
     the hero's own column is sized to suit in landing.css. */
  position: relative; width: min(500px, 100%); aspect-ratio: 1024 / 1536;
  animation: rise 0.7s var(--ease-decel) both;
}
@keyframes rise { from { transform: translateY(60px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.phone .frame-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 2; pointer-events: none;
  filter: drop-shadow(0 22px 40px rgba(0,0,0,0.32));
}
/* The fill is a literal, deliberately NOT var(--green-deep) (#1F3D2C). It has to
   match the clip's rendering of the app's Bottle Green, not the app's declaration
   of it, and those are a few units apart on screen: the clip is BT.709 limited
   range and a browser colour-manages it into the display profile by a different
   path than it takes for an sRGB CSS colour, so on a wide-gamut display the video's
   green lands lighter. #1F4230 is that green sampled off the running clip. Headless
   screenshots do not reproduce the display's colour management, so re-sample this
   from a real browser on a real monitor, never from an extracted frame. */
.phone .stage, .phone .screen {
  position: absolute; top: 6.8%; left: 22.4%; width: 55.4%; height: 83.7%;
  display: block; z-index: 1; background: #1F4230;
}
/* The clip's screen-recording segment is a 584x1271 phone capture sitting on a
   blurred backdrop in the 698x1280 frame, so contain-ing the whole frame would
   bezel the blur, not the app. Scale the video until that capture fills the cutout
   WIDTH. Filling by HEIGHT instead would square the aspect up exactly and leave no
   spare at all, but it costs 12px a side of the capture, and the app's own content
   comes within 9px of that edge on some screens — it would clip real UI, so it is
   not an option.
   Fitting by width leaves ~4% of the cutout height spare, which .stage's background
   fills. That spare all goes ABOVE the app rather than being split: sitting above,
   it extends the status bar to a natural depth, whereas splitting it also pads the
   gap under the nav bar and leaves the nav sitting high. So the window is bottom-
   aligned in the cutout.
   The capture's outermost rows are a blend into the backdrop, so the window is
   inset to the clean pixels vertically: 3 rows at the top, 6 at the bottom, both
   measured rather than guessed — a 2-row top inset still left one blended row, and
   it read as a blue hairline between the green fill and the app's own header.
   Horizontally no inset is needed: the video is wider than the cutout, so the blend
   columns fall outside it and .stage's overflow clips them. Ratios only, so this
   holds at every .phone width
   (Ch/Cw = (0.837*1.5)/0.554 = 2.2662, fixed by the cutout above):
     width   698/584                         = 119.52%  of cutout width
     left    -58/584                         =  -9.93%
     height  1280/584 / (Ch/Cw)              =  96.71%  of cutout height
     top     (Ch - 1274/584*Cw) / Ch         =   3.74%
     clip    3/1280 = 0.23%   6/1280 = 0.47%
   The encode crops 16px off each side before scaling: one intro shot (about 4s to
   11.5s) carries a 15px black column down its right edge. It appears nowhere else
   in the film, which is why sampling only screencast frames missed it.
   Re-measure these if the clip is re-exported with a different capture size. */
.phone .stage { overflow: hidden; }
.phone .stage video {
  position: absolute; display: block;
  width: 119.52%; height: auto; left: -9.93%; top: 3.74%;
  clip-path: inset(0.23% 0 0.47% 0);
}
/* uncropped — the clip's sign-off card is full-bleed and wider than the recording
   window, so past data-video-screen-until the crop lifts and the whole frame sits
   inside the bezel. It letterboxes against .stage, hence the card's own background
   colour there rather than the screen fill. This carries the same drift the fill
   above documents, and it cannot be eyedropped the same way — the card is only on
   screen for a few seconds. It is solved for instead: the fill gives one calibrated
   pair (raw #1B3B29 renders as #1F4230), which fits a per-channel gamma of
   0.938/0.923/0.914, and the card's raw #0C2919 through that fit is #0E2F1F. A
   ratio fit gives #0E2E1D and an additive one #103020, so the models agree to within
   two units a channel. Re-derive, do not re-sample, if the clip is re-exported. */
.phone.uncropped .stage { background-color: #0E2F1F; }
.phone.uncropped .stage video {
  height: 100%; width: 100%; left: 0; top: 0; object-fit: contain;
  clip-path: none;
}
.phone .screen { height: 86%; z-index: 0;
  display: flex; align-items: center; justify-content: center;
  color: #5b6573; font-size: 13px; text-align: center; padding: 20px; }
/* frameless — outside the screen-recording window the clip is full-bleed (live
   footage, the brand card, the sign-off card), so the static bezel and the
   green letterbox fills drop away and the stage opens up to the whole .phone box
   to show the film uncropped. video_controller.js toggles this from
   data-video-frame-show-at / data-video-frame-hide-at; without JS the class never
   applies and the bezel stays, as before. */
/* Both ends of the window are hard cuts in the clip, and the stage geometry snaps
   with them, so the bezel has to snap too. A slow opacity fade reads as a flash:
   a near-black bezel fading up over the sandstone page spends that whole time as a
   pale grey ghost of itself. Keep it short enough to hide rAF jitter and no longer. */
.phone .frame-img, .phone .screen { transition: opacity 0.15s ease; }
.phone .stage { transition: background-color 0.15s ease; }
.phone.frameless .frame-img, .phone.frameless .screen { opacity: 0; }
/* The video's size and position do NOT change here — only what is masked. Letting
   .stage grow to the whole .phone box and contain-ing the film inside it made the
   clip jump ~19% smaller the instant the bezel arrived, and at the brand card's hard
   cut into the recording that reads as a flash. Instead .stage keeps its cutout
   geometry and simply stops clipping, so the film overflows it through the intro and
   the bezel then lands ON the film, masking the edges the recording does not use.
   The app content is exactly the same size on both sides of the cut. */
.phone.frameless .stage {
  overflow: visible;
  background-color: transparent;
}
.phone.frameless .stage video { clip-path: none; }
/* sound — autoplay starts muted; tapping the clip or this pill toggles its music.
   The cursor and the click target are on .stage, not the <video>: the video does
   not fill the cutout. A band of .stage shows above the app all through the clip,
   and at the sign-off the whole thing letterboxes. Those were dead zones — no
   pointer, and no toggle either, since the handler sat on the video element. */
.phone .stage { cursor: pointer; }
/* the transport bar is not a mute target, so it says so and swallows the click */
.phone .video-controls { cursor: default; }
.phone .sound-pill {
  /* below the bezel, on the page background — the in-video and static phones have
     different screen geometry, so any on-screen spot covers app content in one state */
  position: absolute; left: 50%; bottom: -46px; transform: translateX(-50%);
  z-index: 3; border: 0; border-radius: 999px; padding: 8px 14px;
  background: rgba(13, 17, 23, 0.62); color: #fff;
  font-family: "Fragment Mono", monospace; font-size: 12px; line-height: 1;
  cursor: pointer; white-space: nowrap;
}
.phone .sound-pill:hover { background: rgba(13, 17, 23, 0.82); }

/* transport — hover-reveal play/pause/seek for the hero clip.
   Not the native `controls` attribute: those paint inside the <video> box, which
   is scaled to 137% and clip-path'd into the bezel cutout, so the bar would come
   out oversized, shifted left, and clipped out of sight. This one lives INSIDE
   .stage, which already switches geometry for the three crop states, so it tracks
   all of them without an override apiece; .stage's overflow:hidden keeps it in the
   cutout and .frame-img's pointer-events:none lets clicks through the bezel above.
   It sits over the screen rather than the bezel chin because the chin is only
   ~6.5% of the frame height (~42px at the desktop width) and its corners round in
   hard — too shallow to hold a control row. */
.phone .video-controls {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: 8px;
  padding: 9px 10px;
  /* Near-opaque, not a fade-to-transparent gradient: through most of the clip this
     sits on the app's own dark-green nav bar, and a gradient's translucent half let
     the nav labels read straight through the seek track and the time. Even 0.93
     ghosted them enough to read as a rendering fault rather than an overlay, so it
     is fully opaque. Covering that strip while the bar is up is the lesser evil
     against an illegible bar. */
  background: #0a0f0b;
  opacity: 0; transition: opacity 0.18s ease;
}
/* A paused clip keeps its bar: the pointer has usually left by then, and hiding
   the only way to resume it is the one state where hover-to-reveal fails. */
.phone:hover .video-controls,
.phone.is-paused .video-controls { opacity: 1; }
/* Keyboard users never hover, so focus has to reveal it too or the bar cannot be
   reached by tab at all. :focus-visible, not :focus-within — clicking a button
   leaves it focused, and :focus-within would then pin the bar open for the rest
   of the visit once the pointer moved away. Kept in its own rule because an
   engine without :has() drops the whole rule it appears in, and that must not
   take the hover selector with it. */
.phone:has(:focus-visible) .video-controls { opacity: 1; }
/* Touch has no hover, so the clip itself is the affordance: tapping it raises the
   bar for a few seconds (video_controller.js#revealControls owns the class and the
   timer, and every control re-arms it). Leaving the bar permanently up there was
   the alternative, but it covers the app's own nav bar for the whole run and that
   nav is part of what the demo is showing. */
.phone.controls-shown .video-controls { opacity: 1; }

.phone .vc-btn {
  flex: none; display: grid; place-items: center;
  width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: 999px; background: transparent;
  color: #fff; cursor: pointer;
}
.phone .vc-btn:hover { background: rgba(255, 255, 255, 0.18); }
.phone .vc-btn svg { display: block; width: 17px; height: 17px; fill: currentColor; }
/* video_controller.js owns .is-paused on the .phone element; the two glyphs both
   ship and CSS picks one, so no icon markup is built in JS.
   These must out-specify `.phone .vc-btn svg` above, which sets display:block on
   every glyph in the button — two classes plus a type selector, so a bare
   `.phone .vc-icon--play` ties on classes and loses, leaving both icons stacked.
   Hence .vc-btn in the chain: three classes beats two-plus-a-type. */
.phone .vc-btn .vc-icon--play { display: none; }
.phone.is-paused .vc-btn .vc-icon--play { display: block; }
.phone.is-paused .vc-btn .vc-icon--pause { display: none; }

.phone .vc-time {
  flex: none; color: #fff; opacity: 0.82;
  font-family: "Fragment Mono", monospace; font-size: 10px;
  line-height: 1; white-space: nowrap;
}

/* --vc-progress is written by video_controller.js; the track reads it to paint the
   played portion, since a range input has no styleable "filled" side of its own. */
.phone .vc-seek {
  flex: 1 1 auto; min-width: 0; height: 14px; margin: 0;
  -webkit-appearance: none; appearance: none;
  background: transparent; cursor: pointer;
}
.phone .vc-seek::-webkit-slider-runnable-track {
  height: 3px; border-radius: 3px;
  background: linear-gradient(to right,
    #fff 0 var(--vc-progress, 0%),
    rgba(255, 255, 255, 0.32) var(--vc-progress, 0%) 100%);
}
.phone .vc-seek::-moz-range-track {
  height: 3px; border-radius: 3px;
  background: linear-gradient(to right,
    #fff 0 var(--vc-progress, 0%),
    rgba(255, 255, 255, 0.32) var(--vc-progress, 0%) 100%);
}
.phone .vc-seek::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 11px; height: 11px; margin-top: -4px;
  border: 0; border-radius: 50%; background: #fff;
}
.phone .vc-seek::-moz-range-thumb {
  width: 11px; height: 11px;
  border: 0; border-radius: 50%; background: #fff;
}
.phone .vc-seek:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* Narrow viewports: fill the column, and reserve room below for the sound pill's
   -46px overhang so it never collides with what follows. */
@media (max-width: 880px) {
  .phone { width: 100%; max-width: 380px; margin-bottom: 52px; }
}
