/* ============================================================================
   QP MOTION — the propagation layer (Stage 2 proposal)
   markdimercurio.com · the concrete motion contract from review/qp-motion-vocabulary.md.
   Quiet = slow + decelerate + NO overshoot. Five tokens, five named patterns, dialed per tier.
   This is what LIVE pages load on rollout (not yet wired). Does not redefine the design-system QP spec.

   CONTENT-STRANDING FIX (refines the vocabulary's literal pattern):
   The vocab's sample hides with `opacity:0` inside @media(no-preference) and reveals via `.is-in`.
   That still strands **no-JS + motion-allowed** users (the .is-in never gets added). So the hidden
   start-state is gated behind **html.js** (added by an inline script before paint). Result:
     · reduced-motion      → @media off            → VISIBLE (resting default)
     · no-JS (any motion)  → no html.js             → VISIBLE (resting default)
     · crawlers/SSR        → no html.js             → VISIBLE
     · motion-allowed + JS → html.js + IO adds is-in → animates in
   Visible-by-default is therefore true for everyone except the one case that opted into motion.
   ========================================================================== */

/* ---------- Primitives (the token contract) ---------- */
:root{
  /* easing — all decelerate, none overshoot (no y>1) */
  --qp-ease-settle: cubic-bezier(0.16, 1, 0.30, 1);   /* signature: long smooth decel */
  --qp-ease-rise:   cubic-bezier(0.22, 1, 0.36, 1);   /* editorial default ease-out */
  --qp-ease-state:  cubic-bezier(0.40, 0, 0.20, 1);   /* toggles / hovers (in-out) */
  /* duration */
  --qp-dur-quick:     160ms;
  --qp-dur-base:      300ms;
  --qp-dur-reveal:    520ms;
  --qp-dur-signature: 700ms;
  /* travel — short; lg is the hard max for any quiet reveal */
  --qp-travel-sm:  8px;
  --qp-travel-md: 16px;
  --qp-travel-lg: 24px;
  /* orchestration — a group must fully settle in < ~300ms (so cap stagger at 3) */
  --qp-stagger: 60ms;
}

/* ---------- Named reveal patterns (visible-by-default; hidden only for motion-allowed+JS) ---------- */
/* resting state = visible. No rule needed here — the element is its normal self. */
@media (prefers-reduced-motion: no-preference){
  html.js :is(.qp-fade,.qp-rise,.qp-settle,.qp-attend){ will-change:opacity,transform; }

  html.js .qp-fade{ opacity:0; transition:opacity var(--qp-dur-reveal) var(--qp-ease-rise); }
  html.js .qp-fade.is-in{ opacity:1; }

  html.js .qp-rise{ opacity:0; transform:translateY(var(--qp-travel-md));
    transition:opacity var(--qp-dur-reveal) var(--qp-ease-rise), transform var(--qp-dur-reveal) var(--qp-ease-rise); }
  html.js .qp-rise.is-in{ opacity:1; transform:none; }

  html.js .qp-settle{ opacity:0; transform:translateY(var(--qp-travel-lg));
    transition:opacity var(--qp-dur-signature) var(--qp-ease-settle), transform var(--qp-dur-signature) var(--qp-ease-settle); }
  html.js .qp-settle.is-in{ opacity:1; transform:none; }

  html.js .qp-attend{ opacity:0; transform:translateY(var(--qp-travel-sm));
    transition:opacity var(--qp-dur-signature) var(--qp-ease-settle), transform var(--qp-dur-signature) var(--qp-ease-settle); }
  html.js .qp-attend.is-in{ opacity:1; transform:none; }

  /* stagger — only on explicit groups, capped at 3 so the set settles in < ~300ms */
  html.js [data-qp-delay="1"]{ transition-delay:var(--qp-stagger); }
  html.js [data-qp-delay="2"]{ transition-delay:calc(var(--qp-stagger)*2); }
  html.js [data-qp-delay="3"]{ transition-delay:calc(var(--qp-stagger)*3); }
}

/* qp-quick — functional feedback (hover/focus/press/validation). Not an entrance; safe under reduced-motion. */
.qp-quick{ transition: color var(--qp-dur-quick) var(--qp-ease-state),
  background-color var(--qp-dur-quick) var(--qp-ease-state),
  border-color var(--qp-dur-quick) var(--qp-ease-state),
  opacity var(--qp-dur-quick) var(--qp-ease-state); }

/* ---------- CLS: metric-matched font fallbacks (vocab rule #3) ----------
   Gives the fallback DM Sans/Playfair metrics so the web font swaps with no reflow.
   CALIBRATED 2026-06-24 from the ACTUAL Google Fonts files (DM Sans v17, Playfair v40):
   fontaine-style — frequency-weighted avg advance width + typo metrics, extracted via fonttools.
   (Re-extract if the GF font version changes.) Strategy: font-display:swap + these matched fallbacks. */
@font-face{ font-family:"DM Sans Fallback"; src:local("Arial");
  size-adjust:105.51%; ascent-override:94.02%; descent-override:29.38%; line-gap-override:0%; }
@font-face{ font-family:"Playfair Fallback"; src:local("Georgia");
  size-adjust:84.99%; ascent-override:127.30%; descent-override:29.53%; line-gap-override:0%; }
/* engage the matched fallbacks under body.qp (CLS-measured 0.00 on the T4 reference) */
.qp{ font-family:'DM Sans','DM Sans Fallback',system-ui,sans-serif; }
.qp :is(.ser,.accent){ font-family:'Playfair Display','Playfair Fallback',serif; }
/* usage on rollout: body{ font-family:'DM Sans','DM Sans Fallback',system-ui,sans-serif }
   .ser/.accent{ font-family:'Playfair Display','Playfair Fallback',serif } */
