tink-harness 1.9.6 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tink",
3
3
  "description": "A small harness layer for Claude Code and Codex.",
4
- "version": "1.9.6",
4
+ "version": "1.9.8",
5
5
  "author": {
6
6
  "name": "dotori"
7
7
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ All notable changes to Tink are tracked here.
6
6
 
7
7
  No unreleased changes yet.
8
8
 
9
+ ## [1.9.8] - 2026-06-11
10
+
11
+ ### Added
12
+
13
+ - Galaxy-styled harness map background: a slowly rotating 4-arm spiral of 760 particles (pink-to-cyan, screen-blended), 7 colored nebula glows, a breathing core glow, and a deeper space gradient — inspired by a Three.js galaxy reference while keeping the SVG map fully interactive.
14
+ - Neural signal pulses: glowing dots travel along every edge from source to target (3.2-7.4s, staggered), so connections read like firing synapses.
15
+
16
+ ### Changed
17
+
18
+ - Signal pulses follow the map state: they hide with core mode/filters, dim in focus mode, and stay bright only on edges related to the selected node. Clicking the galaxy background clears the selection. prefers-reduced-motion disables rotation and pulses.
19
+
20
+ ## [1.9.7] - 2026-06-10
21
+
22
+ ### Added
23
+
24
+ - The harness map now looks like a planetary system: usage/evidence/score satellites orbit their harness slowly (28-63s per revolution, alternating directions) along dashed orbit rings, top harnesses get a Saturn-style ring, and a starfield with twinkling stars sits behind the graph.
25
+ - Selecting a node now dims and blurs everything unrelated (focus mode); clicking the background clears the selection and restores the detail panel.
26
+
27
+ ### Fixed
28
+
29
+ - Dragging the map no longer triggers text selection on labels and captions (user-select disabled on the map panel).
30
+
9
31
  ## [1.9.6] - 2026-06-10
10
32
 
11
33
  ### Added
package/VERSIONING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Versioning
2
2
 
3
- Current version: `1.9.6`
3
+ Current version: `1.9.8`
4
4
 
5
5
  Tink follows semver from `1.0.0` onward.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tink-harness",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
4
4
  "description": "Self-growing harnesses for Claude Code and Codex.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -618,8 +618,7 @@ function buildGraphLayout(summary) {
618
618
  glow: score >= 50 || radius >= 20
619
619
  };
620
620
  });
621
- const augmented = [...positioned];
622
- const virtualEdges = [];
621
+ const orbitSystems = [];
623
622
  for (const node of positioned.filter((item) => item.type === 'harness')) {
624
623
  const harnessId = shortLabel(node.id);
625
624
  const harness = harnessById.get(harnessId);
@@ -627,38 +626,25 @@ function buildGraphLayout(summary) {
627
626
  const uses = Number(harness.signals?.uses || 0);
628
627
  const evidenceCount = (harness.evidence_handles || []).length;
629
628
  const factorCount = (harness.candidate_score?.factors || []).length;
630
- const satellites = [
631
- ...Array.from({ length: Math.min(14, Math.ceil(uses / 2)) }, (_, index) => ({ kind: 'signal', index, total: Math.min(14, Math.ceil(uses / 2)), radius: 2.8 + Math.min(3.5, uses / 12) })),
632
- ...Array.from({ length: Math.min(6, evidenceCount) }, (_, index) => ({ kind: 'evidence', index, total: Math.min(6, evidenceCount), radius: 3.5 })),
633
- ...Array.from({ length: Math.min(5, factorCount) }, (_, index) => ({ kind: 'score', index, total: Math.min(5, factorCount), radius: 3.8 }))
634
- ];
635
- satellites.forEach((satellite, offset) => {
636
- const seed = hashString(`${node.id}:${satellite.kind}:${satellite.index}`);
637
- const angle = ((seed % 6283) / 1000) + offset * 0.45;
638
- const distance = node.radius + 24 + (seed % 42);
639
- const child = {
640
- id: `${satellite.kind}:${harnessId}:${satellite.index}`,
641
- type: satellite.kind,
642
- label: satellite.kind,
643
- weight: 1,
644
- x: clamp(node.x + Math.cos(angle) * distance, 25, 1065),
645
- y: clamp(node.y + Math.sin(angle) * distance, 25, 655),
646
- radius: satellite.radius,
647
- color: TYPE_COLORS[satellite.kind] || TYPE_COLORS.unknown,
648
- glow: satellite.kind === 'score'
649
- };
650
- augmented.push(child);
651
- virtualEdges.push({
652
- source: node.id,
653
- target: child.id,
654
- type: satellite.kind,
655
- count: 1
656
- });
657
- });
629
+ const seed = hashString(node.id);
630
+ const rings = [
631
+ { kind: 'signal', count: Math.min(10, Math.ceil(uses / 2)), distance: node.radius + 18, dotRadius: 2.4 },
632
+ { kind: 'evidence', count: Math.min(6, evidenceCount), distance: node.radius + 29, dotRadius: 2.9 },
633
+ { kind: 'score', count: Math.min(5, factorCount), distance: node.radius + 40, dotRadius: 3.2 }
634
+ ]
635
+ .filter((ring) => ring.count > 0)
636
+ .map((ring, ringIndex) => ({
637
+ ...ring,
638
+ duration: (28 + ((seed >> (ringIndex * 3)) % 36)).toFixed(0),
639
+ reverse: ringIndex % 2 === 1,
640
+ phase: ((seed >> (ringIndex * 5)) % 628) / 100
641
+ }));
642
+ if (rings.length) {
643
+ orbitSystems.push({ parentId: node.id, x: node.x, y: node.y, rings });
644
+ }
658
645
  }
659
- const byId = new Map(augmented.map((node) => [node.id, node]));
660
- const filteredEdges = getRenderableEdges(edges);
661
- const drawnEdges = [...filteredEdges, ...virtualEdges]
646
+ const byId = new Map(positioned.map((node) => [node.id, node]));
647
+ const drawnEdges = getRenderableEdges(edges)
662
648
  .map((edge) => ({
663
649
  ...edge,
664
650
  sourceNode: byId.get(edge.source),
@@ -667,15 +653,70 @@ function buildGraphLayout(summary) {
667
653
  .filter((edge) => edge.sourceNode && edge.targetNode)
668
654
  .slice(0, 240);
669
655
 
670
- return { nodes: augmented, edges: drawnEdges };
656
+ return { nodes: positioned, edges: drawnEdges, orbitSystems };
671
657
  }
672
658
 
673
659
  function renderGraphCanvas(summary, copy) {
674
- const { nodes, edges } = buildGraphLayout(summary);
660
+ const { nodes, edges, orbitSystems } = buildGraphLayout(summary);
675
661
  const strongest = nodes
676
662
  .filter((node) => node.type === 'harness')
677
663
  .sort((a, b) => Number(b.weight || 0) - Number(a.weight || 0))
678
664
  .slice(0, 8);
665
+ const stars = Array.from({ length: 110 }, (_, index) => {
666
+ const seed = hashString(`star:${index}`);
667
+ return {
668
+ x: seed % 1090,
669
+ y: (seed >> 4) % 680,
670
+ r: (0.5 + ((seed >> 8) % 12) / 10).toFixed(1),
671
+ opacity: (0.1 + ((seed >> 6) % 45) / 100).toFixed(2),
672
+ twinkle: index % 4 === 0,
673
+ delay: seed % 5000
674
+ };
675
+ });
676
+ const GALAXY = { count: 760, arms: 4, radius: 360, spin: 2.3, power: 2.4, cx: 545, cy: 340, flatten: 0.6 };
677
+ const lerpChannel = (from, to, t) => Math.round(from + (to - from) * t);
678
+ const galaxyDots = Array.from({ length: GALAXY.count }, (_, index) => {
679
+ const s1 = (hashString(`gal:${index}:a`) % 10000) / 10000;
680
+ const s2 = (hashString(`gal:${index}:b`) % 10000) / 10000;
681
+ const s3 = (hashString(`gal:${index}:c`) % 10000) / 10000;
682
+ const s4 = (hashString(`gal:${index}:d`) % 10000) / 10000;
683
+ const radius = Math.pow(s1, GALAXY.power) * GALAXY.radius;
684
+ const t = radius / GALAXY.radius;
685
+ const branchAngle = ((index % GALAXY.arms) / GALAXY.arms) * Math.PI * 2;
686
+ const spinAngle = t * GALAXY.spin;
687
+ const randomX = (s2 - 0.5) * 0.42 * radius;
688
+ const randomY = (s3 - 0.5) * 0.42 * radius;
689
+ const totalAngle = branchAngle + spinAngle;
690
+ // inside #FF66FF -> outside #66FFFF, like the reference galaxy
691
+ const color = `rgb(${lerpChannel(255, 102, t)}, ${lerpChannel(102, 255, t)}, 255)`;
692
+ return {
693
+ x: (GALAXY.cx + (Math.cos(totalAngle) * radius + randomX)).toFixed(1),
694
+ y: (GALAXY.cy + (Math.sin(totalAngle) * radius + randomY) * GALAXY.flatten).toFixed(1),
695
+ r: (0.5 + s4 * 1.1).toFixed(1),
696
+ color,
697
+ opacity: (0.12 + s4 * 0.34).toFixed(2)
698
+ };
699
+ });
700
+ const nebulae = Array.from({ length: 7 }, (_, index) => {
701
+ const seed = hashString(`nebula:${index}`);
702
+ return {
703
+ id: index,
704
+ hue: seed % 360,
705
+ x: 80 + (seed % 930),
706
+ y: 60 + ((seed >> 5) % 560),
707
+ r: 70 + ((seed >> 3) % 150),
708
+ opacity: (0.05 + ((seed >> 7) % 8) / 100).toFixed(2)
709
+ };
710
+ });
711
+ const pulses = edges.slice(0, 120).map((edge, index) => {
712
+ const seed = hashString(`pulse:${edge.source}:${edge.target}:${index}`);
713
+ return {
714
+ path: `M ${edge.sourceNode.x.toFixed(1)},${edge.sourceNode.y.toFixed(1)} L ${edge.targetNode.x.toFixed(1)},${edge.targetNode.y.toFixed(1)}`,
715
+ dur: (3.2 + (seed % 42) / 10).toFixed(1),
716
+ begin: -((seed >> 4) % 7000),
717
+ index
718
+ };
719
+ });
679
720
  const mapTitle = copy.knowledgeGraph || copy.harnessMap || 'Harness map';
680
721
  const mapEyebrow = copy.harnessMap && copy.harnessMap !== mapTitle ? copy.harnessMap : '';
681
722
  return `
@@ -700,9 +741,26 @@ function renderGraphCanvas(summary, copy) {
700
741
  </div>
701
742
  <svg class="graph-canvas" viewBox="0 0 1090 680" role="img" aria-label="Harness health graph">
702
743
  <defs>
703
- <radialGradient id="graph-bg-grad" cx="50%" cy="42%" r="75%">
704
- <stop offset="0%" style="stop-color: #16181D"/>
705
- <stop offset="100%" style="stop-color: #0C0D10"/>
744
+ <radialGradient id="graph-bg-grad" cx="50%" cy="42%" r="80%">
745
+ <stop offset="0%" style="stop-color: #0B0E1A"/>
746
+ <stop offset="60%" style="stop-color: #05060F"/>
747
+ <stop offset="100%" style="stop-color: #000005"/>
748
+ </radialGradient>
749
+ <radialGradient id="galaxy-core-grad" cx="50%" cy="50%" r="50%">
750
+ <stop offset="0%" style="stop-color: #FFFFFF; stop-opacity: 0.55"/>
751
+ <stop offset="35%" style="stop-color: #C9B8FF; stop-opacity: 0.18"/>
752
+ <stop offset="100%" style="stop-color: #C9B8FF; stop-opacity: 0"/>
753
+ </radialGradient>
754
+ ${nebulae.map((nebula) => `
755
+ <radialGradient id="nebula-grad-${nebula.id}" cx="50%" cy="50%" r="50%">
756
+ <stop offset="0%" style="stop-color: hsl(${nebula.hue}, 80%, 60%); stop-opacity: 0.6"/>
757
+ <stop offset="100%" style="stop-color: hsl(${nebula.hue}, 80%, 60%); stop-opacity: 0"/>
758
+ </radialGradient>
759
+ `).join('')}
760
+ <radialGradient id="pulse-grad" cx="50%" cy="50%" r="50%">
761
+ <stop offset="0%" style="stop-color: #FFFFFF; stop-opacity: 1"/>
762
+ <stop offset="45%" style="stop-color: #9DC4FF; stop-opacity: 0.85"/>
763
+ <stop offset="100%" style="stop-color: #5B8DEF; stop-opacity: 0"/>
706
764
  </radialGradient>
707
765
  ${Object.entries(TYPE_COLORS).map(([type, color]) => `
708
766
  <radialGradient id="node-grad-${escapeAttr(type)}" cx="32%" cy="28%" r="78%">
@@ -713,6 +771,20 @@ function renderGraphCanvas(summary, copy) {
713
771
  `).join('')}
714
772
  </defs>
715
773
  <rect class="graph-bg" width="1090" height="680" fill="url(#graph-bg-grad)"/>
774
+ <g class="starfield" aria-hidden="true">
775
+ ${stars.map((star) => `
776
+ <circle cx="${star.x}" cy="${star.y}" r="${star.r}" fill="#FFFFFF" opacity="${star.opacity}"${star.twinkle ? ` class="star-twinkle" style="--twinkle-delay: ${star.delay}ms"` : ''}/>
777
+ `).join('')}
778
+ </g>
779
+ <g class="galaxy-layer" aria-hidden="true">
780
+ ${nebulae.map((nebula) => `
781
+ <circle class="nebula" cx="${nebula.x}" cy="${nebula.y}" r="${nebula.r}" fill="url(#nebula-grad-${nebula.id})" opacity="${nebula.opacity}"/>
782
+ `).join('')}
783
+ <g class="galaxy-spiral">
784
+ ${galaxyDots.map((dot) => `<circle cx="${dot.x}" cy="${dot.y}" r="${dot.r}" fill="${dot.color}" opacity="${dot.opacity}"/>`).join('')}
785
+ </g>
786
+ <circle class="galaxy-core" cx="${GALAXY.cx}" cy="${GALAXY.cy}" r="120" fill="url(#galaxy-core-grad)"/>
787
+ </g>
716
788
  <g id="graph-viewport">
717
789
  <g class="edges">
718
790
  ${edges.map((edge, index) => `
@@ -731,6 +803,32 @@ function renderGraphCanvas(summary, copy) {
731
803
  />
732
804
  `).join('')}
733
805
  </g>
806
+ <g class="pulses" aria-hidden="true">
807
+ ${pulses.map((pulse) => `
808
+ <g class="pulse-wrap" data-pulse-index="${pulse.index}">
809
+ <circle class="edge-pulse" r="2.6" fill="url(#pulse-grad)" opacity="0">
810
+ <animateMotion dur="${pulse.dur}s" begin="${pulse.begin}ms" repeatCount="indefinite" path="${escapeAttr(pulse.path)}"/>
811
+ <animate attributeName="opacity" values="0;0.95;0.95;0" keyTimes="0;0.12;0.85;1" dur="${pulse.dur}s" begin="${pulse.begin}ms" repeatCount="indefinite"/>
812
+ </circle>
813
+ </g>
814
+ `).join('')}
815
+ </g>
816
+ <g class="orbits" aria-hidden="true">
817
+ ${orbitSystems.map((system) => `
818
+ <g class="orbit-system" data-parent="${escapeAttr(system.parentId)}" transform="translate(${system.x.toFixed(1)} ${system.y.toFixed(1)})">
819
+ ${system.rings.map((ring) => {
820
+ const dots = Array.from({ length: ring.count }, (_, dotIndex) => {
821
+ const angle = ring.phase + (dotIndex * Math.PI * 2) / ring.count;
822
+ return `<circle class="orbit-dot" cx="${(Math.cos(angle) * ring.distance).toFixed(1)}" cy="${(Math.sin(angle) * ring.distance).toFixed(1)}" r="${ring.dotRadius}" fill="url(#node-grad-${escapeAttr(ring.kind)})"/>`;
823
+ }).join('');
824
+ return `
825
+ <circle class="orbit-ring" cx="0" cy="0" r="${ring.distance.toFixed(1)}"/>
826
+ <g class="orbit-spin${ring.reverse ? ' is-reverse' : ''}" style="--orbit-dur: ${ring.duration}s">${dots}</g>
827
+ `;
828
+ }).join('')}
829
+ </g>
830
+ `).join('')}
831
+ </g>
734
832
  <g class="nodes">
735
833
  ${nodes.map((node, index) => {
736
834
  const seed = hashString(node.id);
@@ -740,6 +838,9 @@ function renderGraphCanvas(summary, copy) {
740
838
  const floatY = (((seed >> 3) % 7) - 3).toFixed(1);
741
839
  return `
742
840
  <g class="node-float" style="--float-dur: ${floatDuration}s; --float-delay: ${floatDelay}ms; --float-x: ${floatX}px; --float-y: ${floatY}px">
841
+ ${node.type === 'harness' && node.radius >= 15 ? `
842
+ <ellipse class="planet-ring" cx="${node.x.toFixed(1)}" cy="${node.y.toFixed(1)}" rx="${(node.radius * 1.75).toFixed(1)}" ry="${(node.radius * 0.5).toFixed(1)}" transform="rotate(-16 ${node.x.toFixed(1)} ${node.y.toFixed(1)})"/>
843
+ ` : ''}
743
844
  <circle
744
845
  class="graph-node ${node.type === 'harness' ? 'is-interactive' : ''}"
745
846
  style="--enter-delay: ${Math.min(index * 9, 1100)}ms"
@@ -1358,14 +1459,42 @@ function renderScript(harnesses, copy) {
1358
1459
  const setFilterStatus = (value) => {
1359
1460
  if (filterStatus) filterStatus.textContent = value;
1360
1461
  };
1462
+ const graphCanvas = document.querySelector('.graph-canvas');
1463
+ const orbitSystems = Array.from(document.querySelectorAll('.orbit-system'));
1464
+ const pulseWraps = Array.from(document.querySelectorAll('.pulse-wrap'));
1465
+ const defaultSelectedPanel = selectedPanel ? selectedPanel.innerHTML : '';
1466
+ function syncPulses() {
1467
+ const hasSelection = graphCanvas && graphCanvas.classList.contains('has-selection');
1468
+ pulseWraps.forEach((wrap) => {
1469
+ const edge = edges[Number(wrap.dataset.pulseIndex)];
1470
+ if (!edge) return;
1471
+ const hidden = edge.classList.contains('is-hidden') || edge.classList.contains('is-filtered-out');
1472
+ wrap.classList.toggle('is-hidden', hidden);
1473
+ wrap.classList.toggle('is-dimmed', Boolean(hasSelection && !edge.classList.contains('is-related')));
1474
+ });
1475
+ }
1476
+ function syncOrbits() {
1477
+ const hasSelection = graphCanvas && graphCanvas.classList.contains('has-selection');
1478
+ orbitSystems.forEach((system) => {
1479
+ const parent = nodeById(system.dataset.parent);
1480
+ const hidden = !parent || parent.classList.contains('is-hidden') || parent.classList.contains('is-filtered-out');
1481
+ system.classList.toggle('is-hidden', hidden);
1482
+ const related = parent && (parent.classList.contains('is-selected') || parent.classList.contains('is-related'));
1483
+ system.classList.toggle('is-dimmed', Boolean(hasSelection && !related));
1484
+ });
1485
+ syncPulses();
1486
+ }
1361
1487
  function clearSelection() {
1362
1488
  nodes.forEach((item) => item.classList.remove('is-selected', 'is-related'));
1363
1489
  edges.forEach((item) => item.classList.remove('is-related'));
1364
1490
  cards.forEach((item) => item.classList.remove('is-selected'));
1491
+ if (graphCanvas) graphCanvas.classList.remove('has-selection');
1492
+ syncOrbits();
1365
1493
  }
1366
1494
  function selectNode(node) {
1367
1495
  clearSelection();
1368
1496
  node.classList.add('is-selected');
1497
+ if (graphCanvas) graphCanvas.classList.add('has-selection');
1369
1498
  const id = node.dataset.nodeId;
1370
1499
  const item = byHarnessId.get(id);
1371
1500
  edges.forEach((edge) => {
@@ -1393,6 +1522,7 @@ function renderScript(harnesses, copy) {
1393
1522
  } else if (selectedPanel) {
1394
1523
  selectedPanel.innerHTML = '<p class="eyebrow">' + esc(copy.selected) + '</p><h2>' + esc(node.dataset.nodeLabel) + '</h2><p>' + esc(id) + '</p><dl><div><dt>' + esc(copy.type) + '</dt><dd>' + esc(displayValue(node.dataset.nodeType)) + '</dd></div><div><dt>' + esc(copy.weight) + '</dt><dd>' + esc(node.dataset.nodeWeight) + '</dd></div></dl>';
1395
1524
  }
1525
+ syncOrbits();
1396
1526
  }
1397
1527
  function selectHarness(id) {
1398
1528
  const node = nodeById('harness:' + id);
@@ -1410,7 +1540,9 @@ function renderScript(harnesses, copy) {
1410
1540
  nodes.forEach((node) => node.classList.toggle('is-hidden', mode === 'core' && node.dataset.core !== 'true'));
1411
1541
  const visibleIds = new Set(nodes.filter((node) => !node.classList.contains('is-hidden')).map((node) => node.dataset.nodeId));
1412
1542
  edges.forEach((edge) => edge.classList.toggle('is-hidden', mode === 'core' && (!visibleIds.has(edge.dataset.source) || !visibleIds.has(edge.dataset.target))));
1543
+ document.querySelectorAll('.orbit-system').forEach((system) => system.classList.toggle('is-mode-hidden', mode === 'core'));
1413
1544
  setStatus(mode === 'core' ? copy.coreMode : copy.showingAll);
1545
+ syncOrbits();
1414
1546
  }
1415
1547
  function filterRecommendation(value, button) {
1416
1548
  const alreadyActive = button.classList.contains('active-filter');
@@ -1424,6 +1556,7 @@ function renderScript(harnesses, copy) {
1424
1556
  cards.forEach((card) => card.classList.remove('is-filtered-out'));
1425
1557
  setStatus(copy.showingAll);
1426
1558
  setFilterStatus(copy.showingAll);
1559
+ syncOrbits();
1427
1560
  return;
1428
1561
  }
1429
1562
  button.classList.add('active-filter');
@@ -1438,6 +1571,7 @@ function renderScript(harnesses, copy) {
1438
1571
  const label = recLabelByFilter[value] || value;
1439
1572
  setStatus(copy.filteredTo + ': ' + label);
1440
1573
  setFilterStatus(copy.filteredTo + ': ' + label);
1574
+ syncOrbits();
1441
1575
  }
1442
1576
  interactiveNodes.forEach((node) => {
1443
1577
  node.addEventListener('click', () => selectNode(node));
@@ -1520,12 +1654,24 @@ function renderScript(harnesses, copy) {
1520
1654
  panState.y = event.clientY;
1521
1655
  applyView();
1522
1656
  });
1657
+ let suppressClick = false;
1523
1658
  const endPan = () => {
1659
+ suppressClick = Boolean(panState && panState.moved);
1524
1660
  panState = null;
1525
1661
  graphSvg.classList.remove('is-panning');
1526
1662
  };
1527
1663
  graphSvg.addEventListener('pointerup', endPan);
1528
1664
  graphSvg.addEventListener('pointercancel', endPan);
1665
+ graphSvg.addEventListener('click', (event) => {
1666
+ if (suppressClick) {
1667
+ suppressClick = false;
1668
+ return;
1669
+ }
1670
+ if (event.target.classList.contains('graph-bg') || event.target.closest('.starfield') || event.target.closest('.galaxy-layer')) {
1671
+ clearSelection();
1672
+ if (selectedPanel && defaultSelectedPanel) selectedPanel.innerHTML = defaultSelectedPanel;
1673
+ }
1674
+ });
1529
1675
  graphSvg.addEventListener('dblclick', (event) => {
1530
1676
  event.preventDefault();
1531
1677
  resetView();
@@ -2193,6 +2339,103 @@ function renderStyles() {
2193
2339
  transition: opacity 160ms ease, transform 160ms ease;
2194
2340
  }
2195
2341
 
2342
+ .map-panel,
2343
+ .graph-canvas,
2344
+ .graph-canvas text {
2345
+ user-select: none;
2346
+ -webkit-user-select: none;
2347
+ -webkit-user-drag: none;
2348
+ }
2349
+
2350
+ .star-twinkle {
2351
+ animation: star-twinkle 3.4s ease-in-out var(--twinkle-delay, 0ms) infinite alternate;
2352
+ }
2353
+
2354
+ @keyframes star-twinkle {
2355
+ from { opacity: 0.08; }
2356
+ to { opacity: 0.6; }
2357
+ }
2358
+
2359
+ .galaxy-layer { pointer-events: none; }
2360
+
2361
+ .nebula { mix-blend-mode: screen; }
2362
+
2363
+ .galaxy-spiral {
2364
+ transform-origin: 545px 340px;
2365
+ transform-box: view-box;
2366
+ animation: galaxy-rotate 420s linear infinite;
2367
+ mix-blend-mode: screen;
2368
+ }
2369
+
2370
+ @keyframes galaxy-rotate {
2371
+ to { transform: rotate(360deg); }
2372
+ }
2373
+
2374
+ .galaxy-core {
2375
+ mix-blend-mode: screen;
2376
+ animation: core-breathe 9s ease-in-out infinite alternate;
2377
+ }
2378
+
2379
+ @keyframes core-breathe {
2380
+ from { opacity: 0.5; }
2381
+ to { opacity: 0.9; }
2382
+ }
2383
+
2384
+ .pulse-wrap { transition: opacity 260ms ease; }
2385
+ .pulse-wrap.is-hidden { opacity: 0; }
2386
+ .pulse-wrap.is-dimmed { opacity: 0.08; }
2387
+
2388
+ .edge-pulse {
2389
+ filter: drop-shadow(0 0 4px rgba(157, 196, 255, 0.9));
2390
+ }
2391
+
2392
+ .orbit-ring {
2393
+ fill: none;
2394
+ stroke: var(--text-secondary);
2395
+ stroke-opacity: 0.14;
2396
+ stroke-width: 0.7;
2397
+ stroke-dasharray: 2 5;
2398
+ }
2399
+
2400
+ .orbit-spin {
2401
+ animation: orbit-rotate var(--orbit-dur, 40s) linear infinite;
2402
+ }
2403
+
2404
+ .orbit-spin.is-reverse { animation-direction: reverse; }
2405
+
2406
+ @keyframes orbit-rotate {
2407
+ to { transform: rotate(360deg); }
2408
+ }
2409
+
2410
+ .orbit-dot { opacity: 0.85; }
2411
+
2412
+ .orbit-system {
2413
+ transition: opacity 260ms ease;
2414
+ }
2415
+
2416
+ .orbit-system.is-hidden,
2417
+ .orbit-system.is-mode-hidden { opacity: 0; pointer-events: none; }
2418
+ .orbit-system.is-dimmed { opacity: 0.08; }
2419
+
2420
+ .planet-ring {
2421
+ fill: none;
2422
+ stroke: var(--text-secondary);
2423
+ stroke-opacity: 0.4;
2424
+ stroke-width: 1.4;
2425
+ pointer-events: none;
2426
+ }
2427
+
2428
+ .graph-canvas.has-selection .graph-node:not(.is-selected):not(.is-related) {
2429
+ opacity: 0.14;
2430
+ filter: blur(1.2px);
2431
+ }
2432
+
2433
+ .graph-canvas.has-selection .graph-edge:not(.is-related) { opacity: 0.05; }
2434
+
2435
+ .graph-canvas.has-selection .planet-ring { stroke-opacity: 0.1; }
2436
+
2437
+ .graph-canvas.has-selection .labels text { opacity: 0.18; transition: opacity 220ms ease; }
2438
+
2196
2439
  .map-controls-row {
2197
2440
  display: flex;
2198
2441
  gap: var(--space-2);
@@ -2300,8 +2543,13 @@ function renderStyles() {
2300
2543
  .graph-node,
2301
2544
  .graph-edge,
2302
2545
  .graph-canvas text,
2546
+ .orbit-spin,
2547
+ .star-twinkle,
2548
+ .galaxy-spiral,
2549
+ .galaxy-core,
2303
2550
  .page.is-active { animation: none; }
2304
2551
  .graph-node { transition: none; }
2552
+ .pulses { display: none; }
2305
2553
  }
2306
2554
 
2307
2555
  .map-caption {