/* ============================================================================
   THERMAL CURSOR — molten-glass melt on hover-dwell
   ============================================================================
   Pairs with cursor.js. Both files are self-contained: style.css and main.js
   are never modified, and nothing here depends on either having run. To remove
   the feature entirely, delete the <link> and <script> tags — nothing else in
   the site references it.

   The engine writes ONLY custom properties (--melt-*, --warm, --heat) and the
   [data-melt] attribute. It never writes to style.transform / style.filter /
   style.opacity directly, because main.js already owns those inline slots on
   .work and .hero__media. Everything below composes rather than replaces.
   ========================================================================= */

/* ------------------------------- THE MELT -------------------------------- */
/* Applied only while an element is hot; the attribute is removed once it has
   settled back, so no element carries a will-change layer at rest.

   Every melt target is either a block, a replaced element, or a flex/grid item
   (which CSS blockifies) — transforms do not apply to non-replaced INLINE
   boxes, so any new target added to MELT_SEL in cursor.js must be checked
   against that. See the selector list there. */
[data-melt] {
  transform: translateY(var(--melt-y, 0px))
             scaleX(var(--melt-sx, 1))
             scaleY(var(--melt-sy, 1))
             skewX(var(--melt-skew, 0deg));
  /* The heat pivots where the cursor actually is, not at the element's centre,
     so parking near an edge sags that edge. Frozen on release by cursor.js so
     the settle doesn't pivot-jump. */
  transform-origin: var(--melt-ox, 50%) var(--melt-oy, 50%);
  filter: var(--melt-fx, none);
  will-change: transform, filter;
}

/* .hero__video already carries grayscale(var(--q)) from style.css — COMPOSE
   with it, never replace it, or the scatter desaturation breaks. The fallback
   is deliberately non-empty (opacity(1) is an identity filter) so this stays a
   valid declaration in the frame between the attribute landing and the first
   --melt-fx write. */
.hero__video[data-melt] {
  filter: grayscale(var(--q)) var(--melt-fx, opacity(1));
  /* The hero video is the one element that runs edge-to-edge with the viewport.
     Displacement pulls its own edges inward, which on a full-bleed frame lets
     the cream page show through as a ragged border — it reads as a rendering
     fault rather than as melting. Everywhere else that ragged silhouette IS the
     effect and is kept. Its parent clips (overflow:hidden), so overscaling
     while molten keeps the frame full without altering the melt itself.
     This is the one whole-element transform left in place, and it is driven by
     --melt-edge, NOT --melt-h: the cover is only needed when the hot spot is
     actually within reach of the video's own edge, and on a full-bleed frame
     that is rare. Keying it to raw heat meant melting the middle of the hero
     silently zoomed the entire viewport — a whole-element effect from a cursor
     nowhere near an edge, which is exactly the complaint. It now stays at 1
     unless the pointer is within RADIUS of a border, and the zoom itself is
     down to 2%. */
  transform: translateY(var(--melt-y, 0px))
             scaleX(var(--melt-sx, 1))
             scaleY(var(--melt-sy, 1))
             skewX(var(--melt-skew, 0deg))
             scale(calc(1 + 0.03 * var(--melt-edge, 0)));
}

/* The turbulence field lives here. Zero-sized and out of flow so it can never
   disturb a layout; cursor.js fills it with the filter pool at startup. */
.mh-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* ------------------------------ THE CURSOR ------------------------------- */
/* .mh-on is added to <html> by cursor.js only after the capability gate passes
   (fine pointer, hover, no reduced-motion request). Without it nothing here
   applies and the native arrow is untouched.

   !important is required, not lazy: .work, .works__index li and .about__link
   all set cursor:pointer in style.css, and the UA sheet sets it on every <a>.
   The blanket rule is scoped behind the opt-in class, so it can only ever be
   active when a replacement cursor is actually on screen. */
html.mh-on,
html.mh-on * {
  cursor: none !important;
}

.mh-cursor {
  --amber: #C4551A;

  position: fixed;
  top: 0;
  left: 0;
  z-index: 90;                    /* .nav is the site's ceiling at 50 */
  pointer-events: none;           /* stays transparent to elementFromPoint */
  opacity: 0;                     /* until the pointer is first seen */
  will-change: transform;

  /* Inherit the cream -> oxblood flip main.js already drives on --nav-color,
     so the readout is legible over the dark hero video AND over the cream page
     with no logic of our own. On bathed-in-light.html (no main.js) this picks
     up the body.page-cream value instead.
     The READOUT deliberately never takes the heat colour: tinting it amber made
     it near-invisible against both the cream page and the lighter passages of
     the hero video. --nav-color is the site's own contract for "readable
     against whatever is behind it right now", so the numbers keep it and the
     ring, dot and halo carry the temperature instead. The numbers are the
     information; the ring is the gauge. */
  color: var(--nav-color, var(--oxblood));

  /* Heat colour, for the gauge only. The plain declaration is the fallback for
     browsers without color-mix(). */
  --heat-col: var(--nav-color, var(--oxblood));
  --heat-col: color-mix(in oklab,
                        var(--amber) calc(var(--heat, 0) * 88%),
                        var(--nav-color, var(--oxblood)));

  font-family: "neue-haas-grotesk-display", "Neue Haas Grotesk Display Pro",
               "Helvetica Neue", Arial, sans-serif;
}
.mh-cursor.is-live { opacity: 1; }

/* All four children share the pointer's origin; each offsets itself from there. */
.mh-cursor__halo,
.mh-cursor__ring,
.mh-cursor__dot,
.mh-cursor__lag {
  position: absolute;
  top: 0;
  left: 0;
}

/* Radial bloom. Faint while the cursor is merely charging (--warm), and the
   real tell once it is past working temperature (--heat).
   Sized to land at roughly the RADIUS in cursor.js at full heat (190px across
   = 95px reach), so the bloom shows you how far the heat is actually carrying
   instead of being decorative. Tracks RADIUS — retune it whenever RADIUS moves,
   or it promises a reach the melt does not have. */
.mh-cursor__halo {
  width: calc(36px + 154px * var(--heat, 0));
  height: calc(36px + 154px * var(--heat, 0));
  transform: translate(-50%, -50%);
  border-radius: 50%;
  opacity: calc(var(--warm, 0) * 0.20 + var(--heat, 0) * 0.62);
  background: radial-gradient(circle,
              rgba(196, 85, 26, 0.34) 0%,
              rgba(196, 85, 26, 0.11) 42%,
              rgba(196, 85, 26, 0) 72%);
}

/* The ring is the temperature gauge: it swells through the charge phase and
   again through the melt phase, so the state is readable without the numbers. */
.mh-cursor__ring {
  width: calc(9px + 3px * var(--warm, 0) + 9px * var(--heat, 0));
  height: calc(9px + 3px * var(--warm, 0) + 9px * var(--heat, 0));
  transform: translate(-50%, -50%);
  border: 1px solid var(--heat-col);
  border-radius: 50%;
  /* Size/border are driven per-frame; only the hover state is transitioned. */
  transition: background-color .18s ease, border-width .18s ease;
}

/* Hiding the arrow costs link affordance, so the ring takes it over. */
.mh-cursor.is-link .mh-cursor__ring {
  border-width: 1.5px;
  background: color-mix(in oklab, var(--heat-col) 18%, transparent);
}

.mh-cursor__dot {
  width: 3px;
  height: 3px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--heat-col);
}

/* Two nested boxes so the trailing lag and the edge-flip offset never have to
   share a calc(): --read-l* is the plume lag in px, --read-o* is the offset
   from the pointer, which flips sign near the viewport edges. */
.mh-cursor__lag {
  transform: translate(var(--read-lx, 0px), var(--read-ly, 0px));
}

.mh-cursor__read {
  position: absolute;
  top: 0;
  left: 0;
  transform: translate(var(--read-ox, 16px), var(--read-oy, 8px));
  white-space: nowrap;            /* so -100% resolves against the real width */
  font-size: 10px;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* A fixed 4ch numeric column keeps the units in a straight line as the reading
   climbs from 21 to 1600, and tabular figures stop the digits jittering. */
.mh-cursor__row {
  display: grid;
  grid-template-columns: 4ch auto;
  column-gap: 3px;
}
.mh-cursor__n {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.mh-cursor__u { opacity: .62; }
