﻿/* ============================================================
   DOT MATRIX — Site-wide decorative component
   ------------------------------------------------------------
   A reusable dot-matrix texture for hero sections, CTA bands
   and feature highlights. CSS-first: the ambient drift and
   opacity breathing are pure keyframes. JavaScript adds only a
   desktop pointer-proximity interaction (see dot-matrix.js).

   USAGE
     <div class="dot-matrix" aria-hidden="true" data-dot-matrix>
       <div class="dot-matrix__field"></div>
       <div class="dot-matrix__ripple"></div>
     </div>

   COLOR
     Reads --dm-color-rgb (R, G, B). Falls back through
     --product-accent-rgb (the product token system) to the
     house brand emerald. Every product page gets its own colour
     without per-product CSS.

   POINTER INTERACTION
     The ripple layer renders the SAME dot pattern at higher
     opacity and slightly larger size, revealed through a mask
     that tracks the cursor. Nearby dots grow and brighten;
     distant dots are unchanged. No spotlight, no flashlight.

   PERFORMANCE
     Zero canvas, zero SVG, zero DOM inflation. The field is one
     element with a CSS radial-gradient. Both animations use only
     transform and opacity — compositor-only, no repaints.
   ============================================================ */

/* ---------- The container ---------- */
.dot-matrix {
  --dm-color-rgb: var(--product-accent-rgb, 10, 129, 88);
  --dm-dot-size: 1.6px;
  --dm-pitch: 16px;
  --dm-opacity: 0.32;

  position: absolute;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  will-change: opacity, transform;
}

/* ---------- The dot field ----------
   ~1.6px dots on a 16px pitch via radial-gradient. The inset is
   negative by one pitch so the drift animation never exposes an
   empty edge (same technique as the page-hero dither). */
.dot-matrix__field {
  position: absolute;
  inset: calc(var(--dm-pitch) * -1);
  background-image: radial-gradient(
    circle,
    rgba(var(--dm-color-rgb), var(--dm-opacity)) var(--dm-dot-size),
    transparent calc(var(--dm-dot-size) + 0.1px)
  );
  background-size: var(--dm-pitch) var(--dm-pitch);

  /* Radial dissolve mask — the field fades from full in the
     centre to invisible at the edge. */
  mask-image: radial-gradient(closest-side, #000 25%, transparent 76%);
  -webkit-mask-image: radial-gradient(closest-side, #000 25%, transparent 76%);

  /* Ambient movement: two independent keyframes at different
     durations so they never synchronise. Drift is exactly one
     pitch per axis — seamless because the pattern is periodic. */
  animation:
    dmDrift 20s ease-in-out infinite alternate,
    dmBreathe 22s ease-in-out infinite alternate;
  will-change: transform, opacity;
}

/* ---------- The ripple layer — DOT REACTION, NOT SPOTLIGHT --
   Renders the SAME dot pattern as the field but with slightly
   larger dots (1.8px vs 1.6px) at higher opacity. A radial mask
   reveals this brighter layer only within --dm-influence pixels
   of the cursor position.

   The visual result: individual dots near the cursor appear to
   grow and brighten, while dots farther away are completely
   unchanged. No plain gradient, no flashlight, no background
   wash. The dots themselves react.

   The mask position is driven by --dm-x / --dm-y (set by JS on
   pointermove). The default position (-999px) keeps the mask
   entirely off-screen, so the layer is invisible until the
   cursor enters. .is-near controls the layer's overall opacity
   for a smooth fade in/out.

   Influence radius and active values are tuned per mode below. */
.dot-matrix__ripple {
  --dm-influence: 100px;
  --dm-active-opacity: 0.55;
  --dm-active-size: 1.8px;

  position: absolute;
  inset: calc(var(--dm-pitch) * -1);
  background-image: radial-gradient(
    circle,
    rgba(var(--dm-color-rgb), var(--dm-active-opacity)) var(--dm-active-size),
    transparent calc(var(--dm-active-size) + 0.1px)
  );
  background-size: var(--dm-pitch) var(--dm-pitch);
  opacity: 0;
  transition: opacity 500ms ease;
  pointer-events: none;

  /* Mask reveals brighter dots only near the cursor.
     The soft edge (30% solid, fading to 70% transparent) creates
     natural distance falloff: dots at the centre are fully
     brightened, dots at the edge are barely affected, dots
     outside are unchanged. */
  mask-image: radial-gradient(
    circle var(--dm-influence) at var(--dm-x, -999px) var(--dm-y, -999px),
    #000 0%, #000 30%, transparent 70%
  );
  -webkit-mask-image: radial-gradient(
    circle var(--dm-influence) at var(--dm-x, -999px) var(--dm-y, -999px),
    #000 0%, #000 30%, transparent 70%
  );
  will-change: opacity;
}

/* When the cursor is near, fade in the brighter dot layer. */
.dot-matrix.is-near .dot-matrix__ripple {
  opacity: 1;
}

/* ---------- Full-bleed coverage ----------
   Fills the entire parent section as a subtle background
   texture. The parent must have position: relative and
   overflow: hidden (product-hero, page-hero, cta-band all do).

   Opacity drops dramatically compared to clusters: at full
   coverage the dots are a whisper of texture, not a pattern.
   The radial dissolve mask is replaced with a gentle linear
   fade at top/bottom edges only.

   The pointer interaction: no scale (would shift edges), just
   the per-dot brightening from the ripple mask. Wider influence
   radius for full-bleed sections. */
.dot-matrix--full {
  inset: 0;
  width: auto;
  height: auto;
  --dm-opacity: 0.045;
}

/* Override mask for full-bleed: gentle linear fade at top and
   bottom edges. The parent overflow: hidden provides clipping. */
.dot-matrix--full .dot-matrix__field {
  mask-image: linear-gradient(
    to bottom,
    transparent 0%, #000 6%, #000 94%, transparent 100%
  );
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0%, #000 6%, #000 94%, transparent 100%
  );
}

/* Full-bleed ripple: wider influence radius, gentler contrast.
   No scale on the container — just per-dot brightening. */
.dot-matrix--full .dot-matrix__ripple {
  --dm-influence: 140px;
  --dm-active-opacity: 0.12;
  --dm-active-size: 1.8px;
}

/* Full-bleed ink variant — white dots on dark surfaces need
   even lower opacity at full coverage. */
.dot-matrix--full.dot-matrix--ink {
  --dm-opacity: 0.035;
}

.dot-matrix--full.dot-matrix--ink .dot-matrix__ripple {
  --dm-active-opacity: 0.09;
}

/* ---------- Cluster position modifiers ----------
   Named placements for small dot clusters. These keep the
   original radial dissolve mask from the base .dot-matrix__field
   rule. Use these for selective placement in corners. */

/* Top-right cluster. */
.dot-matrix--top-right {
  top: 2%;
  right: 0;
  width: 280px;
  height: 200px;
}

/* Bottom-left cluster. */
.dot-matrix--bottom-left {
  bottom: 8%;
  left: -1%;
  width: 220px;
  height: 160px;
}

/* Centre field — wide CTA bands and feature highlights. */
.dot-matrix--center {
  top: 50%;
  left: 50%;
  width: 60%;
  height: 80%;
  transform: translate(-50%, -50%);
}

/* Smaller cluster for tighter spaces. */
.dot-matrix--sm {
  width: 160px;
  height: 120px;
}

/* ---------- Ink (dark surface) variant ----------
   White dots at low opacity for dark backgrounds. */
.dot-matrix--ink {
  --dm-color-rgb: 255, 255, 255;
  --dm-opacity: 0.08;
}

/* ---------- Responsive ----------
   Below 980px the dot clusters hide entirely. */
@media (max-width: 980px) {
  .dot-matrix {
    display: none;
  }
}

/* ---------- Pointer gate ----------
   Touch devices get the ambient CSS animation only — no JS
   touch tracking, no battery cost. */
@media (pointer: coarse) {
  .dot-matrix__ripple {
    display: none;
  }
}

/* ---------- Accessibility ----------
   prefers-reduced-motion freezes everything. The dots still
   show as a static texture — only movement and interaction
   are disabled. */
@media (prefers-reduced-motion: reduce) {
  .dot-matrix__field {
    animation: none;
    opacity: 0.85;
  }

  .dot-matrix__ripple {
    display: none;
  }

  .dot-matrix {
    transition: none;
  }
}

/* ---------- Print ---------- */
@media print {
  .dot-matrix {
    display: none !important;
  }
}

/* ---------- Keyframes ----------
   Both use compositor-only properties (transform, opacity).

   dmDrift: exactly one pitch per axis. The background is
   periodic at var(--dm-pitch), so the loop has no visible seam.

   dmBreathe: 0.75 -> 1 opacity over 22s. The eye cannot compare
   two brightnesses 11 seconds apart, so this reads as "the
   field is there" rather than "the field is pulsing". */
@keyframes dmDrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(16px, 16px, 0); }
}

@keyframes dmBreathe {
  from { opacity: 0.75; }
  to   { opacity: 1; }
}
