/* ============================================================
   motion.css — animated and interactive components

   Network fabric · patch cables · pose transition · console ·
   rack LEDs · card tilt · rail grab

   Every animated rule here is switched off in the reduced-motion block
   at the foot of the file. Nothing in here carries content, so losing
   all of it costs atmosphere and nothing else.
   ============================================================ */

/* ══════════════════════════════════════════════════════════
   NETWORK FABRIC
   Fixed behind everything. Sits above the page background but below
   all content, and never intercepts a pointer event.
   ══════════════════════════════════════════════════════════ */
.netfx {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Fades out top and bottom so the mesh does not fight the nav bar or
     crowd the footer. */
  mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 14%, #000 86%, transparent);
}
.netfx canvas { width: 100%; height: 100%; display: block; }

/* Content sits above the fabric.
   Only `main` and the footer are restated here. `.nav` and `.drawer`
   are deliberately left alone: they are already positioned and stacked
   in main.css, and setting `position: relative` on the fixed nav would
   drop it into document flow — it would take up 68px at the top of the
   page and scroll away instead of sticking. */
main, .foot { position: relative; z-index: 1; }

/* ══════════════════════════════════════════════════════════
   PATCH CABLES
   ══════════════════════════════════════════════════════════ */
/* Scales uniformly rather than stretching: a non-uniform
   preserveAspectRatio would squash the packet dot into an ellipse and
   skew the connector shells. Height therefore follows width. */
.cable {
  position: relative;
  z-index: 1;
  width: 100%;
  margin: 0;
  pointer-events: none;
  overflow: visible;
  /* Clamp how tall the cable can get on very wide screens. */
  max-height: 170px;
}
.cable svg { width: 100%; height: auto; display: block; overflow: visible; }

.cable__glow {
  fill: none;
  stroke: var(--ac);
  stroke-width: 7;
  stroke-linecap: round;
  opacity: .16;
  filter: blur(6px);
}
.cable__line {
  fill: none;
  stroke: var(--ac);
  stroke-width: 1.7;
  stroke-linecap: round;
  opacity: .55;
}
.cable__dot {
  fill: #b4f5ff;
  filter: drop-shadow(0 0 6px var(--ac));
}
/* Connector shells at each end — reads as a patch lead rather than a
   decorative squiggle. */
.cable__end {
  fill: var(--bg-3);
  stroke: var(--line-2);
  stroke-width: 1;
}
.cable__pin { fill: var(--ac); opacity: .5; }

/* ══════════════════════════════════════════════════════════
   POSE TRANSITION
   A pinned band where the character moves from standing to seated as
   you scroll. Frames are stacked and cross-faded by scroll progress.
   ══════════════════════════════════════════════════════════ */
/* The stage is mounted inside the hero, not in a section of its own.
   Height is pinned to the hero's usable area and aspect-ratio derives
   the width, so the frames never crop. */
.morph__stage {
  position: absolute;
  top: var(--nav-h);
  right: max(1.5vw, calc((100vw - var(--wrap)) / 2 - 3rem));
  height: calc(100svh - var(--nav-h));
  aspect-ratio: 3 / 4;
  width: auto;
  pointer-events: none;
  z-index: var(--z-figure);
}
.morph__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  /* The frames' black point is corrected at export to match the page,
     but the first frame still carries a little ambient glow. Feathering
     generously on every edge dissolves whatever residual difference is
     left, so no frame ever reads as a rectangle. */
  mask-image: radial-gradient(ellipse 80% 88% at 52% 48%, #000 26%, transparent 90%);
  -webkit-mask-image: radial-gradient(ellipse 80% 88% at 52% 48%, #000 26%, transparent 90%);
  /* Cyan bloom, reading as light spilling off the rim lighting baked
     into the renders themselves. */
  filter: drop-shadow(0 0 70px rgba(34, 211, 238, 0.18));
}
.morph__frame:first-child { opacity: 1; }

/* The living end frame. Positioned and feathered identically to the
   stills so the cross-fade from frame 10 into it is invisible — any
   difference in size, fit or masking would show as a jump at exactly the
   moment it has to be seamless. Deliberately NOT given the .morph__frame
   class: morph.js counts those to build the strip, and an eleventh member
   would corrupt the frame maths. */
.morph__live {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  pointer-events: none;
  mask-image: radial-gradient(ellipse 80% 88% at 52% 48%, #000 26%, transparent 90%);
  -webkit-mask-image: radial-gradient(ellipse 80% 88% at 52% 48%, #000 26%, transparent 90%);
  filter: drop-shadow(0 0 70px rgba(34, 211, 238, 0.18));
}

/* The seated figure in the About section is a loop of the same footage,
   so it needs the same edge feathering as the hero stack — a rounded,
   shadowed card (the default .sec__figure treatment) would show its dark
   background as a visible box. Applied to the video, its poster and the
   no-video fallback image alike. */
.sec__figure--seated video,
.sec__figure--seated img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0;
  mask-image: radial-gradient(ellipse 82% 88% at 52% 50%, #000 28%, transparent 92%);
  -webkit-mask-image: radial-gradient(ellipse 82% 88% at 52% 50%, #000 28%, transparent 92%);
  filter: drop-shadow(0 0 60px rgba(34, 211, 238, 0.16));
}
/* The poster is drawn by the browser inside the video box, so object-fit
   keeps it matching the footage rather than stretching. */
.loopvid { object-fit: cover; background: transparent; }

/* ══════════════════════════════════════════════════════════
   CONSOLE
   ══════════════════════════════════════════════════════════ */
.term {
  position: relative;
  z-index: 1;
  margin-top: var(--s-7);
  background: linear-gradient(160deg, #0b0f15, #080a0e);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-2);
  cursor: pointer;
}
.term:hover { border-color: var(--ac-30); }

.term__bar {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .7rem 1rem;
  border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, .02);
}
.term__dots { display: flex; gap: .35rem; }
.term__dots i {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--txt-4);
}
.term__dots i:nth-child(1) { background: #f87171; opacity: .7; }
.term__dots i:nth-child(2) { background: #fbbf24; opacity: .7; }
.term__dots i:nth-child(3) { background: #34d399; opacity: .7; }

.term__title {
  font-family: var(--f-mono);
  font-size: var(--t-2xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--txt-3);
}
.term__status {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-family: var(--f-mono);
  font-size: var(--t-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--txt-3);
}
.term__status::before {
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--txt-4);
}
.term__status[data-state="busy"]::before {
  background: var(--warm);
  box-shadow: 0 0 0 3px rgba(251, 191, 36, .15);
  animation: blink 1s steps(2, start) infinite;
}
.term__status[data-state="done"]::before {
  background: #34d399;
  box-shadow: 0 0 0 3px rgba(52, 211, 153, .15);
}
@keyframes blink { 50% { opacity: .25; } }

.term__body {
  padding: 1.1rem 1.2rem 1.3rem;
  font-family: var(--f-mono);
  font-size: .8rem;
  line-height: 1.75;
  /* Fixed height stops the section reflowing as commands of different
     lengths type in and out. */
  min-height: 11.5rem;
}
.term__row { display: flex; gap: .6rem; }
.term__ps { color: var(--ac); flex: none; user-select: none; }
.term__cmd {
  color: var(--txt);
  white-space: pre-wrap;
  word-break: break-word;
}
.term__cmd::after {
  content: '';
  display: inline-block;
  width: 7px;
  height: 1em;
  margin-left: 2px;
  background: var(--ac);
  vertical-align: text-bottom;
  animation: caret 1.05s steps(2, start) infinite;
}
@keyframes caret { 50% { opacity: 0; } }

.term__out { margin-top: .7rem; }
.term__line {
  color: var(--txt-2);
  padding-left: 1.5rem;
  position: relative;
}
.term__line::before {
  content: '›';
  position: absolute;
  left: .3rem;
  color: var(--txt-4);
}
.term__hint {
  padding: 0 1.2rem 1rem;
  font-family: var(--f-mono);
  font-size: var(--t-2xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--txt-4);
}

/* ══════════════════════════════════════════════════════════
   RACK LEDS
   ══════════════════════════════════════════════════════════ */
.leds {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: .4rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: rgba(0, 0, 0, .35);
  vertical-align: middle;
}
.led {
  width: 4px;
  height: 11px;
  border-radius: 1px;
  background: var(--ac);
  opacity: .28;
  animation: led var(--dur, 1.4s) var(--delay, 0s) steps(2, start) infinite;
}
.led[data-kind="warn"] { background: var(--warm); }
.led[data-kind="ok"] { background: #34d399; opacity: .85; animation: none; }
@keyframes led {
  0%, 45%  { opacity: .18; }
  46%, 100% { opacity: .95; box-shadow: 0 0 6px currentColor; }
}

/* ══════════════════════════════════════════════════════════
   CARD TILT
   ══════════════════════════════════════════════════════════ */
.tiltable {
  transform: perspective(900px) rotateX(var(--tx, 0deg)) rotateY(var(--ty, 0deg));
  transform-style: preserve-3d;
  transition: transform 240ms var(--e-out);
}
/* A highlight that follows the cursor across the card face. */
.cap.tiltable::after,
.cred__col.tiltable::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    22rem circle at var(--gx, 50%) var(--gy, 50%),
    var(--ac-08),
    transparent 62%
  );
  opacity: 0;
  transition: opacity var(--d) var(--e-out);
  pointer-events: none;
}
.cap.tiltable, .cred__col.tiltable { position: relative; }
.cap.tiltable:hover::after,
.cred__col.tiltable:hover::after { opacity: 1; }

/* The tilt transform replaces the translate-based hover lift, so those
   rules would otherwise fight each other. */
.cap.tiltable:hover, .cred__col.tiltable:hover, .tl__card { transform: none; }
.tl:hover .tl__card.tiltable {
  transform: perspective(900px) rotateX(var(--tx, 0deg)) rotateY(var(--ty, 0deg));
}

/* ══════════════════════════════════════════════════════════
   RAIL GRAB
   ══════════════════════════════════════════════════════════ */
#rail { cursor: grab; }
#rail.rail--grabbing { cursor: grabbing; user-select: none; }
#rail.rail--grabbing .tl__card { transition: none; }

/* ══════════════════════════════════════════════════════════
   SECTION SCAN LINE
   A thin cyan sweep along each section's top edge, drawn as the section
   arrives. Cheap, and it ties the sections together visually.
   ══════════════════════════════════════════════════════════ */
.sec::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--gutter);
  right: var(--gutter);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--ac-30), transparent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 1.1s var(--e-out);
}
.sec.is-in::before { transform: scaleX(1); }

/* ══════════════════════════════════════════════════════════
   REDUCED MOTION
   One switch, at the end, so it always wins.
   ══════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
  .led,
  .term__status[data-state="busy"]::before,
  .term__cmd::after { animation: none !important; }

  .led { opacity: .5; }

  .tiltable { transform: none !important; transition: none !important; }

  .sec::before { transform: scaleX(1); transition: none; }

  .morph__frame { opacity: 0; }
  /* A single still poster frame; no scrubbing, no video. */
  .morph__frame:first-child { opacity: 1; }
  .morph__live { display: none; }

  .cable__line, .cable__glow {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }
  .cable__dot { display: none; }
}

/* ══════════════════════════════════════════════════════════
   NARROW SCREENS
   ══════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .cable { height: clamp(60px, 14vw, 92px); }

  /* In flow rather than absolutely placed. The stage sits BEFORE the
     hero text in the markup (it is the hero's figure), so on mobile the
     grid order has to be set explicitly or the character would appear
     above the headline. aspect-ratio is dropped because width is now
     the constrained dimension. */
  .morph__stage {
    position: relative;
    top: auto;
    right: auto;
    width: min(23rem, 78vw);
    height: min(46vh, 24rem);
    aspect-ratio: auto;
    margin: var(--s-6) auto 0;
    order: 2;
  }
  .hero__body { order: 1; }

  .term__body { min-height: 13rem; font-size: .74rem; }
  /* The fabric is decorative; on a phone the battery matters more. */
  .netfx { opacity: .55; }
}
