/* ════════════════════════════════════════════════════════════════════════
   css/components/app-preview.css — THE APP DISPLAYS
   service.tnaado.ca · reusable · pairs with js/app-preview.js

   PORTED from the old live site's css/app-preview.css + js/app-preview.js,
   the pair that runs the phone on https://service.tnaado.ca/technology.
   Ethan named it by hand:

     "i want all the anaimtions from .../technology loads and actual app
      dispalays come on"

   WHAT IT SHOWS. A drawn iPhone with a real capture on its screen, the app
   names as a tablist beside it, and the copy beside it changing with the
   selection. Every capture is taken from the application itself running on
   a phone, so the section is a display case rather than a claim.

   WHAT IS NEW, AND WHY. "come on" is the operative half of what he asked
   for. The old component was static — a 220ms cross-fade and nothing else,
   so the phone was a photograph that happened to have tabs. This port adds
   the beat the words describe: the device draws itself, the display POWERS
   ON with one scan line down the glass, and the capture resolves behind it.
   Switching apps replays a fast 300ms version of the same power-on rather
   than a plain fade, so a person clicking through eight apps sees eight
   screens come on instead of eight cross-fades.

   WHICH PROSE IT REPLACES. Any sentence that asserts the firm writes its
   own iPhone apps: "we build our own apps", "written natively in Swift",
   "on the App Store". The display case says all three and the reader can
   check it. When this lands on a page, DELETE the paragraph that was
   asserting it and keep the app names, which are the only part a reader
   cannot get from the picture.

   ── THE RUN IS 100% CSS ────────────────────────────────────────────────
   One class, `.is-on`, added once by js/app-preview.js on first
   intersection and never removed. Every beat after that is
   animation-delay. There is NO requestAnimationFrame loop, NO scroll
   listener and NO setTimeout chain stepping the phases in this component.
   That is load-bearing, not stylistic: js/camera.js runs the storm's
   damped scroll follower on its own rAF with a frame-rate-independent TAU,
   and a second per-frame consumer is exactly how that follower loses time.
   CSS animations are composited off the main thread, so this runs while
   the WebGL2 scene is still settling and neither one can starve the other.

   ══ THE FINISHED STATE IS THE DEFAULT. READ THIS BEFORE EDITING. ══
   Every element is authored in its FINAL appearance and the animations run
   BACKWARDS out of a `from` keyframe, held through their delay by
   animation-fill-mode: both. Author it the other way round and a script
   that 404s or is blocked leaves the reader a black rectangle where the
   work should be. Authored this way a blocked script costs the animation
   and the switcher, and nothing else: the first app's real capture is on
   the screen, its name and description are in flow, and the other seven
   captions are still in the DOM and still findable.

   The scan line is the one exception and it is authored the safe way
   round: opacity 0 at rest, so with no script it is simply never seen.

   ── COLOUR ────────────────────────────────────────────────────────────
   The device is LITERAL, not tokenised — it is a depiction of a machined
   aluminium-and-glass object and it has to look the same in an ink band as
   in a bone band or it stops reading as a phone. Everything around it (the
   tabs, the name, the description, the caption) IS tokenised, with a
   light-ground override at the foot, so the component drops into a --bone
   or --ivory band without anybody inventing a second grey.

   ── GEOMETRY ──────────────────────────────────────────────────────────
   The screen is 161/350, which is the captures' own aspect (322x700) to
   four figures, so seven of the eight land with nothing cropped off them.
   The eighth (the Mr. Indecisive feed, 322x618) is `cover` from the top,
   which trims its sides rather than its tab bar — the tab bar is the part
   that says what the app is, and the old component's comment records that
   being cropped off as the bug it spent two passes fixing.
   ════════════════════════════════════════════════════════════════════════ */

.ap {
  /* ── 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. */
  --ap-mono:  var(--mono,  'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace);
  --ap-serif: var(--serif, 'Fraunces', 'Times New Roman', Times, serif);
  --ap-sans:  var(--sans,  'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif);
  --ap-fg:    var(--bone,     #f5f1e8);
  --ap-dim:   var(--dim-ink,  #b9b3a6);
  --ap-acc:   var(--red-ink,  #ef5c6e);
  --ap-rule:  var(--hair-ink, rgba(245, 241, 232, .14));

  /* the device's own palette — literal on purpose, see the header */
  --ap-e:      cubic-bezier(.23, 1, .32, 1);
  --ap-glass:  #050506;
  --ap-body-1: #2a2a2e;
  --ap-body-2: #0e0e10;
  --ap-body-3: #232327;
  --ap-red:    #c8102e;

  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.6rem, 3.5vw, 3rem);
  align-items: center;
  margin: clamp(1.5rem, 3.5vw, 2.25rem) 0 0;
}

/* Two columns from 860px up, with the device on the RIGHT and the reading
   matter on the left — the order the old page used and the one that keeps a
   long description from being pushed under a tall object. Below that the
   device comes first, because a phone at 300px is the strongest thing on a
   narrow screen and burying it under eight lines of copy wastes it. */
@media (min-width: 860px) {
  .ap { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); }
  .ap__stage { order: 2; }
}

/* ══ THE DEVICE ═══════════════════════════════════════════════════════════
   Drawn, not photographed. A photograph of a phone would be another
   workstation picture on an estate that already has too many of them, and a
   drawn frame is correct at every width instead of only above 1180px. */
.ap__stage {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(.8rem, 2vw, 1.2rem);
}

/* SMALLER THAN IT WAS. Ethan: "the app screenshot section is to large".
   Was clamp(228px, 24vw, 296px) — a quarter of a wide viewport spent on one
   phone before the reading matter beside it got anything. A phone screenshot
   only has to be legible, not dominant. */
.ap__device { width: min(100%, clamp(200px, 17vw, 248px)); }

.ap__frame {
  position: relative;
  padding: 9px;
  border-radius: 40px;
  background: linear-gradient(158deg, var(--ap-body-1) 0%, var(--ap-body-2) 55%, var(--ap-body-3) 100%);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .10) inset,
              0 26px 52px -22px rgba(10, 10, 10, .55),
              0 5px 14px -7px rgba(10, 10, 10, .35);
}

.ap__notch {
  position: absolute; z-index: 3;
  top: 17px; left: 50%; transform: translateX(-50%);
  width: 30%; height: 19px; border-radius: 12px;
  background: var(--ap-glass);
}

.ap__screen {
  position: relative;
  aspect-ratio: 161 / 350;
  border-radius: 31px;
  overflow: hidden;
  background: var(--ap-glass);
}

/* ── the captures ─────────────────────────────────────────────────────────
   The glass under them is what a slow or failed capture shows: a correctly
   sized dark rectangle, never a broken-image glyph and never a jump. */
.ap__shot {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  /* CONTAIN, NOT COVER. The screen is 161/350 = 0.4600 and seven of the eight
     captures are exactly that, so for those two values render identically.
     mrindecisive-feed.png is 322x618 = 0.5210, and `cover` scaled it 13% and
     cropped ~21px off each side — on a UI capture, that is the nav rail and the
     right-hand column, i.e. the content the screenshot exists to show.
     `contain` cannot crop. Where a capture does not match, the glass shows
     instead, which is the same surface a slow or failed load already reveals. */
  object-fit: contain; object-position: top center;
  opacity: 0; visibility: hidden;
  transition: opacity 260ms var(--ap-e);
}
.ap__shot.is-on { opacity: 1; visibility: visible; }

/* THE CAP THAT BREAKS THIS IF IT IS NOT RESTATED. css/site.css carries
       img[src*="/apps/screens/"] { max-height: clamp(280px, 34vw, 440px);
                                    object-fit: cover; object-position: top center }
   and an attribute selector is (0,1,1), which beats a single class (0,1,0).
   Inside a drawn phone the DEVICE is the containment, so the cap is
   restated away at two-class specificity rather than fought with
   !important. Without these three lines the capture renders 296x440 in a
   296x643 screen with 200px of dead glass under it. */
.ap__screen .ap__shot { max-height: none; height: 100%; }

/* A single specular highlight across the glass, authored visible and never
   animated. It is what stops the screen reading as a flat rectangle. */
.ap__glass {
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(122deg,
    rgba(255, 255, 255, .085) 0%,
    rgba(255, 255, 255, .02) 26%,
    transparent 42%);
}

/* The power-on line. Authored INVISIBLE — the safe direction round — so
   with no script, or with reduced motion, it is simply never seen. */
.ap__scan {
  position: absolute; z-index: 4; left: 0; right: 0; top: 0;
  height: 2px; pointer-events: none; opacity: 0;
  background: linear-gradient(to right,
    transparent, rgba(255, 255, 255, .10) 12%,
    rgba(255, 255, 255, .72) 50%,
    rgba(255, 255, 255, .10) 88%, transparent);
  box-shadow: 0 0 14px 3px rgba(255, 255, 255, .28);
}

.ap__caption {
  margin: 0;
  font-family: var(--ap-mono); font-size: .66rem;
  letter-spacing: .06em; color: var(--ap-dim);
  text-align: center; max-width: 34ch;
}

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

        0 -  560   the device draws in and settles
      300 -  760   the glass goes dark — the display is off
      520 -  980   one scan line crosses the screen, top to bottom
      620 - 1180   the capture resolves behind it
      880 - 1400   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 apRise   { from { opacity: 0; transform: translateY(14px) scale(.965); }
                      to   { opacity: 1; transform: none; } }
@keyframes apDark   { from { opacity: 1; } 58% { opacity: 1; } to { opacity: 0; } }
@keyframes apWake   { from { opacity: 0; filter: brightness(.35) saturate(.6); }
                      to   { opacity: 1; filter: none; } }
@keyframes apTab    { from { opacity: 0; transform: translateY(6px); }
                      to   { opacity: 1; transform: none; } }

.ap.is-on .ap__frame  { animation: apRise 560ms var(--ap-e) 0ms both; }
.ap.is-on .ap__shot.is-on { animation: apWake 560ms var(--ap-e) 620ms both; }
.ap.is-on .ap__tab    { animation: apTab 380ms var(--ap-e) calc(880ms + var(--i, 0) * 55ms) both; }

/* THE SCAN TRAVEL. translateY on a 2px element cannot be expressed as a
   percentage of the SCREEN, only of itself, so the distance is carried as a
   custom property the keyframe multiplies. 32000% of 2px is 640px, which
   covers the tallest screen this component draws (296px wide / 161*350 =
   643px) with a margin. Overshoot is free: the line is clipped by the
   screen's overflow and has already faded by the time it leaves. */
@keyframes apScanRun {
  from { opacity: 0; transform: translateY(0); }
  14%  { opacity: 1; }
  86%  { opacity: 1; }
  to   { opacity: 0; transform: translateY(32000%); }
}
.ap.is-on .ap__scan { animation: apScanRun 460ms linear 520ms both; }

/* ── THE SWAP ─────────────────────────────────────────────────────────────
   Picking a different app replays a compressed power-on rather than a plain
   cross-fade, so a person clicking through eight apps sees eight screens
   come on. `.is-swap` is added by js/app-preview.js and removed on
   animationend, which is what lets the same animation restart. */
.ap__screen.is-swap .ap__scan { animation: apScanRun 300ms linear 0ms both; }
.ap__screen.is-swap .ap__shot.is-on { animation: apWake 320ms var(--ap-e) 40ms both; }

/* A click ends the intro wherever it had got to: the animations are DROPPED
   rather than reversed, which lands on the authored finished state in one
   frame instead of rewinding through a dark screen the reader has already
   watched come on. */
.ap.is-done .ap__frame,
.ap.is-done .ap__scan,
.ap.is-done .ap__tab,
.ap.is-done .ap__shot { animation: none; }

/* ══ THE TABS ═════════════════════════════════════════════════════════════
   A real tablist: real <button>s, aria-selected, roving tabindex handled in
   js/app-preview.js. With no script they are inert buttons and the first
   app stays on screen — the reader loses the switcher, not the work. */
.ap__tabs { display: flex; flex-wrap: wrap; gap: .35rem; margin: 0 0 1.15rem; }
.ap__tab {
  font-family: var(--ap-mono); font-size: .68rem;
  letter-spacing: .08em; text-transform: uppercase;
  padding: .44rem .72rem;
  color: var(--ap-dim);
  background: transparent;
  border: 1px solid var(--ap-rule);
  cursor: pointer;
}
.ap__tab[aria-selected="true"] {
  color: var(--ap-fg);
  border-color: var(--ap-red);
  background: rgba(200, 16, 46, .10);
}
.ap__tab:focus-visible { outline: 2px solid var(--ap-acc); outline-offset: 1px; }
@media (hover: hover) {
  .ap__tab { transition: color .2s var(--ap-e), border-color .2s var(--ap-e), background-color .2s var(--ap-e); }
  .ap__tab:not([aria-selected="true"]):hover { color: var(--ap-fg); border-color: var(--ap-rule); }
}

/* ══ THE COPY ═════════════════════════════════════════════════════════════
   Every app's caption is in the DOM at all times. The inactive ones are NOT
   display:none and NOT removed — they are hidden with hidden="until-found",
   which keeps the text indexable and lets the browser's own find-in-page
   reveal it. That is the rebuild's rule for all collapsed content. */
.ap__meta { max-width: 46ch; }
.ap__meta[hidden] { display: none; }

.ap__kind {
  display: block;
  font-family: var(--ap-mono); font-size: .66rem; font-weight: 400;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--ap-acc); margin-bottom: .5rem;
}
.ap__name {
  font-family: var(--ap-serif); font-weight: 400;
  font-size: clamp(1.3rem, 2.2vw, 1.75rem); line-height: 1.12;
  color: var(--ap-fg); margin: 0 0 .55rem;
}
.ap__desc {
  font-family: var(--ap-sans); font-size: .95rem; line-height: 1.62;
  color: var(--ap-dim); margin: 0 0 1rem;
}
.ap__go {
  display: inline-block;
  font-family: var(--ap-mono); font-size: .66rem;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--ap-acc); text-decoration: none;
  border-bottom: 1px solid currentColor; padding-bottom: 2px;
}
.ap__go--soon { color: var(--ap-dim); border-bottom-style: dashed; }
a.ap__go:focus-visible { outline: 2px solid var(--ap-acc); outline-offset: 3px; }

/* ══ LIGHT-GROUND VARIANT ═════════════════════════════════════════════════
   So the component can drop into a --bone or --ivory band without anybody
   inventing a second palette. The device itself does not change: it is a
   depiction of a black object and it stays one. */
.fp-band--bone .ap,
.fp-band--ivory .ap,
.ap--on-bone {
  --ap-fg:   var(--ink,       #0a0a0a);
  --ap-dim:  var(--dim-bone,  #5c574f);
  --ap-acc:  var(--red-bone,  #a4132c);
  --ap-rule: var(--hair-bone, rgba(10, 10, 10, .16));
}

/* ══ REDUCED MOTION ═══════════════════════════════════════════════════════
   Two rules, not a parallel stylesheet, because the finished state is
   already the authored default. js/app-preview.js adds `.is-done` instead
   of `.is-on` here, so nothing is ever queued; this is the belt to that
   braces, and it also covers a page that hard-codes `.is-on` in markup. */
@media (prefers-reduced-motion: reduce) {
  .ap.is-on *,
  .ap.is-on *::before,
  .ap.is-on *::after,
  .ap__screen.is-swap * { animation: none !important; }
  .ap__shot, .ap__tab { transition: none; }
  .ap__scan { display: none; }
}

/* ══ NARROW ═══════════════════════════════════════════════════════════════ */
@media (max-width: 420px) {
  .ap__caption { font-size: .62rem; }
  .ap__tab     { font-size: .62rem; padding: .4rem .6rem; }
}
