Fan funding: the price sets the fans needed; limited edition as a switch
Deploy test site / deploy (push) Successful in 1m4s

The artist sets only the fan price; the number of fans needed follows from
it: the plant's 100-copy minimum, or the break-even if that is higher. The
goal selector of 6a62505 is removed from both demos.

make-a-record.html:
- step 4 leads with "Your price to fans" (input, slider from 300 THB,
  presets 800 to 2,000) and a live "You need N fans to press it, X THB in
  pre-orders", plus the price below which the minimum no longer covers
  the costs (about 640 THB at the defaults)
- limited edition is now a switch, off by default; the edition size stays
  visible but disabled until it is on, and switching it on adds the
  numbered edition to the record
- "per title" renamed for end users: "One-off, for this release", "once +
  per copy", "One-off setup", with a note that one-off costs count sides,
  not songs (a double LP has four)
- the tour lowers the price to 500 THB and back to show the fans needed
  rising from 100 to 152
artist-order.html: goal field removed, "Fans needed" label and hint.
artist-platform.md: the price-sets-the-goal decision (October 2026).
This commit is contained in:
grabowski committed 2026-10-16 08:54:15 +07:00
1 parent 6a62505aa6
commit e36d7b00a0
3 files changed
+46 -54

No files matched your search

+31 -34
View File
@@ -221,7 +221,9 @@ document.documentElement.setAttribute("data-theme",t);})();
.qchips .chip { padding: 4px 12px; font-size: 13px; }
.qchips .chip.on::before { content: ""; }
.qchips .chip:disabled { opacity: 0.35; cursor: not-allowed; }
#goalChips { margin: 2px 0 4px; }
.sw.acc { color: var(--ink); font-size: 14px; } .sw.acc input:checked + .tr { background: var(--acc); }
input:disabled { opacity: 0.4; cursor: not-allowed; }
.need { font-size: 15px; margin: 4px 0 2px; } .need b { color: var(--acc); font-size: 19px; }
.kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 16px 0 10px; }
.kpis.k4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 640px) { .kpis, .kpis.k4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@@ -553,7 +555,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="eyebrow">Step 3 of 5</div>
<h1>Your price</h1>
<p class="lead">Every line itemised, with no fees that appear later. The price per copy falls fast as the run grows.</p>
<div class="inv"><span class="il">Investor note</span><p><b>Why short runs work here.</b> The one Thai plant on file quotes a 150-copy minimum that is "rarely accepted because the cost is way too high": its per-title cost (imported stampers, outsourced cutting) does not shrink with the run. In-house cutting and plating collapses that cost.</p><p>Prices on this page are indicative, at Western benchmark rates. The Thai market pays 650 to 950 THB a copy at 300 copies today; the real price list follows market validation.</p></div>
<div class="inv"><span class="il">Investor note</span><p><b>Why short runs work here.</b> The one Thai plant on file quotes a 150-copy minimum that is "rarely accepted because the cost is way too high": its one-off cost per release (imported stampers, outsourced cutting) does not shrink with the run. In-house cutting and plating collapses that cost.</p><p>Prices on this page are indicative, at Western benchmark rates. The Thai market pays 650 to 950 THB a copy at 300 copies today; the real price list follows market validation.</p></div>
<div class="panel block">
<div class="qtyhead">
<div class="field"><span class="fl">Copies</span><div class="qbig"><input type="number" id="qty" min="100" max="5000" step="10" aria-label="Copies"><span>copies</span></div></div>
@@ -584,10 +586,9 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="inv"><span class="il">Investor note</span><p><b>A plant-owned platform, not a broker.</b> Comparable platforms broker other people's plants; CNX controls price, schedule and quality and keeps the manufacturing margin instead of a commission. Fan money is ring-fenced: the lesson of Qrates, whose service is suspended with funded projects caught in the wind-down.</p></div>
<div class="panel block">
<div class="fgrid">
<div class="field"><span class="fl">Fan price per copy</span><div class="inline"><input type="number" id="fanPrice" min="300" max="5000" step="50" aria-label="Fan price"><span>THB</span></div><input type="range" id="fanR" min="500" max="3000" step="50" aria-label="Fan price"></div>
<div class="field" style="grid-column:1/-1"><span class="fl">Your price to fans, per copy</span><div class="inline"><input type="number" id="fanPrice" min="300" max="5000" step="50" aria-label="Fan price"><span>THB</span></div><input type="range" id="fanR" min="300" max="3000" step="50" aria-label="Fan price"><div class="qchips" id="fpChips"><button type="button" class="chip" data-fp="800">800</button><button type="button" class="chip" data-fp="1000">1,000</button><button type="button" class="chip" data-fp="1200">1,200</button><button type="button" class="chip" data-fp="1500">1,500</button><button type="button" class="chip" data-fp="2000">2,000</button></div><div class="need" id="need"></div><small class="mute" id="needHint"></small></div>
<div class="field"><span class="fl">Campaign length</span><div class="inline"><b id="daysV"></b><span>days</span></div><input type="range" id="days" min="14" max="45" step="1" aria-label="Days"></div>
<div class="field" style="grid-column:1/-1"><span class="fl">Goal: how many fans you need</span><div class="inline"><input type="number" id="goalF" min="100" max="5000" step="10" aria-label="Goal in fans"><span>fans</span><span class="mute" id="goalAmt"></span></div><input type="range" id="goalR" min="100" max="1000" step="10" aria-label="Goal in fans"><div class="qchips" id="goalChips"></div><small class="mute" id="goalHint"></small></div>
<div class="field"><span class="fl">Limited edition</span><div class="inline"><input type="number" id="cap" min="0" max="5000" step="50" aria-label="Edition cap"><small class="mute">copies at most, 0 = no limit</small></div></div>
<div class="field" id="fCap"><span class="fl">Limited edition</span><div class="inline"><label class="sw acc"><input type="checkbox" id="limT"><span class="tr"></span><span>Stop at</span></label><input type="number" id="cap" min="100" max="5000" step="50" aria-label="Edition size"><small class="mute">numbered copies</small></div></div>
<div class="field"><span class="fl">Copies for yourself</span><div class="inline"><input type="number" id="acop" min="0" max="200" step="5" aria-label="Copies for yourself"><small class="mute">pressed with the run</small></div></div>
</div>
<label class="rights" style="margin:16px 0 0"><input type="checkbox" id="vat"> <span>Show the numbers with 7 % VAT taken out of the fan price</span></label>
@@ -699,7 +700,7 @@ const S={
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
mastering:true,tps:5,loud:false,artist:"Mae Ping Collective",title:"Night Market Songs",
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
fanPrice:1200,goalFans:0,days:30,cap:0,artistCopies:0,vat:false,
fanPrice:1200,days:30,cap:0,limited:false,capN:300,artistCopies:0,vat:false,
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
visited:{1:true}
};
@@ -767,10 +768,10 @@ function campEcon(){
const cAt=n=>quote(Math.max(1,n)).cSum;
let be=null;
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
/* the artist chooses the goal in fans; it can never go below the plant minimum or the break-even (the floor) */
const min=R.minRun.v,floor=be==null?null:Math.max(min,be),goal=floor==null?null:Math.max(floor,Math.round(S.goalFans||0));
/* the artist sets the fan price; the fans needed follow: the plant minimum, or the break-even if that is higher */
const min=R.minRun.v,goal=be==null?null:Math.max(min,be);
const at=n=>{const run=n+ac,c=cAt(run),net=n*net1,cost=tSum+run*c;return {n,run,c,net,cost,keep:net-cost,per:(net-cost)/n};};
return {p,fee,vat,net1,tSum,be,min,floor,goal,at,cAt};
return {p,fee,vat,net1,tSum,be,min,goal,at,cAt};
}
function finalPledges(E){
if(!E.goal)return 0;const g=E.goal;
@@ -1035,11 +1036,11 @@ function renderPrice(){
if(document.activeElement!==$("#qty"))$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
$("#kpis").innerHTML=`<div class="kpi hl"><small>Per copy, all in</small><b>${money(Q.per)}</b></div><div class="kpi"><small>Order total</small><b>${money(Q.total)}</b></div><div class="kpi"><small>Ordered today, ships</small><b>${monthRange(ship[0],ship[1])}</b></div>`;
$("#curve").innerHTML=curveSVG();
$("#bdSum").textContent=`${money(Q.tSum)} per title + ${money(Q.cSum)} per copy`;
$("#bdSum").textContent=`${money(Q.tSum)} once + ${money(Q.cSum)} per copy`;
const tr=x=>`<tr><td>${x.t}${x.pass!=null?`<br><span class="pass">partner price ${money(x.pass)} + ${Math.round(R.handling.v*100)} % handling</span>`:""}</td><td class="n">${x.u?x.n+" "+x.u:""}</td><td class="n">${money(x.a)}</td></tr>`;
$("#offer").innerHTML=`<table><tbody><tr><th>Once per title</th><th class="n"></th><th class="n">Amount</th></tr>${Q.T.map(tr).join("")}<tr class="sub"><td colspan="2">Per title</td><td class="n">${money(Q.tSum)}</td></tr>
$("#offer").innerHTML=`<table><tbody><tr><th>One-off, for this release</th><th class="n"></th><th class="n">Amount</th></tr>${Q.T.map(tr).join("")}<tr class="sub"><td colspan="2">One-off total</td><td class="n">${money(Q.tSum)}</td></tr>
<tr><th>Per copy</th><th class="n"></th><th class="n">Per copy</th></tr>${Q.C.map(tr).join("")}<tr class="sub"><td colspan="2">${money(Q.cSum)} × ${fmtInt(S.qty)} copies</td><td class="n">${money(Q.cSum*S.qty)}</td></tr>
<tr class="tot"><td colspan="2">Order total</td><td class="n">${money(Q.total)}</td></tr></tbody></table><p class="hint">Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.</p>`;
<tr class="tot"><td colspan="2">Order total</td><td class="n">${money(Q.total)}</td></tr></tbody></table><p class="hint">One-off costs are paid once per release, however many copies. They count sides, not songs: mastering, cutting and plating are done side by side, so 8 songs or 12 cost the same and a double LP has four sides. Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.</p>`;
$("#leadP").innerHTML=`<h2>When it ships</h2><div class="tl2"><div class="a" style="flex:${L.pw[0]+L.pw[1]}"><b>${L.pw[0]} to ${L.pw[1]} weeks</b>your approvals and the metal work</div><div class="b" style="flex:${L.qw[0]+L.qw[1]}"><b>${L.qw[0]} to ${L.qw[1]} weeks, quoted</b>from your test-pressing approval to dispatch</div></div><p class="hint">Ordered today, it ships about <b>${monthRange(ship[0],ship[1])}</b>. The quote counts from your last approval, because the plant cannot run a clock that is waiting for you. ${L.printed?"Supplying your own jacket saves 1 to 2 weeks.":"No printed jacket to wait for, which saves 1 to 2 weeks."}</p>`;
const E=campEcon();
$("#pDirect").innerHTML=`Deposit today <b>${money(Q.total*R.deposit.v)}</b>`;
@@ -1056,20 +1057,18 @@ function renderFund(){
if(E.goal==null)h=`<div class="note bad">At ${money(E.p)} a copy, what is left after payment fees${S.vat?" and VAT":""} does not cover the cost per copy. Raise the fan price.</div>`;
else{
const g=E.at(E.goal),ns=[...new Set([E.goal,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b),rows=ns.map(E.at),mx=Math.max(...rows.map(r=>r.keep),1);
h=`<div class="kpis k4"><div class="kpi hl"><small>Campaign goal</small><b>${E.goal} fans</b><em>${money(E.goal*S.fanPrice)} in pre-orders</em></div><div class="kpi"><small>Your break-even</small><b>${E.be} fans</b></div><div class="kpi"><small>You keep at the goal</small><b>${money(g.per)}</b><em>a copy</em></div><div class="kpi"><small>That is</small><b>${money(g.keep)}</b><em>in total</em></div></div>
h=`<div class="kpis k4"><div class="kpi hl"><small>Fans needed</small><b>${E.goal} fans</b><em>${money(E.goal*S.fanPrice)} in pre-orders</em></div><div class="kpi"><small>Your break-even</small><b>${E.be} fans</b></div><div class="kpi"><small>You keep at the goal</small><b>${money(g.per)}</b><em>a copy</em></div><div class="kpi"><small>That is</small><b>${money(g.keep)}</b><em>in total</em></div></div>
${S.cap&&S.cap<E.goal?`<div class="note bad">The edition is capped at ${S.cap}, below the goal of ${E.goal}: this campaign cannot succeed.</div>`:""}
<div class="panel"><h2>What you keep if more fans join</h2><div class="keep">${rows.map(r=>`<div class="krow${r.n===E.goal?" goal":""}"><span>${fmtInt(r.n)} fans</span><div class="kb"><i style="width:${Math.max(2,r.keep/mx*100).toFixed(1)}%"></i></div><div class="kv"><b>${money(r.keep)}</b><small>${money(r.per)} a copy</small></div></div>`).join("")}</div>
<p class="hint">You choose the goal; it cannot go below the plant's ${E.min}-copy minimum or your break-even, whichever is larger. A higher goal is all or nothing too: miss it and every fan is refunded. What you keep is after payment fees of about ${(R.fee.v*100).toFixed(1)} %${S.vat?" and VAT":""}, the per-title cost and the pressing. Shipping is paid by each fan at cost.</p></div>`;
<p class="hint">What you keep is after payment fees of about ${(R.fee.v*100).toFixed(1)} %${S.vat?" and VAT":""}, the one-off setup and the pressing. More fans than needed is good news: the one-off setup spreads over more copies. Shipping is paid by each fan at cost.</p></div>`;
}
$("#fundEcon").innerHTML=h;
const gi=$("#goalF"),gr=$("#goalR"),none=E.floor==null;gi.disabled=none;gr.disabled=none;
if(none){$("#goalAmt").textContent="";$("#goalHint").textContent="Raise the fan price first: below cost there is no goal to set.";$("#goalChips").innerHTML="";}
else{gi.min=E.floor;if(document.activeElement!==gi){gi.value=E.goal;gi.classList.remove("err");}
const top=Math.max(E.floor+50,S.cap&&S.cap>=E.floor?S.cap:1000,E.goal);gr.min=E.floor;gr.max=top;gr.value=E.goal;
$("#goalAmt").textContent="= "+money(E.goal*S.fanPrice)+" in pre-orders";
const why=E.be>E.min?"your break-even of "+E.be+" fans":"the plant's "+E.min+"-copy minimum";
$("#goalChips").innerHTML=`<button type="button" class="chip${E.goal===E.floor?" on":""}" data-g="0" title="The lowest goal possible">Lowest: ${E.floor}</button>`+[150,200,300,500,1000].map(n=>{const lo=n<E.floor,hi=S.cap>0&&n>S.cap,dis=lo||hi;return `<button type="button" class="chip${!dis&&n===E.goal?" on":""}" data-g="${n}"${dis?` disabled title="${lo?"Below "+esc(why):"Above your edition cap of "+S.cap}"`:""}>${fmtInt(n)}</button>`;}).join("");
$("#goalHint").textContent=E.goal===E.floor?"The lowest goal possible: "+why+".":"Your choice, above the lowest possible goal of "+E.floor+" fans ("+why+").";}
$$("#fpChips .chip").forEach(c=>c.classList.toggle("on",+c.dataset.fp===S.fanPrice));
/* the lowest fan price at which the plant minimum covers the costs; below it the break-even sets the fans needed */
const ac=S.artistCopies,keepShare=1-R.fee.v-(S.vat?7/107:0),pMin=Math.ceil((E.tSum+(E.min+ac)*E.cAt(E.min+ac))/E.min/keepShare/10)*10;
$("#need").innerHTML=E.goal==null?`<span class="bad-t">This price does not cover the cost per copy.</span>`:`You need <b>${fmtInt(E.goal)} fans</b> to press it, ${money(E.goal*S.fanPrice)} in pre-orders`;
$("#needHint").textContent=E.goal==null?"Raise the price.":E.be>E.min?"Your break-even is "+E.be+" fans at this price, above the plant's "+E.min+"-copy minimum. From "+money(pMin)+" a copy, "+E.min+" fans are enough.":"The plant presses from "+E.min+" copies. Below about "+money(pMin)+" a copy you would need more fans to cover the costs.";
$("#fCap").classList.toggle("off",!S.limited);$("#cap").disabled=!S.limited;$("#cap").title=S.limited?"":"Switch on to make it a limited edition";
$("#bLaunch").disabled=!canLaunch();
$("#launchMsg").textContent=launchHint()||"Simulated: no page goes live and nobody is charged.";
}
@@ -1169,7 +1168,7 @@ function updateS5(){
let oc="";
if(ended&&goal){
if(funded){const A=E.at(n);
oc=`<div class="panel ok-card block"><h2>Funded</h2><p class="mute" style="margin-top:0">${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.</p><div class="kvl"><span>Fan money after fees${S.vat?" and VAT":""}</span><b>${money(A.net)}</b><span>Per-title work</span><b>${money(-E.tSum)}</b><span>Pressing and packaging, ${fmtInt(A.run)} copies</span><b>${money(-A.run*A.c)}</b><span><b style="color:var(--ink)">You keep</b></span><b style="color:var(--acc);font-size:17px">${money(A.keep)}</b></div></div>`;}
oc=`<div class="panel ok-card block"><h2>Funded</h2><p class="mute" style="margin-top:0">${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.</p><div class="kvl"><span>Fan money after fees${S.vat?" and VAT":""}</span><b>${money(A.net)}</b><span>One-off setup</span><b>${money(-E.tSum)}</b><span>Pressing and packaging, ${fmtInt(A.run)} copies</span><b>${money(-A.run*A.c)}</b><span><b style="color:var(--ink)">You keep</b></span><b style="color:var(--acc);font-size:17px">${money(A.keep)}</b></div></div>`;}
else{const run=Math.max(n,E.min)+S.artistCopies,c=E.cAt(run),cost=E.tSum+run*c,net=n*E.net1,diff=net-cost,spare=Math.max(0,E.min-n);
if(S.outcomeAct==="refund")oc=`<div class="panel block"><h2>Refunded</h2><p class="mute">${fmtInt(n)} refunds of ${money(S.fanPrice)} went back to each fan's original payment method. Nothing was pressed; your master and artwork stay in your vault for another try.</p><button type="button" class="btn ghost" data-act="retry">Run it again</button></div>`;
else if(S.outcomeAct==="min")oc=`<div class="panel ok-card block"><h2>Going ahead at the minimum</h2><p class="mute">${diff<0?"You paid the gap of <b>"+money(-diff)+"</b>":"The fans' money covered the minimum run; you keep <b>"+money(diff)+"</b>"}, and you receive the ${spare} copies no fan claimed.</p></div>`;
@@ -1228,8 +1227,8 @@ function renderStage(){
$("#stSpecs").innerHTML=sp.map(x=>`<span>${esc(x)}</span>`).join("");
const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb;
if(camp){const E=campEcon();
pr=`<div class="sp"><div><small>Fan price</small><b class="pc">${money(S.fanPrice)}</b></div><div class="r"><small>Campaign goal</small><b>${E.goal?E.goal+" fans":"not reachable"}</b></div></div><div class="sl"><span>You keep at the goal</span><b>${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}</b></div>`;
mb=`<small>Fan price · goal</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?E.goal+" fans":""}</small>`;}
pr=`<div class="sp"><div><small>Fan price</small><b class="pc">${money(S.fanPrice)}</b></div><div class="r"><small>Fans needed</small><b>${E.goal?E.goal+" fans":"not reachable"}</b></div></div><div class="sl"><span>You keep at the goal</span><b>${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}</b></div>`;
mb=`<small>Fan price · fans needed</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?E.goal+" fans":""}</small>`;}
else{pr=`<div class="sp"><div><small>Per copy, all in</small><b class="pc">${money(Q.per)}</b></div><div class="r"><small>${fmtInt(S.qty)} copies</small><b>${money(Q.total)}</b></div></div><div class="sl"><span>Ordered today, ships</span><b>${monthRange(ship[0],ship[1])}</b></div>`;
mb=`<small>${fmtInt(S.qty)} copies · per copy</small><b>${money(Q.per)}</b>`;}
$("#stPrice").innerHTML=pr;
@@ -1247,11 +1246,11 @@ function go(s){
}
function approve(){if(!canApprove())return;S.gate1=true;go(3);}
function launch(){if(!canLaunch()){if(!S.gate1)go(2);return;}resetCampaign();S.mode="campaign";S.launched=true;S.launchDate=new Date(TODAY);go(5);startSim();renderAll();}
function setGoal(n){const E=campEcon();S.goalFans=E.floor!=null&&n<=E.floor?0:Math.max(0,Math.round(n));if(S.mode==="campaign")resetCampaign();renderAll();}
function setFanPrice(v){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}
function setQty(q){S.qty=Math.min(5000,Math.max(100,Math.round(q)));if(S.mode==="direct")resetCampaign();renderAll();}
function syncInputs(){
$("#artist").value=S.artist;$("#title").value=S.title;$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
$("#fanPrice").value=S.fanPrice;$("#fanR").value=Math.min(3000,Math.max(500,S.fanPrice));$("#days").value=S.days;$("#cap").value=S.cap;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
$("#fanPrice").value=S.fanPrice;$("#fanR").value=Math.min(3000,Math.max(500,S.fanPrice));$("#days").value=S.days;$("#cap").value=S.capN;$("#limT").checked=S.limited;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
}
function resetDemo(){
stopSim();stopProd();if(AU){AU.pause();AU=null;AUid=null;}
@@ -1299,7 +1298,7 @@ document.addEventListener("click",e=>{
const sw=e.target.closest(".swatch");if(sw){if(sw.disabled)return;const c=sw.closest("[data-c]").dataset.c;S[c]=sw.dataset.col;renderAll();return;}
const chip=e.target.closest(".chip[data-b]");if(chip){S[chip.dataset.b]=!S[chip.dataset.b];if(S.mode)resetCampaign();renderAll();return;}
const qc=e.target.closest(".chip[data-q]");if(qc){setQty(+qc.dataset.q);return;}
const gc=e.target.closest(".chip[data-g]");if(gc){if(gc.disabled)return;setGoal(+gc.dataset.g);return;}
const fc=e.target.closest(".chip[data-fp]");if(fc){setFanPrice(+fc.dataset.fp);return;}
const b=e.target.closest("button");if(!b||b.disabled)return;
const a=b.dataset.act;if(a&&ACT[a]){ACT[a](b);return;}
const g=b.closest("[data-k]");
@@ -1314,17 +1313,15 @@ document.addEventListener("input",e=>{
else if(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;renderSides();}
else if(id==="fanPrice"){const v=+t.value;if(v>=100&&v<=20000){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="fanR"){S.fanPrice=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="goalR")setGoal(+t.value);
else if(id==="goalF"){const v=Math.round(+t.value),E=campEcon();if(E.floor!=null&&v>=E.floor&&v<=5000){t.classList.remove("err");setGoal(v);}else t.classList.add("err");}
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="cap"){S.cap=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="cap"){const v=Math.round(+t.value)||0;if(v>=1){S.capN=v;if(S.limited)S.cap=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="acop"){S.artistCopies=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="simDay"){stopSim();S.simDay=+t.value;checkEnd();renderAll();}
});
document.addEventListener("change",e=>{
const t=e.target;
if(t.id==="qty"){setQty(Math.round((+t.value||100)/10)*10);t.value=S.qty;}
else if(t.id==="goalF"){const E=campEcon();if(E.floor==null)return;const v=Math.round(+t.value)||0;if(v<E.floor)toast("The goal cannot go below "+E.floor+" fans: "+(E.be>E.min?"your break-even.":"the plant's "+E.min+"-copy minimum."));setGoal(Math.min(5000,Math.max(E.floor,v)));t.classList.remove("err");t.value=campEcon().goal;}
else if(t.id==="limT"){S.limited=t.checked;S.cap=S.limited?S.capN:0;if(S.limited&&!S.numbered){S.numbered=true;toast("Numbered edition added to the record (step 1).");}if(S.mode)resetCampaign();renderAll();}
else if(t.id==="fanPrice"){S.fanPrice=Math.min(20000,Math.max(100,Math.round(+t.value||1200)));t.value=S.fanPrice;renderAll();}
else if(t.classList.contains("tdur")){const v=parseDur(t.value);if(v==null||v<=0){t.classList.add("err");return;}t.classList.remove("err");trkOf(t).sec=v;t.value=mmss(v);S.demoTracks=false;revokePlan();renderAll();}
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
@@ -1383,8 +1380,8 @@ async function tour(){
approve();
cap(4,"An itemised price, every line visible. The price per copy falls fast with the run, because cutting and plating are in-house.");
await w(900);for(const q of [100,150,200,300,500,700,1000,700,500,300]){setQty(q);await w(280);}await w(2000);
go(4);cap(5,"No money up front: fans pre-order instead. The artist picks the goal in fans, never below the plant's minimum run or their break-even.");await w(3200);
for(const g of [110,120,130,140,150]){setGoal(g);await w(220);}await w(2200);
go(4);cap(5,"No money up front: the artist sets the price for fans, and the page works out how many fans are needed: the plant's 100-copy minimum, more if the price is low.");await w(2600);
for(const v of [1000,800,650,550,500,550,650,800,1000,1200]){setFanPrice(v);await w(330);}await w(1800);
S.scen="sell";launch();
cap(6,"A 30-day campaign, simulated. Fan money sits in a separate account until the goal is met; if it is missed, everyone is refunded.");
while(!S.outcome)await w(250);await w(1800);