/* ── surfaces ────────────────────────────────────────────────────────────
   The main colour of this product is not a colour, it is a MATERIAL: a
   slowly morphing mix of cloud, galaxy and marble over near black.
   Anything that wants to be "the ground" wears `.surface` and gets it.

       <div class="surface">…</div>
       <div class="surface surface--ground">…</div>

   Reusable is the whole point — chat bubbles, panels, sheets and bars are
   all meant to take this class later without any of them knowing how the
   texture is made.

   HOW IT WORKS, and why it is built this way:

   TWO seamlessly tiling images, generated once by surface.js and handed
   to every surface through `--surf-tex` (stone: marble, cracks, contour)
   and `--surf-tex2` (weather: drifting streaks). Two generations,
   two uploads to the GPU, unlimited surfaces. They differ because the
   layers do different jobs — one image used twice just slides over itself
   and reads as a repeat.

   It is used as a MASK, not as a picture. The image is white-on-
   transparent, so what a surface paints is its own `--surf-vein` and
   `--surf-glow` colours showing through it. That is what makes the
   material re-tintable per element with one custom property, instead of
   being nine hard-coded colours nobody can change together.

   Two layers, and only ONE of them moves. Relative motion is all the
   morphing needs, so a static layer under a drifting one costs half of
   two drifting layers and looks identical. The moving one animates
   `transform` and `opacity` ONLY — both compositor properties, so the
   layer is rasterised once and never repainted. Animating
   `background-position` or `mask-position` instead would repaint a
   full-screen layer every frame, which is exactly the bill this avoids.

   The drift translates by EXACTLY ONE TILE, which is why the loop has no
   seam and why the texture has to be generated tileable. If you change
   `--surf-scale`, the keyframe follows it automatically — don't hardcode
   a distance in there.
   ──────────────────────────────────────────────────────────────────── */

:root {
  /* The material's three colours. Override any of them on an element to
     re-tint that surface alone. */
  --surf-deep: #06000f;   /* the ground: near black, a trace of violet */
  --surf-vein: #7655c3;   /* stone: marble, cracks, contour */
  --surf-glow: #9755aa;   /* weather: the streaks drifting over it */

  /* The two layer colours are far more saturated than the result. They are
     laid over near black at low opacity, so what reaches the eye is dark
     and dusty — read the opacities below with them, never on their own.
     Chroma in the source is also what gives the hue drift something to
     turn: a neutral grey has no hue to rotate and the drift would do
     nothing. Degrees of swing, and how long a full round trip takes. */
  --surf-hue-range: 19;
  --surf-hue-time:  240s;

  /* Peak opacity of the drifting layer; it breathes down to just over
     half. These are what keep the saturated colours above dark. */
  --surf-glow-op: .44;
  --surf-vein-op: .22;

  /* A wash of very soft light under everything, so the material has
     somewhere to be deepest and somewhere to lift. Overridable per
     surface; a small panel usually wants `none`. */
  --surf-wash:
    radial-gradient(130% 85% at 50% 4%,  rgba(38, 52, 84, .55) 0%, transparent 62%),
    radial-gradient(90% 55% at 12% 96%,  rgba(46, 44, 58, .40) 0%, transparent 70%),
    radial-gradient(80% 50% at 88% 78%,  rgba(24, 38, 62, .45) 0%, transparent 72%);

  /* Tile edge in CSS px. Smaller reads as stone, larger as sky. */
  --surf-tile: 320px;
  /* The moving layer runs at a different scale from the still one; their
     beat against each other is the morph. Keep it non-integer. */
  --surf-scale: 2.2;

  /* Replaced by surface.js once the texture exists. A fully transparent
     mask means a surface is simply flat `--surf-deep` until then, which
     is the right thing to show rather than an unmasked block of vein
     colour. */
  --surf-tex:  linear-gradient(#0000, #0000);
  --surf-tex2: linear-gradient(#0000, #0000);
}

.surface {
  position: relative;
  /* Contains the blend of the layers below, so a surface can never blend
     with whatever happens to sit behind it. */
  isolation: isolate;
  overflow: hidden;
  background-color: var(--surf-deep);
  background-image: var(--surf-wash);
}

/* Content inside a surface has to be lifted over the texture layers. */
.surface > * { position: relative; z-index: 1; }

/* A registered property, because a plain custom property does not
   interpolate — an unregistered `--surf-hue` would jump between keyframes
   instead of drifting. */
@property --surf-hue {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

@keyframes surface-hue {
  0%, 100% { --surf-hue: -1; }
  50%      { --surf-hue: 1; }
}

.surface::before,
.surface::after {
  content: "";
  filter: hue-rotate(calc(var(--surf-hue) * var(--surf-hue-range) * 1deg));
  position: absolute;
  pointer-events: none;
  z-index: 0;
  -webkit-mask-repeat: repeat;
          mask-repeat: repeat;
}

/* The still layer: marble and cloud. */
.surface::before {
  inset: 0;
  background: var(--surf-vein);
  opacity: var(--surf-vein-op);
  animation: surface-hue var(--surf-hue-time) ease-in-out infinite;
  -webkit-mask-image: var(--surf-tex);
          mask-image: var(--surf-tex);
  -webkit-mask-size: var(--surf-tile) var(--surf-tile);
          mask-size: var(--surf-tile) var(--surf-tile);
}

/* The drifting layer. Oversized by more than its own tile so that
   translating it can never expose an uncovered edge. */
.surface::after {
  inset: calc(var(--surf-tile) * var(--surf-scale) * -1.15);
  background: var(--surf-glow);
  mix-blend-mode: screen;
  -webkit-mask-image: var(--surf-tex2);
          mask-image: var(--surf-tex2);
  -webkit-mask-size: calc(var(--surf-tile) * var(--surf-scale))
                     calc(var(--surf-tile) * var(--surf-scale));
          mask-size: calc(var(--surf-tile) * var(--surf-scale))
                     calc(var(--surf-tile) * var(--surf-scale));
  /* Its own hue clock, deliberately not a factor of the other one, so the
     two layers are never the same colour twice in the same way. */
  animation: surface-drift 170s linear infinite,
             surface-breathe 53s ease-in-out infinite,
             surface-hue calc(var(--surf-hue-time) * .72) ease-in-out infinite;
  will-change: transform, opacity;
}

@keyframes surface-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(var(--surf-tile) * var(--surf-scale) * -1),
                                calc(var(--surf-tile) * var(--surf-scale)), 0); }
}

@keyframes surface-breathe {
  0%, 100% { opacity: calc(var(--surf-glow-op) * .55); }
  50%      { opacity: var(--surf-glow-op); }
}

/* ── variants ─────────────────────────────────────────────────────────
   One, because one is what exists. A card, a bubble and a bar were all
   guessed at here before any of them existed, and a guessed variant is
   worse than none: the next person styles against it instead of against
   the thing they are actually making. Small elements will want
   `--surf-tile` brought down — that is a property, not a class. */

/* Full-bleed backdrop. */
.surface--ground {
  position: fixed;
  inset: 0;
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .surface::after { animation: none; opacity: var(--surf-glow-op); }
}
