/* ── Mesh: 2D color nodes you sculpt directly on the light ──────────────────
   In mesh mode the preview becomes a real canvas: a taller stage with color
   nodes you drag anywhere. The node layer overlays the preview and is
   pointer-transparent except on the nodes themselves, so the compare toggle
   underneath stays clickable. */

.gs-mesh-layer {
  position: absolute;
  inset: 0;
  z-index: 3;
  /* Catches clicks on empty canvas to add a swatch; the dock/toolset (higher z)
     stay clickable, and the copy layer is pointer-events:none. */
  pointer-events: auto;
}

.gs-mesh-node {
  position: absolute;
  width: 1.7rem;
  height: 1.7rem;
  margin-top: -0.85rem;  /* centre on (mx,my) with margins — never a transform */
  margin-left: -0.85rem;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.55), inset 0 0 0 1px rgba(0, 0, 0, 0.2);
  padding: 0;
  cursor: grab;
  pointer-events: auto;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.gs-mesh-node:hover {
  border-color: #fff;
  box-shadow: 0 2px 16px rgba(0, 0, 0, 0.6), 0 0 0 5px rgba(255, 255, 255, 0.16);
}
.gs-mesh-node.is-active {
  border-color: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.95), 0 3px 16px rgba(0, 0, 0, 0.55);
}
.gs-mesh-node.is-dragging {
  cursor: grabbing;
  box-shadow: 0 0 0 3px #fff, 0 8px 26px rgba(0, 0, 0, 0.65);
}

/* Mesh edge points: the locked rectangular boundary ring (Illustrator-style).
   Smaller than the draggable colour dots and pinned to the rectangle. Auto edges
   read faint (they're just the base tone); an overridden edge shows its colour. */
.gs-mesh-edge {
  position: absolute;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: -0.575rem;
  margin-left: -0.575rem;
  border-radius: 0.3rem; /* rounded square — distinct from the round colour dots */
  border: 2px solid #fff;
  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6), inset 0 0 0 1.5px rgba(0, 0, 0, 0.45);
  padding: 0;
  cursor: pointer;
  pointer-events: auto;
  transition: transform 120ms ease, border-color 160ms ease, box-shadow 160ms ease;
}
.gs-mesh-edge:hover {
  border-color: #fff;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.65), 0 0 0 5px rgba(255, 255, 255, 0.22),
    inset 0 0 0 1.5px rgba(0, 0, 0, 0.45);
}
/* Auto edges (still on the base tone) read as a faint hollow chip so they're
   visibly "unset" without framing the stage; an overridden edge fills with its color. */
.gs-mesh-edge:not(.is-set) {
  border-color: rgba(255, 255, 255, 0.5);
  background: rgba(20, 16, 28, 0.55) !important;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.gs-mesh-edge.is-set {
  width: 1.35rem;
  height: 1.35rem;
  margin-top: -0.675rem;
  margin-left: -0.675rem;
}
.gs-mesh-edge.is-active {
  border-color: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.95), 0 2px 12px rgba(0, 0, 0, 0.6),
    inset 0 0 0 1.5px rgba(0, 0, 0, 0.45);
}

/* Mesh topology lines linking neighbouring nodes (Delaunay edges, behind pucks).
   Subtle at rest; they brighten while a node is being dragged (`.is-meshing`). */
.gs-mesh-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
  overflow: visible;
}
.gs-mesh-edges line {
  stroke: rgba(255, 255, 255, 0.32);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-dasharray: 1 6;
  transition: stroke 180ms ease, stroke-width 180ms ease, stroke-dasharray 180ms ease;
}
.gs-mesh-layer.is-meshing .gs-mesh-edges line {
  stroke: rgba(255, 255, 255, 0.85);
  stroke-width: 1.75;
  stroke-dasharray: none;
}
/* Gradient-mesh GRID lines (curved row/column patch boundaries). Kept faint so
   they hint at the lattice without competing with the colour; they brighten
   gently only while a control point is being dragged. */
.gs-mesh-edges path {
  stroke: rgba(255, 255, 255, 0.14);
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 180ms ease, stroke-width 180ms ease;
}
.gs-mesh-layer.is-meshing .gs-mesh-edges path {
  stroke: rgba(255, 255, 255, 0.45);
  stroke-width: 1.2;
}

/* Color readout chip: OKLCH + ΔE-to-nearest, for color pros (hover/focus) */
.gs-node-info {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 0.55rem);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  white-space: nowrap;
  padding: 0.4rem 0.6rem;
  border-radius: 0.55rem;
  background: rgba(8, 11, 17, 0.88);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: #fff;
  font-family: var(--mono, 'JetBrains Mono', Consolas, monospace);
  font-size: 0.62rem;
  line-height: 1.35;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  z-index: 6;
}
.gs-node-info b { font-size: 0.72rem; letter-spacing: 0.02em; }
.gs-node-info span { color: rgba(232, 240, 255, 0.72); }
.gs-mesh-node:hover .gs-node-info,
.gs-mesh-node:focus-visible .gs-node-info { opacity: 1; }

/* Resize ring on the active node: a faint circle (radius = size) + a drag knob */
.gs-node-ring {
  position: absolute;
  width: 0;
  height: 0;
  z-index: 2;
  pointer-events: none;
}
.gs-node-ring::before {
  content: '';
  position: absolute;
  width: calc(var(--ring-r, 5rem) * 2);
  height: calc(var(--ring-r, 5rem) * 2);
  margin-left: calc(var(--ring-r, 5rem) * -1);
  margin-top: calc(var(--ring-r, 5rem) * -1);
  border-radius: 50%;
  border: 1px dashed rgba(255, 255, 255, 0.32);
}
.gradient-studio .gs-node-resize {
  position: absolute;
  left: 0;
  top: 0;
  width: 0.95rem;
  height: 0.95rem;
  min-width: 0;
  min-height: 0;
  margin: -0.475rem;
  translate: var(--ring-r, 5rem) 0; /* sits on the ring's right edge */
  padding: 0;
  box-sizing: border-box;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid #fff;
  background: rgba(10, 14, 22, 0.6);
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.5);
  cursor: ew-resize;
  pointer-events: auto;
  touch-action: none;
}
.gs-hero.gs-focus .gs-node-ring { display: none; }

/* Drag-to-delete bin: hidden until a node drag starts, then it floats in at the
   bottom-right of the stage. Arms (red) when a deletable color hovers it. */
.gs-trash {
  position: absolute;
  right: 1.4rem;
  bottom: 1.4rem;
  display: grid;
  place-items: center;
  width: 3.4rem;
  height: 3.4rem;
  border-radius: 1rem;
  color: rgba(255, 255, 255, 0.82);
  background: rgba(8, 11, 17, 0.7);
  border: 1px dashed rgba(255, 255, 255, 0.28);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  opacity: 0;
  transform: translateY(0.6rem) scale(0.9);
  pointer-events: none; /* never intercepts; hit-testing is done in JS */
  transition: opacity 180ms ease, transform 180ms ease, background 160ms ease,
    border-color 160ms ease, color 160ms ease;
  z-index: 7;
}
.gs-trash.is-live {
  opacity: 1;
  transform: translateY(0) scale(1);
}
.gs-trash.is-armed {
  color: #fff;
  background: rgba(220, 38, 38, 0.9);
  border-color: #fff;
  border-style: solid;
  transform: translateY(0) scale(1.12);
  box-shadow: 0 12px 36px rgba(220, 38, 38, 0.5);
}
/* Node being dragged toward the bin: warn it's about to be removed. */
.gs-mesh-node.is-trashing {
  border-color: rgba(255, 160, 160, 0.95);
  box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.85), 0 8px 26px rgba(0, 0, 0, 0.65);
  opacity: 0.6;
}
.gs-hero.gs-focus .gs-trash { display: none; }
@media (prefers-reduced-motion: reduce) {
  .gs-trash { transition: opacity 120ms ease; transform: none; }
  .gs-trash.is-live, .gs-trash.is-armed { transform: none; }
}

/* ── On-stage geometry handles (linear angle / radial+conic origin) ──────────
   Rendered into the full-stage node layer; pointer-transparent except the
   handles. The orbit is a 0-size point at the origin that rotates to swing the
   angle puck around it on a 7rem arm. */
.gs-geo { position: absolute; inset: 0; z-index: 4; pointer-events: none; }

.gs-geo-orbit { position: absolute; width: 0; height: 0; }

.gs-geo-arm {
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 7rem;
  margin-left: -1px;
  translate: 0 -7rem;
  border-radius: 2px;
  background: linear-gradient(to top, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.12));
  pointer-events: none;
}

.gradient-studio .gs-geo-handle {
  position: absolute;
  min-width: 0;
  min-height: 0;
  width: 1.5rem;
  height: 1.5rem;
  aspect-ratio: 1;
  box-sizing: border-box;
  padding: 0;
  border-radius: 50%;
  border: 2px solid #fff;
  background: rgba(10, 14, 22, 0.55);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(0, 0, 0, 0.15);
  cursor: grab;
  pointer-events: auto;
  line-height: 0;
  touch-action: none;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}
.gradient-studio .gs-geo-handle:hover { box-shadow: 0 2px 14px rgba(0, 0, 0, 0.6), 0 0 0 5px rgba(255, 255, 255, 0.16); }
.gradient-studio .gs-geo-handle:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.gradient-studio .gs-geo-handle.is-dragging { cursor: grabbing; box-shadow: 0 0 0 3px #fff, 0 8px 26px rgba(0, 0, 0, 0.65); }

/* Angle puck sits at the end of the arm (7rem up in the orbit's local frame) */
.gradient-studio .gs-geo-handle--angle { left: 0; top: 0; margin: -0.75rem; translate: 0 -7rem; }
/* Origin puck centres on the center%; a crosshair dot marks the exact point */
.gradient-studio .gs-geo-handle--center { margin-left: -0.75rem; margin-top: -0.75rem; }
.gradient-studio .gs-geo-handle--center::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0.35rem;
  height: 0.35rem;
  border-radius: 50%;
  background: #fff;
}

/* Pucks/nodes: reset global chrome but KEEP their own ring shadow (set via the
   high-specificity rules below so they win the cascade). */
.gradient-studio .gs-mesh-node {
  min-width: 0;
  min-height: 0;
  padding: 0;
  width: 1.7rem;
  height: 1.7rem;
  aspect-ratio: 1;
  box-sizing: border-box;
  border: 2px solid rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.55), inset 0 0 0 1px rgba(0, 0, 0, 0.2);
  line-height: 0;
  touch-action: none;
  -webkit-user-drag: none;
  -webkit-user-select: none;
  user-select: none;
}
.gradient-studio .gs-mesh-node.is-active {
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.95), 0 3px 16px rgba(0, 0, 0, 0.55);
}
.gradient-studio .gs-mesh-node.is-dragging {
  box-shadow: 0 0 0 3px #fff, 0 8px 26px rgba(0, 0, 0, 0.65);
}

/* The top lattice row sits under the transparent site header, where a press
   lands on the header; its chips sit just below it. They are locked, so a chip's
   spot never moves the edge it stands for. */
.gs-mesh-edge.gs-mesh-edge--top { top: calc(var(--gs-header-clear) + 0.75rem) !important; }

/* The global button min-height stretched these squares into 44px-tall pills;
   reset it the way the nodes are reset (their hit area is the ::before below). */
.gradient-studio .gs-mesh-edge {
  min-width: 0;
  min-height: 0;
  aspect-ratio: 1;
  box-sizing: border-box;
  line-height: 0;
}

/* Hit areas: an outline is never hit-tested, so a transparent pseudo-element
   carries each handle's touch target (about 44px) without changing its look. */
.gs-mesh-node::before,
.gs-mesh-edge::before,
.gradient-studio .gs-geo-handle::before,
.gradient-studio .gs-node-resize::before {
  content: '';
  position: absolute;
  inset: -0.75rem;
  border-radius: 50%;
}
.gs-mesh-edge::before { inset: -0.8rem; }

.gradient-studio .gs-mesh-node:focus-visible,
.gs-mesh-edge:focus-visible,
.gradient-studio .gs-node-resize:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}
