-
-

Script Editor

- - -
- - - -
- -
-

Sample Scripts:

- - - - -
- -
-
- -
-

Stack Visualization

-
-
- -

Stack will appear here when you run a script

-
-
-
+
+
+
+

POLYDEUCES32 / BTC DEV LAB

+

Bitcoin Script Lab_

+

Trace Bitcoin Script concepts opcode by opcode with live stack state.

+
+
EDUCATIONAL SIMULATOR
+
+ +
+ This lab uses a simplified JavaScript interpreter. It is not Bitcoin Core-compatible and must not be used to validate real transactions. +
+ +
+
+
01 / SCRIPT INPUT
+ + +
+ + + + +
- -
-

Available Opcodes

-
- -
+
+ + + +
-
- - +
+
+ +
+
02 / EXECUTION TRACE
+
+
+ +
+
03 / STACK STATE
+
+
+
+ +
+
04 / OPCODE REFERENCE
+
+
+
+ + diff --git a/package.json b/package.json index 8f2548d..8c3f899 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,15 @@ { "name": "bitcoin-script-playground", - "version": "1.0.0", - "description": "Interactive Learning Platform for Bitcoin Script Development", + "version": "1.1.0", + "description": "Educational Bitcoin Script playground with stack visualization", "main": "index.html", "scripts": { "start": "python3 -m http.server 8000", - "serve": "npx serve .", "dev": "npx live-server --port=8000 --open=/index.html", - "build": "echo 'No build process needed for vanilla JS'", - "test": "echo 'No tests specified yet'" + "serve": "npx serve .", + "test": "node -e \"console.log('No automated tests yet')\"" }, - "keywords": [ - "bitcoin", - "script", - "blockchain", - "cryptocurrency", - "education", - "learning", - "playground", - "interactive", - "opcodes", - "stack", - "programming" - ], + "keywords": ["bitcoin","script","education","playground","stack"], "author": "polydeuces32 ", "license": "MIT", "repository": { @@ -38,11 +25,6 @@ "serve": "^14.2.1" }, "engines": { - "node": ">=14.0.0" - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not dead" - ] + "node": ">=18" + } } diff --git a/script.js b/script.js index 12da202..d246b4a 100644 --- a/script.js +++ b/script.js @@ -1,535 +1,48 @@ -// Bitcoin Script Playground - Interactive Learning Platform -class BitcoinScriptEngine { - constructor() { - this.stack = []; - this.opcodes = this.initializeOpcodes(); - this.sampleScripts = this.initializeSampleScripts(); - this.initializeUI(); - } - - initializeOpcodes() { - return { - // Stack manipulation - 'OP_DUP': { - name: 'OP_DUP', - description: 'Duplicates the top stack item', - category: 'Stack', - execute: () => this.dup() - }, - 'OP_2DUP': { - name: 'OP_2DUP', - description: 'Duplicates the top two stack items', - category: 'Stack', - execute: () => this.dup2() - }, - 'OP_DROP': { - name: 'OP_DROP', - description: 'Removes the top stack item', - category: 'Stack', - execute: () => this.drop() - }, - 'OP_SWAP': { - name: 'OP_SWAP', - description: 'Swaps the top two stack items', - category: 'Stack', - execute: () => this.swap() - }, - 'OP_OVER': { - name: 'OP_OVER', - description: 'Copies the second-to-top item to the top', - category: 'Stack', - execute: () => this.over() - }, - 'OP_ROT': { - name: 'OP_ROT', - description: 'Rotates the top 3 stack items', - category: 'Stack', - execute: () => this.rot() - }, - - // Arithmetic - 'OP_ADD': { - name: 'OP_ADD', - description: 'Adds the top two stack items', - category: 'Arithmetic', - execute: () => this.add() - }, - 'OP_SUB': { - name: 'OP_SUB', - description: 'Subtracts the second item from the first', - category: 'Arithmetic', - execute: () => this.sub() - }, - 'OP_MUL': { - name: 'OP_MUL', - description: 'Multiplies the top two stack items', - category: 'Arithmetic', - execute: () => this.mul() - }, - 'OP_DIV': { - name: 'OP_DIV', - description: 'Divides the second item by the first', - category: 'Arithmetic', - execute: () => this.div() - }, - 'OP_MOD': { - name: 'OP_MOD', - description: 'Returns the remainder of division', - category: 'Arithmetic', - execute: () => this.mod() - }, - - // Comparison - 'OP_EQUAL': { - name: 'OP_EQUAL', - description: 'Returns 1 if top two items are equal', - category: 'Comparison', - execute: () => this.equal() - }, - 'OP_EQUALVERIFY': { - name: 'OP_EQUALVERIFY', - description: 'Same as OP_EQUAL but runs OP_VERIFY afterward', - category: 'Comparison', - execute: () => this.equalVerify() - }, - 'OP_1EQUAL': { - name: 'OP_1EQUAL', - description: 'Returns 1 if input is 1, 0 otherwise', - category: 'Comparison', - execute: () => this.oneEqual() - }, - 'OP_0NOTEQUAL': { - name: 'OP_0NOTEQUAL', - description: 'Returns 1 if input is not 0, 0 otherwise', - category: 'Comparison', - execute: () => this.zeroNotEqual() - }, - - // Bitwise operations - 'OP_AND': { - name: 'OP_AND', - description: 'Bitwise AND of the top two items', - category: 'Bitwise', - execute: () => this.and() - }, - 'OP_OR': { - name: 'OP_OR', - description: 'Bitwise OR of the top two items', - category: 'Bitwise', - execute: () => this.or() - }, - 'OP_XOR': { - name: 'OP_XOR', - description: 'Bitwise XOR of the top two items', - category: 'Bitwise', - execute: () => this.xor() - }, - 'OP_NOT': { - name: 'OP_NOT', - description: 'Bitwise NOT of the top item', - category: 'Bitwise', - execute: () => this.not() - }, - - // Logical operations - 'OP_BOOLAND': { - name: 'OP_BOOLAND', - description: 'Boolean AND of the top two items', - category: 'Logical', - execute: () => this.boolAnd() - }, - 'OP_BOOLOR': { - name: 'OP_BOOLOR', - description: 'Boolean OR of the top two items', - category: 'Logical', - execute: () => this.boolOr() - }, - - // Constants - 'OP_0': { - name: 'OP_0', - description: 'Pushes empty array onto stack', - category: 'Constants', - execute: () => this.pushZero() - }, - 'OP_1': { - name: 'OP_1', - description: 'Pushes 1 onto stack', - category: 'Constants', - execute: () => this.pushOne() - }, - 'OP_2': { - name: 'OP_2', - description: 'Pushes 2 onto stack', - category: 'Constants', - execute: () => this.pushTwo() - }, - 'OP_3': { - name: 'OP_3', - description: 'Pushes 3 onto stack', - category: 'Constants', - execute: () => this.pushThree() - }, - 'OP_4': { - name: 'OP_4', - description: 'Pushes 4 onto stack', - category: 'Constants', - execute: () => this.pushFour() - }, - 'OP_5': { - name: 'OP_5', - description: 'Pushes 5 onto stack', - category: 'Constants', - execute: () => this.pushFive() - }, - - // Verification - 'OP_VERIFY': { - name: 'OP_VERIFY', - description: 'Marks transaction as invalid if top stack value is not true', - category: 'Verification', - execute: () => this.verify() - }, - 'OP_RETURN': { - name: 'OP_RETURN', - description: 'Marks transaction as invalid', - category: 'Verification', - execute: () => this.returnOp() - } - }; - } - - initializeSampleScripts() { - return { - basic: "OP_1 OP_2 OP_ADD OP_DUP", - comparison: "OP_5 OP_3 OP_ADD OP_8 OP_EQUAL", - multisig: "OP_2 OP_1 OP_1 OP_1 OP_3 OP_CHECKMULTISIG", - advanced: "OP_1 OP_2 OP_3 OP_ROT OP_SWAP OP_ADD OP_DUP OP_EQUAL" - }; - } - - initializeUI() { - this.renderOpcodes(); - } - - renderOpcodes() { - const grid = document.getElementById('opcodesGrid'); - const categories = ['Stack', 'Arithmetic', 'Comparison', 'Bitwise', 'Logical', 'Constants', 'Verification']; - - categories.forEach(category => { - const categoryOpcodes = Object.values(this.opcodes).filter(op => op.category === category); - if (categoryOpcodes.length === 0) return; - - const categoryDiv = document.createElement('div'); - categoryDiv.style.gridColumn = '1 / -1'; - categoryDiv.innerHTML = `

${category}

`; - grid.appendChild(categoryDiv); - - categoryOpcodes.forEach(opcode => { - const card = document.createElement('div'); - card.className = 'opcode-card'; - card.innerHTML = ` -
${opcode.name}
-
${opcode.description}
- `; - card.onclick = () => this.insertOpcode(opcode.name); - grid.appendChild(card); - }); - }); - } - - insertOpcode(opcodeName) { - const textarea = document.getElementById('scriptInput'); - const currentValue = textarea.value; - const newValue = currentValue + (currentValue ? ' ' : '') + opcodeName; - textarea.value = newValue; - textarea.focus(); - } - - // Stack manipulation operations - dup() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - this.stack.push(this.stack[this.stack.length - 1]); - } - - dup2() { - if (this.stack.length < 2) throw new Error('Not enough items on stack'); - const top = this.stack[this.stack.length - 1]; - const second = this.stack[this.stack.length - 2]; - this.stack.push(second, top); - } - - drop() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - this.stack.pop(); - } - - swap() { - if (this.stack.length < 2) throw new Error('Not enough items on stack'); - const top = this.stack.pop(); - const second = this.stack.pop(); - this.stack.push(top, second); - } - - over() { - if (this.stack.length < 2) throw new Error('Not enough items on stack'); - this.stack.push(this.stack[this.stack.length - 2]); - } - - rot() { - if (this.stack.length < 3) throw new Error('Not enough items on stack'); - const top = this.stack.pop(); - const second = this.stack.pop(); - const third = this.stack.pop(); - this.stack.push(second, top, third); - } - - // Arithmetic operations - add() { - if (this.stack.length < 2) throw new Error('Not enough numbers to add'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a + b).toString()); - } - - sub() { - if (this.stack.length < 2) throw new Error('Not enough numbers to subtract'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a - b).toString()); - } - - mul() { - if (this.stack.length < 2) throw new Error('Not enough numbers to multiply'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a * b).toString()); - } - - div() { - if (this.stack.length < 2) throw new Error('Not enough numbers to divide'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - if (b === 0) throw new Error('Division by zero'); - this.stack.push(Math.floor(a / b).toString()); - } - - mod() { - if (this.stack.length < 2) throw new Error('Not enough numbers for modulo'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - if (b === 0) throw new Error('Modulo by zero'); - this.stack.push((a % b).toString()); - } - - // Comparison operations - equal() { - if (this.stack.length < 2) throw new Error('Not enough values to compare'); - const b = this.stack.pop(); - const a = this.stack.pop(); - this.stack.push(a === b ? '1' : '0'); - } - - equalVerify() { - this.equal(); - this.verify(); - } +'use strict'; - oneEqual() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.stack.pop(); - this.stack.push(value === '1' ? '1' : '0'); - } - - zeroNotEqual() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.stack.pop(); - this.stack.push(value !== '0' ? '1' : '0'); - } - - // Bitwise operations - and() { - if (this.stack.length < 2) throw new Error('Not enough values for AND'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a & b).toString()); - } - - or() { - if (this.stack.length < 2) throw new Error('Not enough values for OR'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a | b).toString()); - } - - xor() { - if (this.stack.length < 2) throw new Error('Not enough values for XOR'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a ^ b).toString()); - } - - not() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.parseNumber(this.stack.pop()); - this.stack.push((~value >>> 0).toString()); - } - - // Logical operations - boolAnd() { - if (this.stack.length < 2) throw new Error('Not enough values for boolean AND'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a && b) ? '1' : '0'); - } - - boolOr() { - if (this.stack.length < 2) throw new Error('Not enough values for boolean OR'); - const b = this.parseNumber(this.stack.pop()); - const a = this.parseNumber(this.stack.pop()); - this.stack.push((a || b) ? '1' : '0'); - } - - // Constants - pushZero() { this.stack.push('0'); } - pushOne() { this.stack.push('1'); } - pushTwo() { this.stack.push('2'); } - pushThree() { this.stack.push('3'); } - pushFour() { this.stack.push('4'); } - pushFive() { this.stack.push('5'); } - - // Verification - verify() { - if (this.stack.length === 0) throw new Error('Stack is empty'); - const value = this.stack.pop(); - if (value !== '1') throw new Error('Verification failed'); - } - - returnOp() { - throw new Error('OP_RETURN: Transaction marked as invalid'); - } - - // Utility functions - parseNumber(value) { - const num = parseInt(value, 10); - if (isNaN(num)) throw new Error(`Invalid number: ${value}`); - return num; - } - - runScript(script) { - this.stack = []; - const tokens = script.trim().split(/\s+/); - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - if (this.opcodes[token]) { - try { - this.opcodes[token].execute(); - } catch (error) { - throw new Error(`Error at token ${i + 1} (${token}): ${error.message}`); - } - } else { - // Treat as literal value - this.stack.push(token); - } - } - } - - renderStack() { - const container = document.getElementById('stackContainer'); - - if (this.stack.length === 0) { - container.innerHTML = ` -
- -

Stack will appear here when you run a script

-
- `; - return; - } - - container.innerHTML = this.stack.map((item, index) => ` -
- ${item} - #${this.stack.length - index} -
- `).join(''); - } - - showStatus(message, type = 'success') { - const statusDiv = document.getElementById('status'); - statusDiv.innerHTML = `
${message}
`; - setTimeout(() => { - statusDiv.innerHTML = ''; - }, 5000); - } -} - -// Global functions for UI interaction -const engine = new BitcoinScriptEngine(); - -function runScript() { - const script = document.getElementById('scriptInput').value.trim(); - - if (!script) { - engine.showStatus('Please enter a script to run', 'error'); - return; - } - - try { - engine.runScript(script); - engine.renderStack(); - engine.showStatus(`✅ Script executed successfully! Stack has ${engine.stack.length} items.`); - } catch (error) { - engine.showStatus(`❌ ${error.message}`, 'error'); - engine.renderStack(); - } -} - -function clearScript() { - document.getElementById('scriptInput').value = ''; - engine.stack = []; - engine.renderStack(); - document.getElementById('status').innerHTML = ''; -} - -function shareScript() { - const script = document.getElementById('scriptInput').value; - if (!script) { - engine.showStatus('No script to share', 'error'); - return; - } - - const url = `${window.location.origin}${window.location.pathname}?script=${encodeURIComponent(script)}`; - navigator.clipboard.writeText(url).then(() => { - engine.showStatus('🔗 Script URL copied to clipboard!'); - }).catch(() => { - engine.showStatus('Failed to copy URL', 'error'); - }); +class BitcoinScriptEngine { + constructor() { + this.stack = []; + this.opcodes = this.initializeOpcodes(); + this.sampleScripts = this.initializeSampleScripts(); + } + + initializeOpcodes() { + return { + OP_DUP:{description:'Duplicate the top stack item',category:'Stack',execute:()=>this.dup()},OP_2DUP:{description:'Duplicate the top two stack items',category:'Stack',execute:()=>this.dup2()},OP_DROP:{description:'Remove the top stack item',category:'Stack',execute:()=>this.drop()},OP_SWAP:{description:'Swap the top two stack items',category:'Stack',execute:()=>this.swap()},OP_OVER:{description:'Copy the second-to-top item to the top',category:'Stack',execute:()=>this.over()},OP_ROT:{description:'Rotate the top three stack items',category:'Stack',execute:()=>this.rot()}, + OP_ADD:{description:'Add the top two numeric values',category:'Arithmetic',execute:()=>this.add()},OP_SUB:{description:'Subtract top value from second value',category:'Arithmetic',execute:()=>this.sub()},OP_MUL:{description:'Educational-only multiply operation',category:'Arithmetic',execute:()=>this.mul()},OP_DIV:{description:'Educational-only integer division',category:'Arithmetic',execute:()=>this.div()},OP_MOD:{description:'Educational-only modulo',category:'Arithmetic',execute:()=>this.mod()}, + OP_EQUAL:{description:'Return 1 when top two items match',category:'Comparison',execute:()=>this.equal()},OP_EQUALVERIFY:{description:'Run OP_EQUAL then OP_VERIFY',category:'Comparison',execute:()=>this.equalVerify()},OP_1EQUAL:{description:'Return 1 when top item equals 1',category:'Comparison',execute:()=>this.oneEqual()},OP_0NOTEQUAL:{description:'Return 1 when top item is not 0',category:'Comparison',execute:()=>this.zeroNotEqual()}, + OP_AND:{description:'Educational-only bitwise AND',category:'Bitwise',execute:()=>this.and()},OP_OR:{description:'Educational-only bitwise OR',category:'Bitwise',execute:()=>this.or()},OP_XOR:{description:'Educational-only bitwise XOR',category:'Bitwise',execute:()=>this.xor()},OP_NOT:{description:'Return 1 for 0, otherwise 0',category:'Logical',execute:()=>this.not()},OP_BOOLAND:{description:'Boolean AND of top two items',category:'Logical',execute:()=>this.boolAnd()},OP_BOOLOR:{description:'Boolean OR of top two items',category:'Logical',execute:()=>this.boolOr()}, + OP_0:{description:'Push 0',category:'Constants',execute:()=>this.push('0')},OP_1:{description:'Push 1',category:'Constants',execute:()=>this.push('1')},OP_2:{description:'Push 2',category:'Constants',execute:()=>this.push('2')},OP_3:{description:'Push 3',category:'Constants',execute:()=>this.push('3')},OP_4:{description:'Push 4',category:'Constants',execute:()=>this.push('4')},OP_5:{description:'Push 5',category:'Constants',execute:()=>this.push('5')},OP_6:{description:'Push 6',category:'Constants',execute:()=>this.push('6')},OP_7:{description:'Push 7',category:'Constants',execute:()=>this.push('7')},OP_8:{description:'Push 8',category:'Constants',execute:()=>this.push('8')},OP_9:{description:'Push 9',category:'Constants',execute:()=>this.push('9')},OP_10:{description:'Push 10',category:'Constants',execute:()=>this.push('10')}, + OP_VERIFY:{description:'Fail unless top item is true',category:'Verification',execute:()=>this.verify()},OP_RETURN:{description:'Immediately fail execution',category:'Verification',execute:()=>this.returnOp()} + }; + } + + initializeSampleScripts(){return{basic:'OP_1 OP_2 OP_ADD OP_DUP',comparison:'OP_5 OP_3 OP_ADD OP_8 OP_EQUAL',stack:'OP_1 OP_2 OP_3 OP_ROT OP_SWAP',verify:'OP_2 OP_3 OP_ADD OP_5 OP_EQUALVERIFY OP_1'};} + tokenize(script){return script.trim().split(/\s+/).filter(Boolean);} isLiteral(t){return/^-?\d+$/.test(t);} push(v){this.stack.push(String(v));} + requireStackSize(n,m='Not enough items on stack'){if(this.stack.lengththis.runAll());document.querySelector('[data-action="step"]').addEventListener('click',()=>this.step());document.querySelector('[data-action="reset"]').addEventListener('click',()=>this.resetTrace());document.querySelector('[data-action="clear"]').addEventListener('click',()=>this.clearScript());document.querySelector('[data-action="share"]').addEventListener('click',()=>this.shareScript());document.querySelectorAll('[data-sample]').forEach(b=>b.addEventListener('click',()=>this.loadSample(b.dataset.sample)));document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();this.runAll();}});} + compileTrace(){const script=this.scriptInput.value.trim();if(!script){this.showStatus('Enter a script first.','error');return false;}this.trace=this.engine.buildTrace(script);this.activeStep=-1;return true;} + runAll(){if(!this.compileTrace())return;this.activeStep=this.trace.length-1;this.renderTrace();this.renderStack(this.trace.at(-1)?.after||[]);const failed=this.trace.find(t=>!t.ok);this.showStatus(failed?`Execution stopped: ${failed.error}`:`Execution complete. Steps: ${this.trace.length}.`,failed?'error':'success');} + step(){if(this.trace.length===0&&!this.compileTrace())return;if(this.activeStep{const row=document.createElement('div');row.className=`trace-row${i===this.activeStep?' active':''}${!t.ok?' error':''}`;const main=document.createElement('div');main.textContent=`${String(t.index+1).padStart(2,'0')} > ${t.token}`;const before=document.createElement('small');before.textContent=`before [${t.before.join(', ')}] -> after [${t.after.join(', ')}]`;row.append(main,before);if(t.error){const err=document.createElement('small');err.textContent=`error: ${t.error}`;row.appendChild(err);}this.traceContainer.appendChild(row);});} + renderStack(stack=[]){this.stackContainer.textContent='';if(stack.length===0){const p=document.createElement('p');p.textContent='[] empty stack';this.stackContainer.appendChild(p);return;}stack.forEach((item,i)=>{const row=document.createElement('div');row.className='stack-item';const v=document.createElement('span');v.textContent=item;const l=document.createElement('span');l.textContent=`#${stack.length-i}`;row.append(v,l);this.stackContainer.appendChild(row);});} + renderOpcodes(){this.opcodesGrid.textContent='';const cats=[...new Set(Object.values(this.engine.opcodes).map(o=>o.category))];cats.forEach(c=>{const h=document.createElement('h3');h.textContent=c;h.style.gridColumn='1/-1';this.opcodesGrid.appendChild(h);Object.entries(this.engine.opcodes).filter(([,o])=>o.category===c).forEach(([name,o])=>{const card=document.createElement('button');card.type='button';card.className='opcode-card';card.addEventListener('click',()=>this.insertOpcode(name));const n=document.createElement('div');n.textContent=name;const d=document.createElement('small');d.textContent=o.description;card.append(n,d);this.opcodesGrid.appendChild(card);});});} + insertOpcode(name){const current=this.scriptInput.value.trim();this.scriptInput.value=current?`${current} ${name}`:name;this.resetTrace();this.scriptInput.focus();} + showStatus(msg,type){this.status.textContent=`${type.toUpperCase()}: ${msg}`;} clearStatus(){this.status.textContent='';} } - -// Load script from URL parameters -window.addEventListener('load', () => { - const urlParams = new URLSearchParams(window.location.search); - const script = urlParams.get('script'); - if (script) { - document.getElementById('scriptInput').value = decodeURIComponent(script); - runScript(); - } -}); - -// Add keyboard shortcuts -document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'Enter') { - runScript(); - } -}); +window.addEventListener('DOMContentLoaded',()=>new PlaygroundUI(new BitcoinScriptEngine())); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..aa6a5c3 --- /dev/null +++ b/styles.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;background:#050505;color:#33ff66;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}button,textarea{font:inherit}.terminal-shell{max-width:1500px;margin:0 auto;padding:24px}.terminal-header{display:flex;justify-content:space-between;gap:20px;align-items:flex-start}.eyebrow{color:#ff9900;font-size:12px;margin:0 0 8px}.subtitle{color:#9ad8a7}.cursor{animation:blink 1s steps(1) infinite}.status-pill{border:1px solid #33ff66;padding:8px 12px}.notice,.panel{border:1px solid #143d1e;background:#081108}.notice{padding:12px;margin:18px 0}.lab-grid{display:grid;grid-template-columns:1.2fr 1fr .8fr;gap:16px}.panel{padding:16px}.panel-title{color:#ff9900;font-size:12px;margin-bottom:10px}.script-input{width:100%;min-height:220px;background:#000;color:#33ff66;border:1px solid #1f5a2d;padding:12px}.controls,.samples{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.btn,.samples button,.opcode-card{background:#0c180c;color:#33ff66;border:1px solid #1f5a2d;padding:10px 12px;cursor:pointer}.btn.primary{border-color:#ff9900;color:#ff9900}.btn.ghost{color:#9ad8a7}.trace-container,.stack-container{min-height:340px;max-height:340px;overflow:auto;border:1px solid #143d1e;padding:10px;background:#000}.trace-row,.stack-item{padding:8px;border-bottom:1px solid #0f2c16}.trace-row.active{background:#102710}.trace-row.error{color:#ff6666}.opcodes-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}.opcode-card{text-align:left}.status-line{margin-top:12px;color:#9ad8a7}.sr-only{position:absolute;left:-9999px}@keyframes blink{50%{opacity:0}}@media(max-width:1100px){.lab-grid{grid-template-columns:1fr}} \ No newline at end of file