/* ==========================================================================
   hoaxnet — motion layer
   ---------------------------------------------------------------------------
   Loaded AFTER style.css. This file only adds motion: nothing here changes
   what the page looks like at rest. Every rule is additive, so deleting this
   file returns the site to exactly its previous state.

   Two gates guard everything:
     html.mo-js   — set by an inline script only when JS runs AND the user has
                    not asked for less motion. Without it nothing is hidden,
                    so a no-JS visitor sees the whole page immediately.
     body.motion-off / prefers-reduced-motion — stop the loops and the large
                    transforms, keeping short opacity feedback.
   ========================================================================== */

:root {
  /* --- durations: three speeds, and the speed says how big the change is ---
     instant = feedback on a control · quick = hover/press · calm = something
     entering the page. Anything slower reads as lag rather than polish. */
  --m-instant: 90ms;
  --m-quick: 170ms;
  --m-calm: 420ms;
  --m-slow: 620ms;

  /* --- easing --- */
  --m-ease: cubic-bezier(0.4, 0, 0.2, 1);        /* default, symmetric */
  --m-out: cubic-bezier(0.16, 1, 0.3, 1);        /* entrances: fast, then settle */
  --m-in: cubic-bezier(0.55, 0, 1, 0.45);        /* exits */
  --m-spring: cubic-bezier(0.34, 1.4, 0.64, 1);  /* press release, small moves only */

  /* --- stagger: one step per sibling. motion.js caps the total so a long grid
         never leaves its last card waiting. --- */
  --m-stagger: 55ms;
  --m-delay: 0ms;

  /* --- travel: how far things move. Deliberately tiny — motion should be felt
         at the edge of vision, not watched. --- */
  --m-rise: 14px;
  --m-lift: -3px;
  --m-lift-lg: -5px;
  --m-press: 1px;

  /* --- focus/glow rings, layered on the existing --glow tokens --- */
  --m-ring: 0 0 0 3px rgba(188, 19, 254, 0.22);
  --m-ring-cyan: 0 0 0 3px rgba(0, 240, 255, 0.2);
  --m-ring-ok: 0 0 0 3px rgba(61, 220, 151, 0.22);
}

/* ==========================================================================
   1 · Entrance reveals
   ---------------------------------------------------------------------------
   motion.js tags elements with [data-mo] and flips .mo-in when they enter the
   viewport, then strips both once the transition ends so no rule here outlives
   the animation. The hidden state is scoped to html.mo-js so the page is never
   blank without JS, and to no-preference so reduced-motion users get content
   with no delay at all.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  html.mo-js [data-mo] {
    opacity: 0;
    transform: translate3d(0, var(--m-rise), 0);
    will-change: opacity, transform;
  }

  /* Repeated items in a row settle in place instead of rising, so a grid does
     not look like it is falling into position four times over. */
  html.mo-js [data-mo='soft'] { transform: translate3d(0, 6px, 0) scale(0.985); }

  html.mo-js [data-mo].mo-in {
    opacity: 1;
    transform: none;
    transition:
      opacity var(--m-calm) var(--m-out) var(--m-delay),
      transform var(--m-calm) var(--m-out) var(--m-delay);
  }
}

/* The site's own toggle is applied to <body> after paint, so it has to be able
   to cancel a reveal that is already mid-flight. */
body.motion-off [data-mo] {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

/* ==========================================================================
   2 · Buttons and CTAs
   ---------------------------------------------------------------------------
   style.css already says how important each action is. Motion only confirms
   that the control heard you: hover lifts, press settles, focus is obvious,
   and a submitted form says it is working.
   ========================================================================== */

.btn {
  position: relative;
  transition:
    transform var(--m-quick) var(--m-spring),
    box-shadow var(--m-quick) var(--m-ease),
    background var(--m-quick) var(--m-ease),
    border-color var(--m-quick) var(--m-ease),
    color var(--m-quick) var(--m-ease),
    opacity var(--m-quick) var(--m-ease);
}

.btn:hover:not(:disabled):not([disabled]) { transform: translate3d(0, var(--m-lift), 0); }
/* Press has to beat hover: the button must visibly bottom out under the cursor. */
.btn:active:not(:disabled):not([disabled]) {
  transform: translate3d(0, var(--m-press), 0) scale(0.985);
  transition-duration: var(--m-instant);
}

.btn:focus-visible { box-shadow: var(--m-ring-cyan); }
.btn-primary:focus-visible { box-shadow: var(--glow-strong), var(--m-ring); }

/* One sheen crosses the primary CTA on hover. A single pass, never a loop —
   a repeating shine on a purchase button reads as an advert, not as quality.
   No z-index here on purpose: the pseudo paints above the gradient background
   and its peak is only 22% white, so the label stays legible through it. */
.btn-primary { overflow: hidden; }
.btn-primary::after {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 45%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.22), transparent);
  transform: translate3d(-200%, 0, 0) skewX(-18deg);
  opacity: 0;
  pointer-events: none;
}
.btn-primary:hover:not(:disabled)::after,
.btn-primary:focus-visible::after {
  opacity: 1;
  animation: mo-sheen var(--m-slow) var(--m-out) 1;
}
@keyframes mo-sheen {
  from { transform: translate3d(-200%, 0, 0) skewX(-18deg); }
  to   { transform: translate3d(320%, 0, 0) skewX(-18deg); }
}

/* Disabled controls should read as inert, not merely faded. */
.btn:disabled,
.btn[disabled] { transform: none !important; }
.btn:disabled::after,
.btn[disabled]::after { display: none; }

/* --- submitting -----------------------------------------------------------
   motion.js adds this to the button of a form that has already been submitted.
   It never blocks or delays the submit: the browser is mid-navigation by the
   time the class lands, and the button is never disabled, so its name/value
   still reaches the server. */
.btn.is-mo-busy {
  cursor: progress;
  color: transparent !important;
  pointer-events: none;
}
.btn.is-mo-busy::before {
  content: '';
  position: absolute;
  inset-block-start: calc(50% - 8px);
  inset-inline-start: calc(50% - 8px);
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.28);
  border-top-color: #fff;
  animation: mo-spin 620ms linear infinite;
}
@keyframes mo-spin { to { transform: rotate(360deg); } }

/* --- success ping — used on the copy buttons, which already swap their own
   label in site.js. This only adds the colour confirmation around that swap. */
.is-mo-ok { animation: mo-ok-flash var(--m-slow) var(--m-ease) 1; }
@keyframes mo-ok-flash {
  0%   { box-shadow: 0 0 0 0 rgba(61, 220, 151, 0.5); }
  45%  { box-shadow: 0 0 0 5px rgba(61, 220, 151, 0); border-color: var(--ok); color: var(--ok); }
  100% { box-shadow: 0 0 0 0 rgba(61, 220, 151, 0); }
}

.link-btn,
.copy-field button,
.motion-toggle,
.globe-btn,
.location-item,
.nav-toggle,
.filter-tab,
.pill {
  transition:
    color var(--m-quick) var(--m-ease),
    background var(--m-quick) var(--m-ease),
    border-color var(--m-quick) var(--m-ease),
    box-shadow var(--m-quick) var(--m-ease),
    transform var(--m-quick) var(--m-spring);
}
.filter-tab:hover,
.pill:hover { transform: translate3d(0, -1px, 0); }
.copy-field button:active,
.motion-toggle:active,
.globe-btn:active,
.nav-toggle:active,
.location-item:active { transform: scale(0.97); }

/* ==========================================================================
   3 · Cards
   ---------------------------------------------------------------------------
   style.css already picks the hover hue per protocol. Motion only smooths the
   curve, gives keyboard focus the same treatment as hover, and blooms the hue
   spine — all still driven by the existing --p variable, so colour keeps
   carrying its meaning.
   ========================================================================== */

.plan-card,
.purchase-card,
.admin-card,
.stat-card,
.order-summary-item,
.location-card {
  transition:
    transform var(--m-calm) var(--m-out),
    box-shadow var(--m-calm) var(--m-out),
    border-color var(--m-quick) var(--m-ease),
    background var(--m-quick) var(--m-ease);
}

/* Tabbing to a card's buy button lights the card exactly as hovering does —
   without this, keyboard users lose the grouping cue entirely. Values match
   the existing :hover rule so the two states are indistinguishable. */
.plan-card:focus-within {
  border-color: color-mix(in srgb, var(--p, var(--accent)) 60%, transparent);
  transform: translate3d(0, -4px, 0);
  box-shadow: var(--lift), 0 0 26px -6px color-mix(in srgb, var(--p, var(--accent)) 34%, transparent);
}
.plan-card:focus-within .plan-poster img { transform: scale(1.05); }

.purchase-card:hover,
.order-summary-item:hover { transform: translate3d(0, var(--m-lift), 0); }
.stat-card:hover,
.admin-card:hover { transform: translate3d(0, -2px, 0); box-shadow: var(--lift-lg); }

/* The 3px hue spine goes to full strength and picks up a glow — a cue that
   costs no layout and reinforces which protocol you are looking at. */
.plan-card::before,
.purchase-card[data-protocol]:not([data-protocol=''])::before {
  transition: opacity var(--m-quick) var(--m-ease), box-shadow var(--m-calm) var(--m-out);
}
.plan-card:hover::before,
.plan-card:focus-within::before,
.purchase-card:hover::before {
  opacity: 1;
  box-shadow: 0 0 14px color-mix(in srgb, var(--p, var(--accent)) 70%, transparent);
}

/* The poster zoom already exists; this only gives it a curve that decelerates
   instead of easing evenly. */
.plan-poster img { transition: transform var(--m-slow) var(--m-out); }

.doc-apps li,
.protocol-guide div,
.step-list li,
.contact-row {
  transition:
    border-color var(--m-quick) var(--m-ease),
    background var(--m-quick) var(--m-ease),
    transform var(--m-quick) var(--m-out);
}
/* RTL page, so "toward the reader" is a nudge to the right. */
.doc-apps li:hover,
.protocol-guide div:hover { transform: translate3d(3px, 0, 0); }

/* Admin tables: the existing background swap gains a hue edge, so the row you
   are reading stays findable inside a wide scrolling container. */
tbody tr { transition: background var(--m-quick) var(--m-ease), box-shadow var(--m-quick) var(--m-ease); }
tbody tr:hover { box-shadow: inset -3px 0 0 -1px var(--accent-dim); }

/* ==========================================================================
   4 · Navigation
   ========================================================================== */

.main-nav a,
.main-nav .link-btn,
.footer-col a,
.doc-index a,
.toc a,
.admin-sidebar a,
.admin-sidebar button {
  transition:
    color var(--m-quick) var(--m-ease),
    background var(--m-quick) var(--m-ease),
    border-color var(--m-quick) var(--m-ease),
    transform var(--m-quick) var(--m-out);
}

/* Desktop only. Hover draws the same underline the current page wears, at half
   strength, so the affordance and the state indicator read as one object.
   It uses ::before because ::after is the current-page marker owned by
   style.css — and on mobile that marker becomes a vertical bar, which is why
   this whole block stops at the same 860px breakpoint the nav does. */
@media (min-width: 861px) {
  .main-nav a:not(.btn):not(.is-current)::before {
    content: '';
    position: absolute;
    inset-inline: 12px;
    bottom: 2px;
    height: 2px;
    border-radius: 2px;
    background: var(--accent);
    transform: scaleX(0);
    opacity: 0;
    transition: transform var(--m-quick) var(--m-out), opacity var(--m-quick) var(--m-ease);
    pointer-events: none;
  }
  .main-nav a:not(.btn):not(.is-current):hover::before,
  .main-nav a:not(.btn):not(.is-current):focus-visible::before {
    transform: scaleX(1);
    opacity: 0.55;
  }
}

.admin-sidebar a:hover,
.admin-sidebar button:hover { transform: translate3d(3px, 0, 0); }

.site-header { transition: box-shadow var(--m-calm) var(--m-ease), background var(--m-calm) var(--m-ease); }

/* --- mobile menu ----------------------------------------------------------
   The panel goes display:none → display:flex, which cannot be transitioned, so
   the entrance is a keyframe that fires the moment it is displayed. None of
   this is required for the menu to work: with JS off the toggle never fires,
   body.nav-open is never set, and every rule below stays inert. */
@media (max-width: 860px) {
  body.nav-open .main-nav {
    animation: mo-panel-in var(--m-calm) var(--m-out) 1;
    transform-origin: top center;
  }
  @keyframes mo-panel-in {
    from { opacity: 0; transform: translate3d(0, -10px, 0); }
    to   { opacity: 1; transform: none; }
  }

  body.nav-open .main-nav .nav-group > * {
    animation: mo-nav-item var(--m-calm) var(--m-out) backwards;
  }
  body.nav-open .main-nav .nav-group > *:nth-child(1) { animation-delay: 40ms; }
  body.nav-open .main-nav .nav-group > *:nth-child(2) { animation-delay: 80ms; }
  body.nav-open .main-nav .nav-group > *:nth-child(3) { animation-delay: 120ms; }
  body.nav-open .main-nav .nav-group > *:nth-child(4) { animation-delay: 160ms; }
  body.nav-open .main-nav .nav-group > *:nth-child(n + 5) { animation-delay: 200ms; }
  @keyframes mo-nav-item {
    from { opacity: 0; transform: translate3d(0, -6px, 0); }
    to   { opacity: 1; transform: none; }
  }

  /* Dim the page behind the open panel. pointer-events:none is deliberate: the
     existing close affordances are the toggle button and Escape, and an
     intercepting overlay would swallow taps the site expects to receive.
     z-index 40 sits under the header's 50, so the panel stays lit. */
  body.nav-open::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: 40;
    background: rgba(3, 0, 9, 0.55);
    pointer-events: none;
    animation: mo-fade-in var(--m-quick) var(--m-ease) 1;
  }
  @keyframes mo-fade-in { from { opacity: 0; } to { opacity: 1; } }
}

/* The burger folds into an X. The bars are absolutely positioned at ±5px, so
   translating each back to centre is what closes the cross. */
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  transition: transform var(--m-quick) var(--m-out), background var(--m-instant) var(--m-ease);
}
.nav-toggle[aria-expanded='true'] .nav-toggle-bars { background: transparent; }
.nav-toggle[aria-expanded='true'] .nav-toggle-bars::before { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded='true'] .nav-toggle-bars::after { transform: translateY(-5px) rotate(-45deg); }

/* ==========================================================================
   5 · Forms
   ---------------------------------------------------------------------------
   No field name, rule, action or method is touched here. This is feedback laid
   on top of the validation styling style.css already defines — the focus ring
   itself is still style.css's, this only gives it a curve to arrive on.
   ========================================================================== */

.input,
.field input,
.field textarea,
.field select,
.copy-field input {
  transition:
    border-color var(--m-quick) var(--m-ease),
    box-shadow var(--m-quick) var(--m-ease),
    background var(--m-quick) var(--m-ease);
}

/* One-shot nudge when the browser blocks a submit, so the offending field is
   findable on a long form. 4px of travel cannot cause horizontal overflow. */
.mo-shake { animation: mo-shake 320ms var(--m-ease) 1; }
@keyframes mo-shake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  20% { transform: translate3d(-4px, 0, 0); }
  45% { transform: translate3d(3px, 0, 0); }
  70% { transform: translate3d(-2px, 0, 0); }
}

/* Alerts arrive rather than appear — on a form that reloads with an error, the
   movement is what pulls the eye back up the page. */
.alert { animation: mo-alert-in var(--m-calm) var(--m-out) 1; }
@keyframes mo-alert-in {
  from { opacity: 0; transform: translate3d(0, -6px, 0); }
  to   { opacity: 1; transform: none; }
}
.alert-success::before { animation: mo-pop var(--m-slow) var(--m-spring) 1; }
@keyframes mo-pop {
  0%   { transform: scale(0.4); opacity: 0; }
  60%  { transform: scale(1.15); opacity: 1; }
  100% { transform: scale(1); }
}

.result-icon { animation: mo-pop var(--m-slow) var(--m-spring) 1; }
/* The success glyph gets one confirming ring. The error and pending glyphs do
   not, because celebrating a failed payment is the wrong note. */
.is-ok .result-icon { animation: mo-pop var(--m-slow) var(--m-spring) 1, mo-ring 1.6s var(--m-out) 1; }
@keyframes mo-ring {
  from { box-shadow: 0 0 0 0 rgba(61, 220, 151, 0.4); }
  to   { box-shadow: 0 0 0 22px rgba(61, 220, 151, 0); }
}

/* ==========================================================================
   6 · Existing effects — refined, not replaced
   ---------------------------------------------------------------------------
   Everything below already exists. Nothing here changes an effect's character;
   these are hints that make the existing work cheaper to run.
   ========================================================================== */

/* The three aurora blobs drift continuously behind a 100px blur. Promoting
   them to their own layers keeps that blur off the main-thread paint on every
   frame, which is the largest scroll cost on the home page. No containment
   here — paint containment would clip the blur bleed and change the look. */
.bg-aurora span { will-change: transform; }

/* The footer's status dot was the only static one; the signal and location
   dots already pulse. Matching it makes "online" read consistently sitewide. */
.footer-trust-dot { animation: pulse-dot 2.6s var(--m-ease) infinite; }

/* ==========================================================================
   6b · Audio-reactive mascot glow
   ---------------------------------------------------------------------------
   A single ::before behind the mascot, driven by one custom property that
   motion.js writes each frame from the bass bins. --pulse-intensity is 0..1.

   Why a pseudo-element and not a new div: the mascot is an inline SVG whose
   own float/glitch animations own .mascot-wrap's transform. Adding a child
   would inherit that transform and fight it; ::before is a separate box that
   can scale on its own without the mascot ever moving.

   Layout safety: position:absolute + inset takes it out of flow entirely, so
   no matter how large it scales it cannot reflow the h1, the lede or the
   buttons below. .hero already has overflow:hidden, so the bleed is clipped
   at the section edge rather than adding a scrollbar. It is also
   pointer-events:none so it can never intercept a click on the CTAs.

   At rest (--pulse-intensity: 0) the glow is fully transparent and scaled to
   its base — i.e. invisible and indistinguishable from the site before this
   file existed.
   ========================================================================== */

.mascot-wrap {
  /* The fallback keeps every rule below valid when JS never runs. */
  --pulse-intensity: 0;
}

.mascot-wrap::before {
  content: '';
  position: absolute;
  /* Negative inset lets the glow bloom past the 138px mascot box without
     changing that box's size. */
  inset: -28%;
  z-index: -1;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(
    circle at 50% 45%,
    rgba(188, 19, 254, 0.55) 0%,
    rgba(0, 240, 255, 0.22) 42%,
    rgba(188, 19, 254, 0) 72%
  );
  /* Only transform and opacity change per frame — both composited, so the
     loop never triggers layout or paint. */
  opacity: calc(var(--pulse-intensity) * 0.9);
  transform: scale(calc(0.86 + var(--pulse-intensity) * 0.38));
  /* Slightly slower than a frame: smooths the gap between bass hits so the
     glow breathes instead of strobing on transient peaks. */
  transition: opacity 90ms linear, transform 90ms linear;
}

/* Promotion is applied only while audio is actually driving the loop, so an
   idle home page does not hold a compositor layer it is not using. */
.mascot-wrap.is-pulsing::before { will-change: transform, opacity; }

/* --- idle breathing --------------------------------------------------------
   The audio-reactive layer above only produces light while #bg-audio is
   playing. A visitor who never presses SIGNAL — which is most of them — saw
   --pulse-intensity pinned at 0, i.e. no glow at all, so the effect read as
   missing rather than as subtle.

   This animation is the resting state: a slow breath at roughly a calm
   pulse rate. It animates the same two composited properties, so it costs
   the compositor the same as the audio path and never touches layout.

   Why keyframes rather than driving the custom property from JS: an idle
   loop that exists only to write a number every frame would keep the main
   thread and the rAF queue busy on a page that is doing nothing. Keyframes
   run entirely off the main thread.

   The handoff is the important part. A CSS animation outranks the normal
   opacity/transform declarations above, so while this runs it wins. The
   moment motion.js adds .is-pulsing the animation is switched off and the
   calc() values driven by --pulse-intensity take over, with the existing
   90ms transition smoothing the seam. Removing the class hands it back. */
@keyframes mascot-idle-breath {
  0%,
  100% {
    opacity: 0.34;
    transform: scale(0.87);
  }
  50% {
    opacity: 0.62;
    transform: scale(1.02);
  }
}

.mascot-wrap::before {
  animation: mascot-idle-breath 4.2s var(--m-ease, ease-in-out) infinite;
}

/* Audio is driving now — stand down and let --pulse-intensity own the glow. */
.mascot-wrap.is-pulsing::before { animation: none; }

/* ==========================================================================
   7 · Reduced motion
   ---------------------------------------------------------------------------
   style.css already clamps every duration to 0.01ms under prefers-reduced-
   motion, which handles timing. What a duration clamp cannot do is decide
   which effects are meaningless without their motion, or stop a transform from
   simply snapping to its end state. Those are handled here, for both the OS
   preference and the site's own toggle.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .btn-primary::after,
  .btn.is-mo-busy::before,
  .main-nav a:not(.btn):not(.is-current)::before { display: none; }

  .btn:hover:not(:disabled),
  .btn:active:not(:disabled),
  .plan-card:focus-within,
  .purchase-card:hover,
  .order-summary-item:hover,
  .stat-card:hover,
  .admin-card:hover,
  .doc-apps li:hover,
  .protocol-guide div:hover,
  .admin-sidebar a:hover,
  .admin-sidebar button:hover,
  .filter-tab:hover,
  .pill:hover,
  .location-item:active,
  .copy-field button:active,
  .motion-toggle:active,
  .globe-btn:active,
  .nav-toggle:active { transform: none !important; }

  .plan-card:focus-within .plan-poster img { transform: none; }
  .footer-trust-dot,
  .mo-shake { animation: none; }

  /* Feedback still has to be readable, just not kinetic: the busy button keeps
     its label and dims, rather than hiding text behind a suppressed spinner. */
  .btn.is-mo-busy { color: inherit !important; opacity: 0.6; }

  /* The audio glow is pure decoration with no static information in it, so it
     is removed outright rather than frozen at some arbitrary size. display:none
     also guarantees the compositor layer is never created. motion.js stops
     the rAF loop separately, so nothing is computed either. */
  .mascot-wrap::before { display: none !important; }
}

body.motion-off .btn-primary::after,
body.motion-off .footer-trust-dot,
body.motion-off .result-icon,
body.motion-off .alert,
body.motion-off .alert-success::before { animation: none !important; }
body.motion-off .btn:hover:not(:disabled),
body.motion-off .plan-card:focus-within,
body.motion-off .purchase-card:hover,
body.motion-off .stat-card:hover,
body.motion-off .admin-card:hover { transform: none; }
body.motion-off .btn.is-mo-busy { color: inherit !important; opacity: 0.6; }
body.motion-off .btn.is-mo-busy::before { display: none; }
/* The site's own motion toggle kills the audio glow on the same terms as the
   OS preference. Audio itself keeps playing — this switch is about movement,
   not sound. */
body.motion-off .mascot-wrap::before { display: none !important; }
