/* ═══════════════════════════════════════════════════════════════════════════
   fx-micro — the small hand-made things.

   A button that gives when pressed, copy buttons that drool, a pen-stroke
   under the active nav item, FAQ folds that open like paper instead of
   snapping, and section headings that misprint for a frame as they arrive.
   Everything is transform/opacity, under 300ms, and switched off wholesale by
   prefers-reduced-motion. The one deliberate exception is the FAQ fold, which
   has to animate a box size — an accordion pushes what's below it, by design.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Every duration derives from the tokens, so it collapses to 0ms under
   reduced motion along with the rest of the site. */
:root {
  --fxm-press: calc(var(--t-fast) * 1.6);  /* 224ms  button squash + settle       */
  --fxm-bar:   calc(var(--t-fast) * 1.6);  /* 224ms  faq +/− bar lying down       */
  --fxm-drip:  calc(var(--t-fast) * 2);    /* 280ms  bead forms, lets go, fades   */
  --fxm-nav:   calc(var(--t-fast) * 1.5);  /* 210ms  pen-stroke in / out          */
  --fxm-fold:  calc(var(--t-fast) * 2);    /* 280ms  faq fold                     */
  --fxm-jit:   calc(var(--t-fast) * .6);   /*  84ms  misprint; the slip is 1/4 of it */
}

/* ── 1. button press ─────────────────────────────────────────────────────────
   base.css already drops the button onto its shadow on :active. This adds the
   give: a squash on the individual `scale` property, which composes with the
   translate in base.css instead of overriding it. Origin on the bottom edge so
   the top dips and the bottom stays put — a rubber stamp, not a balloon. */
@media (prefers-reduced-motion: no-preference) {
  .btn:active {
    transform-origin: 50% 100%;
    animation: fxm-press var(--fxm-press) var(--ease) both;
  }
}
@keyframes fxm-press {
  0%   { scale: 1 1; }
  40%  { scale: 1.04 .94; }
  72%  { scale: .99 1.015; }
  100% { scale: 1 1; }
}

/* ── 2. copy-button drool ────────────────────────────────────────────────────
   main.js turns the button drool-blue and adds .is-done on a successful copy;
   fx-micro.js answers with one bead of the same blue that forms on the bottom
   edge and lets go. It sits beside the button rather than inside it because
   main.js rewrites the button's textContent and would wipe it. Resting state
   is invisible, so an injected bead that never animates is simply not there. */
.ca__row { position: relative; }
.fxm-drip {
  position: absolute;
  top: calc(100% - 2px);   /* tip tucked into the border so the outline reads as one line */
  right: 1.1rem;           /* the button is the row's last, flush-right flex item */
  width: 9px; height: 14px;
  opacity: 0;
  pointer-events: none;
  transform-origin: 50% 0;
  overflow: visible;
}
.fxm-drip path {
  fill: var(--drool);
  stroke: var(--ink);
  stroke-width: 1.5;
  stroke-linejoin: round;
}
@media (prefers-reduced-motion: no-preference) {
  .fxm-drip.is-falling { animation: fxm-drip var(--fxm-drip) both; }
}
@keyframes fxm-drip {
  /* Forms first — stretches out of the edge — then accelerates like a drop. */
  0%   { opacity: 1; transform: translateY(0) scaleY(.5);  animation-timing-function: var(--ease); }
  38%  { opacity: 1; transform: translateY(0) scaleY(1);   animation-timing-function: cubic-bezier(.55, 0, 1, .6); }
  100% { opacity: 0; transform: translateY(22px) scaleY(1.1); }
}

/* ── 3. nav pen-stroke ───────────────────────────────────────────────────────
   main.js marks the active link by writing color:var(--cavity-mid) into its
   inline style; fx-micro.js mirrors that into .fxm-nav-on. The stroke is a
   wobbly SVG line used as a mask over currentColor, so it's drawn in whatever
   ink the label is. Origins flip with travel direction so a departing stroke
   shrinks toward the arriving one and the pair reads as one line moving. */
.site-head nav a { position: relative; }
.site-head nav a::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: -1px; height: 5px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 6' preserveAspectRatio='none'%3E%3Cpath d='M2 3.4 C 16 1.4, 30 5.2, 48 3 S 80 1.2, 96 3.6 S 110 5, 118 2.6' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 6' preserveAspectRatio='none'%3E%3Cpath d='M2 3.4 C 16 1.4, 30 5.2, 48 3 S 80 1.2, 96 3.6 S 110 5, 118 2.6' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round'/%3E%3C/svg%3E") center / 100% 100% no-repeat;
  transform: scaleX(0);
  transform-origin: 100% 50%;
  pointer-events: none;
}
.site-head nav a.fxm-nav-on::after { transform: scaleX(1); transform-origin: 0 50%; }
/* Travelling back up the page: leave to the left, arrive from the right. */
.site-head nav.fxm-nav--back a::after            { transform-origin: 0 50%; }
.site-head nav.fxm-nav--back a.fxm-nav-on::after { transform-origin: 100% 50%; }
@media (prefers-reduced-motion: no-preference) {
  .site-head nav a::after { transition: transform var(--fxm-nav) var(--ease); }
}

/* ── 4. faq fold ─────────────────────────────────────────────────────────────
   Native <details> snaps. fx-micro.js wraps everything after the summary in
   .fxm-fold and drives open/close through two transient classes. The fold's
   resting state is fully open, so a class that never lands (JS throws, tab
   hidden, media query flipped mid-flight) can only ever leave content visible,
   never hidden. */
.fxm-fold { display: grid; grid-template-rows: 1fr; }
.fxm-fold__in { min-height: 0; overflow: hidden; }
.faq details.fxm-opening .fxm-fold,
.faq details.fxm-closing .fxm-fold { grid-template-rows: 0fr; }
.faq details.fxm-opening .fxm-fold__in,
.faq details.fxm-closing .fxm-fold__in { opacity: 0; }

/* The +/− is two drawn bars rather than a glyph swap: the upright bar lies
   down onto the flat one. Driven purely off [open], so it works with JS off.
   Both bars sit a few degrees off square — a ruled cross would look like UI. */
.faq summary { position: relative; padding-right: 2rem; }
.faq details summary::before,
.faq details summary::after,
.faq details[open] summary::after {
  content: '';
  position: absolute; right: .15rem; top: 50%;
  width: 14px; height: var(--rule); margin-top: calc(var(--rule) / -2);
  background: var(--cavity-mid);
  border-radius: 2px;
}
.faq details summary::after  { rotate: -4deg; }
.faq details summary::before { rotate: 86deg; }
.faq details[open] summary::before { rotate: -4deg; }
/* Stand the bar back up the moment the fold starts closing, not when it ends. */
.faq details.fxm-closing summary::before { rotate: 86deg; }

@media (prefers-reduced-motion: no-preference) {
  .fxm-fold { transition: grid-template-rows var(--fxm-fold) var(--ease); }
  .fxm-fold__in { transition: opacity calc(var(--fxm-fold) * .7) var(--ease); }
  .faq details summary::before { transition: rotate var(--fxm-bar) var(--ease); }
}

/* ── 5. heading misprint ─────────────────────────────────────────────────────
   As a section heading arrives it slips a pixel for one frame and reprints —
   a photocopier's registration error. Discrete keyframes (step-end), so it's
   never in motion: it's either in place or it isn't. Scroll-driven where
   supported, so the slip lands exactly as the eye does; the IO fallback in
   fx-micro.js plays the same slip on a clock. */
@keyframes fxm-misprint-v {
  0%   { translate: 0 0; }
  60%  { translate: 1px -1px; }
  72%  { translate: 0 0; }
  100% { translate: 0 0; }
}
@keyframes fxm-misprint-t {
  0%   { translate: 0 0; }
  40%  { translate: 1px -1px; }   /* 40–62% of 84ms ≈ 18ms: one frame at 60Hz */
  62%  { translate: 0 0; }
  100% { translate: 0 0; }
}
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .h-sec {
      animation-name: fxm-misprint-v;
      animation-duration: auto;
      animation-timing-function: step-end;
      animation-fill-mode: none;
      animation-timeline: view();
      /* The slip fires at 60–72% of this range: about a fifth of the way up
         the viewport, where the eye lands on a heading. */
      animation-range: cover 0% cover 35%;
    }
  }
  /* Shorthand on purpose: it resets animation-timeline/range to their
     initial values, so the two branches can never stack. */
  .h-sec.fxm-jit { animation: fxm-misprint-t var(--fxm-jit) step-end; }
}
