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

+6 -11
View File
@@ -289,7 +289,6 @@ document.documentElement.setAttribute("data-theme",t);})();
<h2>Campaign</h2> <h2>Campaign</h2>
<div class="row"><span class="lbl">Fan price per copy</span><span><input type="number" id="fanPrice" min="300" max="5000" step="50"> THB</span></div> <div class="row"><span class="lbl">Fan price per copy</span><span><input type="number" id="fanPrice" min="300" max="5000" step="50"> THB</span></div>
<div class="row"><span class="lbl">Runs for</span><span style="display:flex;gap:8px;align-items:center"><input type="range" id="days" min="14" max="45" step="1"> <b id="daysV"></b> days</span></div> <div class="row"><span class="lbl">Runs for</span><span style="display:flex;gap:8px;align-items:center"><input type="range" id="days" min="14" max="45" step="1"> <b id="daysV"></b> days</span></div>
<div class="row"><span class="lbl">Goal</span><span><input type="number" id="goalF" min="100" max="5000" step="10"> fans <small id="goalHint"></small></span></div>
<div class="row"><span class="lbl">Limited edition</span><span><input type="number" id="cap" min="0" max="5000" step="50"> <small>copies at most, 0 = no cap</small></span></div> <div class="row"><span class="lbl">Limited edition</span><span><input type="number" id="cap" min="0" max="5000" step="50"> <small>copies at most, 0 = no cap</small></span></div>
<div class="row"><span class="lbl">Copies for yourself</span><input type="number" id="acop" min="0" max="200" step="5"></div> <div class="row"><span class="lbl">Copies for yourself</span><input type="number" id="acop" min="0" max="200" step="5"></div>
<p><label><input type="checkbox" id="vat"> Take 7 % VAT out of the fan price (CNX as the seller)</label></p> <p><label><input type="checkbox" id="vat"> Take 7 % VAT out of the fan price (CNX as the seller)</label></p>
@@ -402,7 +401,7 @@ const S={
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false, qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
mastering:true,tps:5,loud:false,artist:"Demo Artist",title:"Night Market Songs", mastering:true,tps:5,loud:false,artist:"Demo Artist",title:"Night Market Songs",
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null}, 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,artistCopies:0,vat:false,
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null, mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
visited:{1:true} visited:{1:true}
}; };
@@ -465,10 +464,10 @@ function campEcon(){
const cAt=n=>quote(Math.max(1,n)).cSum; const cAt=n=>quote(Math.max(1,n)).cSum;
let be=null; let be=null;
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}} 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) */ /* 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,floor=be==null?null:Math.max(min,be),goal=floor==null?null:Math.max(floor,Math.round(S.goalFans||0)); 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};}; 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){ function finalPledges(E){
if(!E.goal)return 0;const g=E.goal; if(!E.goal)return 0;const g=E.goal;
@@ -727,14 +726,12 @@ function renderCamp(){
else{ else{
const ns=[...new Set([E.goal,150,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b); const ns=[...new Set([E.goal,150,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b);
const rows=ns.map(E.at);const g=E.at(E.goal); const rows=ns.map(E.at);const g=E.at(E.goal);
el.innerHTML=`<div class="kpis"><div><small>Your break-even</small><b>${E.be} fans</b></div><div><small>Plant minimum run</small><b>${E.min} copies</b></div><div><small>Campaign goal</small><b>${E.goal} fans</b><small>${money(E.goal*S.fanPrice)} in pre-orders</small></div><div><small>You keep at the goal</small><b>${money(g.per)}</b><small>a copy</small></div></div> el.innerHTML=`<div class="kpis"><div><small>Your break-even</small><b>${E.be} fans</b></div><div><small>Plant minimum run</small><b>${E.min} copies</b></div><div><small>Fans needed</small><b>${E.goal} fans</b><small>${money(E.goal*S.fanPrice)} in pre-orders</small></div><div><small>You keep at the goal</small><b>${money(g.per)}</b><small>a copy</small></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>`:""} ${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="tscroll"><table><tbody><tr class="hd"><th>Fans</th><th class="num">Pressed</th><th class="num">Fan money after fees${S.vat?" and VAT":""}</th><th class="num">Per title</th><th class="num">Pressing and packaging</th><th class="num">You keep</th><th class="num">Per fan copy</th></tr> <div class="tscroll"><table><tbody><tr class="hd"><th>Fans</th><th class="num">Pressed</th><th class="num">Fan money after fees${S.vat?" and VAT":""}</th><th class="num">Per title</th><th class="num">Pressing and packaging</th><th class="num">You keep</th><th class="num">Per fan copy</th></tr>
${rows.map(r=>`<tr><td>${fmtInt(r.n)}${r.n===E.goal?" <small>goal</small>":""}</td><td class="num">${fmtInt(r.run)}</td><td class="num">${money(r.net)}</td><td class="num">${money(-E.tSum)}</td><td class="num">${money(-r.run*r.c)}</td><td class="num"><b>${money(r.keep)}</b></td><td class="num">${money(r.per)}</td></tr>`).join("")}</tbody></table></div> ${rows.map(r=>`<tr><td>${fmtInt(r.n)}${r.n===E.goal?" <small>goal</small>":""}</td><td class="num">${fmtInt(r.run)}</td><td class="num">${money(r.net)}</td><td class="num">${money(-E.tSum)}</td><td class="num">${money(-r.run*r.c)}</td><td class="num"><b>${money(r.keep)}</b></td><td class="num">${money(r.per)}</td></tr>`).join("")}</tbody></table></div>
<p class="hint">The artist sets the goal; the lowest it can go is the larger of the plant's minimum run and the break-even. A higher goal is all or nothing too. Payment fees ${pctT(E.fee)} of the fan price${S.vat?", VAT 7/107 of it":""}. Over-subscription is good news: the per-title cost spreads over more copies. ${S.vat?"":"VAT is not taken out here, as in the artist platform example; tick the box above to see it."}</p>`; <p class="hint">The artist sets the fan price; the fans needed follow from it: the plant's minimum run, or the break-even if that is higher. Payment fees ${pctT(E.fee)} of the fan price${S.vat?", VAT 7/107 of it":""}. Over-subscription is good news: the per-title cost spreads over more copies. ${S.vat?"":"VAT is not taken out here, as in the artist platform example; tick the box above to see it."}</p>`;
} }
const gi=$("#goalF");gi.disabled=E.floor==null;
if(E.floor!=null){gi.min=E.floor;if(document.activeElement!==gi)gi.value=E.goal;$("#goalHint").textContent=E.goal===E.floor?"the lowest possible: "+(E.be>E.min?"your break-even":"the plant minimum"):"lowest possible "+E.floor;}else $("#goalHint").textContent="raise the fan price first";
const can=S.gate1&&E.goal!=null&&!(S.cap&&S.cap<E.goal); const can=S.gate1&&E.goal!=null&&!(S.cap&&S.cap<E.goal);
$("#bLaunch").disabled=!can; $("#bLaunch").disabled=!can;
msg=!S.gate1?"Approve the master plan in step 2 first.":!can?"Fix the campaign settings above first.":"Simulated: no page goes live and nobody is charged."; msg=!S.gate1?"Approve the master plan in step 2 first.":!can?"Fix the campaign settings above first.":"Simulated: no page goes live and nobody is charged.";
@@ -916,7 +913,6 @@ document.addEventListener("input",e=>{
else if(id==="artist"||id==="title"){S[id]=t.value;renderAll();} else if(id==="artist"||id==="title"){S[id]=t.value;renderAll();}
else if(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;if(S.mode==="campaign")updateS5();} else if(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;if(S.mode==="campaign")updateS5();}
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==="fanPrice"){const v=+t.value;if(v>=100&&v<=20000){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="goalF"){const v=Math.round(+t.value),E=campEcon();if(E.floor!=null&&v>=E.floor&&v<=5000){S.goalFans=v===E.floor?0:v;t.classList.remove("err");if(S.mode==="campaign")resetCampaign();renderAll();}else t.classList.add("err");}
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();} else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="cap"){const v=Math.max(0,Math.round(+t.value)||0);S.cap=v;if(S.mode==="campaign")resetCampaign();renderAll();} else if(id==="cap"){const v=Math.max(0,Math.round(+t.value)||0);S.cap=v;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="acop"){const v=Math.max(0,Math.round(+t.value)||0);S.artistCopies=v;if(S.mode==="campaign")resetCampaign();renderAll();} else if(id==="acop"){const v=Math.max(0,Math.round(+t.value)||0);S.artistCopies=v;if(S.mode==="campaign")resetCampaign();renderAll();}
@@ -930,7 +926,6 @@ document.addEventListener("change",e=>{
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();} else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
else if(t.dataset.b){S[t.dataset.b]=t.checked;if(S.mode)resetCampaign();renderAll();} else if(t.dataset.b){S[t.dataset.b]=t.checked;if(S.mode)resetCampaign();renderAll();}
else if(t.id==="rights"){S.rights=t.checked;if(!t.checked)revokePlan();renderAll();} else if(t.id==="rights"){S.rights=t.checked;if(!t.checked)revokePlan();renderAll();}
else if(t.id==="goalF"){const E=campEcon();if(E.floor==null)return;const v=Math.min(5000,Math.max(E.floor,Math.round(+t.value)||0));S.goalFans=v===E.floor?0:v;t.value=v;t.classList.remove("err");if(S.mode==="campaign")resetCampaign();renderAll();}
else if(t.id==="vat"){S.vat=t.checked;if(S.mode==="campaign")resetCampaign();renderAll();} else if(t.id==="vat"){S.vat=t.checked;if(S.mode==="campaign")resetCampaign();renderAll();}
}); });
+9 -9
View File
@@ -245,8 +245,9 @@ Nothing goes in front of fans that the plant cannot make:
### The goal ### The goal
The artist sets the goal in fans. The lowest it can go is the larger of The artist sets only the fan price; the goal, the number of fans
two numbers: needed, follows from it (decided October 2026). It is the larger of two
numbers:
- **the plant's minimum campaign run**, set by setup and stamper - **the plant's minimum campaign run**, set by setup and stamper
economics on the semi-automatic line (**100 copies** as a planning economics on the semi-automatic line (**100 copies** as a planning
@@ -254,16 +255,15 @@ two numbers:
- **the artist's break-even**: the copies at which the fan price covers - **the artist's break-even**: the copies at which the fan price covers
the per-title costs, the per-copy costs and the payment fees. the per-title costs, the per-copy costs and the payment fees.
Above that floor the goal is the artist's choice, for example a bigger Both demos show it in fans and as the pre-order amount (fans × fan
first run or a round number to tell fans. A higher goal is all or nothing price); the investor demo also says from which fan price the 100-copy
as well: if it is missed, every fan is refunded, even when the floor was minimum is enough (about 640 THB in the illustration; below it the
passed. Both demos let the artist set it, with the goal shown in fans and break-even sets the goal).
as the pre-order amount (fans × fan price).
With the illustration above and a fan price of 1,200 THB (an example, With the illustration above and a fan price of 1,200 THB (an example,
not a recommendation) and payment fees of about 3.5%: break-even is not a recommendation) and payment fees of about 3.5%: break-even is
38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the lowest goal is the 38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the goal is the 100-copy
100-copy minimum. At 100 copies the artist keeps about **546 THB a copy**, at 300 minimum. At 100 copies the artist keeps about **546 THB a copy**, at 300
about **799 THB**, because the per-title cost spreads. The calculator about **799 THB**, because the per-title cost spreads. The calculator
shows the artist this before launch, for any price they type in. Shipping shows the artist this before launch, for any price they type in. Shipping
is charged to the fan separately at checkout, at cost. is charged to the fan separately at checkout, at cost.
+31 -34
View File
@@ -221,7 +221,9 @@ document.documentElement.setAttribute("data-theme",t);})();
.qchips .chip { padding: 4px 12px; font-size: 13px; } .qchips .chip { padding: 4px 12px; font-size: 13px; }
.qchips .chip.on::before { content: ""; } .qchips .chip.on::before { content: ""; }
.qchips .chip:disabled { opacity: 0.35; cursor: not-allowed; } .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 { 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)); } .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)); } } @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> <div class="eyebrow">Step 3 of 5</div>
<h1>Your price</h1> <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> <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="panel block">
<div class="qtyhead"> <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> <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="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="panel block">
<div class="fgrid"> <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"><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" 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">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"><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 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> </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> <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, 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", 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}, 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, mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
visited:{1:true} visited:{1:true}
}; };
@@ -767,10 +768,10 @@ function campEcon(){
const cAt=n=>quote(Math.max(1,n)).cSum; const cAt=n=>quote(Math.max(1,n)).cSum;
let be=null; let be=null;
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}} 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) */ /* 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,floor=be==null?null:Math.max(min,be),goal=floor==null?null:Math.max(floor,Math.round(S.goalFans||0)); 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};}; 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){ function finalPledges(E){
if(!E.goal)return 0;const g=E.goal; 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); 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>`; $("#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(); $("#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>`; 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><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>`; $("#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(); const E=campEcon();
$("#pDirect").innerHTML=`Deposit today <b>${money(Q.total*R.deposit.v)}</b>`; $("#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>`; 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{ 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); 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>`:""} ${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> <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; $("#fundEcon").innerHTML=h;
const gi=$("#goalF"),gr=$("#goalR"),none=E.floor==null;gi.disabled=none;gr.disabled=none; $$("#fpChips .chip").forEach(c=>c.classList.toggle("on",+c.dataset.fp===S.fanPrice));
if(none){$("#goalAmt").textContent="";$("#goalHint").textContent="Raise the fan price first: below cost there is no goal to set.";$("#goalChips").innerHTML="";} /* the lowest fan price at which the plant minimum covers the costs; below it the break-even sets the fans needed */
else{gi.min=E.floor;if(document.activeElement!==gi){gi.value=E.goal;gi.classList.remove("err");} 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;
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; $("#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`;
$("#goalAmt").textContent="= "+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.";
const why=E.be>E.min?"your break-even of "+E.be+" fans":"the plant's "+E.min+"-copy minimum"; $("#fCap").classList.toggle("off",!S.limited);$("#cap").disabled=!S.limited;$("#cap").title=S.limited?"":"Switch on to make it a limited edition";
$("#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+").";}
$("#bLaunch").disabled=!canLaunch(); $("#bLaunch").disabled=!canLaunch();
$("#launchMsg").textContent=launchHint()||"Simulated: no page goes live and nobody is charged."; $("#launchMsg").textContent=launchHint()||"Simulated: no page goes live and nobody is charged.";
} }
@@ -1169,7 +1168,7 @@ function updateS5(){
let oc=""; let oc="";
if(ended&&goal){ if(ended&&goal){
if(funded){const A=E.at(n); 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); 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>`; 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>`; 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(""); $("#stSpecs").innerHTML=sp.map(x=>`<span>${esc(x)}</span>`).join("");
const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb; const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb;
if(camp){const E=campEcon(); 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>`; 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 · goal</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?E.goal+" fans":""}</small>`;} 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>`; 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>`;} mb=`<small>${fmtInt(S.qty)} copies · per copy</small><b>${money(Q.per)}</b>`;}
$("#stPrice").innerHTML=pr; $("#stPrice").innerHTML=pr;
@@ -1247,11 +1246,11 @@ function go(s){
} }
function approve(){if(!canApprove())return;S.gate1=true;go(3);} 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 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 setQty(q){S.qty=Math.min(5000,Math.max(100,Math.round(q)));if(S.mode==="direct")resetCampaign();renderAll();}
function syncInputs(){ function syncInputs(){
$("#artist").value=S.artist;$("#title").value=S.title;$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty); $("#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(){ function resetDemo(){
stopSim();stopProd();if(AU){AU.pause();AU=null;AUid=null;} 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 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 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 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 b=e.target.closest("button");if(!b||b.disabled)return;
const a=b.dataset.act;if(a&&ACT[a]){ACT[a](b);return;} const a=b.dataset.act;if(a&&ACT[a]){ACT[a](b);return;}
const g=b.closest("[data-k]"); 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(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==="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==="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==="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==="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();} else if(id==="simDay"){stopSim();S.simDay=+t.value;checkEnd();renderAll();}
}); });
document.addEventListener("change",e=>{ document.addEventListener("change",e=>{
const t=e.target; const t=e.target;
if(t.id==="qty"){setQty(Math.round((+t.value||100)/10)*10);t.value=S.qty;} 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.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("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();} else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
@@ -1383,8 +1380,8 @@ async function tour(){
approve(); 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."); 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); 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); 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 g of [110,120,130,140,150]){setGoal(g);await w(220);}await w(2200); 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(); 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."); 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); while(!S.outcome)await w(250);await w(1800);