Ensemble · seating & setup — Map supplied instrument sections to ordered row-chair IDs and reconcile the same chair totals with a stage setup sheet, preserving the…
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ensemble · seating & setup</title><style>*{box-sizing:border-box}body{margin:0;font:16px/1.5 Arial,sans-serif;color:var(--ink);background:var(--bg)}main{max-width:1180px;margin:auto;padding:26px 28px 50px}h1,h2,h3,p{margin:0}h1{font-size:34px;line-height:1.15;letter-spacing:-.025em}h2{font-size:23px;margin-bottom:12px}h3{font-size:17px}header{margin-bottom:22px}header p{margin-top:8px}button,input,select,textarea{font:inherit}button{cursor:pointer;min-height:44px;padding:9px 15px;border:1px solid var(--line);border-radius:7px;background:white;color:var(--ink)}button:hover{filter:brightness(.94)}button.active,.primary{background:var(--accent);color:white;border-color:var(--accent)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #117ec1;outline-offset:3px}input,select,textarea{max-width:100%;padding:9px;border:1px solid #8c9caa;border-radius:5px;background:white;color:var(--ink)}input{width:100%;min-width:0}textarea{width:100%;resize:vertical}label{display:block;font-size:14px;font-weight:bold;margin:12px 0 5px}.small,small{font-size:13px;color:var(--muted)}.eyebrow{font-size:12px;letter-spacing:.13em;text-transform:uppercase;margin-bottom:8px;font-weight:bold}.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}.split{display:grid;grid-template-columns:1.5fr 1fr;gap:24px;align-items:start}.paper{background:white;border:1px solid var(--line);padding:25px;border-radius:10px}.fields{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.message{padding:12px 14px;border-left:4px solid var(--accent);background:var(--tint);margin:15px 0;overflow-wrap:anywhere}.big{font-size:40px;font-weight:700;line-height:1.2}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;padding:11px 8px;border-bottom:1px solid var(--line);vertical-align:top}th{font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}footer{margin-top:28px;padding-top:18px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}footer p+p{margin-top:8px}[hidden]{display:none!important}.stack{display:grid;gap:12px}.tag{font-size:12px;padding:4px 8px;border-radius:20px;background:var(--tint);display:inline-block}.section{margin-top:26px}.error{color:#a32727}svg{max-width:100%;height:auto;display:block}button[disabled]{opacity:.5;cursor:default}@media(max-width:800px){main{padding:22px 18px}.split{grid-template-columns:1fr}h1{font-size:30px}.paper{padding:20px}}@media(max-width:450px){main{padding:18px 12px}h1{font-size:27px}.fields{gap:0 10px}.paper{padding:16px}.big{font-size:34px}th,td{padding:9px 5px}.toolbar button{flex:1;padding:8px}}
:root{--bg:#edf5f1;--ink:#183c34;--muted:#517267;--accent:#23634e;--line:#c7ddd1;--tint:#e4f2e9}.stage{background:#fafdfb;border-radius:50% 50% 8px 8px;border:1px solid #c7ddd1;padding:15px}.chair{font-family:Arial,sans-serif}.setup-row{padding:14px 0;border-top:1px solid var(--line)}.setup-row strong{display:block}.legend{display:flex;gap:12px;margin-bottom:10px}.legend span{padding:4px 10px;border-radius:5px}.W{background:#dbeef9;color:#20556f}.B{background:#f7dfb7;color:#70501d}.summary{display:flex;gap:20px;flex-wrap:wrap;margin:15px 0}.summary b{font-size:28px;display:block}#mapping{font-family:monospace;font-size:14px}.stage svg text{font-family:Arial,sans-serif}textarea{min-height:190px}</style></head><body><main><header><div class="eyebrow">Ensemble setup · fictional fixture v1</div><h1>Eight players. Eight chair IDs.</h1><p>Director at bottom; chair numbering runs viewer left → right.</p></header><div class="split"><section><div class="summary"><div><b id="chair-count">8</b>mapped chairs</div><div><b id="roster-count">8</b>roster players</div><div><b>3 / 3 / 2</b>row capacities</div></div><div class="stage"><div class="legend"><span class="W">W · supplied section</span><span class="B">B · supplied section</span></div><svg id="stage" viewBox="0 0 600 430" role="img" aria-label="Three curved rows of labeled seats, director at bottom"></svg></div><p id="validation" class="message" role="status"></p></section><aside class="paper"><h2>Crew setup sheet</h2><p class="small">Same assignments · fixture v1 · director at bottom</p><div id="setup"></div><p id="unassigned" class="message"></p><div class="fields"><div><label for="player-a">First player ID</label><input id="player-a" value="P4"></div><div><label for="player-b">Second player ID</label><input id="player-b" value="P5"></div></div><div class="toolbar"><button type="button" id="swap" class="primary">Swap players</button><button type="button" id="ninth">Stage ninth player</button><button type="button" id="reset">Reset fixture</button></div><p id="swap-status" role="status" class="error"></p></aside></div><section class="paper section"><h2>Inspect a proposed assignment map</h2><p class="small">One chair=player per line. A proposal commits only after full validation; rejected proposals leave the current chart and crew sheet unchanged.</p><label for="mapping">Eight row-chair assignments</label><textarea id="mapping"></textarea><div class="toolbar"><button type="button" id="apply" class="primary">Validate & apply proposal</button></div></section><footer><p>Ordering schematic, not a venue scale, acoustic recommendation, audition rank or access plan. W/B are supplied codes; exact instruments and actual names are unconfirmed.</p><p>Stands, percussion, equipment, access paths, stage dimensions and director approval: TO CONFIRM. Eight chairs do not imply eight stands. HTML companion only; native drawing/print approval and personal roster storage are not provided.</p></footer></main><script>'use strict';const $=id=>document.getElementById(id);const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const seats=['R1C1','R1C2','R1C3','R2C1','R2C2','R2C3','R3C1','R3C2'];let mapping={},roster=[];function section(p){return /^P[1-4]$/.test(p)?'W':/^P[5-8]$/.test(p)?'B':'?';}function inspect(map,people){const errors=[],values=Object.values(map);if(Object.keys(map).length!==8||seats.some(s=>!map[s]))errors.push('All eight known chair IDs are required.');if(Object.keys(map).some(s=>!seats.includes(s)))errors.push('Unknown chair ID.');const dup=[...new Set(values.filter((v,i)=>values.indexOf(v)!==i))];if(dup.length)errors.push('Duplicate player: '+dup.join(', '));const unknown=values.filter(v=>!people.includes(v));if(unknown.length)errors.push('Unknown player: '+unknown.join(', '));const unassigned=people.filter(p=>!values.includes(p));if(unassigned.length)errors.push('Unassigned: '+unassigned.join(', '));return {errors,unassigned};}
function render(){const check=inspect(mapping,roster);$('chair-count').textContent=Object.keys(mapping).length;$('roster-count').textContent=roster.length;$('validation').textContent=check.errors.length?'Partial draft · '+check.errors.join('. '):'All 8 players have one chair. W 4 + B 4 = 8.';$('unassigned').textContent=check.unassigned.length?'Unassigned: '+check.unassigned.join(', ')+' · capacity shortfall; no chair added.':'Unassigned: none. Equipment remains unconfirmed.';let svg='<path d="M125 85Q300 -25 475 85 M155 195Q300 115 445 195 M205 285Q300 245 395 285" fill="none" stroke="#c2d9ca" stroke-width="2"/>';const coords=[[205,285],[300,265],[395,285],[155,195],[300,155],[445,195],[125,85],[475,85]];seats.forEach((seat,i)=>{const p=mapping[seat],s=section(p),[x,y]=coords[i];svg+='<g><rect x="'+(x-47)+'" y="'+(y-29)+'" width="94" height="65" rx="11" fill="'+(s==='W'?'#dbeef9':s==='B'?'#f7dfb7':'#eee')+'" stroke="#638d79"/><text x="'+x+'" y="'+(y-9)+'" text-anchor="middle" font-size="20">'+seat+'</text><text x="'+x+'" y="'+(y+14)+'" text-anchor="middle" font-size="24" font-weight="bold">'+esc(p)+' / '+s+'</text></g>';});svg+='<path d="M280 393L300 365L320 393Z" fill="#23634e"/><text x="300" y="417" text-anchor="middle" font-size="16">DIRECTOR · not a player chair</text>';$('stage').innerHTML=svg;$('setup').innerHTML=[1,2,3].map(row=>{const ids=seats.filter(s=>s[1]===String(row)),pattern=ids.map(s=>section(mapping[s]));let boundaries=pattern.slice(1).filter((s,i)=>s!==pattern[i]).length;return '<div class="setup-row"><strong>Row '+row+' · '+ids.length+' chairs</strong>'+pattern.join('/')+' · '+boundaries+' section boundaries<br><small>'+ids.map(s=>s+'='+mapping[s]+'/'+section(mapping[s])).join(' · ')+'</small></div>';}).join('');$('mapping').value=seats.map(s=>s+'='+mapping[s]).join('\n');}
function reset(){roster=Array.from({length:8},(_,i)=>'P'+(i+1));mapping=Object.fromEntries(seats.map((s,i)=>[s,'P'+(i+1)]));$('player-a').value='P4';$('player-b').value='P5';$('swap-status').textContent='';render();}
function swap(){const a=$('player-a').value.trim().toUpperCase(),b=$('player-b').value.trim().toUpperCase();if(a===b||!roster.includes(a)||!roster.includes(b)){$('swap-status').textContent='Choose two different known player IDs. Current mapping unchanged.';return;}const proposal={...mapping};seats.forEach(s=>{if(mapping[s]===a)proposal[s]=b;else if(mapping[s]===b)proposal[s]=a;});const c=inspect(proposal,roster);if(c.errors.length){$('swap-status').textContent='Proposal rejected: '+c.errors.join('. ');return;}mapping=proposal;render();$('swap-status').textContent='Swapped '+a+' and '+b+'. Chart and crew list updated together.';}
function apply(){const lines=$('mapping').value.trim().split(/\n/),proposal={};let bad=false;for(const line of lines){const match=line.trim().match(/^(R[1-3]C[1-3])\s*=\s*(P\d+)$/i);if(!match||proposal[match?.[1]?.toUpperCase()]){bad=true;break;}proposal[match[1].toUpperCase()]=match[2].toUpperCase();}const c=inspect(proposal,roster);if(bad||c.errors.length){$('swap-status').textContent='Proposal rejected: '+(bad?'Use unique chair=player lines.':c.errors.join('. '))+' Current chart unchanged.';return;}mapping=proposal;render();$('swap-status').textContent='Validated proposal applied.';}
$('swap').addEventListener('click',swap);$('apply').addEventListener('click',apply);$('ninth').addEventListener('click',()=>{if(!roster.includes('P9'))roster.push('P9');render();$('swap-status').textContent='P9 staged with section unknown. Supply a revised capacity before completion.';});$('reset').addEventListener('click',reset);reset();globalThis.App={inspect,reset,swap,apply,render,getState:()=>({mapping:{...mapping},roster:[...roster]})};</script></body></html>