/* ==========================================================================
   Frame Robotics — motion layer
   Loaded after styles.css; adds nothing to the resting appearance of the page.

   Everything here is gated behind `html.has-motion`, which motion.js sets at
   run time. With JS disabled the class never appears, every rule below is
   inert, and the page renders exactly as it does without this file.

   Two conventions worth knowing before editing:

   1. Reveals use the independent `translate` / `scale` properties rather than
      `transform`, so they compose with the transforms styles.css already puts
      on elements (`.compare__vs`, the parallax layers) instead of clobbering
      them.
   2. `--mo-delay` is written per element by motion.js to stagger a scene. CSS
      only ever reads it.
   ========================================================================== */

:root {
  --mo-fast: 260ms;
  --mo-base: 560ms;
  --mo-slow: 900ms;
  --mo-ease: cubic-bezier(.22, .61, .36, 1);   /* settle: fast out, long tail */
  --mo-ease-soft: cubic-bezier(.4, 0, .2, 1);

  /* narrow highlight band swept across gradient headings on reveal */
  --mo-sheen: linear-gradient(100deg, #ffffff00 0%, #ffffffe6 48%, #ffffff00 100%);
}

/* --- reveal primitive ----------------------------------------------------- */

html.has-motion [data-mo] {
  opacity: 0;
  transition:
    opacity var(--mo-base) var(--mo-ease) var(--mo-delay, 0ms),
    translate var(--mo-base) var(--mo-ease) var(--mo-delay, 0ms),
    scale var(--mo-base) var(--mo-ease) var(--mo-delay, 0ms);
}

html.has-motion [data-mo="rise"]    { translate: 0 14px; }
html.has-motion [data-mo="rise-sm"] { translate: 0 8px; }
html.has-motion [data-mo="left"]    { translate: -18px 0; }
html.has-motion [data-mo="right"]   { translate: 18px 0; }
html.has-motion [data-mo="pop"]     { scale: .82; }
html.has-motion [data-mo="lift"]    { translate: 0 16px; scale: .985; }

html.has-motion [data-mo].is-in {
  opacity: 1;
  translate: none;
  scale: none;
}

/* Hairline rules wipe out from their left edge instead of fading. */
html.has-motion [data-mo="wipe"] {
  opacity: 1;
  scale: 0 1;
  transform-origin: left center;
  transition: scale var(--mo-slow) var(--mo-ease) var(--mo-delay, 0ms);
}
html.has-motion [data-mo="wipe"].is-in { scale: 1 1; }

/* --- sheen sweep across the metal headings -------------------------------- */
/* The highlight rides as a second background layer above the existing metal
   gradient, and background-clip:text clips both — so the heading is
   pixel-identical to its styles.css appearance while the band is parked.

   Parking it is the subtle part: background-position percentages resolve
   against the *remaining* space, not the box. With a 60%-wide band, 100%
   still leaves 40% of it on screen; it only clears the right edge past 250%,
   and the left edge past -150%. Hence 300% at rest, sweeping to -200%. */
html.has-motion .metal,
html.has-motion .mission__figure,
html.has-motion .cta__title {
  background-repeat: no-repeat;
  background-size: 60% 100%, 100% 100%;
  background-position: 300% 0, 0 0;
}

html.has-motion .metal            { background-image: var(--mo-sheen), var(--metal-heading); }
html.has-motion .mission__figure  { background-image: var(--mo-sheen), var(--metal-1t); }
html.has-motion .cta__title       { background-image: var(--mo-sheen), var(--metal-cta); }

html.has-motion .is-sheen {
  animation: mo-sheen 1500ms var(--mo-ease-soft) var(--mo-sheen-delay, 260ms) both;
}

@keyframes mo-sheen {
  from { background-position: 300% 0, 0 0; }
  to   { background-position: -200% 0, 0 0; }
}

/* --- live status dots ----------------------------------------------------- */
/* A halo that pings outward once every few seconds. Restricted to `.pill`
   dots (the four "live" chips) so the seven feed dots stay quiet. Animates
   opacity + scale only, so it stays on the compositor. */

html.has-motion .pill .dot { position: relative; }

html.has-motion .pill .dot::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: radial-gradient(circle, #ffffffb0 0%, #ffffff00 70%);
  opacity: 0;
  pointer-events: none;
}

html.has-motion .pill.is-live .dot::after { animation: mo-halo 3.4s ease-out infinite; }

@keyframes mo-halo {
  0%        { opacity: .5; scale: .5; }
  70%, 100% { opacity: 0;  scale: 2; }
}

/* --- nav ------------------------------------------------------------------ */

html.has-motion .nav {
  transition: background .35s ease, border-color .35s ease, box-shadow .35s ease;
}

/* Deliberately no padding/height change: the nav is sticky and in flow, so
   resizing it would shove the page content while the user is scrolling. */
html.has-motion .nav.is-stuck {
  background: #050505f5;
  border-bottom-color: var(--line-4);
  box-shadow: 0 14px 34px -26px #000;
}

/* --- hardware logos ------------------------------------------------------- */

html.has-motion .logos__row img {
  transition: opacity .35s ease;
}
html.has-motion .logos__row img:hover { opacity: 1; }

/* --- ecosystem ------------------------------------------------------------ */

/* The card's top hairline draws itself outward from centre as the card lands. */
html.has-motion .module::before,
html.has-motion .eco__core::before {
  scale: 0 1;
  transition: scale var(--mo-slow) var(--mo-ease) calc(var(--mo-delay, 0ms) + 140ms);
}
html.has-motion .module.is-in::before,
html.has-motion .eco__core.is-in::before { scale: 1 1; }

html.has-motion .module__arrow {
  transition: translate .25s var(--mo-ease), color .25s ease;
}
html.has-motion .module:hover .module__arrow {
  translate: 2px -2px;
  color: var(--fg-3);
}

html.has-motion .module__index { transition: color .25s ease; }
html.has-motion .module:hover .module__index { color: var(--fg-muted-2); }

/* Core mark: a 14s drift, plus a glow that breathes out of phase with it. */
html.has-motion .eco__core.is-live img { animation: mo-float 14s ease-in-out infinite; }

@keyframes mo-float {
  0%, 100% { translate: 0 -5px; }
  50%      { translate: 0 5px; }
}

/* `isolation` keeps the negative z-index inside the card: the glow then
   paints above the card's own background but below the mark and its label,
   instead of washing over them. */
html.has-motion .eco__core { isolation: isolate; }

html.has-motion .eco__core::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(ellipse 55% 45% at 50% 42%, #23252a 0%, #09090900 72%);
  opacity: 0;
  pointer-events: none;
}
html.has-motion .eco__core.is-live::after { animation: mo-breathe 11s ease-in-out infinite; }

@keyframes mo-breathe {
  0%, 100% { opacity: .25; }
  50%      { opacity: .85; }
}

/* --- how it works: meters ------------------------------------------------- */

/* Fills grow from the left rather than appearing at their final width. The
   inline `width` in the markup still sets the target; scaleX only animates
   the arrival at it. */
html.has-motion .meter__fill {
  transform-origin: left center;
  scale: 0 1;
  transition: scale 1000ms var(--mo-ease) var(--mo-delay, 0ms);
}
html.has-motion .meter__fill.is-in { scale: 1 1; }

/* The in-progress meter keeps a slow highlight travelling along the filled
   portion, so a panel that reads "SYNCING" actually looks like it is. */
/* Rides the filled portion only — a highlight crossing the empty remainder
   would read as the bar being full. Delayed past the fill's own 1s scale, so
   the child is not being stretched underneath it. */
html.has-motion .meter[data-live] .meter__fill {
  position: relative;
  overflow: hidden;
}
html.has-motion .meter[data-live].is-live .meter__fill::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, #ffffff00, #ffffffb0, #ffffff00);
  translate: -100% 0;
  animation: mo-shimmer 2.6s ease-in-out 1.4s infinite;
}

@keyframes mo-shimmer {
  0%        { translate: -100% 0; }
  55%, 100% { translate: 220% 0; }
}

/* --- how it works: chat --------------------------------------------------- */

/* playChat() holds the reply and its status chip out of flow with `hidden`.
   That works by UA style alone for the reply, but `.pill` sets an author
   `display`, which outranks it — so the intent has to be restated here. */
html.has-motion .chat__msg--in[hidden],
html.has-motion .chat__status[hidden] { display: none; }

html.has-motion .chat__typing {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 16px 17px;
  border: 1px solid var(--line-4);
  border-radius: 16px 16px 16px 4px;
  background: var(--surface-4);
  opacity: 0;
  transition: opacity var(--mo-fast) ease;
}
html.has-motion .chat__typing.is-in { opacity: 1; }

html.has-motion .chat__typing span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--fg-muted-2);
  animation: mo-typing 1.15s ease-in-out infinite;
}
html.has-motion .chat__typing span:nth-child(2) { animation-delay: .14s; }
html.has-motion .chat__typing span:nth-child(3) { animation-delay: .28s; }

@keyframes mo-typing {
  0%, 60%, 100% { opacity: .3; translate: 0 0; }
  30%           { opacity: 1;  translate: 0 -3px; }
}

/* --- how it works: fleet feed --------------------------------------------- */

html.has-motion .feed li { transition: background .25s ease, color .25s ease; }
html.has-motion .feed li:hover { background: #ffffff06; color: var(--fg-2); }

/* --- case study ----------------------------------------------------------- */

/* The images sit oversized inside their frame so they have room to drift.
   `translate` is driven by the parallax loop in motion.js; `scale` is the
   one-off settle on reveal. */
html.has-motion .case img {
  scale: 1.08;
  translate: 0 var(--mo-py, 0px);
  transition: filter .5s ease;
}
html.has-motion .case.is-in img { animation: mo-settle 1500ms var(--mo-ease) both; }
html.has-motion .case:hover img { filter: grayscale(1) contrast(1.05) brightness(1.12); }

@keyframes mo-settle {
  from { scale: 1.16; }
  to   { scale: 1.08; }
}

/* --- founder quote -------------------------------------------------------- */

html.has-motion .quote.is-live .quote__glow { animation: mo-glow 12s ease-in-out infinite; }

@keyframes mo-glow {
  0%, 100% { opacity: .7;  scale: .96; }
  50%      { opacity: 1;   scale: 1.04; }
}

/* --- comparison ----------------------------------------------------------- */

/* One-shot bloom on the VS chip as it lands, then it settles to its
   styles.css shadow and stays there. */
html.has-motion .compare__vs.is-in { animation: mo-vs 1200ms var(--mo-ease) 120ms both; }

@keyframes mo-vs {
  0%   { box-shadow: 0 0 24px #ffffff40; }
  35%  { box-shadow: 0 0 46px #ffffff85; }
  100% { box-shadow: 0 0 24px #ffffff40; }
}

/* --- FAQ ------------------------------------------------------------------ */

/* main.js animates `height` inline; these rules cover the trimmings. The
   icon rotation is what carries the open/close — `−` at 180° is identical to
   `−`, so the `+`/`−` glyph swap happens invisibly mid-turn. */
html.has-motion .faq__icon {
  display: inline-block;
  transition: rotate .32s var(--mo-ease), color .32s ease;
}
html.has-motion .faq__q[aria-expanded="true"] .faq__icon {
  rotate: 180deg;
  color: var(--fg-3);
}

html.has-motion .faq__item { transition: border-color .32s ease, background .32s ease; }
html.has-motion .faq__item.is-open {
  border-color: var(--line-6);
  background: var(--surface-2);
}

/* --- closing CTA ---------------------------------------------------------- */

html.has-motion .cta::before {
  scale: 1.1;
  translate: 0 var(--mo-py, 0px);
}

/* ==========================================================================
   Reduced motion.
   styles.css already collapses every duration to ~0, which is enough to make
   the transitions instant — but reveals would then be stuck at opacity 0
   until their class lands, and the ambient loops would still be declared.
   These rules put every element in its resting state outright.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  html.has-motion [data-mo],
  html.has-motion [data-mo].is-in {
    opacity: 1 !important;
    translate: none !important;
    scale: none !important;
  }
  html.has-motion .meter__fill { scale: 1 1 !important; }
  html.has-motion .module::before,
  html.has-motion .eco__core::before { scale: 1 1 !important; }
  /* The oversize is only there to give parallax somewhere to travel; with
     no parallax it is just an unrequested crop. */
  html.has-motion .case img { scale: 1 !important; animation: none !important; }
  html.has-motion .cta::before { scale: 1 !important; }
  html.has-motion .pill .dot::after,
  html.has-motion .eco__core::after { animation: none !important; }
  html.has-motion .eco__core img,
  html.has-motion .quote__glow,
  html.has-motion .chat__typing span,
  html.has-motion .meter[data-live] .meter__fill::after { animation: none !important; }
  html.has-motion .is-sheen { animation: none !important; }
  /* The rotation exists to carry the +/− swap smoothly. Standing still it
     only displaces the glyph, which sits above the centre of its em box and
     so lands ~4px low when flipped. */
  html.has-motion .faq__q[aria-expanded="true"] .faq__icon { rotate: none !important; }
}
