/* ============================================================================
   motion.css — the states motion.js drives. Owner: fullstack.
   Loaded LAST (tokens, base, layout, sections, diagrams, quiz, motion), so the
   reduced-motion blanket at the bottom can override anything above it.
   Tokens only: no colour, duration, easing or radius literal appears here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1 + 2 — [data-reveal]. The hidden start state is gated on `.js`, which
   theme.js adds to <html> before anything else. With JS off it never applies
   and the content is simply visible.
   -------------------------------------------------------------------------- */
.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-y), 0);
  transition:
    opacity var(--dur-slow) var(--ease-out-expo) var(--reveal-delay, 0ms),
    transform var(--dur-slow) var(--ease-out-expo) var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* --------------------------------------------------------------------------
   6 — hero counters. tabular-nums plus the min-width motion.js measures and
   writes inline: the count-up must not reflow the rail.
   -------------------------------------------------------------------------- */
[data-count] {
  display: inline-block;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  text-align: inherit;
}

/* --------------------------------------------------------------------------
   3 — #read-progress. transform: scaleX only; width is never animated.
   motion.js writes --progress (0..1) on the element.
   -------------------------------------------------------------------------- */
#read-progress {
  transform: scaleX(var(--progress, 0));
  transform-origin: left center;
  will-change: transform;
}

/* --------------------------------------------------------------------------
   4 — sticky header condense. The fill/blur belong to the header component;
   this is only the scrolled-state hook plus the transition budget.
   -------------------------------------------------------------------------- */
.site-header {
  transition:
    background-color var(--dur-base) var(--ease-standard),
    box-shadow var(--dur-base) var(--ease-standard);
}

.site-header.is-scrolled {
  box-shadow: var(--shadow-header);
}

/* --------------------------------------------------------------------------
   7 + 8 + 9 — #stack-orbit: continuous ring rotation, cursor parallax, core
   pulse. The parallax vars stay 0 until motion.js writes them, so the orbit
   sits still on touch and under reduced motion.
   -------------------------------------------------------------------------- */
#stack-orbit {
  --px: 0;
  --py: 0;
  transform: translate3d(calc(var(--px) * 12px), calc(var(--py) * 12px), 0);
  transition: transform var(--dur-base) var(--ease-out-quart);
}


@keyframes mdc-core-pulse {
  from { opacity: 0.55; }
  to   { opacity: 1; }
}


/* --------------------------------------------------------------------------
   The reduced-motion contract (DESIGN § 4, stated once).
   Belt and braces: motion.js and diagrams.js also skip their observers, print
   final counter values and never instantiate packets.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .js [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }

  #read-progress {
    transition: none !important;
  }

  #stack-orbit {
    transform: none !important;
    transition: none !important;
  }

  /* A focus ring is not decoration — it is never suppressed here. */
  :focus-visible {
    transition: none !important;
  }
}


/* --------------------------------------------------------------------------
   Orbit, second pass. The rings already turned; what was missing was a focal
   point and any sense of depth. The sweep gives the scene a direction of
   travel, and the chips breathe on their own slow cycle so the composition is
   never completely still even when a ring is mid-rotation behind the deck.
   All of it is transform and opacity only, and all of it stops under
   prefers-reduced-motion with the rest.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   Booking block. The pulse marks the widget as live; the skeleton shimmer runs
   only while the remote script is in flight. Both are opacity and transform,
   and both stop under prefers-reduced-motion — where the skeleton simply sits
   still rather than flashing at someone who asked for no motion.
   -------------------------------------------------------------------------- */
@keyframes mdc-booking-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--c-ok) 55%, transparent); }
  70%  { box-shadow: 0 0 0 10px color-mix(in srgb, var(--c-ok) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--c-ok) 0%, transparent); }
}

@keyframes mdc-skeleton-shimmer {
  from { opacity: 0.45; }
  to   { opacity: 0.85; }
}

@media (prefers-reduced-motion: no-preference) {
  .booking__pulse {
    animation: mdc-booking-pulse 2400ms var(--ease-standard) infinite;
  }

  .booking__skeleton span {
    animation: mdc-skeleton-shimmer 1100ms var(--ease-standard) infinite alternate;
  }

  .booking__skeleton span:nth-child(2) { animation-delay: 140ms; }
  .booking__skeleton span:nth-child(3) { animation-delay: 280ms; }
}

/* --------------------------------------------------------------------------
   Logo. Three loops, all cheap: the lemniscate draws itself, a node travels
   it, and a sheen crosses the wordmark once every eight seconds. The node uses
   offset-path so the browser owns the geometry — no JS, no per-frame maths.
   Every one of them is inside the no-preference query, so under reduced motion
   the mark is a static, fully drawn logo rather than a half-drawn one.
   -------------------------------------------------------------------------- */
@keyframes mdc-logo-draw {
  from { stroke-dashoffset: 62; }
  to   { stroke-dashoffset: 0; }
}

@keyframes mdc-logo-run {
  from { offset-distance: 0%; opacity: 1; }
  to   { offset-distance: 100%; opacity: 1; }
}

@keyframes mdc-logo-sheen {
  0%, 62% { background-position: 100% 0; }
  92%     { background-position: 0% 0; }
  100%    { background-position: 0% 0; }
}

@media (prefers-reduced-motion: no-preference) {
  .logo__loop {
    stroke-dasharray: 62;
    animation: mdc-logo-draw 2600ms var(--ease-out-quart) both;
  }

  .logo__node {
    offset-path: path("M11 16c0-2.5 1.6-4 3.1-4 3 0 3.8 8 6.8 8 1.5 0 3.1-1.5 3.1-4s-1.6-4-3.1-4c-3 0-3.8 8-6.8 8-1.5 0-3.1-1.5-3.1-4z");
    animation: mdc-logo-run 5200ms var(--ease-linear) 2400ms infinite;
  }

  .logo__text {
    animation: mdc-logo-sheen 8000ms var(--ease-standard) 1200ms infinite;
  }

  /* Hover pulls the whole mark forward slightly — the only interactive state
     the logo has, since it is a link back to the top. */
  .logo:hover .logo__mark {
    filter: drop-shadow(0 0 10px color-mix(in srgb, var(--c-accent) 45%, transparent));
  }
}

/* --------------------------------------------------------------------------
   Booking dialog. `allow-discrete` is what lets a display:none element
   animate in at all — without it the open transition is skipped entirely and
   the panel simply appears. The backdrop fades on its own timing so the page
   dims a beat before the panel arrives, which is what makes it feel like a
   layer rather than a jump.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .book {
    opacity: 0;
    transform: translateY(12px) scale(0.98);
    transition:
      opacity   var(--dur-base) var(--ease-out-quart),
      transform var(--dur-base) var(--ease-out-quart),
      overlay   var(--dur-base) allow-discrete,
      display   var(--dur-base) allow-discrete;
  }

  .book[open] {
    opacity: 1;
    transform: translateY(0) scale(1);
  }

  @starting-style {
    .book[open] {
      opacity: 0;
      transform: translateY(12px) scale(0.98);
    }
  }

  .book::backdrop {
    opacity: 0;
    transition:
      opacity var(--dur-base) var(--ease-standard),
      overlay var(--dur-base) allow-discrete,
      display var(--dur-base) allow-discrete;
  }

  .book[open]::backdrop { opacity: 1; }

  @starting-style {
    .book[open]::backdrop { opacity: 0; }
  }

  .book__pulse {
    animation: mdc-booking-pulse 2400ms var(--ease-standard) infinite;
  }
}


/* --------------------------------------------------------------------------
   Radar. The beam turns once per --sweep-period, and each node blips as the
   beam reaches it — the delay is the node's own bearing expressed as a
   fraction of that period, set in sections.css next to the bearing itself.
   Nothing measures anything at runtime: the beam and the blips are two
   animations of identical duration started at a known offset, so they stay in
   step for as long as the page is open and cost nothing to keep in step.

   The blip is short on the way up and slow on the way down, which is how a
   real return behaves — it flares when the beam hits and fades after it has
   passed, rather than pulsing symmetrically.
   -------------------------------------------------------------------------- */
@keyframes mdc-radar-sweep {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes mdc-radar-blip {
  0%   { scale: 1;   border-color: var(--c-border-strong); box-shadow: var(--shadow-1); }
  3%   { scale: 1.4; border-color: var(--c-accent);        box-shadow: var(--glow-accent-md); }
  16%  { scale: 1.4; border-color: var(--c-accent);        box-shadow: var(--glow-accent-sm); }
  38%  { scale: 1;   border-color: var(--c-border-strong); box-shadow: var(--shadow-1); }
  100% { scale: 1;   border-color: var(--c-border-strong); box-shadow: var(--shadow-1); }
}

@keyframes mdc-core-glow {
  from { box-shadow: var(--glow-accent-sm); }
  to   { box-shadow: var(--glow-accent-md); }
}

@media (prefers-reduced-motion: no-preference) {
  #stack-orbit .orbit__sweep {
    animation: mdc-radar-sweep var(--sweep-period) var(--ease-linear) infinite;
  }

  /* `scale` rather than `transform`, deliberately: the node is positioned with
     `translate`, and using transform here would overwrite that and drop every
     icon into the middle of the dial. */
  #stack-orbit .orbit__node {
    animation: mdc-radar-blip var(--sweep-period) var(--ease-standard) infinite;
    animation-delay: var(--blip, 0s);
  }

  #stack-orbit .orbit__core {
    animation: mdc-core-glow 3400ms var(--ease-standard) infinite alternate;
  }
}

/* --------------------------------------------------------------------------
   Book a call. A sheen crosses the button every seven seconds and again on
   hover — enough to catch the eye in peripheral vision, rare enough not to
   nag. Pure transform on a pseudo-element, so it composites on its own layer
   and never touches layout.
   -------------------------------------------------------------------------- */
@keyframes mdc-book-sheen {
  0%, 72% { transform: translateX(0); }
  100%    { transform: translateX(420%); }
}

@media (prefers-reduced-motion: no-preference) {
  .btn--book::after {
    animation: mdc-book-sheen 7000ms var(--ease-standard) 2000ms infinite;
  }

  .btn--book:hover::after {
    animation-duration: 1400ms;
    animation-delay: 0ms;
  }
}
