/* ════════════════════════════════════════════════════════════
   THE LAPTOP SCREEN · /technology hero
   2026-08-14. The showcase stops being a card over a photograph and
   becomes the picture ON the machine in the photograph.

   The measured corner geometry, the homography and the reason the
   layer is painted under the scrim are all documented at the top of
   js/tech-laptop.js — that file owns the maths and this one only
   owns the look. Everything here is inert until the script adds
   .hs--pinned to the hero, so a no-JS or too-narrow visitor gets the
   showcase exactly as it was.

   MOTION (emil-design-eng)
   ────────────────────────
   · The screen track is a CSS transition on transform, matched to the
     showcase's own 260ms / cubic-bezier(0.16, 1, 0.3, 1) so the two
     move as one object. Transition, not keyframes, so hammering the
     next button retargets both tracks instead of restarting them.
   · No entrance animation on the screen itself. It is a photograph of
     a switched-on laptop; a screen that faded up would read as a
     graphic being applied to the picture, which is the one thing this
     component must not look like.
   · The only ambient movement over the glass is .fx-sweep, which was
     already in the hero and now has something to reflect off.
   ════════════════════════════════════════════════════════════ */

/* ── PIN MODE: what changes about the hero ─────────────────────
   The photograph is widened to the full hero and its 50s ambient
   zoom is stopped. Both are prerequisites, not taste: the solve is
   run against the photo's layout box, so a transform on that box
   would skew every corner, and a 78%-wide right-anchored crop puts
   the laptop at roughly 310px across where full width gives ~380 at
   1440 and ~505 at 1920. The left-edge feather stays — the copy
   still has to sit on ink — it just runs to 42% instead, which is
   where the copy column ends and still stops well short of the
   laptop at 48.6%. */
.page-hero.glow-hero--tech.hs--pinned .section-bg-photo {
  left: 0; right: auto; width: 100%;
  object-position: 50% 46%;
  animation: none;
  will-change: auto;
  -webkit-mask-image: linear-gradient(to right, transparent 0%, rgba(0,0,0,.38) 13%, #000 42%);
          mask-image: linear-gradient(to right, transparent 0%, rgba(0,0,0,.38) 13%, #000 42%);
}

/* ── THE PINNED LAYER ──────────────────────────────────────────
   inset:0 over the same box as the photograph. z-index 1 with DOM
   order putting it after .section-bg-overlay and before the two .fx
   layers: above the scrim, so its exposure is set here rather than
   by a gradient that runs .80 black across the top of the hero, and
   below .fx-sweep, so the slow highlight still travels across the
   glass as a reflection. */
.lpin {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  overflow: hidden;
}
.lpin[hidden] { display: none; }

/* The transformed element. Its layout box is the quad's own source
   pixels (631 x 430) and the matrix3d maps that box onto the four
   measured corners. transform-origin must be 0 0 — the homography
   is solved from the top-left. */
.lpin__quad {
  position: absolute; top: 0; left: 0;
  width: 631px; height: 430px;
  transform-origin: 0 0;
  transform: matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
  overflow: hidden;
  /* 9 source px, which is the display's real corner radius. Clipping
     here is what keeps the slides off the bezel. */
  border-radius: 9px;
  /* An LCD is never pure black even switched on. This is the panel's
     own measured floor from the photograph, so a slide with dark
     edges dissolves into the screen instead of ending at a line. */
  background: #0b1020;
  /* EXPOSURE, SET AGAINST THE PHOTOGRAPH RATHER THAN GUESSED.
     The picture is served at opacity .72-.76 under a scrim, so the
     laptop's own display renders around 145/255 at its whites where
     the source file peaks at 250. A screen dropped in at full
     strength reads as a lightbox stuck to a photo; a screen matched
     exactly to 145 cannot be read at all. This lands the whites
     around 1.5x the photo's own display — bright enough that the
     work is legible, dim enough that it belongs in the room — and
     the tiny desaturation keeps the screenshots from out-colouring
     everything else in a frame this muted. */
  filter: brightness(.82) saturate(.94) contrast(1.02);
}

.lpin__track {
  display: flex; list-style: none; margin: 0; padding: 0;
  width: 100%; height: 100%;
  transition: transform .26s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
.lpin__slide {
  flex: 0 0 100%; min-width: 0; height: 100%;
  position: relative;
  display: flex; flex-direction: column;
  background: #0b1020;
}

/* ── WINDOW CHROME ─────────────────────────────────────────────
   A single slim bar. It exists because it is what actually sells
   "this is an application running on that machine" rather than an
   image pasted into the picture — and because it is the honest
   frame for both cases: the phone captures really are viewed in the
   Simulator on this laptop, and the site captures really are viewed
   in a browser on it. Sized in source pixels, so at a 1440 viewport
   this bar is ~15px tall and the title ~11px. */
.lpin__win {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 10px;
  height: 25px; padding: 0 11px;
  background: rgba(255,255,255,.07);
  border-bottom: 1px solid rgba(255,255,255,.09);
}
.lpin__dots {
  flex: 0 0 auto; width: 34px; height: 8px;
  background:
    radial-gradient(circle 4px at 4px 4px,  rgba(255,255,255,.34) 0 3.4px, transparent 3.6px),
    radial-gradient(circle 4px at 17px 4px, rgba(255,255,255,.24) 0 3.4px, transparent 3.6px),
    radial-gradient(circle 4px at 30px 4px, rgba(255,255,255,.24) 0 3.4px, transparent 3.6px);
}
.lpin__title {
  font-family: var(--font-mono);
  font-size: 12px; letter-spacing: .04em;
  color: rgba(255,255,255,.72);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.lpin__art {
  position: relative; flex: 1 1 auto; min-height: 0;
  display: block; overflow: hidden;
}
.lpin__art img { display: block; }

/* A 1206x2622 phone capture, contained and centred — the Simulator
   window. object-fit: cover on a portrait capture inside a landscape
   display shows the status bar and nothing else, which showcases
   nothing, so the device gets its true aspect and the desk around it
   stays dark. */
.lpin__slide--phone .lpin__art {
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(ellipse 60% 70% at 50% 46%, rgba(120,150,220,.10), transparent 70%),
    #0b1020;
}
.lpin__slide--phone .lpin__art img {
  height: 96%; width: auto; max-width: 46%;
  object-fit: contain;
  border-radius: 7px;
  box-shadow: 0 6px 22px rgba(0,0,0,.55);
}

/* A 1200x750 site capture — top-left anchored and filling the
   display, which is what a browser viewport does. */
.lpin__slide--site .lpin__art img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: 0 0;
}

/* No capture exists. A title card, deliberately legible AS a title
   card — inventing an interface here would be the one genuinely
   dishonest thing this component could do. */
.lpin__slide--plain .lpin__art {
  display: flex; align-items: center; justify-content: center;
}
.lpin__plain {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  text-align: center; padding: 0 24px;
}
.lpin__plain b {
  font-family: var(--font-display); font-weight: 400;
  font-size: 38px; line-height: 1.1; color: rgba(255,255,255,.9);
}
.lpin__plain i {
  font-family: var(--font-mono); font-style: normal;
  font-size: 13px; letter-spacing: .16em; text-transform: uppercase;
  color: rgba(255,255,255,.5);
}

/* ── GLASS ─────────────────────────────────────────────────────
   Above the content, inside the clip. Three things the photograph's
   own display does and a flat overlay would not:
     · a soft reflection off the upper left, which is where the room
       light in this frame comes from;
     · the bright specular line down the right-hand edge of the panel
       — it is measurably there in the source (the last lit column
       before the bezel runs ~40 luminance points hotter than the
       content beside it);
     · a shallow darkening into the bottom corners, matching the
       falloff measured across the panel (top row ~85, bottom right
       ~40 in the same units).
   Kept under .07 alpha throughout: any more and it stops reading as
   glass and starts reading as a gradient. */
.lpin__glare {
  position: absolute; inset: 0; z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(105deg,
      rgba(255,255,255,.062) 0%,
      rgba(255,255,255,.028) 22%,
      rgba(255,255,255,.004) 44%,
      transparent 62%),
    linear-gradient(to left,
      rgba(190,215,255,.075) 0px,
      rgba(190,215,255,.03) 3px,
      transparent 10px),
    radial-gradient(ellipse 92% 80% at 42% 26%,
      transparent 46%, rgba(4,7,16,.22) 100%);
}

/* ── THE CAPTION CARD IN PIN MODE ──────────────────────────────
   The card keeps the words and gives up the picture, because the
   picture is now on the laptop. Dropping .hs__shot is not a tidy-up:
   the same screenshot in the card AND on the screen 200px away is
   the component arguing with itself.

   It also has to physically clear the display, and "bottom-aligned
   and hope" did not: measured on the preview at 1440 the card's top
   landed at 358 with the screen's bottom edge at 425, straight
   through the machine. So the clearance is DERIVED — the script
   already knows where the quad's lower corners fell, and writes that
   into --lpin-clear. Nothing here depends on the hero's height, the
   copy length or the breakpoint, which is what makes it hold at 1180
   and at 1920 alike.

   IT IS A RELATIVE OFFSET, NOT PADDING, AND THAT IS LOAD-BEARING.
   .inner is a vertically centred grid, so padding on this column
   makes the column taller, which makes .inner taller, which recentres
   .inner upward — the measurement chases its own tail and settles
   about half way. `top` on an already-relative element moves the card
   without contributing a pixel of layout height, so one pass is
   exact. */
.hs--pinned .hs {
  align-self: start;
  top: var(--lpin-clear, 0px);
}
/* Two lines is the whole budget once the picture has moved to the
   screen — and it is what keeps the derived padding above from
   pushing the hero taller than the viewport on a long Supabase
   short_description. */
.hs--pinned .hs__desc {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.hs--pinned .hs__frame {
  background: rgba(10,10,10,.78);
  border-color: rgba(245,241,232,.18);
}
.hs--pinned .hs__shot { display: none; }
.hs--pinned .hs__link {
  /* height 100%, so .hs__go / .hs__status keep their margin-top:auto and sit
     on the card's bottom edge. The frame is as tall as the tallest slide
     (185px, set by the Supabase rows that carry stack chips) and without this
     the shorter slides left ~50px of dead space UNDER the call to action
     rather than above it, which reads as an empty box. */
  min-height: 0; height: 100%;
  padding: 1.05rem 1.25rem 1.1rem;
  gap: .3rem;
}
/* With no screenshot behind it the copy has the whole card; the 56%
   cap that kept it off busy pixels is now just a short measure. */
.hs--pinned .hs__slide--shot .hs__kind,
.hs--pinned .hs__slide--shot .hs__name,
.hs--pinned .hs__slide--shot .hs__desc,
.hs--pinned .hs__slide--shot .hs__stack { max-width: 34rem; }
.hs--pinned .hs__name { font-size: clamp(1.2rem, 1.9vw, 1.5rem); }
.hs--pinned .hs__desc { font-size: .82rem; }

@media (prefers-reduced-motion: reduce) {
  /* The screen still changes — that is the control doing its job —
     it just changes instantly, in step with .hs__track. */
  .lpin__track { transition: none !important; will-change: auto; }
}

/* ── T-1146 — Ethan: the on-screen display must be back in view, show
   DESKTOP system screenshots only, show the detail on the screen, and put
   the click buttons at the BOTTOM OF THE SCREEN. ─────────────────────── */
.lpin__art img {
  /* Show the interface, not a crop of it. object-fit: cover was hiding
     the detail that proves what the software does. */
  object-fit: contain !important;
  object-position: center top !important;
  background: #0d0f12;
}
/* Controls sit on the laptop screen itself, along its bottom edge. */
.tech-hero .hs__bar {
  position: absolute;
  left: 50%;
  bottom: clamp(10px, 1.4vw, 22px);
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .3rem .7rem;
  background: rgba(8, 9, 11, .72);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(245, 241, 232, .18);
  border-radius: 999px;
  width: auto;
  margin: 0;
}
.tech-hero .hs__count { color: rgba(245, 241, 232, .78); }

/* ════════════════════════════════════════════════════════════════════════
   THE CONTROLS LIVE ON THE SCREEN
   ────────────────────────────────────────────────────────────────────────
   Ethan: "i want the option and info overlayed on the screenshot area on
   laptop screen."

   Before this, the caption card was a separate object beside the machine
   and js/tech-laptop.js computed the padding needed to push it CLEAR of
   the display. Wrong model — the card is the chrome of the thing on the
   screen, so it belongs inside the screen, the way a player's controls sit
   inside the video rather than underneath it.

   js/tech-laptop.js now publishes the solved screen box as --scr-l/-t/-w/-h
   in hero-local pixels, re-solved on every layout change, so this tracks
   the object-fit crop at every viewport exactly as the screenshots do.
   ════════════════════════════════════════════════════════════════════════ */

.hs--pinned .hs__frame {
  position: absolute;
  left: var(--scr-l, 0);
  top: var(--scr-t, 0);
  width: var(--scr-w, 100%);
  height: var(--scr-h, auto);
  display: flex;
  align-items: flex-end;
  padding: 0;
  background: none;
  border: 0;
  overflow: hidden;
  z-index: 3;
  pointer-events: none;
}

/* The slide itself becomes a band across the FOOT of the display — the
   region a real application uses for a status bar, and the one part of a
   screenshot that carries no detail worth reading. */
.hs--pinned .hs__track { width: 100%; }

.hs--pinned .hs__link {
  pointer-events: auto;
  background: linear-gradient(to top, rgba(6,6,8,.94) 0%, rgba(6,6,8,.86) 58%, rgba(6,6,8,0) 100%);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  padding: 1.6rem clamp(.8rem, 1.4vw, 1.15rem) clamp(.55rem, 1vw, .8rem);
  min-height: 0;
  height: auto;
  gap: .18rem;
}

/* Type comes down a step: this is now a caption inside a ~630px display,
   not a card in a 740px column. */
.hs--pinned .hs__kind   { font-size: .55rem; letter-spacing: .13em; }
.hs--pinned .hs__name   { font-size: clamp(.92rem, 1.15vw, 1.08rem); line-height: 1.15; }
.hs--pinned .hs__desc   { font-size: .68rem; line-height: 1.45; -webkit-line-clamp: 2; max-width: 46ch; }
.hs--pinned .hs__go,
.hs--pinned .hs__status { font-size: .55rem; margin-top: .3rem; }
.hs--pinned .hs__stack  { display: none; }

/* Prev/next sit ON the display's bottom-right corner, inside the same
   band, the way transport controls do. */
.hs--pinned .hs__meta {
  position: absolute;
  left: var(--scr-l, 0);
  top: var(--scr-t, 0);
  width: var(--scr-w, 100%);
  height: var(--scr-h, auto);
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 0 clamp(.7rem, 1.2vw, 1rem) clamp(.5rem, .9vw, .75rem);
  margin: 0;
  z-index: 4;
  pointer-events: none;
}
.hs--pinned .hs__nav { pointer-events: auto; gap: .3rem; }
.hs--pinned .hs__btn {
  width: 26px; height: 26px;
  background: rgba(245,241,232,.10);
  border: 1px solid rgba(245,241,232,.24);
  color: #f5f1e8;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  transition: background 160ms cubic-bezier(.23,1,.32,1), border-color 160ms cubic-bezier(.23,1,.32,1);
}
.hs--pinned .hs__btn:hover,
.hs--pinned .hs__btn:focus-visible { background: var(--red); border-color: var(--red); }
.hs--pinned .hs__btn:active { transform: scale(.94); }

/* Nothing is being pushed below the machine any more. */
.hs--pinned { padding-top: 0 !important; }

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

/* THE TRACK MUST KEEP TRANSLATING.
   js/tech-hero-assembly.js drives this carousel with
       track.style.transform = translate3d(-index * 100%, 0, 0)
   An earlier fix here set `transform: none !important` and stacked the
   slides instead, to stop the caption sliding off the laptop screen. It
   stopped the caption sliding off — and it also froze the carousel on
   slide one, so every screen on the machine showed The Dispatch's copy
   whichever capture was behind it.

   The frame is positioned over the display and clips, so the translate is
   already contained. It just has to be allowed to happen. */
.hs--pinned .hs__track {
  display: flex;
  width: 100%;
}
.hs--pinned .hs__slide {
  flex: 0 0 100%;
  min-width: 100%;
}

/* The band must cover the FOOT of the display, not the whole of it. The
   first cut let .hs__link fill the frame, so its backdrop-filter blurred
   the entire screenshot — the one thing the section exists to show. */
.hs--pinned { position: relative; }
.hs--pinned .hs__track { align-content: end; height: 100%; }
.hs--pinned .hs__slide { align-self: end; }
.hs--pinned .hs__link {
  max-height: 46%;
  overflow: hidden;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* Keep the transport controls against the display's own right edge rather
   than the column's — .hs__meta positions against .hs, same as the frame. */
.hs--pinned .hs__meta { position: absolute; }

/* .hs__frame already carried backdrop-filter: blur(3px) from when it was a
   card floating over a photograph. Now that the frame IS the screen, that
   blur covered the entire capture — the screenshot rendered as a smear, and
   showing the detail on the screen is the whole point of the section.
   The blur belongs to the caption band, and only to it. */
.hs--pinned .hs__frame { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
.hs--pinned .hs__link {
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
