diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock
new file mode 100644
index 0000000..112c3b4
--- /dev/null
+++ b/.claude/scheduled_tasks.lock
@@ -0,0 +1 @@
+{"sessionId":"dec1e8b5-dc16-43d2-b20f-0fad3fe0354e","pid":148363,"procStart":"635354","acquiredAt":1777523259923}
\ No newline at end of file
diff --git a/assets/themes/sonic/components/diamond/diamond.editor.html b/assets/themes/sonic/components/diamond/diamond.editor.html
new file mode 100644
index 0000000..ccd0efc
--- /dev/null
+++ b/assets/themes/sonic/components/diamond/diamond.editor.html
@@ -0,0 +1,71 @@
+
+
+
+
+ Component Editor — sonic-diamond
+
+
+
+
+
+ sonic-diamond
+
+
+
+
+
+ Snapshot copied
+
+
+
diff --git a/assets/themes/sonic/components/diamond/diamond.editor.mjs b/assets/themes/sonic/components/diamond/diamond.editor.mjs
new file mode 100644
index 0000000..28bbea2
--- /dev/null
+++ b/assets/themes/sonic/components/diamond/diamond.editor.mjs
@@ -0,0 +1,115 @@
+/**
+ * Component-editor for sonic-diamond.
+ *
+ * MVP: matrix view only. Diamonds need no per-variant tuning (all images
+ * are pre-aligned), so single-mode and anchor editing are not implemented.
+ * The "Copy snapshot" button still works — it produces the current
+ * `anchorsDefault` and `variants` ready to paste between marker comments.
+ */
+await customElements.whenDefined('sonic-diamond');
+
+const Comp = customElements.get('sonic-diamond');
+const { presetProps, chippedShapes } = Comp;
+
+document.getElementById('meta').textContent =
+ `${Object.keys(Comp.variants).length} variants · baseSize ${Comp.baseSize.w}×${Comp.baseSize.h}mm · origin (${Comp.origin.x}, ${Comp.origin.y})`;
+
+const main = document.getElementById('main');
+renderGrid('Full', false);
+renderGrid('Chipped', true);
+
+document.getElementById('btn-copy').addEventListener('click', async () => {
+ const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants);
+ await navigator.clipboard.writeText(text);
+ showToast('Snapshot copied');
+});
+
+function renderGrid(title, chipped) {
+ const shapes = chipped
+ ? presetProps.shape.filter(s => chippedShapes.has(s))
+ : presetProps.shape;
+ const colors = presetProps.color;
+
+ const section = document.createElement('section');
+ const h2 = document.createElement('h2');
+ h2.textContent = `${title} — ${shapes.length} × ${colors.length}`;
+ section.appendChild(h2);
+
+ const grid = document.createElement('div');
+ grid.className = 'grid';
+ grid.style.gridTemplateColumns = `repeat(${colors.length}, 1fr)`;
+
+ for (const shape of shapes) {
+ for (const color of colors) {
+ grid.appendChild(makeCell(shape, color, chipped));
+ }
+ }
+
+ section.appendChild(grid);
+ main.appendChild(section);
+}
+
+function makeCell(shape, color, chipped) {
+ const cell = document.createElement('div');
+ cell.className = 'cell';
+
+ const preview = document.createElement('div');
+ preview.className = 'preview';
+
+ const inst = document.createElement('sonic-diamond');
+ inst.setAttribute('shape', shape);
+ inst.setAttribute('color', color);
+ inst.setAttribute('chipped', String(chipped));
+ preview.appendChild(inst);
+
+ const label = document.createElement('div');
+ label.className = 'label';
+ label.innerHTML = `${shape} · ${color}
`;
+
+ const key = document.createElement('div');
+ key.className = 'key';
+ key.textContent = inst._variantKey();
+
+ cell.appendChild(preview);
+ cell.appendChild(label);
+ cell.appendChild(key);
+ return cell;
+}
+
+function serializeSnapshot(anchorsDefault, variants) {
+ const lines = [];
+ lines.push(' // @editor:anchors-start');
+ lines.push(` static anchorsDefault = ${formatAnchors(anchorsDefault, ' ')};`);
+ lines.push(' // @editor:anchors-end');
+ lines.push('');
+ lines.push(' // @editor:variants-start');
+ lines.push(' static variants = {');
+ for (const [key, v] of Object.entries(variants)) {
+ const a = formatAnchorsInline(v.anchors || {});
+ lines.push(` ${key}: { img: ${JSON.stringify(v.img)}, dx: ${v.dx}, dy: ${v.dy}, scale: ${v.scale}, anchors: ${a} },`);
+ }
+ lines.push(' };');
+ lines.push(' // @editor:variants-end');
+ return lines.join('\n');
+}
+
+function formatAnchors(anchors, indent) {
+ const keys = Object.keys(anchors);
+ if (keys.length === 0) return '{}';
+ const inner = keys.map(k => `${indent} ${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`).join(',\n');
+ return `{\n${inner},\n${indent}}`;
+}
+
+function formatAnchorsInline(anchors) {
+ const keys = Object.keys(anchors);
+ if (keys.length === 0) return '{}';
+ const parts = keys.map(k => `${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`);
+ return `{ ${parts.join(', ')} }`;
+}
+
+function showToast(msg) {
+ const toast = document.getElementById('toast');
+ toast.textContent = msg;
+ toast.classList.add('show');
+ setTimeout(() => toast.classList.remove('show'), 1500);
+}
diff --git a/assets/themes/sonic/components/diamond/diamond.mjs b/assets/themes/sonic/components/diamond/diamond.mjs
new file mode 100644
index 0000000..68cdaca
--- /dev/null
+++ b/assets/themes/sonic/components/diamond/diamond.mjs
@@ -0,0 +1,85 @@
+import { FunPenComponent } from '/src/components/base.mjs';
+
+class SonicDiamond extends FunPenComponent {
+ static origin = { x: 9, y: 9 };
+ static baseSize = { w: 18, h: 18 };
+
+ static presetProps = {
+ shape: ['classic', 'elongated', 'flat', 'round', 'raw'],
+ color: ['blue', 'cyan', 'green', 'magenta', 'pink', 'red'],
+ chipped: ['false', 'true'],
+ };
+
+ // Chipped variants exist only for these shapes. Other combinations
+ // silently fall back to the non-chipped variant.
+ static chippedShapes = new Set(['classic', 'round']);
+
+ // @editor:anchors-start
+ static anchorsDefault = {};
+ // @editor:anchors-end
+
+ // @editor:variants-start
+ static variants = {
+ classic_blue: { img: '/assets/themes/sonic/items/diamonds/01-classic-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_cyan: { img: '/assets/themes/sonic/items/diamonds/01-classic-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_green: { img: '/assets/themes/sonic/items/diamonds/01-classic-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_magenta: { img: '/assets/themes/sonic/items/diamonds/01-classic-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_pink: { img: '/assets/themes/sonic/items/diamonds/01-classic-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_red: { img: '/assets/themes/sonic/items/diamonds/01-classic-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+
+ elongated_blue: { img: '/assets/themes/sonic/items/diamonds/02-elongated-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ elongated_cyan: { img: '/assets/themes/sonic/items/diamonds/02-elongated-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ elongated_green: { img: '/assets/themes/sonic/items/diamonds/02-elongated-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ elongated_magenta: { img: '/assets/themes/sonic/items/diamonds/02-elongated-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ elongated_pink: { img: '/assets/themes/sonic/items/diamonds/02-elongated-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ elongated_red: { img: '/assets/themes/sonic/items/diamonds/02-elongated-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+
+ flat_blue: { img: '/assets/themes/sonic/items/diamonds/03-flat-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ flat_cyan: { img: '/assets/themes/sonic/items/diamonds/03-flat-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ flat_green: { img: '/assets/themes/sonic/items/diamonds/03-flat-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ flat_magenta: { img: '/assets/themes/sonic/items/diamonds/03-flat-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ flat_pink: { img: '/assets/themes/sonic/items/diamonds/03-flat-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ flat_red: { img: '/assets/themes/sonic/items/diamonds/03-flat-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+
+ round_blue: { img: '/assets/themes/sonic/items/diamonds/04-round-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_cyan: { img: '/assets/themes/sonic/items/diamonds/04-round-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_green: { img: '/assets/themes/sonic/items/diamonds/04-round-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_magenta: { img: '/assets/themes/sonic/items/diamonds/04-round-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_pink: { img: '/assets/themes/sonic/items/diamonds/04-round-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_red: { img: '/assets/themes/sonic/items/diamonds/04-round-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+
+ raw_blue: { img: '/assets/themes/sonic/items/diamonds/05-raw-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ raw_cyan: { img: '/assets/themes/sonic/items/diamonds/05-raw-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ raw_green: { img: '/assets/themes/sonic/items/diamonds/05-raw-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ raw_magenta: { img: '/assets/themes/sonic/items/diamonds/05-raw-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ raw_pink: { img: '/assets/themes/sonic/items/diamonds/05-raw-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ raw_red: { img: '/assets/themes/sonic/items/diamonds/05-raw-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+
+ classic_blue_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_cyan_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_green_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_magenta_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_pink_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ classic_red_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+
+ round_blue_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_cyan_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_green_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-green-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_magenta_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_pink_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ round_red_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-red-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
+ };
+ // @editor:variants-end
+
+ _variantKey() {
+ const shape = this.getAttribute('shape') ?? this.constructor.presetProps.shape[0];
+ const color = this.getAttribute('color') ?? this.constructor.presetProps.color[0];
+ const chipped = this.getAttribute('chipped') === 'true';
+ const supportsChipped = this.constructor.chippedShapes.has(shape);
+ return supportsChipped && chipped
+ ? `${shape}_${color}_chipped`
+ : `${shape}_${color}`;
+ }
+}
+
+customElements.define('sonic-diamond', SonicDiamond);
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl1-abandoned.png
new file mode 100644
index 0000000..763f0dd
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1-abandoned.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl1-broken.png
new file mode 100644
index 0000000..bc82fb0
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1-broken.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1-off.png b/assets/themes/sonic/items/conduit/conduit-lvl1-off.png
new file mode 100644
index 0000000..d0a4447
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1-off.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1.png b/assets/themes/sonic/items/conduit/conduit-lvl1.png
new file mode 100644
index 0000000..15cf92a
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl2-abandoned.png
new file mode 100644
index 0000000..59767c0
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2-abandoned.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl2-broken.png
new file mode 100644
index 0000000..1e51d02
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2-broken.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2-off.png b/assets/themes/sonic/items/conduit/conduit-lvl2-off.png
new file mode 100644
index 0000000..dcf48b6
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2-off.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2.png b/assets/themes/sonic/items/conduit/conduit-lvl2.png
new file mode 100644
index 0000000..729d1a5
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl3-abandoned.png
new file mode 100644
index 0000000..af23da6
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3-abandoned.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl3-broken.png
new file mode 100644
index 0000000..d2df8cd
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3-broken.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3-off.png b/assets/themes/sonic/items/conduit/conduit-lvl3-off.png
new file mode 100644
index 0000000..ad9412f
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3-off.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3.png b/assets/themes/sonic/items/conduit/conduit-lvl3.png
new file mode 100644
index 0000000..2c9a5d2
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl4-abandoned.png
new file mode 100644
index 0000000..98db2a9
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4-abandoned.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl4-broken.png
new file mode 100644
index 0000000..76dab33
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4-broken.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4-off.png b/assets/themes/sonic/items/conduit/conduit-lvl4-off.png
new file mode 100644
index 0000000..fed5008
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4-off.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4.png b/assets/themes/sonic/items/conduit/conduit-lvl4.png
new file mode 100644
index 0000000..26bb906
Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4.png differ
diff --git a/assets/themes/sonic/items/conduit/conduit8.png b/assets/themes/sonic/items/conduit/conduit8.png
deleted file mode 100644
index 2da3331..0000000
Binary files a/assets/themes/sonic/items/conduit/conduit8.png and /dev/null differ
diff --git a/assets/themes/sonic/items/diamonds/01-classic-blue.png b/assets/themes/sonic/items/diamonds/01-classic-blue.png
new file mode 100644
index 0000000..2b52e13
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-blue.png differ
diff --git a/assets/themes/sonic/items/diamonds/01-classic-cyan.png b/assets/themes/sonic/items/diamonds/01-classic-cyan.png
new file mode 100644
index 0000000..1d79224
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-cyan.png differ
diff --git a/assets/themes/sonic/items/diamonds/01-classic-green.png b/assets/themes/sonic/items/diamonds/01-classic-green.png
new file mode 100644
index 0000000..eb8f171
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-green.png differ
diff --git a/assets/themes/sonic/items/diamonds/01-classic-magenta.png b/assets/themes/sonic/items/diamonds/01-classic-magenta.png
new file mode 100644
index 0000000..5eaf994
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-magenta.png differ
diff --git a/assets/themes/sonic/items/diamonds/01-classic-pink.png b/assets/themes/sonic/items/diamonds/01-classic-pink.png
new file mode 100644
index 0000000..46c0934
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-pink.png differ
diff --git a/assets/themes/sonic/items/diamonds/01-classic-red.png b/assets/themes/sonic/items/diamonds/01-classic-red.png
new file mode 100644
index 0000000..f80f207
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-red.png differ
diff --git a/assets/themes/sonic/items/diamonds/02-elongated-blue.png b/assets/themes/sonic/items/diamonds/02-elongated-blue.png
new file mode 100644
index 0000000..5b8cfa1
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-blue.png differ
diff --git a/assets/themes/sonic/items/diamonds/02-elongated-cyan.png b/assets/themes/sonic/items/diamonds/02-elongated-cyan.png
new file mode 100644
index 0000000..efc3d0f
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-cyan.png differ
diff --git a/assets/themes/sonic/items/diamonds/02-elongated-green.png b/assets/themes/sonic/items/diamonds/02-elongated-green.png
new file mode 100644
index 0000000..27c4317
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-green.png differ
diff --git a/assets/themes/sonic/items/diamonds/02-elongated-magenta.png b/assets/themes/sonic/items/diamonds/02-elongated-magenta.png
new file mode 100644
index 0000000..ce6ae12
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-magenta.png differ
diff --git a/assets/themes/sonic/items/diamonds/02-elongated-pink.png b/assets/themes/sonic/items/diamonds/02-elongated-pink.png
new file mode 100644
index 0000000..63205e0
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-pink.png differ
diff --git a/assets/themes/sonic/items/diamonds/02-elongated-red.png b/assets/themes/sonic/items/diamonds/02-elongated-red.png
new file mode 100644
index 0000000..aea6208
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-red.png differ
diff --git a/assets/themes/sonic/items/diamonds/03-flat-blue.png b/assets/themes/sonic/items/diamonds/03-flat-blue.png
new file mode 100644
index 0000000..ec572d1
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-blue.png differ
diff --git a/assets/themes/sonic/items/diamonds/03-flat-cyan.png b/assets/themes/sonic/items/diamonds/03-flat-cyan.png
new file mode 100644
index 0000000..fefaa58
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-cyan.png differ
diff --git a/assets/themes/sonic/items/diamonds/03-flat-green.png b/assets/themes/sonic/items/diamonds/03-flat-green.png
new file mode 100644
index 0000000..189ba72
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-green.png differ
diff --git a/assets/themes/sonic/items/diamonds/03-flat-magenta.png b/assets/themes/sonic/items/diamonds/03-flat-magenta.png
new file mode 100644
index 0000000..e642bd3
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-magenta.png differ
diff --git a/assets/themes/sonic/items/diamonds/03-flat-pink.png b/assets/themes/sonic/items/diamonds/03-flat-pink.png
new file mode 100644
index 0000000..bf38324
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-pink.png differ
diff --git a/assets/themes/sonic/items/diamonds/03-flat-red.png b/assets/themes/sonic/items/diamonds/03-flat-red.png
new file mode 100644
index 0000000..96e0f07
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-red.png differ
diff --git a/assets/themes/sonic/items/diamonds/04-round-blue.png b/assets/themes/sonic/items/diamonds/04-round-blue.png
new file mode 100644
index 0000000..775b59d
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-blue.png differ
diff --git a/assets/themes/sonic/items/diamonds/04-round-cyan.png b/assets/themes/sonic/items/diamonds/04-round-cyan.png
new file mode 100644
index 0000000..a2fe600
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-cyan.png differ
diff --git a/assets/themes/sonic/items/diamonds/04-round-green.png b/assets/themes/sonic/items/diamonds/04-round-green.png
new file mode 100644
index 0000000..50289e8
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-green.png differ
diff --git a/assets/themes/sonic/items/diamonds/04-round-magenta.png b/assets/themes/sonic/items/diamonds/04-round-magenta.png
new file mode 100644
index 0000000..9564256
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-magenta.png differ
diff --git a/assets/themes/sonic/items/diamonds/04-round-pink.png b/assets/themes/sonic/items/diamonds/04-round-pink.png
new file mode 100644
index 0000000..f440ef2
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-pink.png differ
diff --git a/assets/themes/sonic/items/diamonds/04-round-red.png b/assets/themes/sonic/items/diamonds/04-round-red.png
new file mode 100644
index 0000000..68d8eff
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-red.png differ
diff --git a/assets/themes/sonic/items/diamonds/05-raw-blue.png b/assets/themes/sonic/items/diamonds/05-raw-blue.png
new file mode 100644
index 0000000..166232c
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-blue.png differ
diff --git a/assets/themes/sonic/items/diamonds/05-raw-cyan.png b/assets/themes/sonic/items/diamonds/05-raw-cyan.png
new file mode 100644
index 0000000..4ecf7a7
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-cyan.png differ
diff --git a/assets/themes/sonic/items/diamonds/05-raw-green.png b/assets/themes/sonic/items/diamonds/05-raw-green.png
new file mode 100644
index 0000000..390e9d9
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-green.png differ
diff --git a/assets/themes/sonic/items/diamonds/05-raw-magenta.png b/assets/themes/sonic/items/diamonds/05-raw-magenta.png
new file mode 100644
index 0000000..016dc54
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-magenta.png differ
diff --git a/assets/themes/sonic/items/diamonds/05-raw-pink.png b/assets/themes/sonic/items/diamonds/05-raw-pink.png
new file mode 100644
index 0000000..50b6dde
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-pink.png differ
diff --git a/assets/themes/sonic/items/diamonds/05-raw-red.png b/assets/themes/sonic/items/diamonds/05-raw-red.png
new file mode 100644
index 0000000..2972e7b
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-red.png differ
diff --git a/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png
new file mode 100644
index 0000000..748e59e
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png
new file mode 100644
index 0000000..b0f37a6
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png
new file mode 100644
index 0000000..331cd26
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png
new file mode 100644
index 0000000..6c974fb
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png
new file mode 100644
index 0000000..312bfc1
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png
new file mode 100644
index 0000000..8d25a59
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png b/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png
new file mode 100644
index 0000000..3abf040
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png b/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png
new file mode 100644
index 0000000..5dfea62
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/07-round-green-chipped.png b/assets/themes/sonic/items/diamonds/07-round-green-chipped.png
new file mode 100644
index 0000000..effc77e
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-green-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png b/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png
new file mode 100644
index 0000000..f9f878a
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png b/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png
new file mode 100644
index 0000000..c070433
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/07-round-red-chipped.png b/assets/themes/sonic/items/diamonds/07-round-red-chipped.png
new file mode 100644
index 0000000..7f148b4
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-red-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-classic-chipped.png b/assets/themes/sonic/items/diamonds/raw/base-classic-chipped.png
new file mode 100644
index 0000000..c4e99ba
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-classic-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-classic.png b/assets/themes/sonic/items/diamonds/raw/base-classic.png
new file mode 100644
index 0000000..8b01920
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-classic.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-elongated.png b/assets/themes/sonic/items/diamonds/raw/base-elongated.png
new file mode 100644
index 0000000..7fc738b
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-elongated.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-flat.png b/assets/themes/sonic/items/diamonds/raw/base-flat.png
new file mode 100644
index 0000000..b59cb81
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-flat.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-raw.png b/assets/themes/sonic/items/diamonds/raw/base-raw.png
new file mode 100644
index 0000000..2d489fd
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-raw.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-round-chipped.png b/assets/themes/sonic/items/diamonds/raw/base-round-chipped.png
new file mode 100644
index 0000000..81479e2
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-round-chipped.png differ
diff --git a/assets/themes/sonic/items/diamonds/raw/base-round.png b/assets/themes/sonic/items/diamonds/raw/base-round.png
new file mode 100644
index 0000000..8fc60f9
Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-round.png differ
diff --git a/assets/themes/sonic/loader.mjs b/assets/themes/sonic/loader.mjs
new file mode 100644
index 0000000..8e03fb4
--- /dev/null
+++ b/assets/themes/sonic/loader.mjs
@@ -0,0 +1,15 @@
+/**
+ * Sonic theme component loader.
+ * Scans the document for `sonic-*` custom elements and lazy-imports
+ * only those component modules. See src/components/CLAUDE.md.
+ */
+const PREFIX = 'sonic-';
+const tags = new Set();
+document.querySelectorAll('*').forEach(el => {
+ const t = el.tagName.toLowerCase();
+ if (t.startsWith(PREFIX)) tags.add(t);
+});
+await Promise.all([...tags].map(t => {
+ const name = t.slice(PREFIX.length);
+ return import(`./components/${name}/${name}.mjs`);
+}));
diff --git a/src/components/CLAUDE.md b/src/components/CLAUDE.md
new file mode 100644
index 0000000..adbe3a5
--- /dev/null
+++ b/src/components/CLAUDE.md
@@ -0,0 +1,306 @@
+# Components System
+
+Themed Web Components for FunPen worksheets. Components wrap images so that:
+- visual variants are picked via props (e.g. `shape="hex" color="blue"`)
+- per-image positioning offsets are baked into the component file once and applied automatically
+- key points (anchors / connectors) are exposed via a stable DOM API for document-level composition
+
+This folder holds the **shared infrastructure** (base class, editor framework). Actual components live next to their assets under `assets/themes/{theme}/components/{name}/`.
+
+---
+
+## Where things live
+
+```
+src/components/ — shared infrastructure (this folder)
+ CLAUDE.md — decisions and conventions (this file)
+ base.mjs — FunPenComponent base class
+ editor/ — reusable component-editor framework
+ matrix-view.mjs — matrix mode (grid of preset variants)
+ single-view.mjs — single mode (tune one variant)
+ write-back.mjs — saves edits back into component .mjs
+ styles.css — shared editor styles
+ README-editor.md — how to build a component-editor
+
+assets/themes/{theme}/
+ loader.mjs — tiny per-theme on-demand loader (~10 lines)
+ components/
+ {component}/
+ {component}.mjs — self-contained Web Component (final)
+ {component}.editor.html — component-editor page (dev only)
+ {component}.editor.mjs — component-editor logic (dev only)
+
+assets/themes/{theme}/items/{cat}/ — image assets (UNCHANGED — components reference these)
+```
+
+The editor framework files (`editor/*`) are written iteratively as we build the first component. Don't pre-create them empty — extract reusable pieces from the first concrete editor.
+
+---
+
+## Decisions log
+
+These were agreed during the design discussion and should not be changed without revisiting:
+
+1. **Web Components with Shadow DOM** — for true encapsulation. Doc-editor cannot peek inside, no style bleed, real isolation. Single `
+
+```
+
+The loader runs as `module` → deferred by default → the DOM is parsed before it scans. If a component is added to the page after load (very rare in our static documents), the loader does not pick it up — that's fine for now.
+
+---
+
+## Component-editor
+
+Two modes, one page (`{component}.editor.html`):
+
+### Matrix mode (default on open)
+
+Cartesian product of all `presetProps` rendered as a grid. Each cell is a live instance of the component with that prop combination plus a label. Clicking a cell enters single mode for those props.
+
+Free props (`x`, `y`, `scale`, `rotation`) are NOT iterated — they don't affect the variant.
+
+### Single mode
+
+One large instance of the selected variant. Sidebar with:
+- Sliders/inputs for the selected variant's `dx`, `dy`, `scale` (adjustments to the image inside the component bounds — this is the main work)
+- Drag handles on each anchor point — moving them stores per-variant overrides on top of `anchorsDefault`
+- "Edit defaults" toggle: adjust `anchorsDefault` for ALL variants at once
+- Save button → write changes back into the component's `.mjs` file
+- Back button → return to matrix
+
+### Save mechanism — Claude in the middle, no automated write-back
+
+Following the project's orchestrator pattern: the editor produces data, the AI agent applies it. **No `/api/save-component` endpoint, no auto-rewrite of the source file.**
+
+The component's `.mjs` file marks two sections that contain editor-tunable data:
+
+```js
+// @editor:anchors-start
+static anchorsDefault = { ... };
+// @editor:anchors-end
+
+// @editor:variants-start
+static variants = { ... };
+// @editor:variants-end
+```
+
+Workflow:
+
+1. User tunes in component-editor (matrix → single → adjusts dx/dy/scale and anchors)
+2. User clicks **"Copy snapshot"** in the editor toolbar
+3. Editor copies a JS source fragment to clipboard — the new `anchorsDefault = { ... };` and `variants = { ... };` blocks, formatted to drop in between the markers
+4. User pastes into chat / asks Claude to apply
+5. Claude opens the component `.mjs` file, replaces ONLY the content between `@editor:anchors-start/end` and `@editor:variants-start/end` markers using the Edit tool
+6. Claude verifies: re-opens the editor, compares visual result against what the user tuned (screenshots of the matrix before/after), confirms nothing else in the file changed (`git diff`)
+
+This intentionally avoids chained automation. The cost of a 30-second manual paste-and-edit is much lower than the cost of a write-back script silently corrupting handwritten code, and the agent's review pass catches mistakes the editor cannot see (markers gone, ordering changed, formatting drift).
+
+If you need to add or rename a marker section, edit the `.mjs` file by hand.
+
+### What lives in `src/components/editor/`
+
+Reusable pieces extracted from the first concrete editor. Expected modules:
+
+- `matrix-view.mjs` — given a component class, render the cartesian grid of preset combinations
+- `single-view.mjs` — given a component instance + sidebar root, wire up controls for `dx`/`dy`/`scale` and anchor drag
+- `serialize.mjs` — turns the editor's in-memory `anchorsDefault` and `variants` into a formatted JS source fragment ready to paste between markers. Used by the "Copy snapshot" button — entirely client-side, no server round-trip.
+- `styles.css` — common styles (toolbar, sidebar, grid, drag handles)
+
+Build these incrementally. Don't pre-create empty files.
+
+---
+
+## Adding a new component
+
+1. Create `assets/themes/{theme}/components/{name}/{name}.mjs` from the skeleton above
+2. Fill `origin`, `baseSize`, `presetProps`, `anchorsDefault`
+3. Fill `variants` with rough defaults (any non-zero starting point — the editor refines them)
+4. Create `{name}.editor.html` from the editor template (see `src/components/editor/`)
+5. Open `http://localhost:3300/assets/themes/{theme}/components/{name}/{name}.editor.html`
+6. Tune in matrix → single → "Copy snapshot" → ask Claude to paste between markers in `{name}.mjs`
+7. Claude verifies the result matches the editor (re-open, compare screenshots) before considering the tune-pass done
+8. Use the component in any document via `<{theme}-{name} x="..." y="..." {presetProps...}>{theme}-{name}>`
+
+---
+
+## Adding a new theme
+
+1. Create `assets/themes/{theme}/loader.mjs` (copy from existing, change `PREFIX`)
+2. Add components under `components/`
+
+Nothing else changes — the base class and editor framework are theme-agnostic.
+
+---
+
+## Document-side rules
+
+**To use components in a document:**
+
+1. Add loader once to the document ``:
+ ```html
+
+ ```
+2. Place components anywhere in the body:
+ ```html
+
+ ```
+
+**Compatibility with doc-editor and generate.mjs:**
+
+- Doc-editor moves components by their `x`/`y` attributes (or wraps them in style.left/top — TBD on first integration). Components are atomic; the doc-editor never touches their internals.
+- generate.mjs treats the component tag as plain HTML and copies it through. No special handling.
+- Puppeteer (PDF) needs to wait for component registration before snapshotting. `generate-pdf.mjs` should `waitForFunction(() => customElements.get('sonic-crystal'))` for each used tag, or `waitForNetworkIdle()` is usually enough.
+
+**Compatibility with the existing "static HTML" rule:**
+
+The project rule "OUTPUT MUST BE STATIC HTML — no embedded `