diff --git a/docs/artist-order.html b/docs/artist-order.html index c487e04..a3a5d77 100644 --- a/docs/artist-order.html +++ b/docs/artist-order.html @@ -289,7 +289,6 @@ document.documentElement.setAttribute("data-theme",t);})();

Campaign

Fan price per copy THB
Runs for days
-
Goal fans
Limited edition copies at most, 0 = no cap
Copies for yourself

@@ -402,7 +401,7 @@ const S={ 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", 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, visited:{1:true} }; @@ -465,10 +464,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; @@ -727,14 +726,12 @@ function renderCamp(){ 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 rows=ns.map(E.at);const g=E.at(E.goal); - el.innerHTML=`
Your break-even${E.be} fans
Plant minimum run${E.min} copies
Campaign goal${E.goal} fans${money(E.goal*S.fanPrice)} in pre-orders
You keep at the goal${money(g.per)}a copy
+ el.innerHTML=`
Your break-even${E.be} fans
Plant minimum run${E.min} copies
Fans needed${E.goal} fans${money(E.goal*S.fanPrice)} in pre-orders
You keep at the goal${money(g.per)}a copy
${S.cap&&S.capThe edition is capped at ${S.cap}, below the goal of ${E.goal}: this campaign cannot succeed.`:""}
${rows.map(r=>``).join("")}
FansPressedFan money after fees${S.vat?" and VAT":""}Per titlePressing and packagingYou keepPer fan copy
${fmtInt(r.n)}${r.n===E.goal?" goal":""}${fmtInt(r.run)}${money(r.net)}${money(-E.tSum)}${money(-r.run*r.c)}${money(r.keep)}${money(r.per)}
-

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."}

`; +

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."}

`; } - 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{ 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(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==="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();} @@ -930,7 +926,6 @@ document.addEventListener("change",e=>{ 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.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();} }); diff --git a/docs/artist-platform.md b/docs/artist-platform.md index 6bcf607..0abf007 100644 --- a/docs/artist-platform.md +++ b/docs/artist-platform.md @@ -245,8 +245,9 @@ Nothing goes in front of fans that the plant cannot make: ### The goal -The artist sets the goal in fans. The lowest it can go is the larger of -two numbers: +The artist sets only the fan price; the goal, the number of fans +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 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 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 -first run or a round number to tell fans. A higher goal is all or nothing -as well: if it is missed, every fan is refunded, even when the floor was -passed. Both demos let the artist set it, with the goal shown in fans and -as the pre-order amount (fans × fan price). +Both demos show it in fans and as the pre-order amount (fans × fan +price); the investor demo also says from which fan price the 100-copy +minimum is enough (about 640 THB in the illustration; below it the +break-even sets the goal). 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 -38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the lowest goal is the -100-copy minimum. At 100 copies the artist keeps about **546 THB a copy**, at 300 +38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the goal is the 100-copy +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 shows the artist this before launch, for any price they type in. Shipping is charged to the fan separately at checkout, at cost. diff --git a/docs/make-a-record.html b/docs/make-a-record.html index 8f1c753..984b5be 100644 --- a/docs/make-a-record.html +++ b/docs/make-a-record.html @@ -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);})();
Step 3 of 5

Your price

Every line itemised, with no fees that appear later. The price per copy falls fast as the run grows.

-
Investor note

Why short runs work here. 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.

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.

+
Investor note

Why short runs work here. 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.

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.

Copies
copies
@@ -584,10 +586,9 @@ document.documentElement.setAttribute("data-theme",t);})();
Investor note

A plant-owned platform, not a broker. 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.

-
Fan price per copy
THB
+
Your price to fans, per copy
THB
Campaign length
days
-
Goal: how many fans you need
fans
-
Limited edition
copies at most, 0 = no limit
+
Limited edition
numbered copies
Copies for yourself
pressed with the run
@@ -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=`
Per copy, all in${money(Q.per)}
Order total${money(Q.total)}
Ordered today, ships${monthRange(ship[0],ship[1])}
`; $("#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=>`${x.t}${x.pass!=null?`
partner price ${money(x.pass)} + ${Math.round(R.handling.v*100)} % handling`:""}${x.u?x.n+" "+x.u:""}${money(x.a)}`; - $("#offer").innerHTML=`${Q.T.map(tr).join("")} + $("#offer").innerHTML=`
Once per titleAmount
Per title${money(Q.tSum)}
${Q.T.map(tr).join("")}${Q.C.map(tr).join("")} -
One-off, for this releaseAmount
One-off total${money(Q.tSum)}
Per copyPer copy
${money(Q.cSum)} × ${fmtInt(S.qty)} copies${money(Q.cSum*S.qty)}
Order total${money(Q.total)}

Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.

`; + Order total${money(Q.total)}

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.

`; $("#leadP").innerHTML=`

When it ships

${L.pw[0]} to ${L.pw[1]} weeksyour approvals and the metal work
${L.qw[0]} to ${L.qw[1]} weeks, quotedfrom your test-pressing approval to dispatch

Ordered today, it ships about ${monthRange(ship[0],ship[1])}. 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."}

`; const E=campEcon(); $("#pDirect").innerHTML=`Deposit today ${money(Q.total*R.deposit.v)}`; @@ -1056,20 +1057,18 @@ function renderFund(){ if(E.goal==null)h=`
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.
`; 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=`
Campaign goal${E.goal} fans${money(E.goal*S.fanPrice)} in pre-orders
Your break-even${E.be} fans
You keep at the goal${money(g.per)}a copy
That is${money(g.keep)}in total
+ h=`
Fans needed${E.goal} fans${money(E.goal*S.fanPrice)} in pre-orders
Your break-even${E.be} fans
You keep at the goal${money(g.per)}a copy
That is${money(g.keep)}in total
${S.cap&&S.capThe edition is capped at ${S.cap}, below the goal of ${E.goal}: this campaign cannot succeed.
`:""}

What you keep if more fans join

${rows.map(r=>`
${fmtInt(r.n)} fans
${money(r.keep)}${money(r.per)} a copy
`).join("")}
-

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.

`; +

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.

`; } $("#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=``+[150,200,300,500,1000].map(n=>{const lo=n0&&n>S.cap,dis=lo||hi;return ``;}).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?`This price does not cover the cost per copy.`:`You need ${fmtInt(E.goal)} fans 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=`

Funded

${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.

Fan money after fees${S.vat?" and VAT":""}${money(A.net)}Per-title work${money(-E.tSum)}Pressing and packaging, ${fmtInt(A.run)} copies${money(-A.run*A.c)}You keep${money(A.keep)}
`;} + oc=`

Funded

${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.

Fan money after fees${S.vat?" and VAT":""}${money(A.net)}One-off setup${money(-E.tSum)}Pressing and packaging, ${fmtInt(A.run)} copies${money(-A.run*A.c)}You keep${money(A.keep)}
`;} 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=`

Refunded

${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.

`; else if(S.outcomeAct==="min")oc=`

Going ahead at the minimum

${diff<0?"You paid the gap of "+money(-diff)+"":"The fans' money covered the minimum run; you keep "+money(diff)+""}, and you receive the ${spare} copies no fan claimed.

`; @@ -1228,8 +1227,8 @@ function renderStage(){ $("#stSpecs").innerHTML=sp.map(x=>`${esc(x)}`).join(""); const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb; if(camp){const E=campEcon(); - pr=`
Fan price${money(S.fanPrice)}
Campaign goal${E.goal?E.goal+" fans":"not reachable"}
You keep at the goal${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}
`; - mb=`Fan price · goal${money(S.fanPrice)} ${E.goal?E.goal+" fans":""}`;} + pr=`
Fan price${money(S.fanPrice)}
Fans needed${E.goal?E.goal+" fans":"not reachable"}
You keep at the goal${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}
`; + mb=`Fan price · fans needed${money(S.fanPrice)} ${E.goal?E.goal+" fans":""}`;} else{pr=`
Per copy, all in${money(Q.per)}
${fmtInt(S.qty)} copies${money(Q.total)}
Ordered today, ships${monthRange(ship[0],ship[1])}
`; mb=`${fmtInt(S.qty)} copies · per copy${money(Q.per)}`;} $("#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(vE.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);