/* ════════════════════════════════════════════════════════════════════════
   css/components/laptop.css, THE LAPTOP
   service.tnaado.ca · reusable · pairs with js/laptop.js

   PORTED, IN CONCEPT, from the old live site's js/tech-laptop.js +
   css/tech-laptop.css. Ethan:

     "on the tech page in new build use the alptop concept instead of
      tradional copaursel"

   WHAT IT SHOWS. A laptop opens, its display strikes, and one of the
   firm's own systems is running on it. The app names sit under the machine
   as a tablist; picking one re-lights the display with that system's real
   capture. It replaces the horizontal scroller on technology.html, and the
   reason is the whole point: a carousel is a UI convention that says
   nothing about the subject, and a laptop is the object the subject
   actually lives on. It says "this is software, running, on a real
   machine" before a word is read.

   WHICH PROSE IT REPLACES. Nothing, in the first instance, it replaces a
   CAROUSEL. On technology.html the element that comes out is
   `<div class="car car--flush" data-carousel data-car-name="example">`
   with its six `.car__slide.ex` figures and its `.car__bar`. See
   PORTED-ANIMATIONS.md section 04 for the exact swap. Where a page also
   carries a sentence asserting that the firm runs its own internal
   systems, that sentence goes too.

   ── WHAT DID NOT COME ACROSS, AND WHY ─────────────────────────────────
   The old implementation was not a drawn laptop. It solved a 4-point
   HOMOGRAPHY onto the lit panel of one photograph, bts-claude-code-desk-
   wide.jpg, and painted the screens through a matrix3d, re-projecting on
   resize and following a second track's transform with a MutationObserver.
   It is a beautiful piece of work and it is the wrong thing here, for
   three reasons that are all load-bearing:

     1. IT DELIBERATELY DOES NOTHING BELOW 1180px. The old file's own
        comment says so: below 1180, in portrait, or under a 300px quad,
        "the pin never activates". That was acceptable when the laptop was
        a hero flourish over a photograph. It is not acceptable now that
        the laptop IS how the systems are browsed, a phone reader would be
        put straight back on the carousel this is replacing.
     2. IT WRITES matrix3d FROM A SECOND SOURCE. This estate's storm
        renderer already drives DOM matrix3d transforms from ONE shared
        view matrix in js/camera.js. A component that measures rects and
        emits its own matrix3d, inside a plate that the camera is already
        transforming, is the exact collision the rebuild is set up to
        avoid. A drawn machine needs no measurement at all.
     3. IT WAS COUPLED TO THAT PAGE. It read its slides out of
        #techShowcaseTrack, inherited the auto-advance and pausing from
        js/tech-hero-assembly.js, and rendered rows appended from Supabase.
        None of those exist in the rebuild and none should be recreated to
        carry a laptop.

   So the machine is DRAWN, which is what this estate already does for the
   browser in css/components/webbuild.css and the phone in
   css/components/app-preview.css. It is correct at every width, it needs
   no photograph, and it cannot be misregistered.

   The photo-pinned version is still worth building one day and the frame
   for it is in the repo, assets/bts/bts-claude-code-desk-wide.jpg, which
   PHOTO-RULING rates STRONG. It is written up as a proposal rather than
   smuggled in here. Note that if it is ever built, the screen-legibility
   caveat on that frame is satisfied by construction: the overlay covers
   the terminal.

   ── THE RUN IS 100% CSS ────────────────────────────────────────────────
   One class, `.is-on`, added once by js/laptop.js on first intersection.
   Every beat after that is animation-delay. NO requestAnimationFrame
   loop, NO scroll listener, NO resize listener, NO measurement, NO
   MutationObserver. That is what makes it safe beside the storm: nothing
   in this component reads layout, so nothing in it can be wrong about
   layout, and nothing in it competes for a frame with the camera's damped
   scroll follower.

   ══ THE FINISHED STATE IS THE DEFAULT. READ THIS BEFORE EDITING. ══
   The lid is authored OPEN, the display authored LIT, the first capture
   authored ON. The animations run BACKWARDS out of a `from` keyframe, held
   through their delay by animation-fill-mode: both. So the machine is never
   caught mid-assembly and there is no second set of declarations to keep in
   sync with the first.

   ⚠ WITH ONE CORRECTION, MADE WHEN THE DRIVER WAS WRITTEN. That paragraph
   used to end "a blocked script leaves an open laptop with a real system
   screenshot on it". It does not, and it should not: one screenshot is five
   fewer than the markup carries. A `.lt` with no `.is-live` on it now
   renders as the STACK, all six captures, visible, in order. See THE
   DEGRADATION at the foot of this file, which is the load-bearing half of
   this component's honesty. All six captures stay in the served HTML with
   real alt text in both states, which is the point: they are the strongest
   evidence on the site and they have to stay visible and indexable.

   ── GEOMETRY ──────────────────────────────────────────────────────────
   The screen is 16/10, which is exactly the captures' aspect (1600x1000
   and 1280x800 both reduce to 8/5), so every one lands with nothing
   cropped and nothing stretched.

   IT HAS TO WORK AT ~400px. On technology.html the plate it drops into is
   `--sc-w: min(26rem, 29vw)`, which is 416px at 1440. That is the real
   constraint on this component, not the viewport: everything below is
   sized in the component's own width, and the tabs wrap.
   ════════════════════════════════════════════════════════════════════════ */

.lt {
  /* ── TOKEN BRIDGE. Every global is re-declared with a literal fallback so
     the component is drop-in on a page that links only css/scene.css and
     css/chrome.css, neither of which declares these. */
  --lt-mono: var(--mono, 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace);
  --lt-sans: var(--sans, 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);
  --lt-fg:   var(--bone,      #f5f1e8);
  --lt-dim:  var(--dim-ink,   #b9b3a6);
  --lt-acc:  var(--red-ink,   #ef5c6e);
  --lt-rule: var(--hair-ink,  rgba(245, 241, 232, .14));

  /* the machine's own palette, literal on purpose. It is a depiction of an
     aluminium object and it must read the same in an ink band as in a bone
     band, or it stops reading as a machine. */
  --lt-e:      cubic-bezier(.23, 1, .32, 1);
  --lt-alu-1:  #3a3a3f;
  --lt-alu-2:  #202024;
  --lt-alu-3:  #56565d;
  --lt-glass:  #08080a;
  --lt-red:    #c8102e;

  margin: clamp(1.25rem, 3vw, 2rem) 0 0;
}

/* ══ THE MACHINE ══════════════════════════════════════════════════════════
   Drawn, not photographed, the same decision as the browser in
   webbuild.css and the phone in app-preview.css. The perspective lives
   here so the lid has something to rotate in. */
.lt__machine {
  perspective: 1600px;
  perspective-origin: 50% 82%;
}

/* ── the lid ──────────────────────────────────────────────────────────────
   Authored OPEN at 0deg. The open animation runs backwards out of a closed
   `from` state, so with no script the machine is simply open. */
.lt__lid {
  position: relative;
  transform-origin: 50% 100%;
  padding: 7px 7px 9px;
  border-radius: 10px 10px 3px 3px;
  background: linear-gradient(168deg, var(--lt-alu-1) 0%, var(--lt-alu-2) 62%, var(--lt-alu-1) 100%);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .10) inset,
              0 20px 44px -26px rgba(10, 10, 10, .60);
}

.lt__cam {
  position: absolute; z-index: 3;
  top: 2.5px; left: 50%; transform: translateX(-50%);
  width: 4px; height: 4px; border-radius: 50%;
  background: rgba(245, 241, 232, .16);
}

.lt__screen {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 3px;
  overflow: hidden;
  background: var(--lt-glass);
}

/* ── the window chrome INSIDE the screen ──────────────────────────────────
   A title bar on the capture, not around it. The captures are of real
   applications and most of them were taken without browser chrome, so the
   bar is what makes the screen read as a machine running an app rather
   than a picture pasted onto glass. */
.lt__win {
  position: absolute; z-index: 3; top: 0; left: 0; right: 0;
  display: flex; align-items: center; gap: .4rem;
  padding: .3rem .45rem;
  background: rgba(10, 10, 10, .82);
  border-bottom: 1px solid rgba(245, 241, 232, .08);
}
.lt__dots { display: flex; gap: 3px; flex: 0 0 auto; }
.lt__dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(245, 241, 232, .22); }
.lt__title {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--lt-mono); font-size: .52rem;
  letter-spacing: .08em; text-transform: uppercase;
  color: rgba(245, 241, 232, .56);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── the captures ─────────────────────────────────────────────────────────
   16/10 into a 16/10 box, so `cover` and `contain` agree and neither can
   crop. The glass under them is what a slow or failed capture shows: a
   correctly sized dark rectangle, never a broken-image glyph. */
.lt__shot {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  opacity: 0; visibility: hidden;
  transition: opacity 240ms var(--lt-e);
}
.lt__shot.is-on { opacity: 1; visibility: visible; }

/* css/site.css caps images under /apps/screens/; the systems captures are
   not under it today, but the same class of rule has bitten this estate
   twice, so the containment is restated where the DEVICE is the container. */
.lt__screen .lt__shot { max-height: none; height: 100%; }

/* A single specular sheet across the glass, authored visible, never
   animated. It is what stops the screen reading as a flat rectangle. */
.lt__glare {
  position: absolute; z-index: 4; inset: 0; pointer-events: none;
  background: linear-gradient(114deg,
    rgba(255, 255, 255, .07) 0%,
    rgba(255, 255, 255, .015) 30%,
    transparent 46%);
}

/* The backlight striking. Authored INVISIBLE, the safe direction round,
   so with no script, or with reduced motion, it is never seen. */
.lt__boot {
  position: absolute; z-index: 5; inset: 0; pointer-events: none; opacity: 0;
  background:
    radial-gradient(120% 80% at 50% 50%, rgba(226, 236, 255, .55) 0%, rgba(226, 236, 255, 0) 62%);
}

/* ── the base ─────────────────────────────────────────────────────────────
   Two elements, not five: the deck and the thumb notch. A drawn keyboard at
   400px wide is fifty grey rectangles that read as noise, so there is not
   one, the deck is closed aluminium, which is what you actually see of a
   laptop from the front at this size. */
.lt__base {
  position: relative;
  height: clamp(7px, 1.6vw, 11px);
  border-radius: 0 0 7px 7px;
  background: linear-gradient(to bottom, var(--lt-alu-3) 0%, var(--lt-alu-1) 44%, var(--lt-alu-2) 100%);
  box-shadow: 0 14px 26px -16px rgba(10, 10, 10, .70);
}
.lt__base::before {
  content: ""; position: absolute; inset: 0 auto auto 0;
  width: 100%; height: 1px; background: rgba(255, 255, 255, .16);
}
.lt__notch {
  position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 16%; height: 45%;
  border-radius: 0 0 4px 4px;
  background: rgba(10, 10, 10, .34);
}

.lt__caption {
  margin: .9rem 0 0;
  font-family: var(--lt-mono); font-size: .62rem;
  letter-spacing: .06em; color: var(--lt-dim);
}

/* ══ THE RUN ══════════════════════════════════════════════════════════════
   Everything below fires off ONE class. Timeline, in ms from .is-on:

        0 -  560   the machine arrives, deck and lid rise together
      180 -  980   the lid OPENS, -64deg to 0
      820 - 1180   the backlight strikes, one bloom in and out
      940 - 1520   the capture resolves behind it
     1180 - 1520   the title bar slides in
     1360 - 1800   the tabs arrive, 55ms apart

   Held on `both`, so an element whose delay has not elapsed sits at its
   from-state and cannot flash its finished appearance first. */
@keyframes ltArrive { from { opacity: 0; transform: translateY(16px); }
                      to   { opacity: 1; transform: none; } }
@keyframes ltOpen   { from { transform: rotateX(-64deg); }
                      to   { transform: none; } }
@keyframes ltBoot   { from { opacity: 0; } 40% { opacity: 1; } to { opacity: 0; } }
@keyframes ltWake   { from { opacity: 0; filter: brightness(.3) saturate(.55); }
                      to   { opacity: 1; filter: none; } }
@keyframes ltBar    { from { opacity: 0; transform: translateY(-100%); }
                      to   { opacity: 1; transform: none; } }
@keyframes ltTab    { from { opacity: 0; transform: translateY(6px); }
                      to   { opacity: 1; transform: none; } }

.lt.is-on .lt__machine   { animation: ltArrive 560ms var(--lt-e) 0ms both; }
.lt.is-on .lt__lid       { animation: ltOpen 800ms var(--lt-e) 180ms both; }
.lt.is-on .lt__boot      { animation: ltBoot 360ms linear 820ms both; }
.lt.is-on .lt__shot.is-on { animation: ltWake 580ms var(--lt-e) 940ms both; }
.lt.is-on .lt__win       { animation: ltBar 340ms var(--lt-e) 1180ms both; }
.lt.is-on .lt__tab       { animation: ltTab 380ms var(--lt-e) calc(1360ms + var(--i, 0) * 55ms) both; }

/* ── THE SWAP ─────────────────────────────────────────────────────────────
   Picking a different system re-lights the display rather than cross-fading
   it, so clicking through six systems is six screens coming on. `.is-swap`
   is added by js/laptop.js and removed on a timer, which is what lets the
   same animation restart. The LID does not move: re-opening a laptop every
   time somebody clicks a tab would be a cartoon. */
.lt__screen.is-swap .lt__boot { animation: ltBoot 260ms linear 0ms both; }
.lt__screen.is-swap .lt__shot.is-on { animation: ltWake 300ms var(--lt-e) 40ms both; }

/* A click ends the intro wherever it had got to: dropped, not reversed, so
   it lands on the authored finished state in one frame instead of rewinding
   through a closing lid. */
.lt.is-done .lt__machine,
.lt.is-done .lt__lid,
.lt.is-done .lt__boot,
.lt.is-done .lt__win,
.lt.is-done .lt__tab,
.lt.is-done .lt__shot { animation: none; }

/* ══ THE TABS ═════════════════════════════════════════════════════════════
   This is now the PRIMARY way the systems are browsed, so it is a real
   tablist and not a pair of arrows: real <button>s, aria-selected, roving
   tabindex and Home/End in js/laptop.js, visible focus, and no scroll
   handling of any kind, which is why it cannot trap the page's scroll the
   way a horizontal scroller does. With no script they do not render at all,
   see THE DEGRADATION at the foot of this file. */
.lt__tabs { display: flex; flex-wrap: wrap; gap: .3rem; margin: 1rem 0 0; }
.lt__tab {
  font-family: var(--lt-mono); font-size: .6rem;
  letter-spacing: .07em; text-transform: uppercase;
  padding: .38rem .6rem;
  color: var(--lt-dim);
  background: transparent;
  border: 1px solid var(--lt-rule);
  cursor: pointer;
}
.lt__tab[aria-selected="true"] {
  color: var(--lt-fg);
  border-color: var(--lt-red);
  background: rgba(200, 16, 46, .10);
}
.lt__tab:focus-visible { outline: 2px solid var(--lt-acc); outline-offset: 1px; }
@media (hover: hover) {
  .lt__tab { transition: color .2s var(--lt-e), border-color .2s var(--lt-e), background-color .2s var(--lt-e); }
  .lt__tab:not([aria-selected="true"]):hover { color: var(--lt-fg); border-color: var(--lt-rule); }
}

/* ══ THE CAPTION ══════════════════════════════════════════════════════════
   Every system's caption is in the DOM at all times. The inactive ones are
   hidden with hidden="until-found", so the text stays indexable and
   find-in-page can still reveal it. */
.lt__meta { margin: .85rem 0 0; max-width: 52ch; }
.lt__meta[hidden] { display: none; }
.lt__kind {
  display: block;
  font-family: var(--lt-mono); font-size: .62rem; font-weight: 400;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--lt-acc); margin-bottom: .35rem;
}
.lt__name {
  font-family: var(--lt-sans); font-weight: 500;
  font-size: .95rem; line-height: 1.3;
  color: var(--lt-fg); margin: 0 0 .3rem;
}
.lt__desc {
  font-family: var(--lt-sans); font-size: .88rem; line-height: 1.6;
  color: var(--lt-dim); margin: 0;
}

/* ══ LIGHT-GROUND VARIANT ═════════════════════════════════════════════════ */
.fp-band--bone .lt,
.fp-band--ivory .lt,
.lt--on-bone {
  --lt-fg:   var(--ink,       #0a0a0a);
  --lt-dim:  var(--dim-bone,  #5c574f);
  --lt-acc:  var(--red-bone,  #a4132c);
  --lt-rule: var(--hair-bone, rgba(10, 10, 10, .16));
}

/* ══ REDUCED MOTION ═══════════════════════════════════════════════════════
   js/laptop.js adds `.is-done` instead of `.is-on` here, so the lid never
   moves and the display never flashes. These rules cover a page that
   hard-codes `.is-on`. */
@media (prefers-reduced-motion: reduce) {
  .lt.is-on *,
  .lt.is-on *::before,
  .lt.is-on *::after,
  .lt__screen.is-swap * { animation: none !important; }
  .lt__shot, .lt__tab { transition: none; }
  .lt__boot { display: none; }
}

/* ══ NARROW ═══════════════════════════════════════════════════════════════
   Under ~360px of component width the title bar's mono caps stop being
   type and start being texture, so the bar keeps its dots and drops its
   label. Container queries, because the constraint is the PLATE's width
   (min(26rem, 29vw) on technology.html), not the viewport's. */
@container (max-width: 360px) {
  .lt__title { display: none; }
  .lt__tab   { font-size: .55rem; padding: .34rem .5rem; }
}
.lt { container-type: inline-size; }

/* Fallback for a browser with no container queries: the viewport width at
   which that plate is around 360px. */
@supports not (container-type: inline-size) {
  @media (max-width: 1240px) {
    .lt__title { display: none; }
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE DEGRADATION, added with js/laptop.js, which this file was written
   before. READ THIS; it changes which state is the default.
   ──────────────────────────────────────────────────────────────────────────
   Everything above authors the FINISHED MACHINE as the default: lid open,
   display lit, first capture on. That is the right default for a component
   whose animation is the only thing scripting adds, and it is the WRONG
   default here, which I only saw once the driver existed.

   The reason is arithmetic, not taste. A tablist can show exactly one panel.
   This one has six, and each panel is a real screenshot of a system the firm
   runs, the strongest evidence anywhere on this site. So "the finished
   state" costs a reader with no JavaScript five of the six captures and
   leaves the other five as invisible boxes inside an aluminium shell. Every
   other component on this estate degrades to MORE content than it shows
   running. This one degraded to less.

   So the stack is the default and the MACHINE is what scripting adds:

     no JS  →  six real captures, full width, in order, all six descriptions
               under them, no controls, no laptop, nothing pretending.
     JS     →  js/laptop.js adds `.is-live` synchronously at mount, before it
               wires anything, and the stack folds into the machine.

   This is the same reasoning as `car-live` in technology.html's own carousel
   script and `.chrome-live` in js/chrome.js: a control that cannot control
   anything is a lie in the accessibility tree, so the tabs do not exist
   until something can answer them.

   Nothing is duplicated to achieve it. There is ONE set of six <img> and ONE
   set of six descriptions, in the served HTML in reading order, in both
   states. The two states are the same DOM under different rules.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── the stack: no scripting, or scripting that has not mounted yet ────── */
.lt:not(.is-live) .lt__machine { perspective: none; }

/* The aluminium comes OFF. A tall metal column with six screenshots in it is
   a machine that does not exist and cannot be operated; six screenshots are
   just six screenshots, which is honest and is also the content. */
.lt:not(.is-live) .lt__lid {
  padding: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.lt:not(.is-live) .lt__base,
.lt:not(.is-live) .lt__cam,
.lt:not(.is-live) .lt__glare,
.lt:not(.is-live) .lt__boot,
.lt:not(.is-live) .lt__win { display: none; }

/* A tab that cannot switch anything does not render. */
.lt:not(.is-live) .lt__tabs { display: none; }

.lt:not(.is-live) .lt__screen {
  position: static;
  aspect-ratio: auto;
  overflow: visible;
  border-radius: 0;
  background: none;
}

/* Every capture, visible, in order, each one bounded so a tall screenshot
   cannot become the layout. `cover` + top anchor keeps the header and nav of
   each captured UI, the part that identifies the software, rather than
   its footer, which is the same rule css/site.css applies site-wide. */
.lt:not(.is-live) .lt__shot {
  position: static;
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  max-height: clamp(280px, 34vw, 440px);
  object-fit: cover;
  object-position: top center;
  opacity: 1;
  visibility: visible;
  transition: none;
  border: 1px solid var(--lt-rule);
}
.lt:not(.is-live) .lt__shot + .lt__shot { margin-top: .75rem; }

/* The screen-height override above is for the DEVICE, where the image fills
   a 16/10 box. In the stack there is no device, so the site-wide cap has to
   be allowed to do its job. */
.lt.is-live .lt__screen .lt__shot { max-height: none; height: 100%; }

/* All six descriptions, in order, separated. `[hidden]` is never authored,
   js/laptop.js applies hidden="until-found" to the inactive five once it has
   somewhere for the reader to put them back. */
.lt:not(.is-live) .lt__meta { margin-top: 1.1rem; }
.lt:not(.is-live) .lt__meta + .lt__meta {
  padding-top: 1.1rem;
  border-top: 1px solid var(--lt-rule);
}

/* ── the caption ───────────────────────────────────────────────────────────
   One line, true in both states, which is why it says nothing about counts
   or controls. It answers the only question a reader actually has about a
   screenshot on an agency site. */
.lt__caption { margin: 0 0 .9rem; }
.lt:not(.is-live) .lt__caption { margin: 0 0 1rem; }

/* ── FIRST PAINT ───────────────────────────────────────────────────────────
   `.is-live` is added from a DOMContentLoaded handler, so between first paint
   and mount the stack is briefly the rendered state. That is correct and it
   is not a flash: it is six real images in flow, which is what the markup
   says, and the machine that replaces them occupies the first capture's own
   box. Nothing here is `visibility: hidden` waiting on a script, a reader on
   a slow connection sees content, never a reserved void. */
