diff --git a/public/color-rings.css b/public/color-rings.css
new file mode 100644
index 0000000..fecae95
--- /dev/null
+++ b/public/color-rings.css
@@ -0,0 +1,20 @@
+:root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; color: #202020; background: #fff; }
+* { box-sizing: border-box; }
+body { margin: 0; }
+.color-rings { width: min(100% - 40px, 640px); margin: 0 auto; padding: 44px 0 36px; }
+.color-rings h1 { margin: 0; font-size: 18px; font-weight: 500; letter-spacing: -.025em; }
+.color-rings header p { margin: 8px 0 0; color: #666; font-size: 13px; }
+.rings { display: block; width: min(100%, 560px); height: auto; margin: 20px auto 24px; }
+.controls { max-width: 460px; margin: auto; }
+.control-heading { display: flex; align-items: center; gap: 16px; font-size: 14px; }
+.control-heading output { margin-left: auto; font-variant-numeric: tabular-nums; }
+.controls button { font: inherit; color: inherit; background: white; border: 1px solid #bbb; border-radius: 4px; min-height: 44px; padding: 0 14px; cursor: pointer; }
+.controls input { display: block; width: 100%; height: 44px; margin: 8px 0 0; accent-color: #252525; cursor: pointer; }
+.controls :focus-visible { outline: 2px solid #202020; outline-offset: 4px; }
+.range-ends { display: flex; justify-content: space-between; color: #666; font-size: 12px; }
+.palette { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 30px 0 20px; }
+.palette dt { font-size: 12px; color: #666; }
+.palette dd { margin: 6px 0 0; font-size: 14px; }
+.palette code { font-variant-numeric: tabular-nums; }
+#color-help, noscript p { font-size: 12px; line-height: 1.6; color: #666; margin: 0; }
+@media (max-width: 400px) { .color-rings { padding-top: 28px; } .rings { margin: 20px auto; } .palette dt { font-size: 11px; } }
diff --git a/public/color-rings.html b/public/color-rings.html
new file mode 100644
index 0000000..65045a1
--- /dev/null
+++ b/public/color-rings.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+ Color rings
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/color-rings.js b/public/color-rings.js
new file mode 100644
index 0000000..d5637b4
--- /dev/null
+++ b/public/color-rings.js
@@ -0,0 +1,45 @@
+(() => {
+ const slider = document.querySelector('#intensity');
+ const output = document.querySelector('#intensity-value');
+ const bases = { clay: '#C98576', ochre: '#D6B779', patina: '#93AAA7' };
+
+ function toHsl(hex) {
+ const [r, g, b] = hex.match(/[a-f\d]{2}/gi).map(v => parseInt(v, 16) / 255);
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
+ const delta = max - min, l = (max + min) / 2;
+ if (!delta) return [0, 0, l];
+ const s = delta / (1 - Math.abs(2 * l - 1));
+ let h = max === r ? (g - b) / delta : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4;
+ return [((h * 60) + 360) % 360, s, l];
+ }
+
+ function toHex([h, s, l]) {
+ const a = s * Math.min(l, 1 - l);
+ const channel = n => {
+ const k = (n + h / 30) % 12;
+ const value = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
+ return Math.round(value * 255).toString(16).padStart(2, '0');
+ };
+ return `#${channel(0)}${channel(8)}${channel(4)}`.toUpperCase();
+ }
+
+ function render() {
+ const intensity = Number(slider.value);
+ output.value = `${intensity}%`;
+ slider.setAttribute('aria-valuetext', `${intensity}% of original saturation`);
+ for (const [name, base] of Object.entries(bases)) {
+ const [h, s, l] = toHsl(base);
+ const hex = toHex([h, Math.min(1, s * intensity / 100), l]);
+ document.querySelector(`#ring-${name}`).setAttribute('stroke', hex);
+ document.querySelector(`#hex-${name}`).textContent = hex;
+ }
+ }
+
+ slider.addEventListener('input', render);
+ document.querySelector('.controls').addEventListener('reset', event => {
+ event.preventDefault();
+ slider.value = slider.defaultValue;
+ render();
+ });
+ render();
+})();
diff --git a/scripts/color-rings.test.mjs b/scripts/color-rings.test.mjs
new file mode 100644
index 0000000..686dba6
--- /dev/null
+++ b/scripts/color-rings.test.mjs
@@ -0,0 +1,37 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import vm from 'node:vm';
+
+test('color rings preserve the base palette, stay in sRGB, and reset', () => {
+ const elements = new Map();
+ const element = selector => {
+ if (!elements.has(selector)) elements.set(selector, {
+ value: '125', defaultValue: '125', attributes: {}, listeners: {},
+ setAttribute(key, value) { this.attributes[key] = value; },
+ addEventListener(key, handler) { this.listeners[key] = handler; },
+ });
+ return elements.get(selector);
+ };
+ vm.runInNewContext(readFileSync(new URL('../public/color-rings.js', import.meta.url), 'utf8'), {
+ document: { querySelector: element },
+ });
+ const slider = element('#intensity');
+ const colors = () => ['clay', 'ochre', 'patina'].map(name => {
+ const hex = element(`#hex-${name}`).textContent;
+ assert.equal(element(`#ring-${name}`).attributes.stroke, hex);
+ return hex;
+ });
+ const initial = colors();
+ for (let value = 0; value <= 220; value++) {
+ slider.value = String(value);
+ slider.listeners.input();
+ for (const hex of colors()) assert.match(hex, /^#[0-9A-F]{6}$/);
+ if (value === 0) for (const hex of colors()) assert.match(hex, /^#([0-9A-F]{2})\1\1$/);
+ if (value === 100) assert.deepEqual(colors(), ['#C98576', '#D6B779', '#93AAA7']);
+ }
+ element('.controls').listeners.reset({ preventDefault() {} });
+ assert.equal(slider.value, '125');
+ assert.deepEqual(colors(), initial);
+ assert.equal(element('#intensity-value').value, '125%');
+});
diff --git a/src/index.tsx b/src/index.tsx
index 71f7681..75c28c8 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -72,6 +72,13 @@ app.get("/apple-touch-icon.png", (c) => c.redirect("/public/apple-touch-icon.png
app.get("/healthz", (c) => c.json({ status: "ok" }));
+// Direct-link abstract art; intentionally absent from indexes and navigation.
+app.use("/artifacts/color-rings", async (c, next) => {
+ c.header("X-Robots-Tag", "noindex, nofollow, noarchive");
+ await next();
+});
+app.get("/artifacts/color-rings", serveStatic({ path: "./public/color-rings.html" }));
+
// Direct-link design experiment: deliberately outside the site shell and navigation.
app.use("/mindco", async (c, next) => {
c.header("X-Robots-Tag", "noindex, nofollow");