/*
 * Styles for the Liquid Glass Bubble code component.
 *
 * Hand-written rather than compiled from Tailwind, matching the Star Field and
 * Floating Icons components: the rules ship exactly as written, with no build
 * step that can silently drop a class from the bundle.
 *
 * Everything is scoped under .wf-glassbubble so nothing leaks onto the page.
 * Every knob the Designer exposes arrives as a custom property set inline on
 * the root, so a prop change repaints without React touching a single rule.
 */

/*
 * No `isolation` and no `z-index` anywhere on the path down to the lens.
 *
 * `backdrop-filter` can only see what is painted behind the element inside the
 * nearest backdrop root, and every stacking context is one of those. Isolating
 * the root cut the page out of the backdrop entirely, so the lens had nothing
 * to refract and the glass rendered blank. Paint order here comes from document
 * order instead, which costs nothing and keeps the page in view.
 */
.wf-glassbubble {
  position: relative;
  display: flex;
  width: 100%;
  /* Overwritten by the measuring effect. Declared so the keyframe that reads it
     is still valid on the first paint, before that effect has run. */
  --wf-gb-text-scale-from: 1;

  /* Written by the physics loop. Only the spin pair lives here now — it feeds
     gradient positions rather than a transform. Offset and squash are set
     straight onto the layers, so they never invalidate this subtree. */
  --wf-gb-spin-x: 0;
  --wf-gb-spin-y: 0;
}

/* The sphere is grabbable, so it should say so. */
/* A mouse only has to be over it, so there is nothing to grab and no cursor
   change to make. Touch still presses, and that drag must not scroll the page. */
.wf-glassbubble[data-draggable='true'] .wf-glassbubble__stage {
  touch-action: none;
}

.wf-glassbubble *,
.wf-glassbubble *::before,
.wf-glassbubble *::after {
  box-sizing: border-box;
}

/* The filter lives in the document but must never take up space. */
.wf-glassbubble__defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

/* ------------------------------------------------------------ background -- */

/*
 * The gradient behind the glass, when there is one.
 *
 * Positioned and painted first, and with no z-index, for the same reason
 * nothing above has one: it has to share a backdrop root with the lens or the
 * glass cannot see it to bend it. Document order alone keeps it behind.
 *
 * The CSS gradient is a still of roughly where the scene settles. It is what
 * shows before the first frame is drawn, and all that shows where WebGL is not
 * available — so a missing GPU loses the motion, not the background.
 */
.wf-glassbubble__backdrop {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  background:
    radial-gradient(
      18% 26% at 70% 24%,
      var(--wf-gb-bg-base) 55%,
      transparent 100%
    ),
    radial-gradient(55% 65% at 52% 42%, var(--wf-gb-bg-warm) 0%, transparent 72%),
    radial-gradient(85% 90% at 30% 62%, var(--wf-gb-bg-cool) 0%, transparent 78%),
    var(--wf-gb-bg-base);
}

.wf-glassbubble__canvas {
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
}

/* Shown once it has actually drawn. An unpainted WebGL canvas is black, which
   would flash over the still for a frame. */
.wf-glassbubble__backdrop[data-ready='true'] .wf-glassbubble__canvas {
  opacity: 1;
}

/* ---------------------------------------------------------------- layout -- */

.wf-glassbubble--fullscreen {
  min-height: 100svh;
}

.wf-glassbubble--fill {
  height: 100%;
}

.wf-glassbubble--fixed {
  height: auto;
}

.wf-glassbubble__inner {
  position: relative;
  display: flex;
  width: 100%;
  align-items: center;
  padding: var(--wf-gb-inset);
}

.wf-glassbubble--align-center .wf-glassbubble__inner {
  justify-content: center;
}

.wf-glassbubble--align-left .wf-glassbubble__inner {
  justify-content: flex-start;
}

.wf-glassbubble--align-right .wf-glassbubble__inner {
  justify-content: flex-end;
}

/* ----------------------------------------------------------------- stage -- */

/*
 * The stage reserves the bubble's finished box so the surrounding page never
 * reflows while the bubble grows. Its size is the smaller of the container's
 * two dimensions, measured in JavaScript — a square that always fits.
 */
.wf-glassbubble__stage {
  position: relative;
  width: var(--wf-gb-size);
  height: var(--wf-gb-size);
  flex: 0 0 auto;
}

/* ---------------------------------------------------------------- bubble -- */

.wf-glassbubble__bubble {
  position: absolute;
  /* The glass covers the text now that the text sits behind it, so it must not
     take the clicks meant for it — without this the text cannot be selected on
     the page or double-clicked into on the Designer canvas. */
  pointer-events: none;
  /* Locked 1:1, and centred by margin rather than a transform — `transform` is
     the physics' to write and `scale` is the growth's, so neither can be spent
     on merely sitting in the middle. */
  top: 50%;
  left: 50%;
  width: var(--wf-gb-size);
  height: var(--wf-gb-size);
  margin-top: calc(var(--wf-gb-size) / -2);
  margin-left: calc(var(--wf-gb-size) / -2);
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--wf-gb-radius);
  /*
   * The lens is applied to this element itself, not to a child, and that is
   * load-bearing. This element is transformed, and a transform creates a
   * stacking context — which is also a backdrop root, so a child's
   * `backdrop-filter` can only ever see what is painted inside this box, i.e.
   * nothing. An element's own transform does not fence off its own backdrop,
   * so the filter has to live here. It arrives as an inline style, since the
   * filter id is unique per instance.
   *
   * Blur, saturation and brightness are folded into that same SVG filter rather
   * than added as a second `backdrop-filter`, because stacking two of them
   * nests them and the inner one then reads an already-filtered, clipped
   * backdrop instead of the page.
   */
}

/*
 * The cast shadow, as a masked halo rather than a `box-shadow` on the bubble.
 *
 * A box-shadow paints behind its element, and this glass is almost fully
 * transparent — so the shadow showed straight through the middle and greyed out
 * everything the lens was supposed to be refracting. Drawing it as a ring with
 * the disc masked out keeps the shadow strictly outside the glass, where a
 * shadow actually belongs.
 */
.wf-glassbubble__cast {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: calc(var(--wf-gb-size) / -2);
  margin-left: calc(var(--wf-gb-size) / -2);
  width: var(--wf-gb-size);
  height: var(--wf-gb-size);
  border-radius: var(--wf-gb-radius);
  box-shadow:
    0 12px 26px -16px rgba(5, 8, 24, var(--wf-gb-shadow)),
    0 34px 64px -34px rgba(5, 8, 24, var(--wf-gb-shadow));
  /* Knock the disc out, leaving only the halo that spills past the rim. */
  -webkit-mask:
    radial-gradient(closest-side, #000 0 100%) center / 100% 100% no-repeat,
    linear-gradient(#000 0 0);
  mask:
    radial-gradient(closest-side, #000 0 100%) center / 100% 100% no-repeat,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

/*
 * The glass edge.
 *
 * Two rings, because one colour cannot read on both a light and a dark page: a
 * white highlight biased to the top-left where the light lands, and a faint
 * dark contact line just inside it. On white the dark line draws the circle; on
 * a dark page the highlight does. Painted with the two-layer mask trick so only
 * the ring shows and the middle of the glass stays clear.
 */
.wf-glassbubble__surface::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  padding: var(--wf-gb-rim-width);
  border-radius: inherit;
  background: conic-gradient(
    from 292deg,
    rgba(255, 255, 255, var(--wf-gb-rim-alpha)) 0%,
    rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.9)) 12%,
    rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.16)) 34%,
    rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.62)) 56%,
    rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.2)) 78%,
    rgba(255, 255, 255, var(--wf-gb-rim-alpha)) 100%
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

.wf-glassbubble__surface::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  box-shadow:
    inset 0 0 0 0.5px rgba(15, 23, 42, calc(var(--wf-gb-rim-alpha) * 0.22)),
    inset 0 0 6px -2px rgba(15, 23, 42, calc(var(--wf-gb-rim-alpha) * 0.18));
  pointer-events: none;
}


/*
 * The glass surface, deliberately split from the lens.
 *
 * The lens refracts everything painted behind it, and the text is behind it —
 * so at any real magnification the words came out warped and hard to read. The
 * refraction stays on the layer below the text; the tint, shading and rim move
 * here, and this paints over it. The type keeps the glass sitting on top of it
 * without being bent by it.
 */
.wf-glassbubble__surface {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-top: calc(var(--wf-gb-size) / -2);
  margin-left: calc(var(--wf-gb-size) / -2);
  width: var(--wf-gb-size);
  height: var(--wf-gb-size);
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--wf-gb-radius);
  pointer-events: none;
  /*
   * Sphere shading, which is most of what separates a ball from a flat disc.
   * Three layers, all keyed to one light sitting up and to the left:
   *
   *  - a small, bright specular spot where that light actually hits;
   *  - a soft bounce low and to the right, the light that has come back up
   *    through the glass, which is what gives it volume rather than a rim;
   *  - a broad, faint darkening on the shadow side.
   *
   * All of them stay well clear of the middle. A wash across the whole face is
   * exactly what makes CSS glass look like frosted plastic, and the centre has
   * to stay clear enough to read the refraction through.
   */
  background:
    /* A tight, bright catch where the light lands. Small on purpose: a broad
       one reads as a painted ball, and this material is meant to be thin. */
    /* The catch light travels as the sphere is rolled. A ball turning under a
       fixed light is exactly this: the silhouette never changes, the highlight
       moves across it. Without that, dragging only slides the sphere around
       and nothing about it reads as rotating. */
    radial-gradient(
      20% 17% at calc(31% - var(--wf-gb-spin-x) * 22%)
        calc(21% - var(--wf-gb-spin-y) * 22%),
      rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.34)) 0%,
      rgba(255, 255, 255, 0) 72%
    ),
    /* Light that has come back up through the glass, low and opposite. */
    radial-gradient(
      42% 34% at calc(70% - var(--wf-gb-spin-x) * 14%)
        calc(83% - var(--wf-gb-spin-y) * 14%),
      rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.13)) 0%,
      rgba(255, 255, 255, 0) 76%
    ),
    /* A whisper on the shadow side. Enough to turn the form, not to shade it
       like an object — the volume should come from the lensing. */
    radial-gradient(
      130% 130% at 20% 12%,
      rgba(255, 255, 255, 0) 62%,
      rgba(10, 14, 32, calc(var(--wf-gb-rim-alpha) * 0.08)) 100%
    ),
    var(--wf-gb-tint);
  box-shadow:
    /* A crisp bright line right on the edge, top and bottom. */
    inset 0 1.5px 1px -0.5px rgba(255, 255, 255, var(--wf-gb-rim-alpha)),
    inset 0 -1.5px 1px -0.5px
      rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.7)),
    /* Thickness, held tight against the rim with a negative spread so it
       reads as the depth of the glass and never fogs the middle. */
    inset 0 0 var(--wf-gb-thickness) calc(var(--wf-gb-thickness) * -0.72)
      rgba(255, 255, 255, calc(var(--wf-gb-rim-alpha) * 0.5)),
    inset 6px 0 14px -12px rgba(120, 220, 255, var(--wf-gb-chroma)),
    inset -6px 0 14px -12px rgba(255, 130, 210, var(--wf-gb-chroma));
}

/* --------------------------------------------------------------- content -- */

/*
 * Centred on the bubble's centre, which is also the stage's centre, so the text
 * holds still while the bubble opens around it and the circle clips it into
 * view from the middle out.
 */
/*
 * Two elements, because the circle mask and the zoom-out need different
 * coordinate systems. A mask is applied before the element's own transform, so
 * a radius set on a scaled element comes out multiplied by that scale — the
 * text spilled well past the bubble. The wrapper stays unscaled and owns the
 * mask in real pixels; the inner element owns the zoom.
 */
.wf-glassbubble__content {
  position: absolute;
  inset: 0;

  /* Deliberately unclipped. While the sphere is still small the text is larger
     than it, and it should hang outside — the words arriving before the glass
     does is the whole gesture, and a mask here would swallow it. */
  /* The wrapper spans the whole stage, so it would otherwise swallow clicks
     across the entire section; only the words themselves should be hit. */
  pointer-events: none;
}

.wf-glassbubble__text {
  pointer-events: auto;
}

.wf-glassbubble__text {
  position: absolute;
  top: 50%;
  left: 50%;
  width: max-content;
  /* Wraps against the section, not the sphere. Holding it to the circle's width
     is what stopped the words being able to hang outside it. */
  max-width: var(--wf-gb-content-max);
  margin: 0;
  transform: translate(-50%, -50%) scale(1);
  /* No type styling here: the slot brings its own, and its natural size is
     the size the animation settles at. */
}

/* ------------------------------------------------------------- animation -- */

/*
 * A dot opening into a circle. Width and height are animated rather than
 * scaled: scaling would smear the rim and drag the refraction with it, and the
 * whole effect rests on that edge staying true. Only this element reflows — it
 * is out of flow inside a stage that already holds the finished size.
 */
/*
 * Nothing, past full, then settle. The overshoot is a real keyframe rather than
 * a spring easing curve: a curve that overshoots does it by however much its
 * control points happen to imply, which is not a number anyone can set. This
 * way the peak is exactly what the designer asked for.
 */
/*
 * Grown by scale, not by width and height.
 *
 * Those are layout properties: animating them ran layout on three layers every
 * frame, re-rasterised the surface's gradients and its masked ring at a new size
 * each time, and re-ran the backdrop filter over a region that never stopped
 * changing — for the whole 1.4 seconds. Scale is a compositor transform on a
 * layer that is rasterised once.
 *
 * Its own property rather than inside `transform`, so the physics can hold
 * `translate` and `transform` at the same time without either overwriting the
 * other. The layers are centred by margin now, so nothing needs a transform
 * simply to sit in the middle.
 */
@keyframes wf-gb-grow {
  0% {
    scale: 0;
    opacity: 0;
  }
  8% {
    opacity: 1;
  }
  68% {
    scale: var(--wf-gb-over);
  }
  100% {
    scale: 1;
    opacity: 1;
  }
}

/* The counter-motion: the text starts oversized and settles down to fit as the
   circle opens up around it. */
@keyframes wf-gb-text-shrink {
  0% {
    transform: translate(-50%, -50%) scale(var(--wf-gb-text-scale-from));
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
  }
}

.wf-glassbubble__stage[data-play='on'] .wf-glassbubble__bubble,
.wf-glassbubble__stage[data-play='on'] .wf-glassbubble__surface,
.wf-glassbubble__stage[data-play='on'] .wf-glassbubble__cast {
  animation: wf-gb-grow var(--wf-gb-duration) var(--wf-gb-ease) var(--wf-gb-delay)
    both;
  will-change: scale;
}

.wf-glassbubble__stage[data-play='on'] .wf-glassbubble__text {
  animation: wf-gb-text-shrink var(--wf-gb-text-duration)
    var(--wf-gb-text-ease) var(--wf-gb-text-delay) both;
  /* No `will-change: transform` on purpose. It pins the text to one rasterised
     layer, and this scales down from several times its final size — the browser
     has to re-raster as it goes or the words arrive as a blurry enlargement. */
}

/* Armed and waiting on a scroll trigger: held invisible so the bubble is never
   seen at its finished size a frame before it grows.

   Gated on having been measured, deliberately. If measurement never happens —
   no ResizeObserver, a container that never lays out — the bubble falls back to
   the plain static state rather than staying invisible forever. Measuring runs
   in a layout effect, so there is no frame in between to flash. */
.wf-glassbubble__stage[data-measured='true'][data-play='armed']
  .wf-glassbubble__bubble,
.wf-glassbubble__stage[data-measured='true'][data-play='armed']
  .wf-glassbubble__surface,
.wf-glassbubble__stage[data-measured='true'][data-play='armed']
  .wf-glassbubble__cast {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .wf-glassbubble__stage .wf-glassbubble__bubble,
  .wf-glassbubble__stage .wf-glassbubble__surface,
  .wf-glassbubble__stage .wf-glassbubble__cast,
  .wf-glassbubble__stage .wf-glassbubble__content,
  .wf-glassbubble__stage .wf-glassbubble__text {
    animation: none !important;
    opacity: 1;
  }
}

