/* ══════════════════════════════════════════════════════════════════════════
   nx-live.css — the socket that holds a LIVE product instead of a picture
   ══════════════════════════════════════════════════════════════════════════
   The box must behave exactly like the <img> it replaced, because the page's
   own layout, tilt and parallax were all written against an image. So:
   width:100%, its own aspect-ratio, and no margin of its own.
   ══════════════════════════════════════════════════════════════════════════ */
.nxlive {
  position: relative; width: 100%; display: block; overflow: hidden;
  /* ★ A PROVISIONAL HEIGHT, OR THE PRODUCT NEVER LAYS OUT.
     A solo socket has no aspect-ratio until its product has been measured —
     so before measurement the box is 0px tall, and `overflow:hidden` clips the
     iframe away entirely. A fully clipped iframe is not rendered, its document
     never lays out, and it reports offsetWidth 0 forever. Every solo socket
     failed exactly this way, deterministically, while the square cards were
     saved by their own aspect-ratio. The mounter replaces this the moment it
     has the product's real ratio. */
  aspect-ratio: 16 / 9;
  /* until the iframe has loaded and been measured, hold the socket open at
     the square the card grid expects — otherwise the grid jumps */
  border-radius: inherit;
}
/* ★ NO GROUND BEHIND THE PRODUCT.
   Ross, 2026-09-21: "there's now no longer a need for each interface to have a
   background colour. the UI itself should fill the socket, doing this will help
   us see the details."

   He is right, and it is a consequence of the reflow: a painted #262C31 panel
   was there to give an ill-fitting product something to sit ON. Once each one
   reflows to near-square there is nothing to sit on — the panel and its safe
   margin were spending a third of the socket on backdrop, and every plugin
   already draws its own frame. The card behind it is the surface now. */
.nxlive--square {
  aspect-ratio: 1 / 1;
  background: transparent;
  border-radius: 14px;
}
.nxlive__f {
  position: absolute; top: 50%; left: 50%; border: 0; display: block;
  transform-origin: center center;
  /* centred first, then scaled by the mounter's transform — which REPLACES
     this one, so the translate is folded into the scale below */
  translate: -50% -50%;
  background: transparent;
  opacity: 0; transition: opacity 320ms cubic-bezier(.16,1,.3,1);
}
.nxlive.is-live .nxlive__f { opacity: 1; }

/* a solo socket has no ground of its own: the product fills it edge to edge */
.nxlive:not(.nxlive--square) { background: transparent; }

@media (prefers-reduced-motion: reduce) {
  .nxlive__f { transition: none; }
}

/* ── THE COLLAPSING SOCKET ────────────────────────────────────────────────
   `.ui-shot` in the `.showcase` band sizes to its CONTENT. That worked while
   the content was an <img>, which carries an intrinsic width; a <div> at
   width:100% inside a shrink-to-fit parent resolves to 0, and the parent then
   resolves to 0 too. One of the five solo sockets did exactly that — it
   mounted, went "live", and measured 0px wide.

   Giving the chain a definite width breaks the circularity. Scoped with :has()
   so sockets that still hold a picture are untouched. */
.ui-shot:has(.nxlive),
.ui-shot__frame:has(.nxlive) { width: 100%; }

/* ── PARKING ──────────────────────────────────────────────────────────────
   An iframe has its OWN viewport, so each product's internal IntersectionObserver
   always sees itself on screen and never parks its animation loop. Eleven live
   UIs would therefore run forever, whatever the reader is looking at.

   `display:none` on the iframe makes its document non-rendered, and the spec
   only runs animation-frame callbacks for rendered documents — so the loop
   stops without the product needing to know it is embedded. The box keeps its
   size because the aspect-ratio lives on the box, not the frame, so nothing
   moves when a product parks or wakes. */
.nxlive.is-parked .nxlive__f { display: none; }

/* ── THE SOCKET IS A BUTTON ───────────────────────────────────────────────
   It covers the whole socket and sits above the iframe, which is why the
   iframe itself can stay inert (tabindex -1, aria-hidden): there is exactly
   one thing here to focus and one thing to activate. */
.nxlive__open {
  position: absolute; inset: 0; z-index: 3; cursor: zoom-in;
  background: transparent; border: 0; padding: 0;
  border-radius: inherit;
  transition: background 180ms cubic-bezier(.16,1,.3,1),
              box-shadow 180ms cubic-bezier(.16,1,.3,1);
}
.nxlive__open::after {
  content: "";
  position: absolute; right: 10px; bottom: 10px; width: 26px; height: 26px;
  border-radius: 6px; opacity: 0;
  background: rgba(26,30,34,.82) no-repeat center /14px 14px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23A7E163' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M6 2H2v4M10 14h4v-4M2 10v4h4M14 6V2h-4'/%3E%3C/svg%3E");
  box-shadow: 0 0 0 1px rgba(167,225,99,.35);
  transition: opacity 180ms cubic-bezier(.16,1,.3,1);
}
.nxlive:hover .nxlive__open::after,
.nxlive__open:focus-visible::after { opacity: 1; }
.nxlive__open:hover { background: rgba(167,225,99,.06); }
.nxlive__open:focus-visible {
  outline: 2px solid #A7E163; outline-offset: 3px;
}

/* ── THE MODAL ────────────────────────────────────────────────────────────── */
.nxmodal {
  border: 0; background: transparent; color: #E8EEF1;
  font-family: "Open Sans", system-ui, sans-serif;
  /* a <dialog> centres itself with auto margins, but only when it has a size
     to centre — this one's content is absolutely positioned inside a stage the
     script measures, so it needs the margins said out loud */
  margin: auto; padding: 16px;
  max-width: 100vw; max-height: 100vh; width: fit-content; height: fit-content;
}
.nxmodal::backdrop { background: rgba(10,13,15,.82); backdrop-filter: blur(3px); }
.nxmodal__bar {
  display: flex; align-items: center; gap: 14px; margin-bottom: 12px;
}
.nxmodal__t {
  font-size: 13px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,.72);
}
.nxmodal__x {
  margin-left: auto; width: 32px; height: 32px; border-radius: 8px; cursor: pointer;
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.14);
  color: #E8EEF1; font: 400 20px/1 system-ui, sans-serif;
}
.nxmodal__x:hover { background: rgba(255,255,255,.12); }
.nxmodal__x:focus-visible { outline: 2px solid #A7E163; outline-offset: 2px; }
.nxmodal__stage { position: relative; overflow: hidden; }
.nxmodal__f {
  position: absolute; top: 0; left: 0; border: 0; display: block;
  transform-origin: top left; background: transparent;
}
@media (prefers-reduced-motion: reduce) {
  .nxlive__open, .nxlive__open::after { transition: none; }
}


/* ── I/O: THE ONE THAT WILL NOT GO SQUARE ─────────────────────────────────
   Ross: "the only one that won't work naturally is Source-Nexus IO and i think
   we could just give it a reflection that skew towards the viewing perspective
   to fill the 1:1 framing."

   Even reflowed it measures 470x281 — 1.67 against a 1.00 socket — so fitting
   it whole leaves a third of the square empty.

   FIRST ATTEMPT WAS `-webkit-box-reflect`: one property, no second element, no
   second live plugin. But it mirrors FLAT and cannot skew, and a flat mirror is
   a duplicate rather than a reflection. Ross chose to pay for the real thing.

   SO THIS IS A SECOND LIVE INSTANCE, tilted away in perspective with its top
   edge pinned where the original ends — which is where a reflection meets the
   object it reflects. The bottom recedes, so it foreshortens the way a surface
   seen from slightly above does, and the mask takes it out before the far edge
   could show a crisp detail no real reflection would keep.

   THE UI ITSELF IS NEVER TILTED. Only the reflection is, which is the whole
   difference between a perspective reflection and a tilted product shot — and
   why the five flat cards beside it still agree with this one. */
.nxlive--square[data-product="io"] .nxlive__f {
  top: 0; translate: -50% 0;
  /* ★ AND THE ORIGIN, or `top:0` means nothing. The mounter scales with a
     transform whose origin is the element's CENTRE, so a shrunken element
     pinned to the top still RENDERS inset by half the slack — here exactly
     61.5px of a 281px socket, which read as the rule not applying at all.
     It was applying; the box was at the top and the pixels were not. */
  transform-origin: top center;
}

.nxlive__mirror {
  position: absolute; left: 50%; translate: -50% 0;
  overflow: hidden; pointer-events: none; z-index: 1;
  transform-origin: top center;
  /* the skew. The top edge is pinned (origin: top), so only the far edge moves
     back — 34deg is enough to read as a surface without turning the plugin
     into a wedge. The perspective is deliberately long: a short one exaggerates
     the convergence and the reflection starts to look like a corridor. */
  transform: perspective(1100px) rotateX(34deg);
  /* ★ A REFLECTION IS DIMMER AND SOFTER THAN THE THING IT REFLECTS. At .42
     and perfectly sharp this read as a second copy of the plugin lying down —
     every label still crisp, which glass never does. Dropping it and adding a
     touch of blur is what turns a duplicate into a reflection; the eye reads
     "surface" from the loss of detail, not from the geometry. */
  opacity: .30;
  filter: blur(.7px);
  /* fades out before the far edge, where a real reflection has already lost
     its detail to the surface */
  -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,.35) 42%, rgba(0,0,0,0) 78%);
          mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,.35) 42%, rgba(0,0,0,0) 78%);
}
.nxlive__mf {
  position: absolute; top: 50%; left: 50%;
  translate: -50% -50%;
  transform-origin: center center;
  border: 0; display: block; background: transparent;
}
/* it parks with its socket, like the original — otherwise the one thing that
   doubled the cost would be the one thing that never stopped */
.nxlive.is-parked .nxlive__mirror { display: none; }

@media (prefers-reduced-motion: reduce) {
  /* the reflection is decoration that animates; reduced motion keeps the
     product and drops the second moving copy entirely */
  .nxlive__mirror { display: none; }
}


/* ── THE SURFACE IT IS STANDING ON ────────────────────────────────────────
   Ross: "needs a surface plane that the interface is resting on. like it's
   sitting on a piece of glass where it casts a shadow and light reflects on
   the table. it's missing the 3d realism to pull off the reflection effect."

   He is right, and it is the difference between a reflection and a mirrored
   copy. A reflection is evidence of a SURFACE; with no surface the eye has
   nothing to attribute it to, so it reads as a duplicate hanging in space.
   Three things make the surface, and all three are cheap:

     THE PLANE      shares the reflection's exact perspective, runs wider than
                    the object and further back than the reflection, and is
                    masked to nothing at its rim. A surface does not stop where
                    the thing on it stops — that edge is the tell.
     THE LIGHT      two gradients on the plane: a pool of bounce just behind the
                    object where it is brightest, and one long specular streak
                    across the glass at an angle to the object's own edges. The
                    angle matters; parallel reads as a seam.
     THE CONTACT    a tight dark line where the two meet, and the ONLY part not
                    in perspective. A contact shadow that spreads is a floating
                    object — the tightness is what puts it ON the glass.

   All of it is paint. No extra live instance, nothing to animate. */
.nxlive__plane {
  position: absolute; left: 50%; translate: -50% 0;
  pointer-events: none; z-index: 0;
  transform-origin: top center;
  transform: perspective(1100px) rotateX(34deg);
  background:
    /* the object's own bounce, pooling right behind it */
    radial-gradient(86% 54% at 50% 0%, rgba(150,195,220,.085), rgba(150,195,220,.022) 44%, transparent 66%),
    /* one specular streak, deliberately off-axis to the plugin's edges */
    linear-gradient(104deg, transparent 22%, rgba(200,226,245,.05) 38%,
                    rgba(200,226,245,.015) 52%, transparent 66%),
    /* the glass itself, falling away from the viewer */
    linear-gradient(to bottom, rgba(255,255,255,.035), rgba(255,255,255,.010) 30%, transparent 66%);
  /* ELLIPTICAL, and it reaches zero well before the plane's own rectangle —
     the mask has to be what ends the surface, never the clip */
  -webkit-mask-image: radial-gradient(ellipse 72% 78% at 50% 4%, #000 18%, rgba(0,0,0,.30) 55%, transparent 80%);
          mask-image: radial-gradient(ellipse 72% 78% at 50% 4%, #000 18%, rgba(0,0,0,.30) 55%, transparent 80%);
}

.nxlive__contact {
  position: absolute; left: 50%; translate: -50% 0;
  pointer-events: none; z-index: 2;
  background:
    radial-gradient(50% 100% at 50% 0%, rgba(0,0,0,.85), rgba(0,0,0,.42) 38%, transparent 72%);
  filter: blur(3px);
}

/* both are decoration for the reflection, so they go when it does */
.nxlive.is-parked .nxlive__plane,
.nxlive.is-parked .nxlive__contact { display: none; }
@media (prefers-reduced-motion: reduce) {
  .nxlive__plane, .nxlive__contact { display: none; }
}
