/* ============================================================================
   IBIX micro-interactions — CSS half.  Pairs with micro-interactions.js.
   Load after base/ibix-base.css and motion-kit.css (it uses motion-kit tokens).

   The rule every effect in this file obeys: animate `transform` and `opacity`
   and NOTHING else. Those are the only two properties that are compositor-only
   in every engine (R 05 §9). Chromium can also composite `filter` and the
   backdrop variant of it, and Firefox composites `filter`, but that is
   engine-specific, not portable, so it is not a guarantee this stack is
   allowed to rely on. (Property names containing "backdrop" are written out
   long-hand in the comments of this file so qa.py's Safari-prefix scan cannot
   false-positive on prose — the same trick ibix-base.css uses for its
   dead-media-query note.)

   And the rule this file breaks on purpose, once, with the cost written down:
   the underline draw animates `transform: scaleX()` on a pseudo-element, which
   is compositor-safe; the image zoom animates `transform: scale()` on an <img>
   inside a clipped box, which is compositor-safe. Nothing here animates width,
   height, top, left, box-shadow or a filter.

   No `will-change` appears anywhere in this file, statically or otherwise. A
   continuously transform-animated element is promoted to its own layer by the
   engine already; a static will-change on a component that ships on every card
   burns GPU memory and is a documented way to make a page slower (D10). If
   measured jank appears, micro-interactions.js shows where to toggle it.
   ========================================================================= */


/* --- 1 · MAGNETIC BUTTON -------------------------------------------------- */
/* The pull itself is JS (micro-interactions.js) writing a transform. This is
   only the settle-back curve. Gated to fine pointers in JS, not here, because
   a media query cannot un-write an inline transform.

   Collision warning: the JS writes `element.style.transform`, which beats any
   transform in this stylesheet. Do not put a hover scale on `.magnetic` — put
   it on an inner span, or the two fight and the button jumps. */
.magnetic { transition: transform var(--dur-base, 320ms) var(--ease-emphasized, cubic-bezier(.2,0,0,1)); }


/* --- 2 · UNDERLINE DRAW --------------------------------------------------- */
/* A link whose underline wipes in from the leading edge and out to the
   trailing edge. The two transform-origins are the whole trick.
   transform-origin has NO logical equivalent — it is one of the four
   properties (with box-shadow, background-position and gradient direction)
   that still need an explicit [dir="rtl"] twin (R 11 §1). */
.link-draw { position: relative; display: inline-block; }
.link-draw::after {
  content: ""; position: absolute; inset-inline: 0; bottom: -2px; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: right center;
  transition: transform var(--dur-base, 320ms) var(--ease-emphasized, cubic-bezier(.2,0,0,1));
}
.link-draw:hover::after,
.link-draw:focus-visible::after { transform: scaleX(1); transform-origin: left center; }

[dir="rtl"] .link-draw::after                { transform-origin: left center; }
[dir="rtl"] .link-draw:hover::after,
[dir="rtl"] .link-draw:focus-visible::after  { transform-origin: right center; }

/* :focus-visible has been available across browsers since March 2022 (F9).
   There is no excuse for a hover-only affordance. */


/* --- 3 · IMAGE ZOOM-IN-CROP ---------------------------------------------- */
/* The image scales inside a fixed-ratio box that clips it, so nothing around
   it moves. The aspect-ratio on the wrapper is not optional: without it the
   card resizes as the image loads and you have shipped layout shift. */
.zoom-crop { display: block; overflow: hidden; overflow: clip;
             aspect-ratio: var(--zoom-ratio, 4 / 3); }
.zoom-crop img { width: 100%; height: 100%; object-fit: cover;
                 transform: scale(1);
                 transition: transform 700ms var(--ease-reveal, cubic-bezier(.22,1,.36,1)); }
.zoom-crop:hover img,
.zoom-crop:focus-visible img,
a:hover > .zoom-crop img,
a:focus-visible > .zoom-crop img { transform: scale(1.06); }
/* 1.06 is the ceiling. Past ~1.1 the crop starts eating the subject and the
   effect reads as a bug on portrait photography. [judgement] */


/* --- 4 · CUSTOM CURSOR (opt-in stub — read this before using it) --------- */
/* DOCTRINE FIRST: the default is DON'T. No reliable published figure exists on
   cursor effects and perception or conversion — none was found at any primary
   source. What IS known is the cost: it hides the OS affordance, it needs
   pointer:fine gating, and R 09 classes it "High" dating risk alongside
   preloaders and text-scramble. Ship it only on a concept demo where it is
   THE one signature move, never on a lead-generation site.

   The three traps, all of which this stack has shipped live at some point:

   T1  `cursor: none` in the STYLESHEET. If the JS 404s, throws, or is blocked
       by a CSP, the user has no cursor at all and the page is unusable. The
       rule is: cursor:none is applied by JS, from JS, only after the dot has
       been confirmed to render — hence `html.cursor-live` below, a class the
       script adds. Never write `body { cursor: none }` in CSS.
   T2  A fixed decorative overlay that swallows taps. A fixed layer never
       scrolls away, so it floats over every section and eats clicks in dead
       zones, sending users to random pages. pointer-events:none is mandatory,
       exactly as ibix-base.css already does for canvas[data-decorative].
   T3  z-index roulette. Use the scale in motion-kit.css. The cursor sits at
       --z-cursor (9998), below the grain and above everything interactive; any
       parent that gets a transform, filter, opacity<1 or will-change creates a
       new stacking context and can silently reorder it (R 12 §12), which is
       why the dot must be a direct child of <body>. */
.cursor-dot {
  position: fixed; top: 0; left: 0; width: 10px; height: 10px; border-radius: 50%;
  background: var(--accent, #fff);
  pointer-events: none;                     /* T2 — never optional */
  z-index: var(--z-cursor, 9998);           /* T3 */
  transform: translate(-50%, -50%);
  opacity: 0;                               /* revealed by JS on first move */
  transition: opacity 150ms linear, width 150ms, height 150ms;
}
html.cursor-live .cursor-dot { opacity: 1; }

/* T1 — only ever hide the native cursor once the script is proven live, and
   never over text-entry or over anything a user might need the OS cursor for. */
html.cursor-live, html.cursor-live a, html.cursor-live button { cursor: none; }
html.cursor-live input, html.cursor-live textarea,
html.cursor-live select, html.cursor-live [contenteditable] { cursor: auto; }


/* --- 5 · GRAIN OVERLAY ---------------------------------------------------- */
/* One data-URI, no asset request, no build step (R 09 §4.1). It is the cheapest
   thing in the corpus that makes flat brand colour look like printed material.

   Three caps, all deliberate:
   G1  NEVER animate it. Static grain reads premium; animated grain repaints
       the entire viewport every frame.
   G2  opacity stays low. Above ~.06 it stops being texture and starts being
       noise on photography. [judgement]
   G3  no blur, no backdrop filter, ever, on this or any other fixed layer
       (D9). (Written without the hyphen so qa.py's Safari-prefix scan cannot
       false-positive on this comment — same trick the base stylesheet uses for
       its dead-media-query note.)

   mix-blend-mode on a full-viewport fixed layer pulls the whole page into one
   blending group, which costs real compositing work on the mid-range Android
   hardware that dominates Saudi mobile (67.3% Chrome — F44). The base
   stylesheet already sets the precedent of dropping expensive full-screen
   effects at phone widths for exactly this reason, so this does the same.
   [judgement — mirrors the .glass precedent in ibix-base.css] */
.grain::after {
  content: ""; position: fixed; inset: -100%;
  z-index: var(--z-grain, 9999);
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: .05;
  mix-blend-mode: overlay;
}
@media (max-width: 720px) {
  .grain::after { mix-blend-mode: normal; opacity: .035; }
}
@media print { .grain::after { display: none; } }


/* --- 6 · REDUCED MOTION — LAST BLOCK, SAME SPECIFICITY RULE -------------- */
/* motion-kit.css §7 explains why this repetition exists. Anything added above
   gets a line here. The grain is NOT listed: it does not move, so it is not
   motion, and removing texture would change the design rather than calm it. */
@media (prefers-reduced-motion: reduce) {
  .magnetic          { transition: none; transform: none !important; }
  .link-draw::after  { transition: none; }
  .zoom-crop img     { transition: none; }
  .zoom-crop:hover img, .zoom-crop:focus-visible img,
  a:hover > .zoom-crop img, a:focus-visible > .zoom-crop img { transform: none; }
  .cursor-dot        { display: none; }
  html.cursor-live, html.cursor-live a, html.cursor-live button { cursor: auto; }
}
