@jossuealcala/madre 1.0.1 → 1.0.3

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.
Files changed (189) hide show
  1. package/ADDITIONAL-TERMS.md +93 -0
  2. package/CHANGELOG.en.md +2013 -0
  3. package/CHANGELOG.md +102 -1
  4. package/CONTRIBUTING.md +1 -1
  5. package/LICENSE +661 -202
  6. package/NOTICE +26 -14
  7. package/README.en.md +241 -0
  8. package/README.md +21 -12
  9. package/TRADEMARKS.en.md +50 -0
  10. package/TRADEMARKS.md +33 -16
  11. package/bin/madre.cjs +40 -0
  12. package/bin/madre.mjs +7 -0
  13. package/docs/INTERNALS.md +6 -0
  14. package/docs/REFERENCE.md +40 -5
  15. package/docs/SDK.md +4 -2
  16. package/docs/en/INTERNALS.md +103 -0
  17. package/docs/en/REFERENCE.md +365 -0
  18. package/docs/en/SDK.md +308 -0
  19. package/docs/en/training/README.md +59 -0
  20. package/docs/training/README.md +48 -9
  21. package/docs/training/train.sh +7 -2
  22. package/package.json +13 -5
  23. package/public/abduction.js +2 -0
  24. package/public/app.js +3154 -539
  25. package/public/attribution.js +114 -0
  26. package/public/brands.js +23 -6
  27. package/public/code-tools.js +143 -0
  28. package/public/editor.js +22 -4
  29. package/public/es.js +1004 -416
  30. package/public/hardware.js +50 -0
  31. package/public/i18n.js +25 -1
  32. package/public/index.html +18 -11
  33. package/public/inquiry.js +2 -0
  34. package/public/loader.js +2 -0
  35. package/public/macroverso.js +189 -96
  36. package/public/market-art.js +161 -0
  37. package/public/market-view.js +529 -0
  38. package/public/market.js +189 -0
  39. package/public/neural.js +110 -0
  40. package/public/nostromo-engine.js +414 -47
  41. package/public/power.js +100 -0
  42. package/public/resay.js +11 -0
  43. package/public/review-ask.js +2 -0
  44. package/public/routing.js +23 -1
  45. package/public/salas-mac.css +119 -18
  46. package/public/salas-mac.html +14 -2
  47. package/public/salas-mac.js +358 -56
  48. package/public/salas.css +2 -0
  49. package/public/salas.html +1 -0
  50. package/public/salas.js +4 -2
  51. package/public/styles.css +1793 -77
  52. package/public/syntax.js +2 -0
  53. package/public/touch.js +2 -0
  54. package/public/tour-chapters.js +2 -0
  55. package/public/tour.js +84 -5
  56. package/public/troubleshooting.js +24 -17
  57. package/public/wallpaper.js +275 -0
  58. package/src/adapters/claude.mjs +2 -0
  59. package/src/adapters/codex.mjs +2 -0
  60. package/src/adapters/cursor.mjs +96 -0
  61. package/src/adapters/gemini.mjs +13 -4
  62. package/src/adapters/madre.mjs +5 -2
  63. package/src/adapters/opencode.mjs +7 -3
  64. package/src/adapters/process.mjs +2 -0
  65. package/src/asking.mjs +2 -0
  66. package/src/auth-probe.mjs +145 -12
  67. package/src/bridge/devices.mjs +2 -0
  68. package/src/bridge/handoff.mjs +2 -0
  69. package/src/bridge/host.mjs +2 -0
  70. package/src/bridge/network.mjs +2 -0
  71. package/src/bridge/policy.mjs +3 -1
  72. package/src/bridge/qr.mjs +2 -0
  73. package/src/bridge/server.mjs +2 -0
  74. package/src/capabilities.mjs +11 -0
  75. package/src/chats.mjs +2 -0
  76. package/src/checkpoint.mjs +18 -1
  77. package/src/choices.mjs +2 -0
  78. package/src/cli-args.mjs +2 -0
  79. package/src/cold.mjs +2 -0
  80. package/src/commands.mjs +2 -0
  81. package/src/config.mjs +2 -0
  82. package/src/conversation-context.mjs +15 -1
  83. package/src/credentials.mjs +35 -3
  84. package/src/dataset.mjs +78 -4
  85. package/src/directives.mjs +2 -0
  86. package/src/distiller.mjs +2 -0
  87. package/src/embeddings.mjs +2 -0
  88. package/src/enlaces.mjs +214 -6
  89. package/src/event-store.mjs +2 -0
  90. package/src/exam.mjs +6 -4
  91. package/src/extensions.mjs +13 -3
  92. package/src/eyecat-watch.mjs +2 -0
  93. package/src/eyecat.mjs +2 -0
  94. package/src/file-ops.mjs +2 -0
  95. package/src/files.mjs +32 -0
  96. package/src/github.mjs +2 -0
  97. package/src/heartbeat.mjs +2 -0
  98. package/src/i18n.mjs +31 -1
  99. package/src/image-studio.mjs +2 -0
  100. package/src/json-file.mjs +2 -0
  101. package/src/launch.mjs +2 -0
  102. package/src/lease.mjs +5 -1
  103. package/src/lobby-work.mjs +2 -0
  104. package/src/local-ai.mjs +2 -0
  105. package/src/looks.mjs +127 -2
  106. package/src/market-key.mjs +8 -0
  107. package/src/market-remote.mjs +239 -0
  108. package/src/maturity.mjs +3 -1
  109. package/src/mcp/connect.mjs +345 -0
  110. package/src/mcp/github-server.mjs +2 -0
  111. package/src/mcp/image-server.mjs +2 -0
  112. package/src/mcp/memory-server.mjs +2 -0
  113. package/src/mcp/proxy.mjs +153 -0
  114. package/src/mcp/smtp-server.mjs +2 -0
  115. package/src/memory-dashboard.mjs +2 -0
  116. package/src/memory-tools.mjs +2 -0
  117. package/src/memory.mjs +2 -0
  118. package/src/models.mjs +49 -3
  119. package/src/module-scope.mjs +68 -0
  120. package/src/modules/ahp.mjs +104 -7
  121. package/src/modules/ash-policy.mjs +2 -0
  122. package/src/modules/ash.mjs +3 -1
  123. package/src/modules/bishop-exam.mjs +122 -0
  124. package/src/modules/bishop-plan.mjs +5 -3
  125. package/src/modules/bishop.mjs +84 -23
  126. package/src/modules/choices.mjs +2 -0
  127. package/src/modules/correo.mjs +2 -0
  128. package/src/modules/enlaces.mjs +89 -6
  129. package/src/modules/git-pulse.mjs +2 -0
  130. package/src/modules/github.mjs +2 -0
  131. package/src/modules/helpers.mjs +2 -0
  132. package/src/modules/image-studio.mjs +13 -2
  133. package/src/modules/index.mjs +4 -1
  134. package/src/modules/intents-calibration.mjs +2 -0
  135. package/src/modules/intents-desk.mjs +4 -2
  136. package/src/modules/intents-observe.mjs +2 -0
  137. package/src/modules/intents-policy.mjs +4 -1
  138. package/src/modules/intents-try.mjs +2 -0
  139. package/src/modules/intents.mjs +11 -7
  140. package/src/modules/local-models.mjs +124 -0
  141. package/src/modules/mcp-connect.mjs +293 -0
  142. package/src/modules/ollama.mjs +15 -4
  143. package/src/modules/playwright.mjs +88 -15
  144. package/src/modules/ripley.mjs +2 -0
  145. package/src/modules/runs.mjs +5 -3
  146. package/src/modules/sdk.mjs +63 -9
  147. package/src/modules/updates.mjs +2 -0
  148. package/src/mother.mjs +2 -0
  149. package/src/npm-launch.mjs +18 -0
  150. package/src/ollama.mjs +8 -5
  151. package/src/outbound.mjs +16 -6
  152. package/src/override.mjs +2 -0
  153. package/src/ports.mjs +6 -2
  154. package/src/prices.mjs +2 -0
  155. package/src/privacy.mjs +2 -0
  156. package/src/quota-monitor.mjs +2 -0
  157. package/src/quota-sources.mjs +2 -0
  158. package/src/request-guard.mjs +2 -0
  159. package/src/review-ask.mjs +2 -0
  160. package/src/room/archivist.mjs +2 -0
  161. package/src/room/attachments.mjs +2 -0
  162. package/src/room/budget.mjs +2 -0
  163. package/src/room/context.mjs +2 -0
  164. package/src/room/control.mjs +7 -1
  165. package/src/room/copilot.mjs +76 -0
  166. package/src/room/economy.mjs +2 -0
  167. package/src/room/escalation.mjs +2 -0
  168. package/src/room/ghost.mjs +2 -0
  169. package/src/room/guard.mjs +2 -0
  170. package/src/room/prompt.mjs +25 -5
  171. package/src/room/returns.mjs +82 -0
  172. package/src/room/runs.mjs +4 -0
  173. package/src/room/vectors.mjs +2 -0
  174. package/src/room.mjs +223 -35
  175. package/src/router.mjs +3 -1
  176. package/src/runs.mjs +6 -1
  177. package/src/runtime-detection.mjs +11 -0
  178. package/src/salas.mjs +28 -0
  179. package/src/sentinel-errors.mjs +2 -0
  180. package/src/server-sentinel.mjs +2 -0
  181. package/src/server.mjs +757 -81
  182. package/src/setup.mjs +3 -1
  183. package/src/smtp.mjs +2 -0
  184. package/src/tour.mjs +2 -0
  185. package/src/updates.mjs +2 -0
  186. package/src/usage-sentinel.mjs +2 -0
  187. package/src/vault.mjs +2 -0
  188. package/src/verdict.mjs +3 -1
  189. package/src/whats-new.mjs +2 -0
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // THE MACROVERSE: every room on this Mac as a NOSTROMO in open space (docs/MACROVERSO.md, phase 3a).
2
4
  // It is entered from SALAS and only on a large screen; on a phone SALAS is the list and nothing more.
3
5
  //
@@ -22,7 +24,8 @@
22
24
  // for less motion gets one still frame redrawn when something changes. No line joins two universes
23
25
  // until a link exists (phase 3c).
24
26
 
25
- import { COLLAPSED, FOCUS_VEIL, MEMORY_COLORS, PITCH_LIMIT, TURN_PER_PIXEL, alongCurve, collapsedChip, createNostromo, currentAlong, dwarfChip, hexAlpha, hexMix, magmaMap, texturesReady } from './nostromo-engine.js';
27
+ import { COLLAPSED, FOCUS_VEIL, MEMORY_COLORS, NEURAL_AXON, NEURAL_CROSSFADE, NEURAL_IGNITION, PITCH_LIMIT, TURN_PER_PIXEL, alongCurve, collapsedChip, createNostromo, currentAlong, drawNeuralBulb, dwarfChip, hexAlpha, hexMix, magmaMap, neuralGround, neuronGlow, texturesReady } from './nostromo-engine.js';
28
+ import { createStrain, inviteNeural, neuralKey, paintNeuralKey } from './neural.js';
26
29
 
27
30
  const KINDS = Object.keys(MEMORY_COLORS);
28
31
  const TILT = 0.42; // how flat the orbits read: the plane seen a little from above
@@ -327,7 +330,7 @@ function farMemories(data) {
327
330
 
328
331
  // One universe painted once: its scene placed, settled and drawn into a sprite by the engine. A
329
332
  // room that is asleep, sealed, outdated or unreachable is MOTHER alone: cold, or caged.
330
- function paintUniverse(one, dpr, shown = null) {
333
+ function paintUniverse(one, dpr, shown = null, neural = false) {
331
334
  const side = Math.min(SPRITE_MAX, Math.round(Math.max(SIDE_MIN, shown ?? one.reach * 2) * dpr));
332
335
  const css = side / dpr;
333
336
  const canvas = canvasOf(side, side);
@@ -343,6 +346,8 @@ function paintUniverse(one, dpr, shown = null) {
343
346
  }
344
347
  scene.cam = { x: 0, y: 0, scale: (css / 2 - 4) / BODY_REACH, manual: true };
345
348
  if (data?.sealed) scene.cage = { at: 0, closed: true };
349
+ // Neural mode (public/neural.js): the universe drawn as a cluster of neurons round its bulb.
350
+ scene.neural = neural;
346
351
  engine.projectNostromo();
347
352
  engine.drawNostromo(0.7);
348
353
  if (!one.room.open) {
@@ -406,7 +411,7 @@ function paintStars(seed, count, biggest, dpr) {
406
411
  return { canvas, side };
407
412
  }
408
413
 
409
- export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bff66', accentInk = '#0a1405', bishop = null, words, actions }) {
414
+ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bff66', accentInk = '#0a1405', bishop = null, words, actions, neural: neuralStart = false }) {
410
415
  const reduced = reducedMotion();
411
416
  const layer = document.createElement('section');
412
417
  layer.className = 'mv';
@@ -452,12 +457,23 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
452
457
  recenter.className = 'mother-close mv-recenter';
453
458
  recenter.textContent = words.recenter ?? 'RECENTER';
454
459
  recenter.hidden = true;
460
+ // Leaving: the room's close, a sunk track and the cross that turns (styles.css, .x-close), as
461
+ // every window closes; what it does is in its name.
462
+ const backTrack = document.createElement('span');
463
+ backTrack.className = 'surface-modes';
455
464
  const back = document.createElement('button');
456
465
  back.type = 'button';
457
- back.className = 'mother-close mv-back';
458
- back.append(words.back);
459
- back.insertAdjacentHTML('beforeend', '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>'); // trusted: our own SVG
460
- keys.append(recenter, back);
466
+ back.className = 'surface-mode close x-close x-only mv-back';
467
+ back.setAttribute('aria-label', words.back);
468
+ back.title = words.back;
469
+ back.insertAdjacentHTML('beforeend', '<span class="x-icon"><svg viewBox="0 0 20 20" focusable="false" aria-hidden="true"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>'); // trusted: our own SVG
470
+ backTrack.append(back);
471
+ // Neural mode (public/neural.js): the same space drawn as neurons, chosen here or in a project.
472
+ const neuralButton = document.createElement('button');
473
+ neuralButton.type = 'button';
474
+ neuralButton.className = 'mother-close neural-key';
475
+ neuralKey(neuralButton);
476
+ keys.append(neuralButton, recenter, backTrack);
461
477
  head.append(title, sub, legend, keys);
462
478
  const card = document.createElement('aside');
463
479
  card.className = 'mv-card';
@@ -486,6 +502,9 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
486
502
  let raf = 0;
487
503
  let last = performance.now();
488
504
  let clock = 0;
505
+ let neural = Boolean(neuralStart); // drawn as neurons (setNeuralMode)
506
+ let neuralSince = null; // when it came on, for the light running out from the centre
507
+ let fade = null; // the space as it was, fading over the new drawing
489
508
  let queued = false;
490
509
  let resizeTimer = 0;
491
510
  const drag = { active: false, moved: false, turning: false, link: null, x: 0, y: 0, x0: 0, y0: 0 };
@@ -495,6 +514,36 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
495
514
  let orb = null;
496
515
  const gaze = { x: 0, y: 0, open: 0, at: 0, jumpAt: 0, jx: 0, jy: 0, blinkAt: -1, nextBlink: 3 + Math.random() * 4, twice: false };
497
516
  let pointer = null;
517
+ // Neural mode, switched from its key or because a project switched it: the space as it was fades
518
+ // over the new drawing, the light runs out from the centre, every universe is drawn again its new
519
+ // way (a sleeping one, painted once, is painted again), and the choice is kept (actions.neural).
520
+ paintNeuralKey(neuralButton, neural);
521
+ function setNeuralMode(on) {
522
+ if (neural === on) return;
523
+ if (!reduced && canvas.width && canvas.height) {
524
+ const held = document.createElement('canvas');
525
+ held.width = canvas.width;
526
+ held.height = canvas.height;
527
+ held.getContext('2d').drawImage(canvas, 0, 0);
528
+ fade = { canvas: held, at: clock };
529
+ }
530
+ neural = on;
531
+ neuralSince = on && !reduced ? clock : null;
532
+ paintNeuralKey(neuralButton, on);
533
+ layer.querySelector('.neural-invite')?.remove();
534
+ const now = performance.now() / 1000;
535
+ for (const one of universes) {
536
+ if (!one.sprite?.scene) continue;
537
+ if (one.room.open && !reducedMotion()) one.sprite.engine.setNeural(on, now);
538
+ else one.dirty = true;
539
+ }
540
+ live?.engine.setNeural(on, now);
541
+ actions.neural?.(on);
542
+ redraw();
543
+ }
544
+ neuralButton.addEventListener('click', () => setNeuralMode(!neural));
545
+ // When this machine does not give the space its frames, the key invites to try the mode.
546
+ const strain = createStrain({ onStrain: () => { if (!neural) inviteNeural({ key: neuralButton, host: layer, map: 'projects', onTry: () => setNeuralMode(true) }); } });
498
547
  const motes = Array.from({ length: ORB_MOTES }, (_, i) => { const rnd = random(hash(`mote${i}`)); return { phase: rnd() * Math.PI * 2, reach: 1.3 + rnd() * 0.65, speed: 0.12 + rnd() * 0.2, tilt: (rnd() - 0.5) * 0.5, size: 0.35 + rnd() * 0.6 }; });
499
548
 
500
549
  function layout(all) {
@@ -593,7 +642,7 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
593
642
  const next = [focus, ...universes].find((one) => one && one.dirty);
594
643
  if (!next) return false;
595
644
  if (!next.data) { if (!next.loading?.data) void load(next); return false; }
596
- next.sprite = paintUniverse(next, view.dpr, next.shown ?? null);
645
+ next.sprite = paintUniverse(next, view.dpr, next.shown ?? null, neural);
597
646
  next.dirty = false;
598
647
  return true;
599
648
  }
@@ -648,6 +697,7 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
648
697
  }
649
698
  engine.placeNostromo();
650
699
  scene.words = words;
700
+ scene.neural = neural;
651
701
  if (data.sealed) scene.cage = { at: 0, closed: true };
652
702
  // The pointer reaches it through its own canvas, which is nowhere on the page: it answers
653
703
  // from the origin, and the pointer is moved into its frame before it is asked.
@@ -711,7 +761,9 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
711
761
  }
712
762
 
713
763
  function frame(now) {
764
+ const started = performance.now();
714
765
  advance(now);
766
+ strain.frame(performance.now() - started, now);
715
767
  raf = reduced && !camera.to ? 0 : requestAnimationFrame(frame);
716
768
  }
717
769
  // One frame's worth of everything, without asking for the next: what frame() does, and what
@@ -764,10 +816,13 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
764
816
  ctx.setTransform(view.dpr, 0, 0, view.dpr, 0, 0);
765
817
  ctx.globalCompositeOperation = 'source-over';
766
818
  ctx.globalAlpha = 1;
767
- if (nebula) ctx.drawImage(nebula, 0, 0, view.w, view.h);
819
+ const beat = reduced ? 0.4 : 0.5 + 0.5 * Math.sin(clock * 5.46);
820
+ // Neural mode: no nebula and no stars, the dark the neurons stand on.
821
+ if (neural) neuralGround(ctx, view.w, view.h, beat);
822
+ else if (nebula) ctx.drawImage(nebula, 0, 0, view.w, view.h);
768
823
  // Stars: the near ones move with the camera, the far ones barely. The tiles repeat under any
769
824
  // movement, so the space has no edge.
770
- stars.forEach((tile, depth) => {
825
+ if (!neural) stars.forEach((tile, depth) => {
771
826
  const speed = [0.04, 0.1, 0.22][depth] * camera.zoom;
772
827
  const ox = (((-camera.x * speed - clock * (reduced ? 0 : 1.5 * (depth + 1))) % tile.side) + tile.side) % tile.side;
773
828
  const oy = (((-camera.y * speed) % tile.side) + tile.side) % tile.side;
@@ -781,14 +836,13 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
781
836
  const span = Math.min(view.w, view.h * 1.7);
782
837
  ctx.lineWidth = 1;
783
838
  for (const orbit of [INNER, OUTER]) {
784
- ctx.strokeStyle = 'rgba(242, 241, 238, .11)';
839
+ ctx.strokeStyle = neural ? `rgba(${NEURAL_AXON}, .1)` : 'rgba(242, 241, 238, .11)';
785
840
  ctx.setLineDash(orbit === OUTER ? [3, 7] : []);
786
841
  ctx.beginPath();
787
842
  ctx.ellipse(centre.x, centre.y, span * orbit * camera.zoom, span * orbit * TILT * camera.zoom, 0, 0, Math.PI * 2);
788
843
  ctx.stroke();
789
844
  }
790
845
  ctx.setLineDash([]);
791
- const beat = reduced ? 0.4 : 0.5 + 0.5 * Math.sin(clock * 5.46);
792
846
  drawVeins(ctx);
793
847
  drawCentre(ctx, beat);
794
848
 
@@ -805,6 +859,12 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
805
859
  if (veil > 0.004) { ctx.fillStyle = `rgba(2, 1, 4, ${veil})`; ctx.fillRect(0, 0, view.w, view.h); }
806
860
  const top = order.find((entry) => entry.one === lifted);
807
861
  if (top) drawUniverse(ctx, top.one, top.at, beat);
862
+ // Changing the drawing: the space as it was fades over the new one.
863
+ if (fade) {
864
+ const k = (clock - fade.at) / NEURAL_CROSSFADE;
865
+ if (k >= 1 || k < 0) fade = null;
866
+ else { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalAlpha = 1 - k * (2 - k); ctx.drawImage(fade.canvas, 0, 0); ctx.restore(); }
867
+ }
808
868
  }
809
869
 
810
870
  // One universe on screen: MOTHER's warmth, the live scene or its own living canvas, its rings
@@ -828,7 +888,7 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
828
888
  const away = at.light * at.light * (0.55 + 0.45 * Math.min(1, camera.zoom));
829
889
  const reach = r * (2.3 + 0.2 * beat);
830
890
  const halo = ctx.createRadialGradient(p.x, p.y, r * 0.6, p.x, p.y, reach);
831
- halo.addColorStop(0, awake ? `rgba(226, 40, 22, ${(0.16 + 0.05 * beat) * away})` : `rgba(120, 150, 200, ${0.08 * away})`);
891
+ halo.addColorStop(0, neural ? (awake ? `rgba(${NEURAL_AXON}, ${(0.12 + 0.04 * beat) * away})` : `rgba(120, 150, 200, ${0.05 * away})`) : awake ? `rgba(226, 40, 22, ${(0.16 + 0.05 * beat) * away})` : `rgba(120, 150, 200, ${0.08 * away})`);
832
892
  halo.addColorStop(1, 'rgba(0, 0, 0, 0)');
833
893
  ctx.fillStyle = halo;
834
894
  ctx.beginPath(); ctx.arc(p.x, p.y, reach, 0, Math.PI * 2); ctx.fill();
@@ -842,8 +902,8 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
842
902
  } else {
843
903
  // Not read yet: an ember where it will be.
844
904
  const ember = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r);
845
- ember.addColorStop(0, awake ? '#ff7a3a' : '#56637a');
846
- ember.addColorStop(1, awake ? 'rgba(168, 23, 12, 0)' : 'rgba(27, 33, 48, 0)');
905
+ ember.addColorStop(0, neural ? (awake ? '#9fdcff' : '#56637a') : awake ? '#ff7a3a' : '#56637a');
906
+ ember.addColorStop(1, neural ? 'rgba(20, 60, 110, 0)' : awake ? 'rgba(168, 23, 12, 0)' : 'rgba(27, 33, 48, 0)');
847
907
  ctx.fillStyle = ember;
848
908
  ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
849
909
  }
@@ -897,85 +957,91 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
897
957
  const centre = toScreen(0, 0);
898
958
  const p = { x: centre.x, y: centre.y + float * Math.max(0.6, camera.zoom) };
899
959
  const r = 30 * clamp(camera.zoom, 0.75, 1.8);
900
- core.screen = { x: p.x, y: p.y, r: r * 1.3 };
901
- // Where it looks: at the person (the pointer) first; at the universe being carried, or the
902
- // chosen one, when the pointer is away; wandering slowly when nothing is there. A living eye
903
- // never holds still: small jumps every second or so.
904
- const seen = linking?.from ?? (focus && !focus.centre ? focus : null);
905
- let target = pointer ?? seen?.screen ?? null;
906
- if (!target && !reduced) target = { x: p.x + Math.cos(clock * 0.21) * 160, y: p.y + Math.sin(clock * 0.34) * 90 };
907
- if (!reduced && clock > gaze.jumpAt) { gaze.jumpAt = clock + 0.6 + Math.random() * 1.2; gaze.jx = (Math.random() - 0.5) * 0.07; gaze.jy = (Math.random() - 0.5) * 0.05; }
908
- const toward = target ? { x: target.x - p.x, y: target.y - p.y } : { x: 0, y: 0 };
909
- const far = Math.hypot(toward.x, toward.y) + 180;
910
- const step = reduced ? 1 : clamp((clock - gaze.at) * 6, 0, 1);
911
- gaze.at = clock;
912
- gaze.x += (clamp(toward.x / far + gaze.jx, -1, 1) - gaze.x) * step;
913
- gaze.y += (clamp(toward.y / far + gaze.jy, -1, 1) - gaze.y) * step;
914
- gaze.open += ((linking?.over === core ? 1 : hover === core ? 0.45 : 0) - gaze.open) * step;
915
- // A blink now and then, sometimes two; the lids close fast and open a little slower.
916
- if (!reduced && awake && clock > gaze.nextBlink) {
917
- gaze.blinkAt = clock;
918
- gaze.twice = !gaze.twice && Math.random() < 0.25;
919
- gaze.nextBlink = clock + (gaze.twice ? 0.32 : 4 + Math.random() * 6);
920
- }
921
- const since = clock - gaze.blinkAt;
922
- const lid = gaze.blinkAt < 0 || since > 0.26 ? 0 : since < 0.09 ? since / 0.09 : 1 - (since - 0.09) / 0.17;
923
- if (clock - orb.paintedAt >= 1 / ORB_FPS || orb.paintedAt < 0 || lid > 0) {
924
- orb.paint(reduced ? 0 : clock, { x: gaze.x, y: gaze.y, open: gaze.open, lid: clamp(lid, 0, 1) * 0.94, asleep: !awake });
925
- orb.paintedAt = clock;
926
- }
927
- const life = awake ? 1 : 0.5;
928
- // The atmosphere: violet into blue, breathing slowly.
929
- ctx.globalCompositeOperation = 'lighter';
930
- const air = ctx.createRadialGradient(p.x, p.y, r * 0.8, p.x, p.y, r * (2.7 + 0.12 * beat));
931
- air.addColorStop(0, `rgba(122, 58, 238, ${0.3 * life})`);
932
- air.addColorStop(0.45, `rgba(72, 112, 255, ${0.12 * life})`);
933
- air.addColorStop(1, 'rgba(40, 20, 120, 0)');
934
- ctx.fillStyle = air;
935
- ctx.beginPath(); ctx.arc(p.x, p.y, r * (2.7 + 0.12 * beat), 0, Math.PI * 2); ctx.fill();
936
- // The wisps, turning two ways at once, as gas does.
937
- const span = r * orb.reach * 2;
938
- for (const [speed, alpha] of [[0.05, 0.8], [-0.033, 0.5]]) {
939
- ctx.save();
940
- ctx.translate(p.x, p.y);
941
- ctx.rotate(reduced ? 0 : clock * speed);
942
- ctx.globalAlpha = alpha * life;
943
- ctx.drawImage(orb.wisps, -span / 2, -span / 2, span, span);
944
- ctx.restore();
945
- }
946
- // The motes behind it, then the sphere, then the motes in front.
947
- const drawMotes = (front) => {
948
- for (const mote of motes) {
949
- const a = mote.phase + (reduced ? 0 : clock * mote.speed);
950
- const ahead = Math.sin(a) > 0;
951
- if (ahead !== front) continue;
952
- const mx = p.x + Math.cos(a) * r * mote.reach;
953
- const my = p.y + Math.sin(a) * r * mote.reach * 0.3 + mote.tilt * r * Math.cos(a);
954
- const size = mote.size * (ahead ? 1 : 0.7) * clamp(camera.zoom, 0.7, 1.4);
955
- const glow = ctx.createRadialGradient(mx, my, 0, mx, my, size * 3);
956
- glow.addColorStop(0, `rgba(214, 206, 255, ${(ahead ? 0.6 : 0.25) * life})`);
957
- glow.addColorStop(1, 'rgba(120, 140, 255, 0)');
958
- ctx.fillStyle = glow;
959
- ctx.beginPath(); ctx.arc(mx, my, size * 3, 0, Math.PI * 2); ctx.fill();
960
+ core.screen = { x: p.x, y: p.y, r: r * (neural ? 1.8 : 1.3) };
961
+ // Neural mode: the centre of the brain, its bulb, in the Coordinator's violet; no eye, no gas.
962
+ if (neural) {
963
+ const ignite = neuralSince === null ? 1 : clamp((clock - neuralSince) / NEURAL_IGNITION, 0, 1);
964
+ drawNeuralBulb(ctx, p.x, p.y, r * 1.6, clock, { beat, light: awake ? 1 : 0.6, ignite, tone: 'bishop', still: reduced });
965
+ } else {
966
+ // Where it looks: at the person (the pointer) first; at the universe being carried, or the
967
+ // chosen one, when the pointer is away; wandering slowly when nothing is there. A living eye
968
+ // never holds still: small jumps every second or so.
969
+ const seen = linking?.from ?? (focus && !focus.centre ? focus : null);
970
+ let target = pointer ?? seen?.screen ?? null;
971
+ if (!target && !reduced) target = { x: p.x + Math.cos(clock * 0.21) * 160, y: p.y + Math.sin(clock * 0.34) * 90 };
972
+ if (!reduced && clock > gaze.jumpAt) { gaze.jumpAt = clock + 0.6 + Math.random() * 1.2; gaze.jx = (Math.random() - 0.5) * 0.07; gaze.jy = (Math.random() - 0.5) * 0.05; }
973
+ const toward = target ? { x: target.x - p.x, y: target.y - p.y } : { x: 0, y: 0 };
974
+ const far = Math.hypot(toward.x, toward.y) + 180;
975
+ const step = reduced ? 1 : clamp((clock - gaze.at) * 6, 0, 1);
976
+ gaze.at = clock;
977
+ gaze.x += (clamp(toward.x / far + gaze.jx, -1, 1) - gaze.x) * step;
978
+ gaze.y += (clamp(toward.y / far + gaze.jy, -1, 1) - gaze.y) * step;
979
+ gaze.open += ((linking?.over === core ? 1 : hover === core ? 0.45 : 0) - gaze.open) * step;
980
+ // A blink now and then, sometimes two; the lids close fast and open a little slower.
981
+ if (!reduced && awake && clock > gaze.nextBlink) {
982
+ gaze.blinkAt = clock;
983
+ gaze.twice = !gaze.twice && Math.random() < 0.25;
984
+ gaze.nextBlink = clock + (gaze.twice ? 0.32 : 4 + Math.random() * 6);
960
985
  }
961
- };
962
- drawMotes(false);
963
- ctx.globalCompositeOperation = 'source-over';
964
- ctx.globalAlpha = awake ? 1 : 0.8;
965
- ctx.drawImage(orb.canvas, p.x - r, p.y - r, r * 2, r * 2);
966
- ctx.globalAlpha = 1;
967
- // Its own thin atmosphere, brighter on the side the light comes from.
968
- ctx.globalCompositeOperation = 'lighter';
969
- const limb = ctx.createRadialGradient(p.x - r * 0.1, p.y - r * 0.12, r * 0.9, p.x, p.y, r * 1.14);
970
- limb.addColorStop(0, 'rgba(120, 150, 255, 0)');
971
- limb.addColorStop(0.55, `rgba(130, 160, 255, ${0.28 * life})`);
972
- limb.addColorStop(1, 'rgba(90, 60, 220, 0)');
973
- ctx.fillStyle = limb;
974
- ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.14, 0, Math.PI * 2); ctx.fill();
975
- ctx.globalCompositeOperation = 'source-over';
976
- ctx.globalCompositeOperation = 'lighter';
977
- drawMotes(true);
978
- ctx.globalCompositeOperation = 'source-over';
986
+ const since = clock - gaze.blinkAt;
987
+ const lid = gaze.blinkAt < 0 || since > 0.26 ? 0 : since < 0.09 ? since / 0.09 : 1 - (since - 0.09) / 0.17;
988
+ if (clock - orb.paintedAt >= 1 / ORB_FPS || orb.paintedAt < 0 || lid > 0) {
989
+ orb.paint(reduced ? 0 : clock, { x: gaze.x, y: gaze.y, open: gaze.open, lid: clamp(lid, 0, 1) * 0.94, asleep: !awake });
990
+ orb.paintedAt = clock;
991
+ }
992
+ const life = awake ? 1 : 0.5;
993
+ // The atmosphere: violet into blue, breathing slowly.
994
+ ctx.globalCompositeOperation = 'lighter';
995
+ const air = ctx.createRadialGradient(p.x, p.y, r * 0.8, p.x, p.y, r * (2.7 + 0.12 * beat));
996
+ air.addColorStop(0, `rgba(122, 58, 238, ${0.3 * life})`);
997
+ air.addColorStop(0.45, `rgba(72, 112, 255, ${0.12 * life})`);
998
+ air.addColorStop(1, 'rgba(40, 20, 120, 0)');
999
+ ctx.fillStyle = air;
1000
+ ctx.beginPath(); ctx.arc(p.x, p.y, r * (2.7 + 0.12 * beat), 0, Math.PI * 2); ctx.fill();
1001
+ // The wisps, turning two ways at once, as gas does.
1002
+ const span = r * orb.reach * 2;
1003
+ for (const [speed, alpha] of [[0.05, 0.8], [-0.033, 0.5]]) {
1004
+ ctx.save();
1005
+ ctx.translate(p.x, p.y);
1006
+ ctx.rotate(reduced ? 0 : clock * speed);
1007
+ ctx.globalAlpha = alpha * life;
1008
+ ctx.drawImage(orb.wisps, -span / 2, -span / 2, span, span);
1009
+ ctx.restore();
1010
+ }
1011
+ // The motes behind it, then the sphere, then the motes in front.
1012
+ const drawMotes = (front) => {
1013
+ for (const mote of motes) {
1014
+ const a = mote.phase + (reduced ? 0 : clock * mote.speed);
1015
+ const ahead = Math.sin(a) > 0;
1016
+ if (ahead !== front) continue;
1017
+ const mx = p.x + Math.cos(a) * r * mote.reach;
1018
+ const my = p.y + Math.sin(a) * r * mote.reach * 0.3 + mote.tilt * r * Math.cos(a);
1019
+ const size = mote.size * (ahead ? 1 : 0.7) * clamp(camera.zoom, 0.7, 1.4);
1020
+ const glow = ctx.createRadialGradient(mx, my, 0, mx, my, size * 3);
1021
+ glow.addColorStop(0, `rgba(214, 206, 255, ${(ahead ? 0.6 : 0.25) * life})`);
1022
+ glow.addColorStop(1, 'rgba(120, 140, 255, 0)');
1023
+ ctx.fillStyle = glow;
1024
+ ctx.beginPath(); ctx.arc(mx, my, size * 3, 0, Math.PI * 2); ctx.fill();
1025
+ }
1026
+ };
1027
+ drawMotes(false);
1028
+ ctx.globalCompositeOperation = 'source-over';
1029
+ ctx.globalAlpha = awake ? 1 : 0.8;
1030
+ ctx.drawImage(orb.canvas, p.x - r, p.y - r, r * 2, r * 2);
1031
+ ctx.globalAlpha = 1;
1032
+ // Its own thin atmosphere, brighter on the side the light comes from.
1033
+ ctx.globalCompositeOperation = 'lighter';
1034
+ const limb = ctx.createRadialGradient(p.x - r * 0.1, p.y - r * 0.12, r * 0.9, p.x, p.y, r * 1.14);
1035
+ limb.addColorStop(0, 'rgba(120, 150, 255, 0)');
1036
+ limb.addColorStop(0.55, `rgba(130, 160, 255, ${0.28 * life})`);
1037
+ limb.addColorStop(1, 'rgba(90, 60, 220, 0)');
1038
+ ctx.fillStyle = limb;
1039
+ ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.14, 0, Math.PI * 2); ctx.fill();
1040
+ ctx.globalCompositeOperation = 'source-over';
1041
+ ctx.globalCompositeOperation = 'lighter';
1042
+ drawMotes(true);
1043
+ ctx.globalCompositeOperation = 'source-over';
1044
+ }
979
1045
  // A ring in the accent while a universe is carried onto it, and while it waits for the person;
980
1046
  // a white one while it is pointed at or chosen.
981
1047
  const target2 = linking?.over === core;
@@ -983,14 +1049,15 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
983
1049
  ctx.strokeStyle = accent;
984
1050
  ctx.globalAlpha = target2 ? 0.95 : reduced ? 0.6 : 0.45 + 0.35 * Math.sin(clock * 2.4);
985
1051
  ctx.lineWidth = 2;
986
- ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.5, 0, Math.PI * 2); ctx.stroke();
1052
+ ctx.beginPath(); ctx.arc(p.x, p.y, r * (neural ? 2.2 : 1.5), 0, Math.PI * 2); ctx.stroke();
987
1053
  ctx.globalAlpha = 1;
988
1054
  } else if (core === hover || core === focus) {
989
1055
  ctx.strokeStyle = core === focus ? 'rgba(255, 255, 255, .85)' : 'rgba(255, 255, 255, .4)';
990
1056
  ctx.lineWidth = core === focus ? 1.4 : 1;
991
- ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.5, 0, Math.PI * 2); ctx.stroke();
1057
+ ctx.beginPath(); ctx.arc(p.x, p.y, r * (neural ? 2.2 : 1.5), 0, Math.PI * 2); ctx.stroke();
992
1058
  }
993
- const below = p.y + r * 1.62 + 8;
1059
+ // The bulb's stem reaches lower than the orb did: its name stands under the stem.
1060
+ const below = p.y + r * (neural ? 3.25 : 1.62) + 8;
994
1061
  ctx.textAlign = 'center';
995
1062
  ctx.textBaseline = 'top';
996
1063
  ctx.lineJoin = 'round';
@@ -1031,6 +1098,32 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
1031
1098
  const from = back ? pb : pa, to = back ? pa : pb;
1032
1099
  // MOTHER's red leaves the room that serves and the accent arrives where it is read; both
1033
1100
  // ways, red at both ends with the accent between them.
1101
+ // Neural mode: an axon between two clusters, in one cold line, and what crosses it a light
1102
+ // with the stretch it has just run lit behind it.
1103
+ if (neural) {
1104
+ ctx.globalCompositeOperation = 'lighter';
1105
+ ctx.strokeStyle = `rgba(${NEURAL_AXON}, ${0.18 + 0.22 * lately})`;
1106
+ ctx.lineWidth = 0.9 + 0.6 * lately;
1107
+ ctx.beginPath(); ctx.moveTo(pa.x, pa.y); ctx.quadraticCurveTo(cx, cy, pb.x, pb.y); ctx.stroke();
1108
+ vein.pulses = vein.pulses.filter((pulse) => clock - pulse.born < VEIN_LIFE);
1109
+ for (const pulse of vein.pulses) {
1110
+ const u = Math.min(1, (clock - pulse.born) / VEIN_LIFE);
1111
+ const along = (k) => alongCurve(pa.x, pa.y, cx, cy, pb.x, pb.y, pulse.back ? 1 - k : k);
1112
+ let behind = along(u);
1113
+ const head = behind;
1114
+ for (let step = 1; step <= 6; step += 1) {
1115
+ const back = along(Math.max(0, u - step * 0.03));
1116
+ ctx.strokeStyle = hexAlpha(accent, 0.5 - step * 0.07);
1117
+ ctx.lineWidth = 2 - step * 0.22;
1118
+ ctx.beginPath(); ctx.moveTo(behind.x, behind.y); ctx.lineTo(back.x, back.y); ctx.stroke();
1119
+ behind = back;
1120
+ }
1121
+ const size = (6 + 4 * Math.sin(u * Math.PI)) * Math.max(0.7, Math.min(1.4, camera.zoom));
1122
+ ctx.drawImage(neuronGlow(accent), head.x - size, head.y - size, size * 2, size * 2);
1123
+ }
1124
+ ctx.globalCompositeOperation = 'source-over';
1125
+ continue;
1126
+ }
1034
1127
  const red = '#e22816';
1035
1128
  const arriving = enlace.direction === 'both' ? red : accent;
1036
1129
  const head = reduced ? 0.5 : ((clock * (0.06 + 0.2 * recent) + vein.seed) % 1 + 1) % 1;
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
3
+ // The Market's pictures (2026-10-18): drawn here, in code, not photographed. One scene for each
4
+ // module the carousel features — what it does, shown the way a person would see it happen — and a
5
+ // poster made from the icon for every other module. No words inside the drawings, so they read the
6
+ // same in both languages; each sits on its module's own colors (market.js, `hue`), whatever the
7
+ // room's light. Every id inside a drawing is made unique, because two of them can share a page.
8
+
9
+ import { cleanSvg, moduleFace, remoteEntry } from './market.js';
10
+
11
+ let drawn = 0;
12
+ const svg = (body, { width = 560, height = 400 } = {}) => `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">${body}</svg>`;
13
+ // A soft shadow under a card, and a card itself: the glass every scene is built from.
14
+ const shadow = (id) => `<filter id="${id}" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#000" flood-opacity=".22"/></filter>`;
15
+ const bar = (x, y, w, h, fill, opacity = 1) => `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${h / 2}" fill="${fill}" fill-opacity="${opacity}"/>`;
16
+
17
+ // TEST BROWSER: a page open in a browser, the pointer pressing its button, the screenshot that
18
+ // comes back, and the console the agent reads.
19
+ function browserScene() {
20
+ const id = `mka${++drawn}`;
21
+ return svg(`<defs>${shadow(`${id}s`)}<linearGradient id="${id}h" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#bfdbfe"/><stop offset="1" stop-color="#60a5fa"/></linearGradient></defs>
22
+ <g filter="url(#${id}s)">
23
+ <rect x="60" y="44" width="440" height="296" rx="18" fill="#ffffff"/>
24
+ <path d="M78 44h404a18 18 0 0 1 18 18v22H60V62a18 18 0 0 1 18-18z" fill="#eef2f7"/>
25
+ <circle cx="84" cy="64" r="5.5" fill="#ff5f57"/><circle cx="101" cy="64" r="5.5" fill="#febc2e"/><circle cx="118" cy="64" r="5.5" fill="#28c840"/>
26
+ <rect x="150" y="54" width="260" height="20" rx="10" fill="#ffffff" stroke="#dbe3ee"/>
27
+ ${bar(166, 61, 120, 6, '#94a3b8', .7)}
28
+ ${bar(88, 108, 200, 16, '#1d4ed8', .92)}
29
+ ${bar(88, 134, 160, 9, '#94a3b8', .55)}${bar(88, 150, 186, 9, '#94a3b8', .4)}
30
+ <rect x="88" y="174" width="96" height="30" rx="15" fill="#1d4ed8"/>${bar(108, 186, 56, 6, '#ffffff', .9)}
31
+ <rect x="316" y="104" width="156" height="104" rx="14" fill="url(#${id}h)"/>
32
+ <circle cx="352" cy="136" r="12" fill="#ffffff" fill-opacity=".8"/><path d="M316 192l40-36 30 26 22-18 64 48v6a14 14 0 0 1-14 14H330a14 14 0 0 1-14-14z" fill="#1d4ed8" fill-opacity=".45"/>
33
+ <rect x="88" y="228" width="118" height="84" rx="14" fill="#f1f5f9"/><rect x="221" y="228" width="118" height="84" rx="14" fill="#f1f5f9"/><rect x="354" y="228" width="118" height="84" rx="14" fill="#f1f5f9"/>
34
+ ${bar(102, 244, 44, 8, '#1d4ed8', .55)}${bar(102, 262, 84, 6, '#94a3b8', .5)}${bar(102, 276, 66, 6, '#94a3b8', .4)}
35
+ ${bar(235, 244, 44, 8, '#1d4ed8', .55)}${bar(235, 262, 84, 6, '#94a3b8', .5)}${bar(235, 276, 66, 6, '#94a3b8', .4)}
36
+ ${bar(368, 244, 44, 8, '#1d4ed8', .55)}${bar(368, 262, 84, 6, '#94a3b8', .5)}${bar(368, 276, 66, 6, '#94a3b8', .4)}
37
+ </g>
38
+ <circle cx="160" cy="196" r="20" fill="#ffffff" fill-opacity=".35"/><circle cx="160" cy="196" r="11" fill="#ffffff" fill-opacity=".55"/>
39
+ <path d="M158 192l26 10-11 4-4 11z" fill="#0f172a" stroke="#ffffff" stroke-width="2.5" stroke-linejoin="round"/>
40
+ <g filter="url(#${id}s)" transform="rotate(-7 455 300)">
41
+ <rect x="380" y="250" width="150" height="104" rx="12" fill="#ffffff"/>
42
+ <rect x="390" y="260" width="130" height="70" rx="7" fill="#e0e7ff"/>${bar(398, 268, 60, 7, '#1d4ed8', .8)}${bar(398, 282, 44, 5, '#94a3b8', .6)}<rect x="398" y="294" width="32" height="12" rx="6" fill="#1d4ed8"/>
43
+ <rect x="466" y="268" width="46" height="34" rx="6" fill="url(#${id}h)"/>
44
+ ${bar(390, 338, 70, 6, '#94a3b8', .5)}
45
+ </g>
46
+ <circle cx="523" cy="252" r="16" fill="#22c55e" stroke="#ffffff" stroke-width="3"/><path d="M516 252l5 5 9-10" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
47
+ <g filter="url(#${id}s)">
48
+ <rect x="24" y="292" width="178" height="78" rx="14" fill="#0f172a" fill-opacity=".94"/>
49
+ ${bar(40, 310, 12, 6, '#4ade80')}${bar(58, 310, 96, 6, '#e2e8f0', .7)}
50
+ ${bar(40, 326, 12, 6, '#4ade80')}${bar(58, 326, 120, 6, '#e2e8f0', .5)}
51
+ ${bar(40, 342, 12, 6, '#60a5fa')}${bar(58, 342, 70, 6, '#e2e8f0', .35)}
52
+ </g>`);
53
+ }
54
+
55
+ // CONTINUITY: one agent hands the work to another, and the project's state stays in .ahp/, beside
56
+ // the code, with what was checked.
57
+ function handoffScene() {
58
+ const id = `mka${++drawn}`;
59
+ const row = (y, done = true) => `<circle cx="210" cy="${y}" r="11" fill="${done ? '#10b981' : '#e2e8f0'}"/>${done ? `<path d="M205 ${y}l4 4 7-8" fill="none" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>` : ''}${bar(232, y - 9, 118, 8, '#0f172a', .7)}${bar(232, y + 3, 82, 6, '#94a3b8', .5)}`;
60
+ return svg(`<defs>${shadow(`${id}s`)}
61
+ <linearGradient id="${id}a" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbbf24"/><stop offset="1" stop-color="#ea580c"/></linearGradient>
62
+ <linearGradient id="${id}b" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#818cf8"/><stop offset="1" stop-color="#7c3aed"/></linearGradient></defs>
63
+ <path d="M120 150C150 80 200 70 236 82" fill="none" stroke="#ffffff" stroke-opacity=".8" stroke-width="3" stroke-dasharray="2 10" stroke-linecap="round"/>
64
+ <path d="M376 300c40 30 70 22 92-6" fill="none" stroke="#ffffff" stroke-opacity=".8" stroke-width="3" stroke-dasharray="2 10" stroke-linecap="round"/>
65
+ <g filter="url(#${id}s)">
66
+ <path d="M176 84h66l14 16h112a20 20 0 0 1 20 20v188a20 20 0 0 1-20 20H176a20 20 0 0 1-20-20V104a20 20 0 0 1 20-20z" fill="#ffffff"/>
67
+ <rect x="182" y="118" width="70" height="22" rx="11" fill="#ecfdf5"/>
68
+ <text x="217" y="133.5" text-anchor="middle" font-family="ui-monospace, Menlo, monospace" font-size="12.5" font-weight="700" fill="#047857">.ahp/</text>
69
+ ${row(170)}${row(214)}${row(258)}${row(302, false)}
70
+ </g>
71
+ <g filter="url(#${id}s)"><circle cx="96" cy="182" r="40" fill="url(#${id}a)"/></g>
72
+ <path d="M96 164l5 12.5 13 5.5-13 5.5-5 12.5-5-12.5-13-5.5 13-5.5z" fill="#ffffff"/>
73
+ <g filter="url(#${id}s)"><circle cx="474" cy="262" r="40" fill="url(#${id}b)"/></g>
74
+ <path d="M458 262l10 10 20-22" fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
75
+ <g filter="url(#${id}s)" transform="rotate(-18 432 112)"><rect x="404" y="98" width="62" height="26" rx="13" fill="#ffffff"/>${bar(418, 108, 34, 6, '#10b981', .9)}</g>
76
+ <circle cx="470" cy="70" r="5" fill="#ffffff" fill-opacity=".7"/><circle cx="74" cy="300" r="4" fill="#ffffff" fill-opacity=".6"/><circle cx="520" cy="170" r="3" fill="#ffffff" fill-opacity=".6"/>`);
77
+ }
78
+
79
+ // IMAGES: what was asked, in a line, and what came back: the picture, with its other takes beside it.
80
+ function imagesScene() {
81
+ const id = `mka${++drawn}`;
82
+ const take = (y, stops, chosen = false) => `<g filter="url(#${id}s)"><rect x="384" y="${y}" width="128" height="68" rx="12" fill="url(#${id}${stops})"/>${chosen ? `<rect x="381" y="${y - 3}" width="134" height="74" rx="14" fill="none" stroke="#ffffff" stroke-width="3"/>` : ''}<path d="M384 ${y + 52}l30-20 22 16 18-12 58 22v2a12 12 0 0 1-12 12H396a12 12 0 0 1-12-12z" fill="#000" fill-opacity=".22"/></g>`;
83
+ return svg(`<defs>${shadow(`${id}s`)}
84
+ <linearGradient id="${id}sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fde68a"/><stop offset=".5" stop-color="#fb7185"/><stop offset="1" stop-color="#7c3aed"/></linearGradient>
85
+ <linearGradient id="${id}t1" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#a7f3d0"/><stop offset="1" stop-color="#0ea5e9"/></linearGradient>
86
+ <linearGradient id="${id}t2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fef3c7"/><stop offset="1" stop-color="#f97316"/></linearGradient>
87
+ <linearGradient id="${id}t3" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbcfe8"/><stop offset="1" stop-color="#9333ea"/></linearGradient>
88
+ <linearGradient id="${id}go" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#fb7185"/><stop offset="1" stop-color="#c026d3"/></linearGradient>
89
+ <clipPath id="${id}c"><rect x="56" y="116" width="304" height="232" rx="18"/></clipPath></defs>
90
+ <g filter="url(#${id}s)">
91
+ <rect x="56" y="36" width="456" height="54" rx="27" fill="#ffffff"/>
92
+ <circle cx="84" cy="63" r="14" fill="#fdf2f8"/><path d="M84 54l2.6 6.4 6.4 2.6-6.4 2.6-2.6 6.4-2.6-6.4-6.4-2.6 6.4-2.6z" fill="#db2777"/>
93
+ ${bar(108, 54, 170, 8, '#0f172a', .65)}${bar(108, 68, 120, 6, '#94a3b8', .55)}
94
+ <rect x="404" y="45" width="96" height="36" rx="18" fill="url(#${id}go)"/>${bar(426, 60, 52, 6, '#ffffff', .9)}
95
+ </g>
96
+ <g filter="url(#${id}s)">
97
+ <rect x="56" y="116" width="304" height="232" rx="18" fill="url(#${id}sky)"/>
98
+ <g clip-path="url(#${id}c)">
99
+ <circle cx="244" cy="206" r="40" fill="#fff7ed" fill-opacity=".92"/>
100
+ <path d="M56 300l70-70 50 44 46-40 70 56 68-46v104H56z" fill="#831843" fill-opacity=".72"/>
101
+ <path d="M56 330l92-52 64 34 58-28 90 46v18H56z" fill="#4c1d95" fill-opacity=".85"/>
102
+ </g>
103
+ <rect x="56" y="116" width="304" height="232" rx="18" fill="none" stroke="#ffffff" stroke-width="5"/>
104
+ </g>
105
+ ${take(116, 't1')}${take(198, 't2', true)}${take(280, 't3')}
106
+ <path d="M30 150l3 7.5 7.5 3-7.5 3-3 7.5-3-7.5-7.5-3 7.5-3z" fill="#ffffff" fill-opacity=".85"/>
107
+ <path d="M530 116l2.2 5.4 5.4 2.2-5.4 2.2-2.2 5.4-2.2-5.4-5.4-2.2 5.4-2.2z" fill="#ffffff" fill-opacity=".8"/>
108
+ <path d="M352 104l1.8 4.4 4.4 1.8-4.4 1.8-1.8 4.4-1.8-4.4-4.4-1.8 4.4-1.8z" fill="#ffffff" fill-opacity=".8"/>`);
109
+ }
110
+
111
+ // LOCAL AI: the computer itself thinking — a memory of linked notes on its screen — with no cloud
112
+ // above it and a lock beside it: nothing leaves.
113
+ function localScene() {
114
+ const id = `mka${++drawn}`;
115
+ const nodes = [[220, 132], [300, 112], [360, 160], [326, 214], [244, 206], [200, 172], [288, 166]];
116
+ const links = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [6, 0], [6, 1], [6, 2], [6, 3], [6, 4], [6, 5]];
117
+ const net = links.map(([a, b]) => `<line x1="${nodes[a][0]}" y1="${nodes[a][1]}" x2="${nodes[b][0]}" y2="${nodes[b][1]}" stroke="#86efac" stroke-opacity=".55" stroke-width="2"/>`).join('')
118
+ + nodes.map(([x, y], index) => (index === 6 ? `<circle cx="${x}" cy="${y}" r="22" fill="#4ade80" fill-opacity=".22"/><circle cx="${x}" cy="${y}" r="13" fill="#4ade80"/>` : `<circle cx="${x}" cy="${y}" r="7" fill="#bbf7d0"/>`)).join('');
119
+ return svg(`<defs>${shadow(`${id}s`)}<radialGradient id="${id}g" cx=".5" cy=".45" r=".6"><stop offset="0" stop-color="#14532d"/><stop offset="1" stop-color="#052e16"/></radialGradient></defs>
120
+ <g filter="url(#${id}s)">
121
+ <rect x="128" y="64" width="320" height="206" rx="16" fill="#ffffff"/>
122
+ <rect x="140" y="76" width="296" height="182" rx="9" fill="url(#${id}g)"/>
123
+ <path d="M96 278h384l-26 30H122z" fill="#ffffff"/><rect x="250" y="282" width="76" height="7" rx="3.5" fill="#cbd5e1"/>
124
+ </g>
125
+ ${net}
126
+ <g filter="url(#${id}s)">
127
+ <rect x="22" y="112" width="104" height="58" rx="12" fill="#ffffff"/>${bar(36, 128, 58, 7, '#15803d', .85)}${bar(36, 142, 76, 6, '#94a3b8', .55)}${bar(36, 154, 48, 6, '#94a3b8', .4)}
128
+ <rect x="452" y="186" width="104" height="58" rx="12" fill="#ffffff"/>${bar(466, 202, 58, 7, '#15803d', .85)}${bar(466, 216, 76, 6, '#94a3b8', .55)}${bar(466, 228, 48, 6, '#94a3b8', .4)}
129
+ </g>
130
+ <path d="M126 141c40 0 50-6 74 28" fill="none" stroke="#ffffff" stroke-opacity=".75" stroke-width="2.5" stroke-dasharray="2 8" stroke-linecap="round"/>
131
+ <path d="M452 215c-46 0-60 2-92-50" fill="none" stroke="#ffffff" stroke-opacity=".75" stroke-width="2.5" stroke-dasharray="2 8" stroke-linecap="round"/>
132
+ <g transform="translate(452 40)">
133
+ <path d="M22 50h52a20 20 0 0 0 2-40 28 28 0 0 0-52-6A22 22 0 0 0 22 50z" fill="#ffffff" fill-opacity=".9"/>
134
+ <path d="M14 8l70 52" stroke="#15803d" stroke-width="6" stroke-linecap="round"/>
135
+ </g>
136
+ <g filter="url(#${id}s)"><circle cx="80" cy="282" r="34" fill="#ffffff"/></g>
137
+ <rect x="66" y="280" width="28" height="22" rx="5" fill="#15803d"/><path d="M71 280v-6a9 9 0 0 1 18 0v6" fill="none" stroke="#15803d" stroke-width="4.5"/>
138
+ <circle cx="80" cy="290" r="3" fill="#ffffff"/>`);
139
+ }
140
+
141
+ const SCENES = { playwright: browserScene, ahp: handoffScene, 'image-studio': imagesScene, ollama: localScene };
142
+
143
+ // Every other module: its icon, large, with the cards of what it touches floating round it.
144
+ function poster(item) {
145
+ const id = `mka${++drawn}`;
146
+ const face = moduleFace(item);
147
+ return svg(`<defs>${shadow(`${id}s`)}<radialGradient id="${id}r" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#ffffff" stop-opacity=".35"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></radialGradient></defs>
148
+ <circle cx="280" cy="200" r="190" fill="url(#${id}r)"/>
149
+ <g filter="url(#${id}s)">
150
+ <rect x="58" y="74" width="138" height="72" rx="14" fill="#ffffff" fill-opacity=".92"/>${bar(74, 92, 70, 8, '#0f172a', .6)}${bar(74, 108, 100, 6, '#94a3b8', .55)}${bar(74, 122, 64, 6, '#94a3b8', .4)}
151
+ <rect x="370" y="258" width="138" height="72" rx="14" fill="#ffffff" fill-opacity=".92"/>${bar(386, 276, 70, 8, '#0f172a', .6)}${bar(386, 292, 100, 6, '#94a3b8', .55)}${bar(386, 306, 64, 6, '#94a3b8', .4)}
152
+ <rect x="200" y="120" width="160" height="160" rx="38" fill="#ffffff" fill-opacity=".2" stroke="#ffffff" stroke-opacity=".7" stroke-width="2"/>
153
+ </g>
154
+ <g transform="translate(220 140) scale(6)" color="#ffffff">${face.icon}</g>
155
+ <circle cx="420" cy="110" r="6" fill="#ffffff" fill-opacity=".7"/><circle cx="140" cy="300" r="5" fill="#ffffff" fill-opacity=".6"/>`);
156
+ }
157
+
158
+ // The picture of a module: its own scene when it has one — drawn here, or sent with the catalog
159
+ // (kept to the shapes a drawing needs, market.js) — and its poster otherwise.
160
+ export const marketArt = (item) => (SCENES[item?.id] ? SCENES[item.id]() : cleanSvg(remoteEntry(item?.id)?.art) ?? poster(item));
161
+ export const hasScene = (item) => Boolean(SCENES[item?.id] || remoteEntry(item?.id)?.art);