axstack 0.36.1 → 0.38.0

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 (137) hide show
  1. package/README.md +5 -4
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/host-operations.md +9 -5
  5. package/docs/installation.md +20 -41
  6. package/docs/workflows.md +12 -9
  7. package/package.json +1 -1
  8. package/profiles/presets/claude-only.json +3 -3
  9. package/profiles/presets/codex-only.json +3 -3
  10. package/profiles/presets/mixed.json +5 -4
  11. package/skills/axstack/references/automations.md +7 -2
  12. package/skills/axstack/references/lifecycle.md +2 -1
  13. package/skills/axstack/references/review-manager-prompt.md +44 -3
  14. package/skills/axstack/references/role-roster.md +13 -6
  15. package/skills/axstack/references/routing.md +2 -1
  16. package/skills/axstack/references/run-record.md +2 -0
  17. package/skills/axstack/references/ui-verification.md +6 -10
  18. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  19. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  20. package/skills/axstack-anatomy/LICENSE +21 -0
  21. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  22. package/skills/axstack-anatomy/README.md +316 -0
  23. package/skills/axstack-anatomy/SKILL.md +74 -0
  24. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  25. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  26. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  27. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  28. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  29. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  30. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  31. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  32. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  33. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  34. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  35. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  36. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  37. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  38. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  41. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  42. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  43. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  44. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  45. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  46. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  47. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  48. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  49. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  50. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  51. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  52. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  53. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  54. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  55. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  56. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  57. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  58. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  59. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  60. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  61. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  62. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  63. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  64. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  65. package/skills/axstack-anatomy/kit/iso.css +106 -0
  66. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  67. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  68. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  69. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  70. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  71. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  72. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  73. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  74. package/skills/axstack-anatomy/references/3d.md +352 -0
  75. package/skills/axstack-anatomy/references/craft.md +325 -0
  76. package/skills/axstack-anatomy/references/kit.md +419 -0
  77. package/skills/axstack-anatomy/references/motion.md +165 -0
  78. package/skills/axstack-anatomy/references/react.md +42 -0
  79. package/skills/axstack-anatomy/references/verify.md +351 -0
  80. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  81. package/skills/axstack-anatomy/references/webgl.md +473 -0
  82. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  83. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  84. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  85. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  86. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  87. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  88. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  89. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  90. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  91. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  92. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  93. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  94. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  95. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  96. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  97. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  98. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  99. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  100. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  101. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  102. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  103. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  104. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  105. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  106. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  107. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  108. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  109. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  110. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  111. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  113. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  114. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  115. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  116. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  117. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  118. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  119. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  120. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  121. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  122. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  123. package/skills/axstack-anatomy/theme.js +56 -0
  124. package/skills/axstack-explain/SKILL.md +28 -15
  125. package/skills/axstack-explain/references/delivery.md +22 -0
  126. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  127. package/skills/axstack-explain/references/inline-pages.md +8 -8
  128. package/skills/axstack-explain/references/visual-qa.md +3 -5
  129. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  130. package/skills/axstack-research/SKILL.md +18 -6
  131. package/src/archify-legacy.js +70 -0
  132. package/src/capabilities.js +1 -29
  133. package/src/installer.js +11 -31
  134. package/skills/axstack-diagram/SKILL.md +0 -28
  135. package/skills/axstack-diagram/references/archify.md +0 -76
  136. package/src/archify-pin.js +0 -5
  137. package/src/archify.js +0 -151
@@ -0,0 +1,114 @@
1
+ export const SCREEN = (() => {
2
+ const W = 224;
3
+ const H = 288;
4
+ const FONT = {
5
+ A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
6
+ C: ["01110", "10001", "10000", "10000", "10000", "10001", "01110"],
7
+ D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
8
+ E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
9
+ F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
10
+ H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
11
+ I: ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
12
+ K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
13
+ L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
14
+ M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
15
+ N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
16
+ O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
17
+ P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
18
+ R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
19
+ S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
20
+ T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
21
+ U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
22
+ Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
23
+ Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
24
+ 0: ["01110", "10011", "10101", "10101", "10101", "11001", "01110"],
25
+ 1: ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
26
+ 2: ["01110", "10001", "00001", "00110", "01000", "10000", "11111"],
27
+ 3: ["11111", "00010", "00100", "00010", "00001", "10001", "01110"],
28
+ 4: ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
29
+ 5: ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
30
+ 6: ["00110", "01000", "10000", "11110", "10001", "10001", "01110"],
31
+ 7: ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
32
+ 8: ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
33
+ 9: ["01110", "10001", "10001", "01111", "00001", "00010", "01100"],
34
+ "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
35
+ ".": ["00000", "00000", "00000", "00000", "00000", "01100", "01100"],
36
+ " ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"],
37
+ };
38
+ const SPRITES = {
39
+ jellyA: ["0001111000", "0011111100", "0110110110", "1111111111", "1111111111", "0010110100", "0100000010", "0010000100"],
40
+ jellyB: ["0001111000", "0011111100", "0110110110", "1111111111", "1111111111", "0010110100", "0001001000", "0010000100"],
41
+ eyeA: ["0011110000", "0111111000", "1101101100", "1111111100", "0110011000", "1100001100"].map((r) => r.slice(0, 8)),
42
+ eyeB: ["0011110000", "0111111000", "1101101100", "1111111100", "0110011000", "0011110000"].map((r) => r.slice(0, 8)),
43
+ ship: ["00000100000", "00001110000", "00001110000", "01111111110", "11111111111", "11011111011", "10000100001"],
44
+ boom: ["1000100010", "0100100100", "0010001000", "1100000011", "0010001000", "0100100100", "1000100010"],
45
+ };
46
+ const n = (v) => Math.round(v * 100) / 100;
47
+ function bitmap(rows, x, y, s = 1) {
48
+ let d = "";
49
+ rows.forEach((row, r) => {
50
+ let c = 0;
51
+ while (c < row.length) {
52
+ if (row[c] === "1") {
53
+ let e = c;
54
+ while (e < row.length && row[e] === "1") e++;
55
+ d += `M${n(x + c * s)} ${n(y + r * s)}h${n((e - c) * s)}v${n(s)}h${n(-(e - c) * s)}z`;
56
+ c = e;
57
+ } else c++;
58
+ }
59
+ });
60
+ return d;
61
+ }
62
+ function text(str, x, y, s = 1, adv = 8) {
63
+ return [...String(str)].map((ch, i) => bitmap(FONT[ch] ?? FONT[" "], x + i * adv * s, y, s)).join("");
64
+ }
65
+ const centred = (str, s = 1, adv = 8) => n((W - (String(str).length * adv - (adv - 5)) * s) / 2);
66
+
67
+ const FORMATION = [];
68
+ for (let row = 0; row < 3; row++) for (let col = 0; col < 6; col++) FORMATION.push({ row, col, x: 34 + col * 26, y: 92 + row * 18, kind: row === 0 ? "eye" : "jelly" });
69
+ const SHIP_Y = 250;
70
+ const px = (d, extra = "") => `<path class="ac-px"${extra} d="${d}"/>`;
71
+
72
+ function svg() {
73
+ const out = [];
74
+ const hatch = [];
75
+ for (let x = 0; x <= W; x += 16) hatch.push(`M${Math.min(x, W - 1)} 0h1v${H}h-1z`);
76
+ for (let y = 0; y <= H; y += 16) hatch.push(`M0 ${Math.min(y, H - 1)}h${W}v1h${-W}z`);
77
+ out.push(`<g data-mode="test" style="display:none">`);
78
+ out.push(`<path class="ac-px ac-dim" d="${hatch.join("")}"/>`);
79
+ out.push(px(text("SELF TEST", centred("SELF TEST"), 56), ` data-step="0"`));
80
+ out.push(px(text("RAM OK", 56, 104), ` data-step="1"`));
81
+ out.push(px(text("ROM OK", 56, 120), ` data-step="2"`));
82
+ out.push(px(text("Z80 3.072 MHZ", 56, 152), ` data-step="3"`));
83
+ out.push(`</g>`);
84
+ out.push(`<g data-mode="attract" style="display:none">`);
85
+ out.push(px(text("1UP", 24, 8) + text("HI-SCORE", 80, 8) + text("2UP", 176, 8)));
86
+ out.push(px(text("00000", 16, 20), ` data-part="score"`));
87
+ out.push(px(text("12500", 92, 20)));
88
+ out.push(px(text("ANATOMY", centred("ANATOMY", 2), 44, 2)));
89
+ out.push(`<g data-part="formation">`);
90
+ FORMATION.forEach((a, i) => {
91
+ const [fa, fb] = a.kind === "eye" ? [SPRITES.eyeA, SPRITES.eyeB] : [SPRITES.jellyA, SPRITES.jellyB];
92
+ const ox = a.kind === "eye" ? 1 : 0;
93
+ out.push(`<g data-alien="${i}">${px(bitmap(fa, a.x + ox, a.y), ' data-frame="a"')}${px(bitmap(fb, a.x + ox, a.y), ' data-frame="b" style="display:none"')}</g>`);
94
+ });
95
+ out.push(`</g>`);
96
+ out.push(px(bitmap(SPRITES.boom, 0, 0), ` data-part="boom" style="display:none"`));
97
+ out.push(px("M0 0h1v5h-1z", ` data-part="shot" style="display:none"`));
98
+ out.push(px(bitmap(SPRITES.ship, -5.5, 0), ` data-part="ship" transform="translate(112 ${SHIP_Y})"`));
99
+ out.push(px(`M8 ${SHIP_Y + 12}h${W - 16}v1h${-(W - 16)}z`));
100
+ out.push(px(text("INSERT COIN", centred("INSERT COIN"), 200), ` data-part="insert"`));
101
+ out.push(px(text("PUSH START", centred("PUSH START"), 200), ` data-part="start" style="display:none"`));
102
+ out.push(px(text("CREDIT 0", 120, 274), ` data-part="credit"`));
103
+ out.push(`</g>`);
104
+ const k = 58 / W;
105
+ const scan = [];
106
+ for (let i = 0; i <= 112; i++) scan.push(`M${n(-29 + i * 0.518)} -37.3V37.3`);
107
+ return [
108
+ `<g data-part="raster"><g transform="translate(-29 -37.3) scale(${n(k * 10000) / 10000})">${out.join("")}</g>`,
109
+ `<path class="ac-scan" d="${scan.join("")}"/></g>`,
110
+ `<ellipse class="ac-dot" data-part="dot" cx="0" cy="0" rx="1.2" ry="1.2" style="opacity:0"/>`,
111
+ ].join("");
112
+ }
113
+ return { W, H, FONT, SPRITES, FORMATION, SHIP_Y, bitmap, text, centred, svg };
114
+ })();
@@ -0,0 +1,435 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import * as k from "../../kit/iso-kit.mjs";
5
+ import { kitScript } from "../../scripts/inline-kit.mjs";
6
+
7
+ const HERE = dirname(fileURLToPath(import.meta.url));
8
+ const theme = process.argv.includes("--light") ? "light" : "dark";
9
+
10
+ const WIDTH = 600;
11
+ const HEIGHT = 440;
12
+
13
+ const PLATE = { x: -100, y: -84, w: 210, d: 242, r: 10 };
14
+ const PLATE_H = 10;
15
+ const FOOT_H = 5;
16
+
17
+ const CASE = { x: -62, y: -62, w: 124, d: 118, r: 8 };
18
+ const FRONT = CASE.y + CASE.d;
19
+ const SIDE = CASE.x + CASE.w;
20
+ const PLINTH_H = 5;
21
+ const TOP = 170;
22
+ const SEAM = FRONT - 12;
23
+
24
+ const BEZEL = { x0: -48, x1: 48, z0: 68, z1: 158, r: 6, depth: 1.6 };
25
+ const CRT = { x0: -40, x1: 40, z0: 77, z1: 150, r: 8, depth: 5 };
26
+ const GLASS_Y = FRONT - BEZEL.depth - CRT.depth;
27
+ const CRT_MID = (CRT.z0 + CRT.z1) / 2;
28
+ const SLOT = { x0: 8, x1: 46, z0: 45, z1: 50.5, r: 2.4, depth: 4 };
29
+
30
+ const KB = { x: -76, y: 84, w: 152, d: 58, r: 4 };
31
+ const KB_Z = 1.6;
32
+ const KB_TOP = 8.6;
33
+ const U = 9.2;
34
+ const KEYS_X = -U * 7.5;
35
+ const KEYS_Y = 88.5;
36
+
37
+ const ROWS = [
38
+ [["`", 1], ["1", 1], ["2", 1], ["3", 1], ["4", 1], ["5", 1], ["6", 1], ["7", 1], ["8", 1], ["9", 1], ["0", 1], ["-", 1], ["=", 1], ["Backspace", 2]],
39
+ [["Tab", 1.5], ["q", 1], ["w", 1], ["e", 1], ["r", 1], ["t", 1], ["y", 1], ["u", 1], ["i", 1], ["o", 1], ["p", 1], ["[", 1], ["]", 1], ["\\", 1.5]],
40
+ [["CapsLock", 1.75], ["a", 1], ["s", 1], ["d", 1], ["f", 1], ["g", 1], ["h", 1], ["j", 1], ["k", 1], ["l", 1], [";", 1], ["'", 1], ["Enter", 2.25]],
41
+ [["Shift", 2.25], ["z", 1], ["x", 1], ["c", 1], ["v", 1], ["b", 1], ["n", 1], ["m", 1], [",", 1], [".", 1], ["/", 1], ["ShiftRight", 2.75]],
42
+ [["Alt", 1.5], ["Meta", 1.5], [" ", 9], ["NumpadEnter", 1.5], ["AltRight", 1.5]],
43
+ ];
44
+ const ROW_H = [5.6, 5.2, 4.8, 4.4, 4];
45
+
46
+ const PORT = { y: 28, z: 9 };
47
+ const COIL = { x: 80, z: 3.6, r: 3.2, from: 44, to: 84, turns: 17 };
48
+
49
+ const P = k.fitProjection(
50
+ [...k.boxCorners(PLATE, -PLATE_H - FOOT_H, 0), ...k.boxCorners(CASE, 0, TOP)],
51
+ WIDTH,
52
+ HEIGHT,
53
+ { pad: 30, azimuth: 56 },
54
+ );
55
+
56
+ const at = (point) => k.iso(point, P);
57
+ const S = (paths, style) => k.solidSvg(paths, style);
58
+ const L = (d, style) => k.lineSvg(d, style);
59
+ const D = (points, style) => k.dotsSvg(points, style);
60
+ const byDepth = (points) => [...points].sort((a, b) => k.depthOf([a[0], a[1], 0], P) - k.depthOf([b[0], b[1], 0], P));
61
+ const planeOf = (y) => k.sideMatrix([0, y], 0, P, "left");
62
+ const flat = (x0, z0, x1, z1, r, steps = 6) => k.pathOf(k.roundedPlan({ x: x0, y: z0, w: x1 - x0, d: z1 - z0, r }, steps).map(([u, z]) => [u, -z]), true);
63
+ const onFront = (points, y = FRONT) => k.pathOf(points.map(([x, z]) => at([x, y, z])));
64
+ const sideSlot = (y, z0, z1, half = 0.8) => k.pathOf(k.roundedPlan({ x: y - half, y: z0, w: half * 2, d: z1 - z0, r: half }, 4).map(([u, z]) => at([SIDE, u, z])), true);
65
+
66
+ let clips = 0;
67
+ function recess({ x0, x1, z0, z1, r, depth }, y, back = "", backClass = "pc-well", inside = "") {
68
+ const id = `pc-clip-${clips++}`;
69
+ const ring = k.roundedPlan({ x: x0, y: z0, w: x1 - x0, d: z1 - z0, r }, 6);
70
+ const cx = (x0 + x1) / 2;
71
+ const cz = (z0 + z1) / 2;
72
+ const { sinA } = k.cameraOf(P);
73
+ const tops = [];
74
+ const sides = [[], [], [], []];
75
+ ring.forEach(([ax, az], index) => {
76
+ const [bx, bz] = ring[(index + 1) % ring.length];
77
+ const length = Math.hypot(bx - ax, bz - az) || 1;
78
+ let [nx, nz] = [(bz - az) / length, -(bx - ax) / length];
79
+ if (nx * (cx - (ax + bx) / 2) + nz * (cz - (az + bz) / 2) < 0) [nx, nz] = [-nx, -nz];
80
+ if (nx <= 0 && nz <= 0) return;
81
+ const quad = k.pathOf([at([ax, y, az]), at([bx, y, bz]), at([bx, y - depth, bz]), at([ax, y - depth, az])], true);
82
+ if (nz >= nx) tops.push(quad);
83
+ else sides[Math.min(3, Math.max(0, Math.floor(((nx * -sinA + 1) / 2) * 4)))].push(quad);
84
+ });
85
+ const shape = flat(x0, z0, x1, z1, r);
86
+ return [
87
+ `<clipPath id="${id}"><path transform="${planeOf(y)}" d="${shape}"/></clipPath>`,
88
+ `<g clip-path="url(#${id})">`,
89
+ `<path class="iso-top" d="${tops.join("")}"/>`,
90
+ ...sides.map((d, shade) => (d.length ? `<path class="iso-shade" data-shade="${shade}" d="${d.join("")}"/>` : "")),
91
+ `<g transform="${planeOf(y - depth)}"><path class="${backClass}" d="${shape}"/>${back}<path class="iso-line" data-tone="faint" d="${shape}"/></g>`,
92
+ inside,
93
+ `</g>`,
94
+ `<g transform="${planeOf(y)}"><path class="iso-line" data-tone="lo" d="${shape}"/></g>`,
95
+ ].join("");
96
+ }
97
+
98
+ function plate() {
99
+ const out = [`<path class="iso-halo" d="${k.haloOf(PLATE, -PLATE_H - FOOT_H, PLATE_H, P)}"/>`];
100
+ for (const [x, y] of byDepth(k.corners(PLATE, 22))) {
101
+ out.push(S(k.cylinder(x, y, 7, -PLATE_H - FOOT_H, 1.6, P, 28), { tone: "lo" }));
102
+ out.push(S(k.cylinder(x, y, 5.4, -PLATE_H - FOOT_H + 1.6, FOOT_H - 1.6, P, 28), { tone: "mid" }));
103
+ }
104
+ out.push(S(k.slabOf(PLATE, -PLATE_H, PLATE_H, P, 8, 1.6), { tone: "mid" }));
105
+ out.push(L(k.planOutline(k.insetPlan(PLATE, 5), 0, P), { tone: "lo" }));
106
+ const screws = k.corners(PLATE, 10);
107
+ out.push(L(screws.map(([x, y]) => k.ring(x, y, 2.2, 0, P, 16)).join(""), { tone: "lo" }));
108
+ out.push(D(screws.map(([x, y]) => at([x, y, 0])), { size: 0.5 }));
109
+ const label = { x: -88, y: 144, w: 40, d: 9, r: 1.2 };
110
+ out.push(S(k.slabOf(label, 0, 0.8, P, 3), { tone: "lo", crease: "none" }));
111
+ out.push(L([30, 22].map((w, i) => k.lineOnTop([label.x + 4, label.y + 3 + i * 3], [label.x + 4 + w, label.y + 3 + i * 3], 0.8, P)).join(""), { tone: "lo", free: true }));
112
+ out.push(D([at([label.x + label.w - 3, label.y + label.d / 2, 0.8])], { size: 0.45 }));
113
+ return out.join("");
114
+ }
115
+
116
+ function screen() {
117
+ const H = 19;
118
+ const left = -H * 0.61;
119
+ const base = CRT_MID + 2;
120
+ const glyph = (points) => k.pathOf(points.map(([gx, gy]) => [left + gx * H, -(base + gy * H)]), true);
121
+ const logo =
122
+ glyph([[0, 0], [0.37, 1], [0.55, 1], [0.92, 0], [0.71, 0], [0.63, 0.23], [0.29, 0.23], [0.21, 0]]) +
123
+ glyph([[0.35, 0.41], [0.57, 0.41], [0.46, 0.72]]) +
124
+ glyph([[0.68, 1], [0.89, 1], [1.26, 0], [1.05, 0]]);
125
+ const lines = [];
126
+ for (let z = CRT.z0 + 1.2; z < CRT.z1; z += 1.6) lines.push(`M${CRT.x0} ${(-z).toFixed(1)}H${CRT.x1}`);
127
+ const glow = flat(-30, CRT.z0 + 8, 30, CRT.z1 - 8, 26, 10);
128
+ const textX = CRT.x0 + 8;
129
+ const textZ = CRT.z0 + 14;
130
+ const crt = [
131
+ `<path class="pc-glow" d="${glow}"/>`,
132
+ `<path class="pc-scan" d="${lines.join("")}"/>`,
133
+ `<path class="pc-logo" d="${logo}"/>`,
134
+ `<text class="pc-text" data-part="text" x="${textX}" y="${-textZ}" font-size="5.2">&gt; </text>`,
135
+ `<rect class="pc-cursor" data-part="cursor" x="${textX + 6.6}" y="${-textZ - 4.2}" width="3" height="5"/>`,
136
+ ].join("");
137
+ const back = [
138
+ `<g data-part="crt" transform="">${crt}</g>`,
139
+ `<path class="pc-beam" data-part="beam" d="M${CRT.x0 + 4} ${-CRT_MID}H${CRT.x1 - 4}" style="opacity:0"/>`,
140
+ `<circle class="pc-dot" data-part="dot" cx="0" cy="${-CRT_MID}" r="1.4" style="opacity:0"/>`,
141
+ `<path class="iso-line" data-tone="lo" d="M${CRT.x0 + 6} ${-(CRT.z1 - 16)}Q${CRT.x0 + 7} ${-(CRT.z1 - 7)} ${CRT.x0 + 16} ${-(CRT.z1 - 6)}"/>`,
142
+ ].join("");
143
+ return recess(BEZEL, FRONT, "", "pc-bezel", recess(CRT, FRONT - BEZEL.depth, back, "pc-glass"));
144
+ }
145
+
146
+ function computer() {
147
+ const out = [`<path class="iso-halo" d="${k.haloOf(CASE, 0, 8, P)}"/>`];
148
+ out.push(S(k.slabOf(k.insetPlan(CASE, 3), 0, PLINTH_H, P, 8), { tone: "lo" }));
149
+ out.push(S(k.slabOf(CASE, PLINTH_H, TOP - PLINTH_H, P, 8, 1.6), { tone: "mid" }));
150
+ const handle = { x: -42, y: CASE.y + 7, w: 84, d: 11, r: 5.5 };
151
+ out.push(L(k.planOutline(handle, TOP, P, 8), { tone: "lo" }));
152
+ out.push(L(k.planOutline(k.insetPlan(handle, 2.2), TOP, P, 8), { tone: "faint" }));
153
+ out.push(L(Array.from({ length: 7 }, (_, i) => k.planOutline({ x: -34, y: CASE.y + 27.3 + i * 3.4, w: 68, d: 1.4, r: 0.7 }, TOP, P, 4)).join(""), { tone: "faint" }));
154
+ out.push(L(k.lineOnTop([CASE.x, SEAM], [SIDE, SEAM], TOP, P) + k.segment([SIDE, SEAM, TOP], [SIDE, SEAM, PLINTH_H], P), { tone: "lo" }));
155
+ const rowOf = (from, to) => Array.from({ length: Math.floor((to - from) / 3.2 + 1e-9) + 1 }, (_, i) => from + i * 3.2);
156
+ out.push(L(rowOf(CASE.y + 10, CASE.y + 52).map((y) => sideSlot(y, TOP - 48, TOP - 14)).join(""), { tone: "faint" }));
157
+ out.push(L(rowOf(CASE.y + 10, CASE.y + 40).map((y) => sideSlot(y, 20, 30)).join(""), { tone: "faint" }));
158
+ out.push(`<g transform="${planeOf(FRONT)}"><path class="iso-line" data-tone="faint" d="${flat(CASE.x + 4, PLINTH_H + 4, SIDE - 4, TOP - 4, 5)}"/></g>`);
159
+ out.push(screen());
160
+ out.push(recess(SLOT, FRONT, `<path class="iso-line" data-tone="lo" d="M${SLOT.x0} ${-(SLOT.z0 + 1.6)}H${SLOT.x1}"/>`, "pc-dark"));
161
+ out.push(D([at([SLOT.x1 + 4, FRONT, SLOT.z0 - 1])], { size: 0.5, tone: "mid" }));
162
+ out.push(`<g transform="${planeOf(FRONT)}">${L(Array.from({ length: 9 }, (_, i) => flat(-46.7 + i * 3, 42, -45.3 + i * 3, 54, 0.7, 4)).join(""), { tone: "lo" })}</g>`);
163
+ out.push(`<g data-part="led">${D([at([-46, FRONT, 36])], { size: 0.8, tone: "hi" })}</g>`);
164
+ out.push(rocker());
165
+ out.push(port());
166
+ return `<g class="pc-computer" data-part="computer">${out.join("")}</g>`;
167
+ }
168
+
169
+ function rocker() {
170
+ const y = CASE.y + 8;
171
+ const out = [S(k.slabOf({ x: SIDE, y, w: 1.2, d: 12, r: 1 }, 12, 15, P, 3), { tone: "mid" })];
172
+ const half = (z, proud) => S(k.slabOf({ x: SIDE + 1.2, y: y + 1.5, w: proud ? 2.4 : 0.8, d: 9, r: 0.6 }, z, 5.6, P, 2), { tone: "mid" });
173
+ out.push(`<g data-rocker="on">${half(13, true)}${half(20.4, false)}</g>`);
174
+ out.push(`<g data-rocker="off" style="display:none">${half(20.4, true)}${half(13, false)}</g>`);
175
+ return out.join("");
176
+ }
177
+
178
+ function port() {
179
+ const out = [];
180
+ out.push(S(k.slabOf({ x: SIDE - 0.4, y: PORT.y - 1, w: 1.4, d: 10, r: 1 }, PORT.z - 1, 9, P, 3), { tone: "lo" }));
181
+ out.push(S(k.slabOf({ x: SIDE + 1, y: PORT.y, w: 4.4, d: 8, r: 1 }, PORT.z, 7, P, 3, 0.4), { tone: "mid" }));
182
+ out.push(S(k.slabOf({ x: SIDE + 5.4, y: PORT.y + 2, w: 5, d: 4, r: 1.6 }, PORT.z + 1.5, 4, P, 4), { tone: "mid" }));
183
+ out.push(D([at([SIDE + 3.2, PORT.y + 1.6, PORT.z + 7]), at([SIDE + 3.2, PORT.y + 6.4, PORT.z + 7])], { size: 0.4 }));
184
+ return out.join("");
185
+ }
186
+
187
+ const keyList = [];
188
+ function keyboard() {
189
+ const out = [`<path class="iso-halo" d="${k.haloOf(KB, 0, 4, P)}"/>`];
190
+ for (const [x, y] of byDepth(k.corners(KB, 8))) out.push(S(k.cylinder(x, y, 2.8, 0, KB_Z, P, 16), { tone: "lo" }));
191
+ out.push(S(k.slabOf(KB, KB_Z, KB_TOP - KB_Z, P, 6, 1), { tone: "mid" }));
192
+ const well = { x: KEYS_X - 1.4, y: KEYS_Y - 1.4, w: U * 15 + 2.8, d: U * 5 + 2.8, r: 2 };
193
+ out.push(L(k.planOutline(well, KB_TOP, P, 4), { tone: "lo" }));
194
+ out.push(D([at([KB.x + 4, KB.y + 2.4, KB_TOP]), at([KB.x + KB.w - 4, KB.y + 2.4, KB_TOP])], { size: 0.45 }));
195
+ ROWS.forEach((row, r) => {
196
+ let x = KEYS_X;
197
+ for (const [code, width] of row) {
198
+ const plan = { x: x + 0.7, y: KEYS_Y + r * U + 0.7, w: width * U - 1.4, d: U - 1.4, r: 1.4 };
199
+ const h = ROW_H[r];
200
+ const index = keyList.length;
201
+ keyList.push(code);
202
+ const dish = code === " " ? k.planOutline(k.insetPlan(plan, 1.8), KB_TOP + h, P, 3) : k.planOutline(k.insetPlan(plan, 1.6), KB_TOP + h, P, 3);
203
+ out.push(`<g class="pc-key" data-key="${index}">${S(k.slabOf(plan, KB_TOP, h, P, 3, 0.4), { tone: "mid", crease: "faint" })}${L(dish, { tone: "faint" })}</g>`);
204
+ x += width * U;
205
+ }
206
+ });
207
+ out.push(S(k.slabOf({ x: KB.x + KB.w, y: 90, w: 4, d: 4, r: 1.5 }, 2.5, 4, P, 4), { tone: "mid" }));
208
+ return out.join("");
209
+ }
210
+
211
+ function cable() {
212
+ const out = [];
213
+ const curve = (points) => k.pathOf(points.map((point) => at(point)));
214
+ const start = [COIL.x + COIL.r, COIL.from, COIL.z];
215
+ const end = [COIL.x + COIL.r, COIL.to, COIL.z];
216
+ out.push(`<path class="iso-line pc-cord" data-tone="mid" d="${curve([[SIDE + 10.4, PORT.y + 4, PORT.z + 3.5], [SIDE + 14, PORT.y + 6, PORT.z + 2.4], [SIDE + 17, PORT.y + 9.5, PORT.z - 1], [SIDE + 19.6, PORT.y + 13, COIL.z + 1.6], start])}"/>`);
217
+ const spring = k.coil(COIL.from, COIL.to, [COIL.x, COIL.z], COIL.r, COIL.turns, "y", P);
218
+ out.push(L(spring.back, { tone: "lo" }), L(spring.front, { tone: "mid" }));
219
+ out.push(`<path class="iso-line pc-cord" data-tone="mid" d="${curve([end, [COIL.x + 2.6, COIL.to + 3, COIL.z + 0.2], [KB.x + KB.w + 6.4, 91.4, 4.4], [KB.x + KB.w + 4, 92, 4.5]])}"/>`);
220
+ return out.join("");
221
+ }
222
+
223
+ const svg = k.figureSvg({
224
+ width: WIDTH,
225
+ height: HEIGHT,
226
+ label:
227
+ "An all-in-one desk computer in the spirit of 1984 on a bench plate: a tall case with a CRT set into its front showing the Anthropic mark and a typing prompt, a floppy slot and speaker grille below, vents and a rocker switch on the side, and a keyboard joined to a side port by a coiled cord.",
228
+ body: [plate(), computer(), keyboard(), cable()],
229
+ });
230
+
231
+ const PHRASES = ["hello, claude", "what is a crt?", "draw it isometric"];
232
+
233
+ const live = `
234
+ const P = ${JSON.stringify(P)};
235
+ const KEYS = ${JSON.stringify(keyList)};
236
+ const PHRASES = ${JSON.stringify(PHRASES)};
237
+ const MAX_CHARS = 20, CRT_MID = ${CRT_MID}, TEXT_X = ${CRT.x0 + 8};
238
+ const SHIFTED = { "~": "\`", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", "_": "-", "+": "=", "{": "[", "}": "]", "|": "\\\\", ":": ";", '"': "'", "<": ",", ">": ".", "?": "/" };
239
+ const stage = document.querySelector(".iso-stage");
240
+ const svg = stage.querySelector("svg");
241
+ const keyEls = KEYS.map((_, i) => svg.querySelector('[data-key="' + i + '"]'));
242
+ const textEl = svg.querySelector('[data-part="text"]');
243
+ const cursor = svg.querySelector('[data-part="cursor"]');
244
+ const crt = svg.querySelector('[data-part="crt"]');
245
+ const beam = svg.querySelector('[data-part="beam"]');
246
+ const dot = svg.querySelector('[data-part="dot"]');
247
+ const glass = svg.querySelector(".pc-glass");
248
+ const led = svg.querySelector('[data-part="led"]');
249
+ const rockerOn = svg.querySelector('[data-rocker="on"]');
250
+ const rockerOff = svg.querySelector('[data-rocker="off"]');
251
+ const readout = document.querySelector("[data-readout]");
252
+ const status = document.querySelector("[data-status]");
253
+ const still = matchMedia("(prefers-reduced-motion: reduce)");
254
+ const DOWN = 2.2;
255
+ const down = axisVector(P, [0, 0, -1]);
256
+
257
+ const keyState = KEYS.map(() => ({ at: 0, held: 0 }));
258
+ const active = new Set();
259
+ let line = "", lastKey = "", power = 1, powerClock = 9, keysTyped = 0;
260
+ let frame = 0, previous = 0, visible = false, touring = !still.matches, idleTimer = 0;
261
+ let tour = { phrase: 0, step: "type", index: 0, wait: 1.4 };
262
+
263
+ const indexOf = (code) => KEYS.indexOf(code);
264
+ function press(code, hold = 0.09) {
265
+ const i = indexOf(code);
266
+ if (i < 0) return;
267
+ keyState[i].held = hold;
268
+ active.add(i);
269
+ run();
270
+ }
271
+ function type(char) {
272
+ if (!power) return;
273
+ const lower = char.toLowerCase();
274
+ const code = SHIFTED[char] || lower;
275
+ if (indexOf(code) < 0) return;
276
+ if (SHIFTED[char] || char !== lower) press("Shift", 0.14);
277
+ press(code);
278
+ keysTyped++;
279
+ lastKey = char === " " ? "space" : char;
280
+ if (line.length < MAX_CHARS) line += char;
281
+ write();
282
+ }
283
+ function erase() { if (!power) return; press("Backspace"); lastKey = "backspace"; line = line.slice(0, -1); write(); }
284
+ function enter() { if (!power) return; press("Enter"); lastKey = "return"; line = ""; write(); }
285
+ function write() {
286
+ textEl.textContent = "> " + line;
287
+ cursor.setAttribute("x", String(TEXT_X + textEl.getComputedTextLength() + 0.4));
288
+ const text = power ? "on · " + line.length + " chars" + (lastKey ? " · key " + lastKey : "") : "off";
289
+ if (readout.textContent !== text) readout.textContent = text;
290
+ }
291
+ function setPower(next) {
292
+ if (next === power) return;
293
+ power = next;
294
+ powerClock = still.matches ? 9 : 0;
295
+ rockerOn.style.display = power ? "" : "none";
296
+ rockerOff.style.display = power ? "none" : "";
297
+ led.style.opacity = power ? "" : "0.15";
298
+ status.textContent = power ? "Computer on." : "Computer off.";
299
+ write();
300
+ run();
301
+ }
302
+ const ease = (t) => 1 - Math.pow(1 - Math.min(1, Math.max(0, t)), 3);
303
+ function crtFrame() {
304
+ const t = powerClock;
305
+ let sx = 1, sy = 1, beamAt = 0, dotAt = 0, lit = power;
306
+ if (power) {
307
+ if (t < 0.14) { sx = ease(t / 0.14); sy = 0.012; beamAt = 1; }
308
+ else if (t < 0.36) { sy = 0.012 + 0.988 * ease((t - 0.14) / 0.22); beamAt = 1 - ease((t - 0.14) / 0.22); }
309
+ } else {
310
+ if (t < 0.1) { sy = 1 - 0.988 * ease(t / 0.1); beamAt = ease(t / 0.1); lit = 1; }
311
+ else if (t < 0.26) { sy = 0.012; sx = 1 - ease((t - 0.1) / 0.16); beamAt = 1; lit = 1; }
312
+ else { sx = 0; sy = 0; dotAt = Math.max(0, 1 - (t - 0.26) / 0.5); }
313
+ }
314
+ crt.setAttribute("transform", "translate(0 " + -CRT_MID + ") scale(" + sx.toFixed(3) + " " + sy.toFixed(3) + ") translate(0 " + CRT_MID + ")");
315
+ beam.setAttribute("transform", "translate(0 " + -CRT_MID + ") scale(" + Math.max(sx, 0.001).toFixed(3) + " 1) translate(0 " + CRT_MID + ")");
316
+ beam.style.opacity = beamAt.toFixed(2);
317
+ dot.style.opacity = dotAt.toFixed(2);
318
+ glass.toggleAttribute("data-on", !!lit);
319
+ return t > 0.8;
320
+ }
321
+ function tourStep(dt) {
322
+ tour.wait -= dt;
323
+ if (tour.wait > 0) return;
324
+ const phrase = PHRASES[tour.phrase];
325
+ if (tour.step === "type") {
326
+ if (tour.index < phrase.length) { type(phrase[tour.index++]); tour.wait = 0.09 + Math.random() * 0.11 + (phrase[tour.index - 1] === " " ? 0.08 : 0); }
327
+ else { tour.step = "erase"; tour.wait = 2.4; }
328
+ } else if (tour.step === "erase") {
329
+ if (line.length) { erase(); tour.wait = 0.055; }
330
+ else if (tour.phrase === PHRASES.length - 1) { tour.step = "off"; tour.wait = 0.9; }
331
+ else { tour.phrase++; tour.index = 0; tour.step = "type"; tour.wait = 0.7; }
332
+ } else if (tour.step === "off") { setPower(0); tour.step = "on"; tour.wait = 1.6; }
333
+ else if (tour.step === "on") { setPower(1); tour = { phrase: 0, step: "type", index: 0, wait: 1.4 }; }
334
+ }
335
+ function tick(now) {
336
+ const dt = previous ? Math.min((now - previous) / 1000, 1 / 30) : 1 / 60;
337
+ previous = now;
338
+ const calm = still.matches;
339
+ if (touring) tourStep(dt);
340
+ powerClock += dt;
341
+ const settledCrt = crtFrame();
342
+ for (const i of active) {
343
+ const s = keyState[i];
344
+ s.held = Math.max(0, s.held - dt);
345
+ const want = s.held > 0 ? DOWN : 0;
346
+ s.at = calm ? want : s.at + (want - s.at) * (1 - Math.exp(-dt / (want ? 0.025 : 0.07)));
347
+ if (Math.abs(want - s.at) < 0.01) s.at = want;
348
+ keyEls[i].setAttribute("transform", "translate(" + (down[0] * s.at).toFixed(2) + " " + (down[1] * s.at).toFixed(2) + ")");
349
+ keyEls[i].toggleAttribute("data-down", s.at > 0.6);
350
+ if (!s.held && s.at === 0) active.delete(i);
351
+ }
352
+ const busy = touring || active.size || !settledCrt;
353
+ frame = visible && busy ? requestAnimationFrame(tick) : 0;
354
+ }
355
+ function run() { if (!frame && visible) { previous = 0; frame = requestAnimationFrame(tick); } }
356
+ function takeOver(switching = false) {
357
+ if (touring) { touring = false; tour = { phrase: 0, step: "type", index: 0, wait: 1.4 }; line = ""; if (!switching) setPower(1); write(); }
358
+ clearTimeout(idleTimer);
359
+ idleTimer = setTimeout(() => { if (still.matches) return; touring = true; line = ""; setPower(1); write(); run(); }, 9000);
360
+ }
361
+ new IntersectionObserver((entries) => { visible = entries[entries.length - 1].isIntersecting; if (visible) run(); }, { rootMargin: "120px 0px" }).observe(stage);
362
+ stage.addEventListener("keydown", (event) => {
363
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
364
+ const key = event.key;
365
+ if (key === "Tab") return;
366
+ takeOver(key === "Escape");
367
+ if (key === "Escape") { event.preventDefault(); setPower(power ? 0 : 1); return; }
368
+ if (key === "Backspace") { event.preventDefault(); erase(); return; }
369
+ if (key === "Enter") { event.preventDefault(); enter(); return; }
370
+ if (key === "Shift") { press(event.code === "ShiftRight" ? "ShiftRight" : "Shift", 0.14); return; }
371
+ if (key.length === 1) { event.preventDefault(); type(key); }
372
+ });
373
+ svg.addEventListener("click", (event) => {
374
+ const key = event.target.closest("[data-key]");
375
+ if (key) {
376
+ takeOver();
377
+ const code = KEYS[Number(key.dataset.key)];
378
+ if (code.length === 1) type(code);
379
+ else if (code === "Backspace") erase();
380
+ else if (code === "Enter" || code === "NumpadEnter") enter();
381
+ else press(code);
382
+ stage.focus({ preventScroll: true });
383
+ return;
384
+ }
385
+ if (event.target.closest('[data-part="computer"]')) { takeOver(true); setPower(power ? 0 : 1); stage.focus({ preventScroll: true }); }
386
+ });
387
+ write();
388
+ crtFrame();
389
+ `;
390
+
391
+ const css = `<style>
392
+ .pc-well{fill:var(--anatomy-shade-1)}
393
+ .pc-bezel{fill:var(--anatomy-top)}
394
+ .pc-dark{fill:var(--anatomy-shade-0)}
395
+ .pc-glass{fill:var(--anatomy-shade-0);transition:fill .5s var(--anatomy-ease)}
396
+ .pc-glass[data-on]{fill:var(--anatomy-lit-shade)}
397
+ .pc-glow{fill:var(--anatomy-lit);opacity:.07;filter:blur(6px)}
398
+ .pc-scan{fill:none;stroke:var(--anatomy-faint);stroke-width:.5px;vector-effect:non-scaling-stroke;opacity:.7}
399
+ .pc-logo{fill:var(--anatomy-lit);fill-rule:evenodd}
400
+ .pc-text{fill:var(--anatomy-lit);font-family:var(--anatomy-mono);letter-spacing:.02em}
401
+ .pc-cursor{fill:var(--anatomy-lit);animation:pc-blink 1.06s steps(1,end) infinite}
402
+ .pc-beam{fill:none;stroke:var(--anatomy-lit);stroke-width:1.4px;vector-effect:non-scaling-stroke;stroke-linecap:round}
403
+ .pc-dot{fill:var(--anatomy-lit)}
404
+ .pc-cord{stroke-width:1.1px}
405
+ .pc-key,.pc-computer{cursor:pointer}
406
+ .pc-key[data-down] .iso-edge{stroke:var(--anatomy-hi)}
407
+ [data-part="led"]{transition:opacity .4s var(--anatomy-ease)}
408
+ @keyframes pc-blink{50%{opacity:0}}
409
+ @media (prefers-reduced-motion:reduce){.pc-cursor{animation:none}}
410
+ .pc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
411
+ .iso-stage{touch-action:pan-y}
412
+ .iso-stage:focus{outline:none}
413
+ .iso-stage:focus-visible{outline:1px solid var(--anatomy-muted);outline-offset:6px}
414
+ </style>`;
415
+
416
+ const body =
417
+ css +
418
+ k.plateHtml({
419
+ fig: "Fig 2",
420
+ title: "Desk computer",
421
+ hint: "Type on it · click it to switch",
422
+ readout: "on · 0 chars",
423
+ keys: [
424
+ { mark: "lit", label: "The screen: the Anthropic mark and what you type" },
425
+ { mark: "raised", label: "Fifty-eight keys; each one goes down as it is typed" },
426
+ { mark: "edge", label: "A coiled cord from the keyboard to the side port" },
427
+ ],
428
+ caption:
429
+ "An all-in-one desk computer in the spirit of 1984: a CRT set into a tall case, a floppy slot and speaker grille under it, vents and a rocker switch on the side. Focus it and type: each key you press goes down on the keyboard and the character lands on the screen beneath the Anthropic mark. Backspace and Return work, Escape or a click on the case switches it off and the picture collapses to a line and a dot, as a CRT does.",
430
+ body: `<div class="iso-stage" tabindex="0" role="group" aria-label="Desk computer. Type to write on its screen; Escape switches the power.">${svg}<span class="pc-sr" role="status" aria-live="polite" data-status></span></div>`,
431
+ });
432
+
433
+ const name = theme === "light" ? "desk-computer-light.html" : "desk-computer.html";
434
+ writeFileSync(join(HERE, name), k.pageHtml({ title: "Desk computer", theme, body, script: kitScript() + live }));
435
+ console.log("wrote", name);