/* ============================================================================
   THE GRAMMAR — the structural half.
   ---------------------------------------------------------------------------
   This file carries ONLY what a mechanic cannot work without: the mask a
   masked reveal rises through, the stacking context a tilt needs, the filter
   that makes gooey gooey. It sets no durations, no easings, no distances, no
   colours — those are the build's, off the build's own tokens, which is the
   whole point. A grammar that shipped its own look would be a catalog.
   ========================================================================= */

/* --- SPLIT: each unit gets a mask it can rise through. `overflow:clip` on the
   outer, transform on the inner. Nothing moves until the build says so. --- */
.g28-u{
  display: inline-block;
  overflow: clip;
  vertical-align: top;
  /* clip-path would cut descenders; padding keeps the mask honest */
  padding-block-end: 0.12em;
  margin-block-end: -0.12em;
}
.g28-u-in{
  display: inline-block;
  will-change: transform;
}

/* --- STICK: the frame that holds while the section's own scroll is spent. The
   TRAVEL is set by JavaScript, never here, so a refused mover leaves no empty
   scroll behind it. --------------------------------------------------------- */
[data-g28-sticking] > *{
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: clip;
}

/* A max-content track inside a grid or flex parent will refuse to shrink,
   because those children default to min-width:auto — and one un-shrunk track
   drags the whole document wider than the viewport. Every mover that holds a
   wide track pins itself instead of trusting the parent. */
[data-g28~="lane"], [data-g28-throwable]{
  min-width: 0;
  max-width: 100%;
}

/* --- LANE: the wrapped marquee. --- */
[data-g28~="lane"]{ overflow: clip; }
[data-g28~="lane"] > *{
  display: flex;
  width: max-content;
  will-change: transform;
}

/* --- THROW: an object under the hand, not a document. --- */
[data-g28-throwable]{
  overflow: clip;
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
}
[data-g28-throwable][data-g28-held]{ cursor: grabbing; }
[data-g28-throwable] > *{
  display: flex;
  width: max-content;
  will-change: transform;
}

/* --- TILT: depth needs a camera. --- */
[data-g28~="tilt"]{
  transform-style: preserve-3d;
  will-change: transform;
}

/* --- GLARE / SPOTLIGHT: the element must be able to hold a light. --- */
[data-g28~="glare"], [data-g28~="spotlight"]{
  position: relative;
  isolation: isolate;
}

/* --- BURST: one shared canvas over everything, never in the way. --- */
.g28-burst{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 9999;
}

/* --- GRAIN: one small tile, scaled. Cheap at any surface size. --- */
[data-g28~="grain"]{ position: relative; isolation: isolate; }
.g28-grain{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  image-rendering: pixelated;
  mix-blend-mode: overlay;
  opacity: 0.5;
  z-index: 1;
}

/* --- GOOEY: blur then threshold, so neighbours merge like liquid. The filter
   is defined here because the mechanic IS the filter.
   It must go on a TRANSPARENT group that holds only the shapes — a filtered
   element blurs its own background and border too, which reads as a smudged
   box rather than liquid. --- */
[data-g28-gooey]{
  filter: url(#g28-gooey);
  background: none;
  border: 0;
}

/* ============================================================================
   TRANCHE TWO (19–30) — structure only, same rule: no look ships from here.
   ========================================================================= */

/* PARALLAX: layers need a stacking context to be layers. */
[data-g28~="parallax"]{ position: relative; isolation: isolate; }

/* CLIP: the shape is the build's; the containment is not optional. */
[data-g28~="clip"]{ overflow: clip; }

/* TRAIL: followers are placed by the grammar, in the viewport, out of flow. */
[data-g28~="trail"] > *{
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  translate: var(--g28-tx, 0) var(--g28-ty, 0);
  will-change: translate;
  z-index: 60;
}

/* ORBIT: a set that circles needs somewhere to circle in. */
[data-g28~="orbit"]{ position: relative; transform-style: preserve-3d; }
[data-g28~="orbit"] > *{ position: absolute; left: 50%; top: 50%; will-change: transform; }

/* RIPPLE / ELASTIC / WEIGHT / PULL / FOLLOW / BLUR emit numbers only and need
   no structure at all — which is the point. */
[data-g28~="ripple"]{ position: relative; overflow: clip; isolation: isolate; }

/* FLIP: the browser animates the difference; it only needs a real box. */
[data-g28-flippable] > *{ will-change: transform; }

/* ============================================================================
   THE STILL PATH — reduced motion is a path, never a subtraction. Anything
   that would have arrived is simply already here; nothing is left invisible
   and nothing is left holding empty scroll.
   ========================================================================= */
@media (prefers-reduced-motion: reduce){
  .g28-u-in{ transform: none !important; }
  [data-g28-sticking]{ height: auto !important; }
  [data-g28-sticking] > *{ position: static; height: auto; }
  [data-g28~="lane"] > *, [data-g28-throwable] > *{ transform: none !important; }
  .g28-grain{ display: none; }
  [data-g28~="trail"]{ display: none; }
  [data-g28~="orbit"] > *{ position: static; }
  [data-g28~="orbit"]{ display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; }
}
