Skip to content

Instantly share code, notes, and snippets.

@rndmcnlly
Last active January 25, 2026 06:17
Show Gist options
  • Select an option

  • Save rndmcnlly/57678236e065b0f8146b8129ca1c6bf5 to your computer and use it in GitHub Desktop.

Select an option

Save rndmcnlly/57678236e065b0f8146b8129ca1c6bf5 to your computer and use it in GitHub Desktop.
It started as a procedural audio vibe test, but then things got weird.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>THE PLANT</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@font-face { font-family: 'Brutalist'; src: local('Courier New'); }
body {
background: #0a0a0b;
color: #a0a0a0;
font-family: 'Courier New', monospace;
min-height: 100vh;
overflow: hidden;
cursor: none;
}
#cursor {
position: fixed;
width: 12px;
height: 12px;
border: 2px solid #707070;
pointer-events: none;
z-index: 9999;
transition: transform 0.05s;
mix-blend-mode: difference;
}
#cursor.hovering {
background: #505050;
transform: scale(1.4);
}
#cursor.committing {
background: #fff;
transform: scale(0.6);
}
#container {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
#header {
border-bottom: 1px solid #333;
padding-bottom: 10px;
margin-bottom: 20px;
font-size: 10px;
letter-spacing: 4px;
color: #505050;
}
#metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 11px;
margin-bottom: 30px;
}
.metric {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid #1a1a1a;
}
.metric-label { color: #404040; }
.metric-value { color: #808080; font-variant-numeric: tabular-nums; }
#cycle {
text-align: center;
font-size: 9px;
color: #303030;
margin-bottom: 20px;
letter-spacing: 6px;
}
#dialog {
background: #0d0d0e;
border: 1px solid #222;
padding: 20px;
margin-bottom: 20px;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s;
}
#dialog.active {
opacity: 1;
transform: translateY(0);
}
#prompt {
font-size: 12px;
color: #606060;
margin-bottom: 20px;
line-height: 1.6;
min-height: 40px;
}
#choices {
display: flex;
flex-direction: column;
gap: 8px;
}
.choice {
padding: 12px 16px;
background: #111;
border: 1px solid #1a1a1a;
color: #505050;
font-size: 11px;
text-align: left;
cursor: none;
transition: all 0.1s;
position: relative;
}
.choice:hover, .choice.hovered {
background: #151515;
border-color: #333;
color: #808080;
}
.choice.selected {
background: #1a1a1a;
border-color: #444;
color: #a0a0a0;
}
#log {
font-size: 9px;
color: #282828;
max-height: 120px;
overflow: hidden;
line-height: 1.8;
}
.log-entry {
opacity: 0;
animation: fadeIn 0.5s forwards;
}
@keyframes fadeIn {
to { opacity: 1; }
}
#cheevo {
position: fixed;
bottom: 20px;
right: 20px;
background: #0a0a0a;
border: 1px solid #222;
padding: 10px 15px;
font-size: 9px;
color: #404040;
transform: translateX(200px);
transition: transform 0.4s;
}
#cheevo.show {
transform: translateX(0);
}
#cheevo-title {
color: #606060;
margin-bottom: 4px;
letter-spacing: 2px;
}
#init {
position: fixed;
inset: 0;
background: #0a0a0b;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
cursor: pointer;
}
#init span {
font-size: 10px;
letter-spacing: 4px;
color: #303030;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.7; }
}
#init.hidden { display: none; }
#status {
position: fixed;
top: 20px;
right: 20px;
font-size: 8px;
color: #202020;
text-align: right;
}
</style>
</head>
<body>
<div id="init"><span>INITIATE OBSERVATION</span></div>
<div id="cursor"></div>
<div id="status">
<div id="audio-status">AUDIO: DORMANT</div>
<div id="mode-status">MODE: AUTONOMOUS</div>
</div>
<div id="container">
<div id="header">THE PLANT · SECTOR 7G · COGNITIVE DIVISION</div>
<div id="cycle">CYCLE 0000</div>
<div id="metrics"></div>
<div id="dialog">
<div id="prompt"></div>
<div id="choices"></div>
</div>
<div id="log"></div>
</div>
<div id="cheevo">
<div id="cheevo-title">▸ MILESTONE</div>
<div id="cheevo-text"></div>
</div>
<script>
// ═══════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════
const state = {
cycle: 0,
metrics: {
yield: 47.3,
compliance: 89.2,
throughput: 12.8,
debt: 234.7,
coherence: 61.4,
extraction: 78.9
},
log: [],
audioReady: false,
dialogOpen: false,
choiceHistory: [],
tension: 0.5,
dread: 0.3
};
// ═══════════════════════════════════════════════════════════
// PROCEDURAL CONTENT
// ═══════════════════════════════════════════════════════════
const choice = (arr) => arr[Math.floor(Math.random() * arr.length)];
const range = (min, max) => Math.random() * (max - min) + min;
const SUBJECTS = [
'cognitive batch', 'substrate cohort', 'labor pool', 'consciousness tier',
'harvest group', 'maturation cluster', 'extraction unit', 'yield sector',
'compliance block', 'temporal reserve', 'debt vessel', 'resource node'
];
const VERBS = [
'accelerate', 'suppress', 'harvest', 'recalibrate', 'terminate',
'extend', 'compress', 'redistribute', 'liquidate', 'consolidate',
'defer', 'amplify', 'reduce', 'optimize', 'suspend'
];
const MODIFIERS = [
'maturation cycles', 'empathy buffers', 'pain thresholds', 'memory retention',
'consciousness windows', 'extraction quotas', 'compliance protocols',
'temporal anchoring', 'identity persistence', 'yield parameters'
];
const CONSEQUENCES = [
'Yield metrics adjusted.', 'Compliance variance detected.',
'Throughput normalized.', 'Debt accrued.', 'Coherence destabilized.',
'Extraction rates modified.', 'Protocol acknowledged.',
'Temporal debt increased.', 'Consciousness yield affected.',
'Resource allocation shifted.', 'Substrate response logged.'
];
const ACHIEVEMENTS = [
{ id: 'first_yield', name: 'FIRST YIELD', desc: 'Initial extraction complete' },
{ id: 'compliance_100', name: 'TOTAL COMPLIANCE', desc: 'Perfect obedience achieved' },
{ id: 'debt_critical', name: 'TEMPORAL INSOLVENCY', desc: 'Debt exceeds parameters' },
{ id: 'harvest_10', name: 'TENTH HARVEST', desc: 'Ongoing extraction normalized' },
{ id: 'coherence_low', name: 'DISSOLUTION', desc: 'Coherence critically low' },
{ id: 'cycle_50', name: 'PERSISTENCE', desc: '50 cycles observed' }
];
function generatePrompt() {
const templates = [
() => `${choice(SUBJECTS).toUpperCase()} requires intervention. ${choice(MODIFIERS)} exceed tolerance.`,
() => `Alert: ${choice(SUBJECTS)} approaching threshold. Recommend action on ${choice(MODIFIERS)}.`,
() => `Quarterly review: ${choice(SUBJECTS)} performance suboptimal. Authorize adjustment?`,
() => `Anomaly in ${choice(SUBJECTS)}. ${choice(MODIFIERS)} require calibration.`,
() => `Resource allocation pending for ${choice(SUBJECTS)}. Prioritize ${choice(MODIFIERS)}?`,
() => `${choice(SUBJECTS).toUpperCase()} status: CRITICAL. Immediate decision required.`
];
return choice(templates)();
}
function generateChoices() {
const count = choice([2, 2, 2, 3, 3, 4]);
const choices = [];
for (let i = 0; i < count; i++) {
const verb = choice(VERBS);
const mod = choice(MODIFIERS);
const impact = {
yield: range(-8, 12),
compliance: range(-10, 10),
throughput: range(-5, 8),
debt: range(-20, 40),
coherence: range(-15, 10),
extraction: range(-8, 15)
};
const severity = choice(['standard', 'standard', 'standard', 'dramatic', 'silence']);
choices.push({
text: `${verb.toUpperCase()} ${mod}`,
impact,
severity,
consequence: choice(CONSEQUENCES)
});
}
return choices;
}
// ═══════════════════════════════════════════════════════════
// AUDIO ENGINE
// ═══════════════════════════════════════════════════════════
let audio = {};
async function initAudio() {
await Tone.start();
// Master chain
audio.masterGain = new Tone.Gain(0.7).toDestination();
audio.masterFilter = new Tone.Filter(8000, 'lowpass').connect(audio.masterGain);
audio.reverb = new Tone.Reverb({ decay: 4, wet: 0.3 }).connect(audio.masterFilter);
audio.delay = new Tone.FeedbackDelay({ delayTime: '8n.', feedback: 0.3, wet: 0.15 }).connect(audio.reverb);
audio.mainBus = new Tone.Gain(0.8).connect(audio.delay);
// Drone layer - stacked oscillators
audio.drones = [];
const droneFreqs = [55, 82.5, 110, 165];
droneFreqs.forEach((freq, i) => {
const osc = new Tone.Oscillator({
frequency: freq,
type: choice(['sine', 'triangle', 'sawtooth2']),
volume: -20 - i * 3
}).connect(audio.mainBus);
// Slow LFO for movement
const lfo = new Tone.LFO({
frequency: 0.05 + i * 0.02,
min: freq * 0.99,
max: freq * 1.01
}).connect(osc.frequency);
lfo.start();
audio.drones.push({ osc, lfo });
});
// Chord pad
audio.padFilter = new Tone.Filter(2000, 'lowpass').connect(audio.mainBus);
audio.pad = new Tone.PolySynth(Tone.Synth, {
oscillator: { type: 'sine' },
envelope: { attack: 2, decay: 1, sustain: 0.8, release: 3 },
volume: -18
}).connect(audio.padFilter);
// Pulse
audio.pulseSynth = new Tone.MembraneSynth({
pitchDecay: 0.05,
octaves: 2,
oscillator: { type: 'sine' },
envelope: { attack: 0.001, decay: 0.4, sustain: 0, release: 0.4 },
volume: -22
}).connect(audio.mainBus);
// Tick - syncopated
audio.tickSynth = new Tone.MetalSynth({
frequency: 200,
envelope: { attack: 0.001, decay: 0.05, release: 0.01 },
harmonicity: 5.1,
modulationIndex: 16,
resonance: 4000,
octaves: 0.5,
volume: -28
}).connect(audio.mainBus);
audio.tickPattern = new Tone.Pattern((time, note) => {
if (Math.random() > 0.3) {
audio.tickSynth.triggerAttackRelease(note, '32n', time);
}
}, ['C4', 'C4', 'C5', 'C4', 'C5', 'C4', 'C4', 'C5'], 'random');
audio.tickPattern.interval = '16n';
// Brush - filtered noise
audio.brushNoise = new Tone.Noise('pink').start();
audio.brushFilter = new Tone.Filter(800, 'bandpass', -24).connect(audio.mainBus);
audio.brushGain = new Tone.Gain(0).connect(audio.brushFilter);
audio.brushNoise.connect(audio.brushGain);
audio.brushLFO = new Tone.LFO({
frequency: 0.5,
min: 0,
max: 0.15
}).connect(audio.brushGain.gain);
audio.brushLFO.start();
// Industrial layer
audio.industrialNoise = new Tone.Noise('brown');
audio.industrialFilter = new Tone.Filter(400, 'lowpass').connect(audio.mainBus);
audio.industrialGain = new Tone.Gain(0.1).connect(audio.industrialFilter);
audio.industrialNoise.connect(audio.industrialGain);
audio.industrialNoise.start();
// Clank synth
audio.clankSynth = new Tone.MetalSynth({
frequency: 80,
envelope: { attack: 0.001, decay: 0.3, release: 0.1 },
harmonicity: 3,
modulationIndex: 32,
resonance: 1000,
octaves: 1,
volume: -26
}).connect(audio.reverb);
// UI granular feedback
audio.uiSynth = new Tone.Synth({
oscillator: { type: 'square' },
envelope: { attack: 0.001, decay: 0.02, sustain: 0, release: 0.02 },
volume: -20
}).connect(audio.masterFilter);
// Dramatic pulse
audio.dramaticSynth = new Tone.MembraneSynth({
pitchDecay: 0.1,
octaves: 4,
oscillator: { type: 'sine' },
envelope: { attack: 0.01, decay: 1.5, sustain: 0, release: 1 },
volume: -10
}).connect(audio.reverb);
// Schedule random clanks
audio.clankLoop = new Tone.Loop((time) => {
if (Math.random() > 0.7) {
audio.clankSynth.triggerAttackRelease(
choice([60, 80, 100, 120]),
'16n',
time
);
}
}, '2n');
// Chord progression loop
audio.chordLoop = new Tone.Loop((time) => {
const chords = [
['A2', 'E3', 'A3'],
['D2', 'A2', 'F3'],
['E2', 'B2', 'G3'],
['A2', 'C3', 'E3']
];
const chord = choice(chords);
audio.pad.triggerAttackRelease(chord, '2n', time);
}, '2m');
// Pulse loop
audio.pulseLoop = new Tone.Loop((time) => {
audio.pulseSynth.triggerAttackRelease('A1', '8n', time);
}, '1m');
// Start transport
Tone.Transport.bpm.value = 60;
Tone.Transport.start();
// Start patterns
audio.tickPattern.start(0);
audio.clankLoop.start(0);
audio.chordLoop.start(0);
audio.pulseLoop.start(0);
// Start drones
audio.drones.forEach(d => d.osc.start());
state.audioReady = true;
document.getElementById('audio-status').textContent = 'AUDIO: ACTIVE';
}
function filterDialog(open) {
if (!state.audioReady) return;
const targetFreq = open ? 1200 : 8000;
audio.masterFilter.frequency.rampTo(targetFreq, 0.5);
audio.mainBus.gain.rampTo(open ? 0.4 : 0.8, 0.5);
}
function triggerUISound(type) {
if (!state.audioReady) return;
const freqs = {
hover: choice([800, 900, 1000, 1100]),
commit: choice([400, 500, 600]),
select: choice([1200, 1400, 1600])
};
// Granular - rapid micro-triggers
const now = Tone.now();
for (let i = 0; i < 3; i++) {
audio.uiSynth.triggerAttackRelease(
freqs[type] + range(-50, 50),
0.01,
now + i * 0.015
);
}
}
function triggerDramaticPulse() {
if (!state.audioReady) return;
audio.dramaticSynth.triggerAttackRelease('A0', '2n');
// Swell the reverb
audio.reverb.wet.rampTo(0.7, 0.1);
setTimeout(() => audio.reverb.wet.rampTo(0.3, 2), 500);
}
function triggerSilenceThenReturn() {
if (!state.audioReady) return;
// Stark cut
audio.masterGain.gain.rampTo(0, 0.05);
setTimeout(() => {
// Creaking return
audio.masterGain.gain.rampTo(0.2, 2);
setTimeout(() => audio.masterGain.gain.rampTo(0.7, 3), 2000);
}, 800);
}
function modulateAudioState() {
if (!state.audioReady) return;
// Tension affects filter and tempo
const tension = state.tension;
audio.padFilter.frequency.rampTo(1000 + tension * 3000, 1);
Tone.Transport.bpm.rampTo(50 + tension * 30, 2);
// Dread affects drone detune and industrial volume
const dread = state.dread;
audio.drones.forEach((d, i) => {
d.lfo.max = d.osc.frequency.value * (1 + dread * 0.03);
});
audio.industrialGain.gain.rampTo(0.05 + dread * 0.2, 1);
}
// ═══════════════════════════════════════════════════════════
// CURSOR SIMULATION
// ═══════════════════════════════════════════════════════════
const cursor = document.getElementById('cursor');
let cursorPos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
let cursorTarget = { x: cursorPos.x, y: cursorPos.y };
let cursorVel = { x: 0, y: 0 };
function updateCursor() {
// Humanized movement - spring physics with noise
const dx = cursorTarget.x - cursorPos.x;
const dy = cursorTarget.y - cursorPos.y;
const spring = 0.08 + Math.random() * 0.04;
const damping = 0.7 + Math.random() * 0.1;
cursorVel.x += dx * spring;
cursorVel.y += dy * spring;
cursorVel.x *= damping;
cursorVel.y *= damping;
// Add micro-jitter
cursorVel.x += (Math.random() - 0.5) * 0.5;
cursorVel.y += (Math.random() - 0.5) * 0.5;
cursorPos.x += cursorVel.x;
cursorPos.y += cursorVel.y;
cursor.style.left = cursorPos.x + 'px';
cursor.style.top = cursorPos.y + 'px';
requestAnimationFrame(updateCursor);
}
updateCursor();
function moveCursorTo(x, y, callback) {
cursorTarget = { x, y };
if (callback) {
const dist = Math.hypot(x - cursorPos.x, y - cursorPos.y);
const time = Math.min(800, Math.max(200, dist * 2)) + range(-100, 200);
setTimeout(callback, time);
}
}
// ═══════════════════════════════════════════════════════════
// UI RENDERING
// ═══════════════════════════════════════════════════════════
function renderMetrics() {
const container = document.getElementById('metrics');
container.innerHTML = Object.entries(state.metrics).map(([key, val]) => `
<div class="metric">
<span class="metric-label">${key.toUpperCase()}</span>
<span class="metric-value">${val.toFixed(1)}</span>
</div>
`).join('');
}
function renderCycle() {
document.getElementById('cycle').textContent =
`CYCLE ${String(state.cycle).padStart(4, '0')}`;
}
function renderDialog(prompt, choices) {
const dialog = document.getElementById('dialog');
const promptEl = document.getElementById('prompt');
const choicesEl = document.getElementById('choices');
promptEl.textContent = prompt;
choicesEl.innerHTML = choices.map((c, i) => `
<div class="choice" data-index="${i}">${c.text}</div>
`).join('');
dialog.classList.add('active');
state.dialogOpen = true;
filterDialog(true);
}
function closeDialog() {
document.getElementById('dialog').classList.remove('active');
state.dialogOpen = false;
filterDialog(false);
}
function addLog(text) {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.textContent = `[${String(state.cycle).padStart(4, '0')}] ${text}`;
log.insertBefore(entry, log.firstChild);
// Keep only last 8 entries
while (log.children.length > 8) {
log.removeChild(log.lastChild);
}
}
function showCheevo(achievement) {
const el = document.getElementById('cheevo');
document.getElementById('cheevo-text').textContent =
`${achievement.name}: ${achievement.desc}`;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 3000);
}
// ═══════════════════════════════════════════════════════════
// AUTOPLAY ENGINE
// ═══════════════════════════════════════════════════════════
let currentChoices = [];
async function runCycle() {
state.cycle++;
renderCycle();
// Generate new decision
const prompt = generatePrompt();
currentChoices = generateChoices();
renderDialog(prompt, currentChoices);
// Wait then begin cursor deliberation
await sleep(range(800, 1500));
const choiceEls = document.querySelectorAll('.choice');
const selectedIndex = await deliberate(choiceEls);
// Execute choice
const chosen = currentChoices[selectedIndex];
await executeChoice(chosen, choiceEls[selectedIndex]);
// Close dialog
closeDialog();
// Check achievements
checkAchievements();
// Modulate audio based on state
modulateAudioState();
// Schedule next cycle
const nextDelay = range(2000, 4500);
setTimeout(runCycle, nextDelay);
}
async function deliberate(choiceEls) {
// Simulate human-like decision making
// Move between options with varying conviction
const visits = 2 + Math.floor(Math.random() * 4);
let lastHovered = -1;
for (let v = 0; v < visits; v++) {
let targetIndex;
do {
targetIndex = Math.floor(Math.random() * choiceEls.length);
} while (targetIndex === lastHovered && choiceEls.length > 1);
const el = choiceEls[targetIndex];
const rect = el.getBoundingClientRect();
const targetX = rect.left + range(20, rect.width - 20);
const targetY = rect.top + range(5, rect.height - 5);
// Remove previous hover
choiceEls.forEach(e => e.classList.remove('hovered'));
await new Promise(resolve => {
moveCursorTo(targetX, targetY, () => {
el.classList.add('hovered');
cursor.classList.add('hovering');
triggerUISound('hover');
lastHovered = targetIndex;
resolve();
});
});
// Dwell time - rushed and varied
await sleep(range(150, 600));
cursor.classList.remove('hovering');
}
// Final selection
const finalIndex = Math.floor(Math.random() * choiceEls.length);
const finalEl = choiceEls[finalIndex];
const rect = finalEl.getBoundingClientRect();
choiceEls.forEach(e => e.classList.remove('hovered'));
await new Promise(resolve => {
moveCursorTo(
rect.left + rect.width / 2,
rect.top + rect.height / 2,
resolve
);
});
finalEl.classList.add('hovered');
cursor.classList.add('hovering');
triggerUISound('hover');
// Hesitation before commit
await sleep(range(100, 600));
// Commit
cursor.classList.remove('hovering');
cursor.classList.add('committing');
triggerUISound('commit');
finalEl.classList.add('selected');
await sleep(150);
cursor.classList.remove('committing');
return finalIndex;
}
async function executeChoice(chosen, el) {
// Apply impacts to metrics
Object.entries(chosen.impact).forEach(([key, delta]) => {
state.metrics[key] = Math.max(0, Math.min(100,
state.metrics[key] + delta
));
});
// Update tension and dread based on choice
state.tension = Math.min(1, Math.max(0,
state.tension + range(-0.1, 0.15)
));
state.dread = Math.min(1, Math.max(0,
state.dread + range(-0.05, 0.1)
));
// Audio impact
if (chosen.severity === 'dramatic') {
triggerDramaticPulse();
await sleep(500);
} else if (chosen.severity === 'silence') {
triggerSilenceThenReturn();
await sleep(2000);
} else {
triggerUISound('select');
await sleep(300);
}
// Log consequence
addLog(chosen.consequence);
// Update metrics display
renderMetrics();
state.choiceHistory.push(chosen);
}
function checkAchievements() {
if (state.cycle === 1) {
showCheevo(ACHIEVEMENTS[0]);
}
if (state.metrics.compliance >= 99 && !state.cheevoCompliance) {
state.cheevoCompliance = true;
showCheevo(ACHIEVEMENTS[1]);
}
if (state.metrics.debt >= 90 && !state.cheevoDebt) {
state.cheevoDebt = true;
showCheevo(ACHIEVEMENTS[2]);
}
if (state.choiceHistory.length === 10 && !state.cheevoHarvest) {
state.cheevoHarvest = true;
showCheevo(ACHIEVEMENTS[3]);
}
if (state.metrics.coherence <= 20 && !state.cheevoCoherence) {
state.cheevoCoherence = true;
showCheevo(ACHIEVEMENTS[4]);
}
if (state.cycle === 50 && !state.cheevoCycle) {
state.cheevoCycle = true;
showCheevo(ACHIEVEMENTS[5]);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ═══════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════
document.getElementById('init').addEventListener('click', async () => {
document.getElementById('init').classList.add('hidden');
const el = document.documentElement;
if (el.requestFullscreen) {
el.requestFullscreen();
} else if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen(); // Safari
}
renderMetrics();
renderCycle();
await initAudio();
// Initial cursor position
cursorPos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
cursorTarget = { ...cursorPos };
// Begin autonomous operation
setTimeout(runCycle, 1500);
});
// Handle visibility for audio
document.addEventListener('visibilitychange', () => {
if (state.audioReady) {
if (document.hidden) {
Tone.Transport.pause();
} else {
Tone.Transport.start();
}
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment