Calculators: options from Record Industry's calculator; its prices as a benchmark
Deploy test site / deploy (push) Successful in 45s

Reviewed Record Industry's (Haarlem, NL) public vinyl calculator: a
seven-step wizard with a live itemised cost list priced on its server.
Read its prices for a reference 12" LP at 200 / 300 / 500 / 1,000 copies
and for each option worth copying (checked October 2026).

Both artist demos (make-a-record and artist-order):
- audio arrives as digital files, analog tape (+ tape transfer) or a cut
  lacquer (no blanks, cutting or mastering; mastering choice disabled with
  the reason; cutting and mastering stages skipped in the timeline; the
  passport starts at "Lacquer received")
- labels printed, plain white or plain black (credit line; the label
  artwork drop is disabled for plain labels; the mock-up shows the plain
  label)
- press cleaning before a colour run, on by default, disabled for black
- 7" dinked (large centre hole), 7" only, drawn in the mock-up
- download card and barcode sticker extras
- run-out inscription (A-Z, 0-9, up to 15), scribed along the dead wax in
  the mock-up and named in the passport
- new rates (tape 3,800, cleaning 2,300 per title; plain labels -12 per
  disc; download card 43, barcode 6 per copy) are estimates with Record
  Industry's prices as the basis, each listed in the detailed demo
- make-a-record investor notes: Record Industry's 9 to 11 week quote after
  test-pressing approval against the 7 to 9 week CNX quote, and its 2 %
  reject allowance against 100 % optical inspection

Defaults unchanged (37,836 THB per title, 232 per copy at 100, break-even
41, goal 100, 548 kept).

Docs: artist-platform.md (choices table, benchmark section: what was
taken, what was left out, commercial terms worth deciding);
revenue-model.md (second Western data point: 219 THB a copy at 300
ex-works, per-title and per-copy reading against the assumptions, not a
re-base); production-timeline.md (its published lead times).
This commit is contained in:
grabowski committed 2026-10-16 09:42:57 +07:00
1 parent 83f1aa94dc
commit 5090609df3
5 files changed
+195 -42

No files matched your search

+51 -16
View File
@@ -76,6 +76,8 @@ document.documentElement.setAttribute("data-theme",t);})();
.drop { display: flex; align-items: center; gap: 8px; border: 1.5px dashed var(--stroke); border-radius: 6px; padding: 8px 12px; margin: 8px 0; color: var(--mute); font-size: 14px; cursor: pointer; }
.drop:hover, .drop.over { border-color: var(--accent); color: var(--fg); }
.drop b { color: var(--fg); font-weight: 600; }
.drop.off { opacity: 0.4; cursor: not-allowed; }
.drop.off:hover { border-color: var(--stroke); color: var(--mute); }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { padding: 6px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: var(--mute); font-weight: 600; font-size: 12.5px; }
@@ -194,9 +196,11 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="row"><span class="lbl">Size</span><div class="seg" data-k="size"><button type="button" data-v="12">12" LP</button><button type="button" data-v="10">10"</button><button type="button" data-v="7">7" single</button></div></div>
<div class="row"><span class="lbl">Speed</span><div class="seg" data-k="rpm"><button type="button" data-v="33">33⅓ rpm</button><button type="button" data-v="45">45 rpm</button></div></div>
<div class="row"><span class="lbl">Discs</span><div class="seg" data-k="discs" data-num><button type="button" data-v="1">Single</button><button type="button" data-v="2">Double</button></div></div>
<div class="row" id="rowDink"><span class="lbl">Centre hole</span><div class="seg" data-k="dinked" data-bool><button type="button" data-v="false">Small</button><button type="button" data-v="true">Large (dinked)</button></div></div>
<div class="row" id="rowWeight"><span class="lbl">Weight</span><div class="seg" data-k="weight" data-num><button type="button" data-v="140">140 g</button><button type="button" data-v="180">180 g</button></div></div>
<div class="row"><span class="lbl">Vinyl colour</span><div class="seg" data-k="colour"><button type="button" data-v="black">Black</button><button type="button" data-v="solid">Solid colour</button><button type="button" data-v="marbled">Marbled</button><button type="button" data-v="splatter">Splatter</button></div></div>
<div class="row" id="rowColours"><span class="lbl">Colours</span><span><input type="color" id="c1" title="Vinyl colour"> <input type="color" id="c2" title="Second colour"> <small id="colHint"></small></span></div>
<div class="row" id="rowClean"><span class="lbl">Press cleaning</span><span><div class="seg" data-k="cleaning" data-bool><button type="button" data-v="true">Clean before the colour</button><button type="button" data-v="false">No cleaning</button></div> <small id="cleanHint"></small></span></div>
</div>
<div class="card">
<h2>Copies, for a direct order</h2>
@@ -206,18 +210,22 @@ document.documentElement.setAttribute("data-theme",t);})();
</div>
<div class="card">
<h2>Sleeves and jacket</h2>
<div class="row"><span class="lbl">Labels</span><div class="seg" data-k="labels"><button type="button" data-v="printed">Printed</button><button type="button" data-v="white">Plain white</button><button type="button" data-v="black">Plain black</button></div></div>
<div class="row"><span class="lbl">Inner sleeve</span><div class="seg" data-k="inner"><button type="button" data-v="paper">Paper</button><button type="button" data-v="poly">Poly-lined</button></div></div>
<div class="row"><span class="lbl">Jacket</span><div class="seg" data-k="jacket"><button type="button" data-v="none">I supply it</button><button type="button" data-v="plain">Plain</button><button type="button" data-v="printed">Printed</button><button type="button" data-v="gatefold" id="bGate">Gatefold</button></div></div>
<div class="row"><span class="lbl">Extras</span><span class="checks2">
<label><input type="checkbox" data-b="shrink"> Shrink wrap</label>
<label><input type="checkbox" data-b="insert"> Printed insert</label>
<label><input type="checkbox" data-b="sticker"> Hype sticker</label>
<label><input type="checkbox" data-b="numbered"> Numbered edition</label></span></div>
<p class="hint">Labels are printed and pressed into the record, the only kind this plant makes. Printed jackets come from a local packaging partner and are passed through at the partner's price plus a stated handling margin.</p>
<label><input type="checkbox" data-b="numbered"> Numbered edition</label>
<label><input type="checkbox" data-b="dlcard"> Download card</label>
<label><input type="checkbox" data-b="barcode"> Barcode sticker</label></span></div>
<p class="hint">Labels are paper labels pressed into the record, printed or plain white or black. Printed jackets come from a local packaging partner and are passed through at the partner's price plus a stated handling margin.</p>
</div>
<div class="card">
<h2>Audio and test pressings</h2>
<div class="row"><span class="lbl">Master</span><div class="seg" data-k="mastering" data-bool><button type="button" data-v="false">I supply a vinyl-ready master</button><button type="button" data-v="true">CNX vinyl mastering</button></div></div>
<div class="row"><span class="lbl">Audio arrives as</span><div class="seg" data-k="source"><button type="button" data-v="digital">Digital files</button><button type="button" data-v="tape">Analog tape</button><button type="button" data-v="lacquer">A cut lacquer</button></div></div>
<div class="row" id="rowMaster"><span class="lbl">Master</span><div class="seg" data-k="mastering" data-bool><button type="button" data-v="false">I supply a vinyl-ready master</button><button type="button" data-v="true">CNX vinyl mastering</button></div></div>
<div class="row"><span class="lbl">Test pressings for you</span><input type="number" id="tps" min="0" max="10" step="1"></div>
<p class="hint">Cutting and plating happen in-house: lacquer, father, mother and stampers. The mother is kept, so a repress later skips cutting.</p>
</div>
@@ -229,6 +237,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<h2>Release</h2>
<div class="row"><span class="lbl">Artist</span><input type="text" id="artist" maxlength="60"></div>
<div class="row"><span class="lbl">Title</span><input type="text" id="title" maxlength="80"></div>
<div class="row"><span class="lbl">Run-out inscription</span><span><input type="text" id="catno" maxlength="15" spellcheck="false" autocomplete="off" style="text-transform:uppercase;font-family:ui-monospace,Consolas,monospace"> <small>scribed next to the run-out groove, A to Z and 0 to 9, up to 15</small></span></div>
</div>
<div class="card">
<h2>Tracks and sides</h2>
@@ -353,6 +362,11 @@ const R={
insert:{v:12,u:"per copy",t:"Printed insert",k:"est",src:"Estimate."},
sticker:{v:6,u:"per copy",t:"Hype sticker",k:"est",src:"Estimate."},
numbered:{v:5,u:"per copy",t:"Hand-numbered edition",k:"est",src:"Estimate."},
tape:{v:3800,u:"per title",t:"Analog tape transfer",k:"est",src:"Estimate. Record Industry (NL) charges 100 EUR more for cutting from tape than from a digital master (online calculator, checked October 2026)."},
clean:{v:2300,u:"per title",t:"Press cleaned before a colour run",k:"est",src:"Estimate. Record Industry charges 60 EUR for press cleaning and warns that without it records can carry the previous colour (online calculator, October 2026)."},
plainLabel:{v:-12,u:"per disc",t:"Plain labels instead of printed (credit)",k:"est",src:"Estimate. Record Industry prices white unprinted labels about 0.32 EUR a record below CMYK printed at 300 copies (online calculator, October 2026)."},
dlcard:{v:43,u:"per copy",t:"Download card, printed both sides",k:"est",src:"Estimate. Record Industry: 361 EUR for a 4/4 card at 300 copies, about 1.13 EUR a copy (online calculator, October 2026)."},
barcode:{v:6,u:"per copy",t:"Barcode sticker on the back",k:"est",src:"Estimate. Record Industry: 0.15 EUR a copy (online calculator, October 2026)."},
fee:{v:0.035,u:"of the fan price",t:"Payment fees",k:"doc",src:"About 3 to 4 % on cards, less on PromptPay, to be confirmed with the provider (artist platform)."},
minRun:{v:100,u:"copies",t:"Minimum campaign run",k:"doc",src:"Planning figure, to be decided (artist platform, open decision 4)."},
deposit:{v:0.5,u:"of a direct order",t:"Deposit on a direct order",k:"est",src:"Placeholder; deposit terms are not set yet."}
@@ -400,6 +414,7 @@ const S={
step:1,cur:"THB",size:"12",rpm:"33",discs:1,weight:140,colour:"black",c1:"#c0392b",c2:"#f2c14e",
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",
source:"digital",labels:"printed",cleaning:true,dinked:false,dlcard:false,barcode:false,catno:"DEMO001",
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
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,
@@ -424,25 +439,32 @@ function addWD(d,wd){const x=new Date(d);let n=Math.ceil(wd);while(n>0){x.setDat
const sideNames=()=>S.discs>1?["A","B","C","D"]:["A","B"];
const rpmT=()=>S.rpm==="33"?"33⅓ rpm":"45 rpm";
const colourT=()=>({black:"black",solid:"solid colour",marbled:"marbled",splatter:"splatter"})[S.colour];
function descT(){return (S.discs>1?"2 × ":"")+S.size+"\", "+rpmT()+", "+(S.size==="12"?S.weight+" g ":"")+colourT()+", "+JNAME[S.jacket].toLowerCase();}
const dinkOn=()=>S.dinked&&S.size==="7",mastOn=()=>S.mastering&&S.source!=="lacquer",cleanOn=()=>S.cleaning&&S.colour!=="black";
/* a stage runs unless the order skips it: no mastering or cutting when the customer sends a cut lacquer */
const stageOn=s=>s.opt==="mastering"?mastOn():s.id==="cut"?S.source!=="lacquer":true;
function descT(){return (S.discs>1?"2 × ":"")+S.size+"\""+(dinkOn()?" dinked":"")+", "+rpmT()+", "+(S.size==="12"?S.weight+" g ":"")+colourT()+", "+JNAME[S.jacket].toLowerCase();}
function toast(t){const el=document.createElement("div");el.className="toast";el.textContent=t;document.body.appendChild(el);setTimeout(()=>el.remove(),2200);}
/* ---- pricing ---- */
function quote(q){
const sz=S.size,d=S.discs,sides=2*d,T=[],C=[],du=d>1?"discs":"disc";
if(S.mastering)T.push({t:"Vinyl mastering",n:sides,u:"sides",r:R.master.v});
T.push({t:"Lacquer blanks, 70 EUR each",n:sides,u:"sides",r:R.blank.v});
T.push({t:"Lacquer cutting",n:sides,u:"sides",r:R.cut.v});
if(mastOn())T.push({t:"Vinyl mastering",n:sides,u:"sides",r:R.master.v});
if(S.source==="tape")T.push({t:"Analog tape transfer",n:1,u:"",r:R.tape.v});
if(S.source!=="lacquer"){T.push({t:"Lacquer blanks, 70 EUR each",n:sides,u:"sides",r:R.blank.v});T.push({t:"Lacquer cutting",n:sides,u:"sides",r:R.cut.v});}
T.push({t:"Electroforming: father, mother (kept for represses), stamper",n:sides,u:"sides",r:R.metal.v});
T.push({t:"Press setup",n:d,u:du,r:R.setup.v});
if(S.tps>0)T.push({t:"Test pressings for you",n:S.tps,u:"copies",r:R.tp.v});
if(cleanOn())T.push({t:"Press cleaned before your colour",n:1,u:"",r:R.clean.v});
C.push({t:"Pressing, "+sz+"\""+(sz==="12"?" 140 g":"")+", printed labels",n:d,u:du,r:R.press.v[sz]});
if(sz==="12"&&S.weight===180)C.push({t:"180 g instead of 140 g",n:d,u:du,r:R.g180.v});
if(S.colour!=="black")C.push({t:"Colour vinyl, "+colourT(),n:d,u:du,r:R.colour.v[S.colour]});
C.push({t:"Inner sleeve, "+(S.inner==="poly"?"poly-lined":"paper"),n:d,u:du,r:R.inner.v[S.inner][sz]});
C.push({t:"Assembly and packing",n:1,u:"",r:R.assy.v});
if(S.jacket!=="none"){const p=R.jacket.v[S.jacket][sz][tierIdx(q)];C.push({t:JNAME[S.jacket]+(sz==="7"&&S.jacket!=="plain"?" (picture sleeve)":""),n:1,u:"",r:Math.round(p*(1+R.handling.v)),pass:p});}
if(S.labels!=="printed")C.push({t:"Plain "+S.labels+" labels instead of printed",n:d,u:du,r:R.plainLabel.v});
if(S.shrink)C.push({t:"Shrink wrap",n:1,u:"",r:R.shrink.v});
if(S.dlcard)C.push({t:"Download card",n:1,u:"",r:R.dlcard.v});
if(S.barcode)C.push({t:"Barcode sticker",n:1,u:"",r:R.barcode.v});
if(S.insert)C.push({t:"Printed insert",n:1,u:"",r:R.insert.v});
if(S.sticker)C.push({t:"Hype sticker",n:1,u:"",r:R.sticker.v});
if(S.numbered)C.push({t:"Hand-numbered",n:1,u:"",r:R.numbered.v});
@@ -452,7 +474,7 @@ function quote(q){
}
function leadInfo(campaign){
const g3=STG.findIndex(s=>s.id==="g3");
const pre=STG.slice(0,g3+1).filter(s=>(!s.opt||S[s.opt])&&!((campaign||S.gate1)&&(s.id==="intake"||s.id==="g1")));
const pre=STG.slice(0,g3+1).filter(s=>stageOn(s)&&!((campaign||S.gate1)&&(s.id==="intake"||s.id==="g1")));
const lo=pre.reduce((a,s)=>a+s.d[0],0),hi=pre.reduce((a,s)=>a+s.d[1],0);
const pw=[Math.max(1,Math.round(lo/5)),Math.max(1,Math.ceil(hi/5))];
const printed=S.jacket==="printed"||S.jacket==="gatefold";
@@ -572,7 +594,9 @@ function artChecks(k){
function checkList(){
const L=[],files=S.tracks.filter(t=>t.file),ready=files.filter(t=>t.info&&!t.info.reading);
if(!S.tracks.length)L.push(["bad","Track list","Add at least one track."]);
if(!files.length)L.push(["pend","Master files","None added: the checks use the lengths typed in. Drop WAV, FLAC or AIFF files to check the files themselves."]);
if(S.source==="lacquer")L.push(["pend","Lacquer","Cut by the customer's studio and sent by express courier the day it is cut: a lacquer has to be plated within days. Inspected under the microscope on arrival. Files are for the side plan and previews only."]);
else if(S.source==="tape")L.push(["pend","Tape","Analog tape is transferred and checked on arrival; format, speed and EQ curve are confirmed with the customer first."]);
if(S.source!=="digital"){}else if(!files.length)L.push(["pend","Master files","None added: the checks use the lengths typed in. Drop WAV, FLAC or AIFF files to check the files themselves."]);
else{
const lossy=ready.filter(t=>!t.info.lossless);
L.push(lossy.length?["bad","File format",lossy.map(t=>esc(t.file.name)).join(", ")+": not WAV, FLAC or AIFF. Send the lossless master."]:["ok","File format",ready.length+" lossless file"+(ready.length===1?"":"s")]);
@@ -588,7 +612,7 @@ function checkList(){
:warn.length?["warn","Side length",warn.map(s=>"side "+s.n+" "+mmss(s.sec)).join(", ")+": past the comfortable "+mmss(lim[0])+", so the cut gets quieter"]
:empty.length?["warn","Side length","Side "+empty.map(s=>s.n).join(", ")+" has no music"]:["ok","Side length","Every side within the comfortable "+mmss(lim[0])]);
if(S.jacket==="printed"||S.jacket==="gatefold"){const c=artChecks("jacket");L.push(!c?["pend","Jacket artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Jacket artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Jacket artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Jacket artwork","Size, resolution and colour space fine"]);}
{const c=artChecks("label");L.push(!c?["pend","Label artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Label artwork","Size, resolution and colour space fine"]);}
if(S.labels==="printed"){const c=artChecks("label");L.push(!c?["pend","Label artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Label artwork","Size, resolution and colour space fine"]);}
L.push(["pend","Engineer check","After upload a cutting engineer confirms that side length and level are cuttable. This decides, not the table above."]);
L.push(["pend","Rights check","An audio fingerprint check catches obvious bootlegs before a person looks; you confirm the rights below."]);
return L;
@@ -610,12 +634,15 @@ function mockSVG(id){
let g=`<g transform="translate(${x.toFixed(1)} ${cy.toFixed(1)}) scale(${k.toFixed(3)})"><circle r="105" fill="${base}"/>`;
if(S.colour==="marbled")g+=`<rect x="-105" y="-105" width="210" height="210" fill="${S.c2}" filter="url(#${id}m)" clip-path="url(#${id}c)"/>`;
if(S.colour==="splatter"){const r=rnd(7);let dots="";for(let i=0;i<80;i++){const a=r()*6.283,d=Math.sqrt(r())*100,s=1+r()*5;dots+=`<circle cx="${(Math.cos(a)*d).toFixed(1)}" cy="${(Math.sin(a)*d).toFixed(1)}" r="${s.toFixed(1)}" fill="${r()<0.8?S.c2:"#ffffff"}"/>`;}g+=`<g clip-path="url(#${id}c)">${dots}</g>`;}
for(let r=100;r>=rl+5;r-=4)g+=`<circle r="${r}" fill="none" stroke="${groove}" stroke-width="${(0.6/k).toFixed(2)}"/>`;
for(let r=100;r>=rl+10;r-=4)g+=`<circle r="${r}" fill="none" stroke="${groove}" stroke-width="${(0.6/k).toFixed(2)}"/>`;
if(S.catno){const rr=(rl+5).toFixed(1);g+=`<path id="${id}ro${x|0}" d="M0 -${rr} A${rr} ${rr} 0 1 1 -0.01 -${rr}" fill="none"/><text font-size="4.6" font-family="ui-monospace,Consolas,monospace" letter-spacing="0.8" fill="${dark?"rgba(255,255,255,0.55)":"rgba(0,0,0,0.5)"}"><textPath href="#${id}ro${x|0}" startOffset="8%">${esc(S.catno)} A</textPath></text>`;}
g+=`<circle r="105" fill="url(#${id}sh)"/>`;
const L=rl.toFixed(1);
if(S.art.label&&S.art.label.url)g+=`<image href="${S.art.label.url}" x="-${L}" y="-${L}" width="${(2*rl).toFixed(1)}" height="${(2*rl).toFixed(1)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${id}l)"/>`;
if(S.labels==="white")g+=`<circle r="${L}" fill="#f3efe6"/>`;
else if(S.labels==="black")g+=`<circle r="${L}" fill="#1c1c1c"/><circle r="${(rl*0.86).toFixed(1)}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.8"/>`;
else if(S.art.label&&S.art.label.url)g+=`<image href="${S.art.label.url}" x="-${L}" y="-${L}" width="${(2*rl).toFixed(1)}" height="${(2*rl).toFixed(1)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${id}l)"/>`;
else g+=`<circle r="${L}" fill="#efe6d2"/><text y="${(-6*fs).toFixed(1)}" text-anchor="middle" font-size="${(7*fs).toFixed(1)}" font-weight="700" fill="#2a2a2a">${esc(S.artist.slice(0,20))}</text><text y="${(14*fs).toFixed(1)}" text-anchor="middle" font-size="${(6*fs).toFixed(1)}" fill="#444">${esc(S.title.slice(0,24))}</text>`;
return g+`<circle r="${(2.6/k).toFixed(2)}" style="fill:var(--panel)"/></g>`;
return g+`<circle r="${dinkOn()?(105*19.1/Z.rec).toFixed(2):(2.6/k).toFixed(2)}" style="fill:var(--panel)"/></g>`;
}
const recs=(S.discs>1?disc(cx+32*k):"")+disc(cx);
let jk="";const J={x:0,y:0,w:212};
@@ -660,6 +687,13 @@ function syncSegs(){
$("#c1").title=blk?"Black vinyl: no colour to choose":"Vinyl colour";
$("#c2").title=two?"Second colour":"Only for marbled and splatter";
$("#colHint").textContent=blk?"black vinyl, no colour to choose":S.colour==="solid"?"vinyl colour":"base and second colour";
const s7=S.size==="7";$$("#rowDink button").forEach(x=>{x.disabled=!s7;x.title=s7?"":"Only 7\" singles come with a large centre hole";});$("#rowDink").classList.toggle("off",!s7);
$$("#rowClean button").forEach(x=>{x.disabled=blk;x.title=blk?"Black vinyl needs no cleaning":"";});$("#rowClean").classList.toggle("off",blk);
$("#cleanHint").textContent=blk?"only for colour vinyl":S.cleaning?"purged first; light colours can still show the odd speck":"some records can carry streaks of the previous colour";
const offJ=!(S.jacket==="printed"||S.jacket==="gatefold"),offL=S.labels!=="printed";
$("#dropJacket").classList.toggle("off",offJ);$("#fileJacket").disabled=offJ;$("#dropJacket").title=offJ?(S.jacket==="none"?"The customer supplies the jacket":"A plain jacket has no print"):"";
$("#dropLabel").classList.toggle("off",offL);$("#fileLabel").disabled=offL;$("#dropLabel").title=offL?"Plain labels have no print":"";
const lq=S.source==="lacquer";$$("#rowMaster button").forEach(x=>{x.disabled=lq;x.title=lq?"The lacquer is already cut and mastered":"";if(lq)x.classList.remove("on");});$("#rowMaster").classList.toggle("off",lq);
}
function renderNav(){
const done={1:!!S.visited[2],2:S.gate1,3:!!S.mode,4:S.launched,5:false};
@@ -769,7 +803,7 @@ function trackerHTML(){
<div class="card"><h2>${P.origin==="direct"?"Your order updates":"Updates fans see on the campaign page"}</h2><ul class="feed">${feed}</ul><p class="hint">In the real platform these come from the factory's own event log, so the progress is the plant's record, not a status someone remembers to update.</p></div></div>`;
}
function newProd(origin,start,fans){
const L=leadInfo(true),st=STG.filter(s=>s.id!=="intake"&&s.id!=="g1"&&(!s.opt||S[s.opt]));
const L=leadInfo(true),st=STG.filter(s=>s.id!=="intake"&&s.id!=="g1"&&stageOn(s));
const mid=s=>(s.d[0]+s.d[1])/2,after=st.filter(s=>["run","fin","ship"].includes(s.id)).reduce((a,s)=>a+mid(s),0),qMid=(L.qw[0]+L.qw[1])/2*5;
let cur=new Date(start);
const rows=st.map(s=>{const d=s.id==="queue"?Math.max(0,qMid-after):mid(s);cur=addWD(cur,d);return Object.assign({},s,{date:new Date(cur)});});
@@ -870,7 +904,7 @@ function renderAll(){syncSegs();renderNav();renderSides();renderArt();renderChec
function go(s){if(s<1||s>5)return;S.step=s;S.visited[s]=true;renderAll();const top=$("header").offsetHeight;if(window.scrollY>top)window.scrollTo(0,top);}
/* ---- events ---- */
const PLAN_KEYS=["size","rpm","discs","loud"],RUN_KEYS=["weight","colour","inner","jacket","mastering"];
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","mastering","labels","cleaning","dinked"];
function setK(k,v){
if(S[k]===v)return;S[k]=v;
if(k==="size"&&v!=="12"&&S.jacket==="gatefold")S.jacket="printed";
@@ -911,6 +945,7 @@ document.addEventListener("input",e=>{
else if(id==="tps"){const v=Math.round(+t.value);if(v>=0&&v<=10){S.tps=v;if(S.mode)resetCampaign();renderAll();}}
else if(id==="c1"||id==="c2"){S[id]=t.value;renderAll();}
else if(id==="artist"||id==="title"){S[id]=t.value;renderAll();}
else if(id==="catno"){const v=t.value.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,15);if(v!==t.value){const c=t.selectionStart;t.value=v;try{t.setSelectionRange(c,c);}catch(e){}}S.catno=v;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==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
@@ -969,7 +1004,7 @@ dropZone($("#dropLabel"),$("#fileLabel"),f=>handleArt("label",f));
/* ---- init ---- */
$("#qty").value=S.qty;$("#qtyR").value=S.qty;$("#tps").value=S.tps;$("#c1").value=S.c1;$("#c2").value=S.c2;
$("#artist").value=S.artist;$("#title").value=S.title;$("#fanPrice").value=S.fanPrice;$("#days").value=S.days;$("#cap").value=S.cap;$("#acop").value=S.artistCopies;
$("#artist").value=S.artist;$("#title").value=S.title;$("#catno").value=S.catno;$("#fanPrice").value=S.fanPrice;$("#days").value=S.days;$("#cap").value=S.cap;$("#acop").value=S.artistCopies;
$$("input[data-b]").forEach(c=>c.checked=!!S[c.dataset.b]);
renderTracks();renderPL();renderAll();
window.__ao={S,quote,campEcon,leadInfo,sideStats,checkList,go,ACT};
+43 -4
View File
@@ -93,19 +93,58 @@ their projects. See "Payments" below.
| Choice | Options |
|---|---|
| Format | 12" LP, 10", 7"; single or double disc; 33⅓ or 45 rpm |
| Format | 12" LP, 10", 7"; single or double disc; 33⅓ or 45 rpm; 7" with a small or a large (dinked) centre hole |
| Weight | 140 g, 180 g |
| Colour | Black, solid colour, marbled or splatter (the semi-automatic line's speciality) |
| Colour | Black, solid colour, marbled or splatter (the semi-automatic line's speciality); for colour, whether the press is cleaned first |
| Quantity | Free entry, with the price-per-copy curve shown live |
| Labels | Printed, pressed into the record (the only kind this plant makes) |
| Labels | Paper labels pressed into the record: printed, or plain white or black (white labels for promos and DJ copies) |
| Run-out inscription | The customer's catalogue number or text, scribed next to the run-out groove of every side (letters and numbers, up to 15) |
| Inner sleeve | Paper, poly-lined |
| Jacket | None (customer supplies), plain, printed single, printed gatefold; printed by the local packaging partner |
| Extras | Insert, hype sticker, shrink wrap, numbered edition |
| Extras | Insert, hype sticker, shrink wrap, numbered edition, download card, barcode sticker |
| Audio source | Digital files, analog tape (transferred in-house), or a lacquer cut by the customer's studio (straight to plating; must arrive within days of cutting) |
| Audio services | Customer supplies a vinyl-ready master, or CNX vinyl mastering per side |
| Metal | Lacquer cutting and stampers (in-house; bridge stampers from Stamper Discs until the galvanics line exists) |
| Test pressings | Number of copies for the artist |
| Delivery | Bulk to one address, dropship to each buyer, or collect at the plant |
### Benchmark: Record Industry's calculator (October 2026)
Record Industry (Haarlem, NL; host plant of Making Vinyl Europe 2026)
runs a public calculator
(https://www.recordindustry.com/calculator/, checked October 2026): seven
steps (pre-production, vinyl, labels, inner sleeve, outer sleeve, extras,
quote or order) with an itemised cost list beside the form that updates
on every choice, an info box under each question, and a draft order
that the plant confirms by hand before the pre-payment invoice. Prices
are computed on its server. What it shows that this plan had not
covered, and what the demos took from it:
- **Taken into both demos:** audio arriving as tape or as a supplied
lacquer (it charges 470 EUR for cutting plus stampers from a digital
master, 570 from tape, 240 for plating a supplied lacquer), plain white
or black labels, press cleaning before a colour run (60 EUR, with the
warning that without it records can carry the previous colour), the
dinked 7", download cards and barcode stickers, and a catalogue number
scribed in the run-out (letters and numbers, up to 15). The new rates
are estimates with its prices as the basis, listed in the detailed
demo.
- **Left out for now:** label print modes (CMYK, Pantone, one colour),
board grades and finishes (kraft, Hansaboard, matt or gloss varnish,
laminates), die-cut holes, 6 mm spines, booklets, foil numbering,
sticker position, protective outer sleeves, BIO PVC and a recycled
colour mix, and multi-address shipping. Each is a price-list line once
the packaging partner and the compound suppliers quote.
- **Commercial terms worth deciding for CNX:** a flat order set-up fee
(60 EUR) and a small-run surcharge (60 EUR under 300, 40 under 500);
20 copies over the order invoiced to cover under- or over-production,
credited if fewer are delivered; customers asked to accept about 2 %
rejects; full pre-payment for new customers; minimum 200 copies.
Its prices and lead times are recorded as a benchmark in the
[revenue model](finance/revenue-model.md) and the
[production timeline](production-timeline.md).
### What the customer gets back
- **Every line itemised**, grouped into **per title** (mastering,
+41
View File
@@ -466,6 +466,47 @@ re-basing the whole model on one supplier's price list, in a market with
one supplier, would be worse than leaving it low. Test it at Making Vinyl,
then re-base.
### Second Western data point: Record Industry's calculator (October 2026)
Record Industry (Haarlem, NL) publishes an online calculator
(https://www.recordindustry.com/calculator/, checked October 2026; ex 21 %
Dutch VAT, ex freight, 20 copies over the order included). A 12" 140 g
black LP, cut from a digital master with stampers, 5 test pressings,
printed labels, white paper inner sleeve, printed 3 mm jacket on 300 gsm
board, shrink wrap:
| Copies | Order total | Per copy | Per copy, USD | Per copy, THB |
|---|---|---|---|---|
| 200 | 1,494.68 EUR | 7.47 EUR | 8.63 | ~285 |
| 300 | 1,721.22 EUR | 5.74 EUR | 6.63 | ~219 |
| 500 | 2,098.12 EUR | 4.20 EUR | 4.85 | ~160 |
| 1,000 | 3,169.78 EUR | 3.17 EUR | 3.66 | ~121 |
At 1 EUR = 1.155 USD and 1 USD = 33 THB. How it is built: an order
set-up fee of 60 EUR plus a small-run charge (60 EUR under 300, 40 under
500); cutting with stampers 470 EUR a title (570 from tape, 240 to plate
a supplied lacquer); black vinyl 1.49 EUR a record; colour +35 EUR
set-up and +0.14 EUR a record, two-colour splatter +85 EUR and +0.99 EUR
a record, press cleaning 60 EUR; the printed jacket steps down with the
print run (1.93 EUR a copy at 200, 0.64 at 1,000).
What it says about the assumptions above:
- **Per title**, its set-up, small-run charge, cutting, stampers and test
pressings come to about 580 to 600 EUR (~670 to 690 USD, ~22,000 to
23,000 THB): the low end of the 580 to 1,730 USD assumption.
- **Per copy** at 300, everything but the per-title items comes to about
3.81 EUR (~4.40 USD, ~145 THB), and black pressing with printed labels
and a paper inner sleeve to about 2.43 USD (2.09 USD at 1,000): below
the 2.90 to 5.80 USD range, which looks high for black at this plant.
- **The Thai market price** of 650 to 950 THB a copy at 300 is about 3 to
4.3 times this plant's ex-works price (before freight, Thai import duty
and VAT, which the Thai buyer pays on an import).
- The artist ordering demos (price list demo-0.1) come to about 343 THB a
copy at 300 with vinyl mastering, 310 THB without: above this plant's
ex-works price and well below the Thai market. Both price levels are
open until roadmap item 1; this is a benchmark, not a re-base.
## Revenue at full capacity
| Scenario | Records/yr | Pressing revenue | Title services | Total USD | Total THB |
+54 -21
View File
@@ -472,6 +472,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="field"><span class="fl">Speed</span><div class="seg" data-k="rpm"><button type="button" data-v="33">33&#x2153; rpm</button><button type="button" data-v="45">45 rpm</button></div></div>
<div class="field"><span class="fl">Discs</span><div class="seg" data-k="discs" data-t="n"><button type="button" data-v="1">Single</button><button type="button" data-v="2">Double</button></div></div>
<div class="field" id="fWeight"><span class="fl">Weight</span><div class="seg" data-k="weight" data-t="n"><button type="button" data-v="140">140 g</button><button type="button" data-v="180">180 g heavyweight</button></div></div>
<div class="field" id="fDink"><span class="fl">Centre hole</span><div class="seg" data-k="dinked" data-t="b"><button type="button" data-v="false">Small</button><button type="button" data-v="true">Large (dinked)</button></div></div>
</div>
</div>
<div class="block">
@@ -486,6 +487,10 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="field" id="fC1"><span class="fl">Colour</span><div class="sw-row" data-c="c1"></div></div>
<div class="field" id="fC2"><span class="fl">Second colour</span><div class="sw-row" data-c="c2"></div></div>
</div>
<div class="rowc mt" style="align-items:center">
<div class="field" id="fClean"><span class="fl">Clean the press before your colour</span><div class="seg" data-k="cleaning" data-t="b"><button type="button" data-v="true">Yes, recommended</button><button type="button" data-v="false">No</button></div></div>
<p class="hint" id="cleanHint" style="max-width:400px;margin:0"></p>
</div>
</div>
<div class="block">
<h2>Packaging</h2>
@@ -496,19 +501,21 @@ document.documentElement.setAttribute("data-theme",t);})();
<button type="button" class="choice" data-k="jacket" data-v="none"><span class="ic"><span class="jic none"></span></span><b>My own jacket</b><span>You supply it</span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl">Labels</span><div class="seg" data-k="labels"><button type="button" data-v="printed">Printed</button><button type="button" data-v="white">Plain white</button><button type="button" data-v="black">Plain black</button></div></div>
<div class="field"><span class="fl">Inner sleeve</span><div class="seg" data-k="inner"><button type="button" data-v="paper">Paper</button><button type="button" data-v="poly">Poly-lined</button></div></div>
<div class="field"><span class="fl">Extras</span><div class="chips"><button type="button" class="chip" data-b="shrink">Shrink wrap</button><button type="button" class="chip" data-b="insert">Printed insert</button><button type="button" class="chip" data-b="sticker">Hype sticker</button><button type="button" class="chip" data-b="numbered">Numbered edition</button></div></div>
<div class="field"><span class="fl">Extras</span><div class="chips"><button type="button" class="chip" data-b="shrink">Shrink wrap</button><button type="button" class="chip" data-b="insert">Printed insert</button><button type="button" class="chip" data-b="sticker">Hype sticker</button><button type="button" class="chip" data-b="numbered">Numbered edition</button><button type="button" class="chip" data-b="dlcard">Download card</button><button type="button" class="chip" data-b="barcode">Barcode sticker</button></div></div>
</div>
</div>
<div class="block">
<h2>Sound</h2>
<div class="field" style="margin-bottom:14px;align-items:flex-start"><span class="fl">Your audio arrives as</span><div class="seg" data-k="source"><button type="button" data-v="digital">Digital files</button><button type="button" data-v="tape">Analog tape</button><button type="button" data-v="lacquer">A cut lacquer</button></div></div>
<div class="cards c2">
<button type="button" class="choice" data-k="mastering" data-v="true" data-t="b"><b>Vinyl mastering</b><span>Our engineer masters your mix for the groove</span></button>
<button type="button" class="choice" data-k="mastering" data-v="false" data-t="b"><b>My master is vinyl-ready</b><span>Cut straight from your files</span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl">Test pressings for you</span><div class="stepper"><button type="button" data-act="tps" data-d="-1" aria-label="Fewer">&#x2212;</button><b id="tpsV">5</b><button type="button" data-act="tps" data-d="1" aria-label="More">+</button></div></div>
<p class="hint" style="max-width:380px;margin:0">You hear and approve a real pressing before the run. The mother plate is archived, so a repress later skips cutting and plating.</p>
<p class="hint" id="srcHint" style="max-width:380px;margin:0">You hear and approve a real pressing before the run. The mother plate is archived, so a repress later skips cutting and plating.</p>
</div>
</div>
</section>
@@ -522,6 +529,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="block g2f">
<div class="field"><span class="fl">Artist</span><input type="text" id="artist" maxlength="60"></div>
<div class="field"><span class="fl">Release title</span><input type="text" id="title" maxlength="80"></div>
<div class="field" style="grid-column:1/-1"><span class="fl">Run-out inscription <span class="mute" style="font-weight:400">scribed by hand next to the run-out groove of every side: letters and numbers, up to 15</span></span><input type="text" id="catno" maxlength="15" spellcheck="false" autocomplete="off" style="max-width:260px;text-transform:uppercase;font-family:ui-monospace,Consolas,monospace;letter-spacing:0.08em"></div>
</div>
<div class="block">
<h2>Tracks and sides <small id="sideHint"></small></h2>
@@ -555,7 +563,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 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="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><p><b>Faster than an established EU plant.</b> Record Industry (NL) quotes 9 to 11 weeks from test-pressing approval to the finished order (its calculator, October 2026); the CNX quote here is 7 to 9.</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>
@@ -610,7 +618,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<div class="eyebrow">Step 5 of 5</div>
<h1 id="s5h">Campaign and production</h1>
<p class="lead" id="s5l"></p>
<div class="inv"><span class="il">Investor note</span><p><b>Traceability is the product.</b> Fan updates come from the factory's own event log, and every copy resolves to its stamper, press, cycle and side. At Making Vinyl Europe 2026 this is what the industry responded to most.</p><p><b>One release becomes a recurring one.</b> The archived mother makes a repress about 1.5 to 2.5 weeks of plant time: the highest-margin order the plant will take.</p></div>
<div class="inv"><span class="il">Investor note</span><p><b>Traceability is the product.</b> Fan updates come from the factory's own event log, and every copy resolves to its stamper, press, cycle and side. At Making Vinyl Europe 2026 this is what the industry responded to most. An established EU plant asks customers to accept about 2 % rejects (Record Industry FAQ, October 2026); CNX inspects every record optically.</p><p><b>One release becomes a recurring one.</b> The archived mother makes a repress about 1.5 to 2.5 weeks of plant time: the highest-margin order the plant will take.</p></div>
<div id="s5"></div>
</section>
</main>
@@ -650,7 +658,9 @@ const R={
press:{v:{12:115,10:95,7:55}},g180:{v:15},colour:{v:{solid:20,marbled:35,splatter:45}},
inner:{v:{paper:{12:7,10:6,7:4},poly:{12:15,10:12,7:8}}},assy:{v:10},
jacket:{v:{printed:{12:[86,73,64,55],10:[68,58,51,44],7:[27,23,20,17]},gatefold:{12:[127,109,95,82]},plain:{12:[27,27,27,27],10:[20,20,20,20],7:[8,8,8,8]}}},
handling:{v:0.10},shrink:{v:5},insert:{v:12},sticker:{v:6},numbered:{v:5},fee:{v:0.035},minRun:{v:100},deposit:{v:0.5}
handling:{v:0.10},shrink:{v:5},insert:{v:12},sticker:{v:6},numbered:{v:5},
/* added October 2026, estimates with Record Industry's calculator as the basis (see artist-order.html) */
tape:{v:3800},clean:{v:2300},plainLabel:{v:-12},dlcard:{v:43},barcode:{v:6},fee:{v:0.035},minRun:{v:100},deposit:{v:0.5}
};
const tierIdx=q=>q>=1000?3:q>=500?2:q>=300?1:0;
/* side length per format, minutes [comfortable, at most]: comfortable = lower end of the microcut.studio format guide;
@@ -659,7 +669,7 @@ const LIM={"12":{"33":[18,22],"45":[12,15]},"10":{"33":[12,15],"45":[8,12]},"7":
const SONG=272; /* seconds: the 4:32 average song the at-most table counts songs with */
const LOUD=0.6;
const JNAME={none:"Your own jacket",plain:"Plain jacket",printed:"Printed jacket",gatefold:"Gatefold jacket"};
const XN={shrink:"Shrink wrap",insert:"Insert",sticker:"Hype sticker",numbered:"Numbered"};
const XN={shrink:"Shrink wrap",insert:"Insert",sticker:"Hype sticker",numbered:"Numbered",dlcard:"Download card",barcode:"Barcode sticker"};
const ART={jacket:{t:"Jacket front",mm:{12:320,10:266,7:190}},label:{t:"Label",mm:{12:106,10:106,7:96}}};
/* stages, working days [low, high]: mature column of the production timeline (same table as artist-order.html) */
const STG=[
@@ -699,6 +709,7 @@ const S={
step:1,cur:"THB",size:"12",rpm:"33",discs:1,weight:140,colour:"black",c1:"#b3202a",c2:"#f2c14e",
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",
source:"digital",labels:"printed",cleaning:true,dinked:false,dlcard:false,barcode:false,catno:"MPC001",
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
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,
@@ -725,7 +736,10 @@ const rpmT=()=>S.rpm==="33"?"33⅓ rpm":"45 rpm";
const colourName=()=>({black:"Black vinyl",solid:"Solid colour",marbled:"Marbled",splatter:"Splatter"})[S.colour];
const colourT=()=>({black:"black",solid:"solid colour",marbled:"marbled",splatter:"splatter"})[S.colour];
const SIZE_MM={"12":{rec:302,jk:314,lab:100,name:"12\" LP"},"10":{rec:250,jk:260,lab:100,name:"10\""},"7":{rec:175,jk:184,lab:90,name:"7\" single"}};
function descT(){return (S.discs>1?"2 × ":"")+SIZE_MM[S.size].name+", "+rpmT()+", "+(S.size==="12"?S.weight+" g ":"")+colourT()+" vinyl";}
function descT(){return (S.discs>1?"2 × ":"")+SIZE_MM[S.size].name+(dinkOn()?" (dinked)":"")+", "+rpmT()+", "+(S.size==="12"?S.weight+" g ":"")+colourT()+" vinyl";}
const dinkOn=()=>S.dinked&&S.size==="7",mastOn=()=>S.mastering&&S.source!=="lacquer",cleanOn=()=>S.cleaning&&S.colour!=="black";
/* a stage runs unless the order skips it: no mastering or cutting when the artist sends a cut lacquer */
const stageOn=s=>s.opt==="mastering"?mastOn():s.id==="cut"?S.source!=="lacquer":true;
function toast(t){$$(".toast").forEach(x=>x.remove());const el=document.createElement("div");el.className="toast";el.textContent=t;document.body.appendChild(el);setTimeout(()=>el.remove(),2600);}
function hsh(s){let h=2166136261;for(const c of String(s)){h^=c.charCodeAt(0);h=Math.imul(h,16777619);}return h>>>0;}
const idOf=(p,s,n)=>p+"-"+hsh(s).toString(36).toUpperCase().padStart(7,"0").slice(0,n||4);
@@ -734,19 +748,23 @@ function rnd(seed){return function(){seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.i
/* ---- pricing (same model as artist-order.html) ---- */
function quote(q){
const sz=S.size,d=S.discs,sides=2*d,T=[],C=[],du=d>1?"discs":"disc";
if(S.mastering)T.push({t:"Vinyl mastering",n:sides,u:"sides",r:R.master.v});
T.push({t:"Lacquer blanks",n:sides,u:"sides",r:R.blank.v});
T.push({t:"Lacquer cutting",n:sides,u:"sides",r:R.cut.v});
if(mastOn())T.push({t:"Vinyl mastering",n:sides,u:"sides",r:R.master.v});
if(S.source==="tape")T.push({t:"Analog tape transfer",n:1,u:"",r:R.tape.v});
if(S.source!=="lacquer"){T.push({t:"Lacquer blanks",n:sides,u:"sides",r:R.blank.v});T.push({t:"Lacquer cutting",n:sides,u:"sides",r:R.cut.v});}
T.push({t:"Plating: father, mother (kept for represses), stamper",n:sides,u:"sides",r:R.metal.v});
T.push({t:"Press setup",n:d,u:du,r:R.setup.v});
if(S.tps>0)T.push({t:"Test pressings for you",n:S.tps,u:"copies",r:R.tp.v});
if(cleanOn())T.push({t:"Press cleaned before your colour",n:1,u:"",r:R.clean.v});
C.push({t:"Pressing, "+sz+"\""+(sz==="12"?" 140 g":"")+", printed labels",n:d,u:du,r:R.press.v[sz]});
if(sz==="12"&&S.weight===180)C.push({t:"180 g heavyweight",n:d,u:du,r:R.g180.v});
if(S.colour!=="black")C.push({t:"Colour vinyl, "+colourT(),n:d,u:du,r:R.colour.v[S.colour]});
C.push({t:"Inner sleeve, "+(S.inner==="poly"?"poly-lined":"paper"),n:d,u:du,r:R.inner.v[S.inner][sz]});
C.push({t:"Assembly and packing",n:1,u:"",r:R.assy.v});
if(S.jacket!=="none"){const p=R.jacket.v[S.jacket][sz][tierIdx(q)];C.push({t:JNAME[S.jacket]+(sz==="7"&&S.jacket!=="plain"?" (picture sleeve)":""),n:1,u:"",r:Math.round(p*(1+R.handling.v)),pass:p});}
if(S.labels!=="printed")C.push({t:"Plain "+S.labels+" labels instead of printed",n:d,u:du,r:R.plainLabel.v});
if(S.shrink)C.push({t:"Shrink wrap",n:1,u:"",r:R.shrink.v});
if(S.dlcard)C.push({t:"Download card",n:1,u:"",r:R.dlcard.v});
if(S.barcode)C.push({t:"Barcode sticker",n:1,u:"",r:R.barcode.v});
if(S.insert)C.push({t:"Printed insert",n:1,u:"",r:R.insert.v});
if(S.sticker)C.push({t:"Hype sticker",n:1,u:"",r:R.sticker.v});
if(S.numbered)C.push({t:"Hand-numbered",n:1,u:"",r:R.numbered.v});
@@ -756,7 +774,7 @@ function quote(q){
}
function leadInfo(campaign){
const g3=STG.findIndex(s=>s.id==="g3");
const pre=STG.slice(0,g3+1).filter(s=>(!s.opt||S[s.opt])&&!((campaign||S.gate1)&&(s.id==="intake"||s.id==="g1")));
const pre=STG.slice(0,g3+1).filter(s=>stageOn(s)&&!((campaign||S.gate1)&&(s.id==="intake"||s.id==="g1")));
const lo=pre.reduce((a,s)=>a+s.d[0],0),hi=pre.reduce((a,s)=>a+s.d[1],0);
const pw=[Math.max(1,Math.round(lo/5)),Math.max(1,Math.ceil(hi/5))];
const printed=S.jacket==="printed"||S.jacket==="gatefold";
@@ -877,7 +895,9 @@ const jacketArt=()=>S.jacket==="printed"||S.jacket==="gatefold";
function checkList(){
const L=[],files=S.tracks.filter(t=>t.file),ready=files.filter(t=>t.info&&!t.info.reading);
if(!S.tracks.length)L.push(["bad","Track list","Add at least one track."]);
if(!files.length)L.push(["pend","Master files","Not added yet: these checks use the lengths typed in. Drop WAV, FLAC or AIFF files to check the files themselves."]);
if(S.source==="lacquer")L.push(["pend","Your lacquer","Cut by your studio and sent by express courier the day it is cut: a lacquer has to be plated within days. Inspected under the microscope on arrival. Files below are for the side plan and the previews."]);
else if(S.source==="tape")L.push(["pend","Your tape","Analog tape is transferred and checked on arrival; format, speed and EQ curve are confirmed with you first."]);
if(S.source!=="digital"){}else if(!files.length)L.push(["pend","Master files","Not added yet: these checks use the lengths typed in. Drop WAV, FLAC or AIFF files to check the files themselves."]);
else{
const lossy=ready.filter(t=>!t.info.lossless);
L.push(lossy.length?["bad","File format",lossy.map(t=>esc(t.file.name)).join(", ")+": not WAV, FLAC or AIFF. Send the lossless master."]:["ok","File format",ready.length+" lossless file"+(ready.length===1?"":"s")]);
@@ -893,7 +913,7 @@ function checkList(){
:warn.length?["warn","Side length",warn.map(s=>"Side "+s.n+" "+mmss(s.sec)).join(", ")+": past the "+mmss(lim[0])+" that sounds best, so the cut gets quieter"]
:empty.length?["warn","Side length","Side "+empty.map(s=>s.n).join(", ")+" has no music"]:["ok","Side length","Every side within the "+mmss(lim[0])+" that sounds best"]);
if(jacketArt()){const c=artChecks("jacket");L.push(!c?["pend","Jacket artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Jacket artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Jacket artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Jacket artwork","Size, resolution and colour space fine"]);}
{const c=artChecks("label");L.push(!c?["pend","Label artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Label artwork","Size, resolution and colour space fine"]);}
if(S.labels==="printed"){const c=artChecks("label");L.push(!c?["pend","Label artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Label artwork","Size, resolution and colour space fine"]);}
L.push(["pend","Engineer check","After upload a cutting engineer confirms the sides are cuttable. This decides, not the bars above."]);
return L;
}
@@ -917,12 +937,16 @@ function mockSVG(id){
let g=`<g transform="translate(${x.toFixed(1)} ${cy.toFixed(1)}) scale(${s.toFixed(3)})"><g class="spin${S.rpm==="45"?" r45":""}"><circle r="105" fill="${base}"/>`;
if(S.colour==="marbled")g+=`<rect x="-105" y="-105" width="210" height="210" fill="${S.c2}" filter="url(#${id}m)" clip-path="url(#${id}c)"/>`;
if(S.colour==="splatter"){const r=rnd(7);let dots="";for(let i=0;i<80;i++){const a=r()*6.283,d=Math.sqrt(r())*100,z=1+r()*5;dots+=`<circle cx="${(Math.cos(a)*d).toFixed(1)}" cy="${(Math.sin(a)*d).toFixed(1)}" r="${z.toFixed(1)}" fill="${r()<0.8?S.c2:"#ffffff"}"/>`;}g+=`<g clip-path="url(#${id}c)">${dots}</g>`;}
for(let r=101;r>=rl+5;r-=3.2)g+=`<circle r="${r.toFixed(1)}" fill="none" stroke="${groove}" stroke-width="${(0.5/s).toFixed(2)}"/>`;
for(let r=101;r>=rl+10;r-=3.2)g+=`<circle r="${r.toFixed(1)}" fill="none" stroke="${groove}" stroke-width="${(0.5/s).toFixed(2)}"/>`;
/* run-out inscription, scribed in the dead wax between the label and the last groove */
if(S.catno){const rr=(rl+5).toFixed(1);g+=`<path id="${id}ro${x|0}" d="M0 -${rr} A${rr} ${rr} 0 1 1 -0.01 -${rr}" fill="none"/><text font-size="4.6" font-family="ui-monospace,Consolas,monospace" letter-spacing="0.8" fill="${dark?"rgba(255,255,255,0.55)":"rgba(0,0,0,0.5)"}"><textPath href="#${id}ro${x|0}" startOffset="8%">${esc(S.catno)} A</textPath></text>`;}
const L=rl.toFixed(1);
if(S.art.label&&S.art.label.url)g+=`<image href="${S.art.label.url}" x="-${L}" y="-${L}" width="${(2*rl).toFixed(1)}" height="${(2*rl).toFixed(1)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${id}l)"/>`;
if(S.labels==="white")g+=`<circle r="${L}" fill="#f3efe6"/><circle r="${(rl*0.86).toFixed(1)}" fill="none" stroke="#e2dccd" stroke-width="0.8"/>`;
else if(S.labels==="black")g+=`<circle r="${L}" fill="#1c1c1c"/><circle r="${(rl*0.86).toFixed(1)}" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.8"/>`;
else if(S.art.label&&S.art.label.url)g+=`<image href="${S.art.label.url}" x="-${L}" y="-${L}" width="${(2*rl).toFixed(1)}" height="${(2*rl).toFixed(1)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${id}l)"/>`;
else g+=`<circle r="${L}" fill="#efe6d2"/><circle r="${(rl*0.86).toFixed(1)}" fill="none" stroke="#d6c7a6" stroke-width="0.8"/><text y="${(-8*fs).toFixed(1)}" text-anchor="middle" font-size="${(6.6*fs).toFixed(1)}" font-weight="700" fill="#2a2a2a" font-family="system-ui,sans-serif">${esc(S.artist.slice(0,22))}</text><text y="${(13*fs).toFixed(1)}" text-anchor="middle" font-size="${(5.4*fs).toFixed(1)}" fill="#555" font-family="system-ui,sans-serif">${esc(S.title.slice(0,26))}</text><text y="${(24*fs).toFixed(1)}" text-anchor="middle" font-size="${(4.4*fs).toFixed(1)}" fill="#8a7d62" font-family="system-ui,sans-serif" letter-spacing="1">SIDE A · ${S.rpm==="33"?"33⅓":"45"}</text>`;
g+=`</g><circle r="105" fill="url(#${id}sh)"/>${wedge(-128,-104)}${wedge(52,76)}<circle r="105" fill="none" stroke="rgba(255,255,255,0.10)" stroke-width="${(1/s).toFixed(2)}"/>`;
return g+`<circle r="${(105*3.6/Z.rec).toFixed(2)}" class="hole"/></g>`;
return g+`<circle r="${(105*(dinkOn()?19.1:3.6)/Z.rec).toFixed(2)}" class="hole"/></g>`;
}
const recs=(S.discs>1?disc(cx+30*s):"")+disc(cx);
let jk="";const J=236;
@@ -986,6 +1010,12 @@ function syncControls(){
inp.disabled=off;pal.title=off?why:"Open the palette and pick any colour";pal.classList.toggle("off",off);pal.classList.toggle("on",!off&&custom);pal.style.setProperty("--cc",S[c]);
pal.querySelector(".pt").textContent=!off&&custom?"Your colour":"Palette";if(inp.value.toLowerCase()!==S[c].toLowerCase())inp.value=S[c];});
$("#tpsV").textContent=S.tps;
const s7=S.size==="7";$$("#fDink button").forEach(x=>{x.disabled=!s7;x.title=s7?"":"Only 7\" singles come with a large centre hole";});$("#fDink").classList.toggle("off",!s7);
$$("#fClean button").forEach(x=>{x.disabled=blk;x.title=blk?"Black vinyl needs no cleaning":"";});$("#fClean").classList.toggle("off",blk);
$("#cleanHint").textContent=blk?"Only for colour vinyl.":S.cleaning?"The press is purged before your run, so no trace of the previous colour gets in. Light colours can still show the odd speck.":"Saves the purge, but some records can carry streaks of the previous colour.";
const lq=S.source==="lacquer";$$(".choice[data-k=mastering]").forEach(x=>{x.disabled=lq;x.title=lq?"Your lacquer is already cut and mastered":"";if(lq){x.classList.remove("on");x.setAttribute("aria-pressed","false");}});
$("#srcHint").textContent=lq?"Your lacquer goes straight to plating. You still hear and approve a test pressing, and the mother is archived for represses.":S.source==="tape"?"Your tape is transferred in-house, then cut. You hear and approve a real pressing before the run.":"You hear and approve a real pressing before the run. The mother plate is archived, so a repress later skips cutting and plating.";
const dl=$("#dropLabel"),offL=S.labels!=="printed";dl.classList.toggle("off",offL);$("#fileLabel").disabled=offL;dl.title=offL?"Plain labels have no print":"";
const dj=$("#dropJacket"),offJ=!jacketArt();dj.classList.toggle("off",offJ);$("#fileJacket").disabled=offJ;dj.title=offJ?(S.jacket==="none"?"You supply your own jacket":"A plain jacket has no print"):"";
$$(".qchips .chip").forEach(c=>c.classList.toggle("on",+c.dataset.q===S.qty));
}
@@ -1106,7 +1136,7 @@ function trackerHTML(){
<div class="panel"><h2>${P.origin==="direct"?"Your order updates":"Updates your fans see"} <small>from the factory log</small></h2><ul class="feed">${feed}</ul><p class="hint">These come from the plant's own event log: the progress is the factory's record, not a status someone remembers to update.</p></div></div>${fin?passportHTML():""}`;
}
function newProd(origin,start,fans){
const L=leadInfo(true),st=STG.filter(s=>s.id!=="intake"&&s.id!=="g1"&&(!s.opt||S[s.opt]));
const L=leadInfo(true),st=STG.filter(s=>s.id!=="intake"&&s.id!=="g1"&&stageOn(s));
const mid=s=>(s.d[0]+s.d[1])/2,after=st.filter(s=>["run","fin","ship"].includes(s.id)).reduce((a,s)=>a+mid(s),0),qMid=(L.qw[0]+L.qw[1])/2*5;
let cur=new Date(start);
const rows=st.map(s=>{const d=s.id==="queue"?Math.max(0,qMid-after):mid(s);cur=addWD(cur,d);return Object.assign({},s,{date:new Date(cur)});});
@@ -1184,9 +1214,10 @@ function passportHTML(){
const run=by("run"),rd=new Date(run.date);rd.setHours(8+hsh(seed+"h")%9,hsh(seed+"m")%60,hsh(seed+"s")%60);
const p2=v=>String(v).padStart(2,"0"),ts=rd.getFullYear()+"-"+p2(rd.getMonth()+1)+"-"+p2(rd.getDate())+" "+p2(rd.getHours())+":"+p2(rd.getMinutes())+":"+p2(rd.getSeconds());
const st=idOf("S",seed+"st"),lac=idOf("L",seed+"l"),fa=idOf("F",seed+"f"),mo=idOf("M",seed+"mo"),lot=idOf("C",seed+"c",3);
const it=[["Master received","Checksums verified at both ends, stored in the client vault",P.start]];
const it=S.source==="lacquer"?[]:[[S.source==="tape"?"Tape received":"Master received",S.source==="tape"?"Transferred in-house, stored in the client vault":"Checksums verified at both ends, stored in the client vault",P.start]];
if(by("g2"))it.push(["Mastered for vinyl","Master approved by the artist",by("g2").date]);
it.push(["Cut to lacquer","Lacquer "+lac+", serial scribed in the run-out groove",by("cut").date]);
if(by("cut"))it.push(["Cut to lacquer","Lacquer "+lac+(S.catno?", \u201C"+S.catno+" A\u201D":", serial")+" scribed in the run-out",by("cut").date]);
else it.push(["Lacquer received","Cut by the artist's studio, inspected under the microscope on arrival",P.start]);
it.push(["Plated in-house","Father "+fa+" → mother "+mo+" (archived for represses) → stamper "+st,by("plate").date]);
it.push(["Test pressing approved","Heard and approved by the artist",by("g3").date]);
it.push(["Pressed","Press D1-B (Newbilt Duplex) at "+ts.slice(11)+", compound lot "+lot+", press data logged for this cycle",run.date]);
@@ -1223,6 +1254,7 @@ function renderStage(){
$("#mock").innerHTML=mockSVG("ms");
$("#stT").textContent=S.title||"Untitled";$("#stA").textContent=S.artist||"";
const sp=[(S.discs>1?"2 × ":"")+SIZE_MM[S.size].name,rpmT()];if(S.size==="12")sp.push(S.weight+" g");sp.push(colourName(),JNAME[S.jacket]);
if(S.labels!=="printed")sp.push("Plain "+S.labels+" labels");if(S.catno)sp.push("Run-out "+S.catno);if(S.source!=="digital")sp.push(S.source==="tape"?"From tape":"Your lacquer");
Object.keys(XN).forEach(k=>{if(S[k])sp.push(XN[k]);});
$("#stSpecs").innerHTML=sp.map(x=>`<span>${esc(x)}</span>`).join("");
const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb;
@@ -1249,7 +1281,7 @@ function launch(){if(!canLaunch()){if(!S.gate1)go(2);return;}resetCampaign();S.m
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);
$("#artist").value=S.artist;$("#title").value=S.title;$("#catno").value=S.catno;$("#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.capN;$("#limT").checked=S.limited;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
}
function resetDemo(){
@@ -1258,7 +1290,7 @@ function resetDemo(){
}
/* ---- events ---- */
const PLAN_KEYS=["size","rpm","discs","loud"],RUN_KEYS=["weight","colour","inner","jacket","mastering"];
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","mastering","labels","cleaning","dinked"];
function setK(k,v){
if(S[k]===v)return;S[k]=v;
if(k==="size"&&v!=="12"&&S.jacket==="gatefold")S.jacket="printed";
@@ -1310,6 +1342,7 @@ document.addEventListener("input",e=>{
else if(id==="qty"){const v=Math.round(+t.value);if(v>=100&&v<=5000)setQty(v);}
else if(t.type==="color"){const c=t.closest("[data-c]").dataset.c;S[c]=t.value;renderAll();}
else if(id==="artist"||id==="title"){S[id]=t.value;renderStage();if(S.mode==="campaign")updateS5();}
else if(id==="catno"){const v=t.value.toUpperCase().replace(/[^A-Z0-9]/g,"").slice(0,15);if(v!==t.value){const c=t.selectionStart;t.value=v;try{t.setSelectionRange(c,c);}catch(e){}}S.catno=v;renderStage();if(S.mode==="campaign")updateS5();}
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();}
+6 -1
View File
@@ -34,7 +34,12 @@ Reported EU/US plant lead times run roughly **10 to 16 weeks** for a
standard black LP, down from the 6-to-9-month peak of 2021–22. That
figure is an industry impression gathered in conversation, not a
surveyed one, and it should be tested at Making Vinyl alongside the
price. Two cautions on the comparison:
price. One published data point: Record Industry (Haarlem, NL) quotes
**3 weeks to test pressings** (from complete files, or from a supplied
lacquer's arrival) and then **9 to 11 weeks** for the finished order
after the customer approves the test pressings and the artwork
(calculator FAQ, checked October 2026), which on the same reference
point compares with the 7 to 9 week mature quote below. Two cautions on the comparison:
- **It is not like-for-like.** The EU/US figure is a *quoted* lead time
carrying queue and buffer; the CNX figures above are unbuffered stage