diff --git a/README.md b/README.md
index eb99976..4108461 100644
--- a/README.md
+++ b/README.md
@@ -170,10 +170,12 @@ The detail lives in dedicated documents:
fan-funded campaign that the plant presses and ships to each fan once
enough have pre-ordered, with refunds if not
- [Artist ordering demo](docs/artist-order.html): the artist platform as a
- clickable single page: configure a record, type the track list or drop
- the master files and artwork (checked in the browser, never uploaded:
- format, sample rate, bit depth, clipping, side length against the
- format, print resolution), approve gate 1, get the itemised offer with
+ clickable single page: configure a record (jacket with a 3 or 5 mm
+ spine), type the track list or drop the master files and artwork
+ (checked in the browser, never uploaded: format, sample rate, bit
+ depth, clipping, side length against the format; artwork as PDF, AI or
+ image against the downloadable jacket and label templates: size and
+ spine, bleed, resolution, colour, fonts), approve gate 1, get the itemised offer with
its price curve and lead time, then order directly or run a fan-funded
campaign to a funded or missed end (refund, or go ahead at the
minimum) and follow the production through the three artist gates
diff --git a/docs/artist-order.html b/docs/artist-order.html
index 9b9614e..76cd4a1 100644
--- a/docs/artist-order.html
+++ b/docs/artist-order.html
@@ -132,6 +132,10 @@ document.documentElement.setAttribute("data-theme",t);})();
.navb { display: flex; justify-content: space-between; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.art { display: flex; gap: 12px; align-items: flex-start; padding: 8px 0; border-top: 1px solid var(--line); }
.art img { width: 84px; height: 84px; object-fit: cover; border-radius: 4px; border: 1px solid var(--stroke); flex: none; }
+ .art .aph { width: 84px; height: 84px; border-radius: 4px; border: 1px solid var(--stroke); flex: none; display: grid; place-items: center; font-weight: 700; color: var(--mute); }
+ .tpl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 10px; align-items: center; margin: 2px 0 6px; }
+ .tpl small { color: var(--mute); }
+ .tpl .btn:disabled { opacity: 0.45; cursor: not-allowed; }
.art ul { list-style: none; padding: 0; margin: 4px 0; font-size: 13.5px; }
.phone { max-width: 390px; margin: 0 auto; border: 1px solid var(--stroke); border-radius: 18px; padding: 14px; background: var(--panel2); }
.fp-t { font-size: 20px; font-weight: 700; margin-top: 10px; }
@@ -213,6 +217,7 @@ document.documentElement.setAttribute("data-theme",t);})();
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.
+
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. The spine is 3 mm for one record and 5 mm for two, or for one with a thick insert.
The real platform checks every file against the CNX templates (size, centre hole, safe area, bleed) and renders a proof. Template sizes here are placeholders until those templates exist.
+
Files are read in this browser and checked against the template for the format and spine chosen in step 1: size, spine, bleed, resolution, colour space, fonts. PDF previews are drawn with pdf.js. Template sizes follow published plant templates until the packaging partner's dielines replace them.
Automatic checks
@@ -357,6 +363,7 @@ const R={
inner:{v:{paper:{12:7,10:6,7:4},poly:{12:15,10:12,7:8}},u:"per disc",t:"Inner sleeve",k:"est",src:"Paper is inside the 132 THB above; a poly-lined sleeve costs 3.0 to 5.7 THB (revenue model); prices are estimates."},
assy:{v:10,u:"per copy",t:"Assembly and packing",k:"est",src:"Part of the 132 THB above; the split is an estimate."},
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]}},u:"per copy, partner price",t:"Jacket from the packaging partner, price breaks at 300 / 500 / 1,000",k:"est",src:"Estimate until the packaging partner quotes. With handling and shrink a printed 12\" jacket comes to about 100 THB a copy at short run, as in the artist platform illustration."},
+ spine:{v:{12:12,10:10},u:"per copy",t:"Thick 5 mm spine instead of 3 mm (two records, or a thick insert)",k:"est",src:"Estimate. Quality Record Pressings (US) lists a generic wide-spine jacket at 0.90 USD against 0.55 for a standard one, about 12 THB more; Kudos (UK) charges 137.50 GBP per order for an extra wide spine (price lists, checked October 2026)."},
handling:{v:0.10,u:"on pass-through",t:"Handling margin on the jacket",k:"est",src:"Placeholder. The artist platform shows pass-through items at the partner price plus a stated margin."},
shrink:{v:5,u:"per copy",t:"Shrink wrap",k:"est",src:"Film costs 0.8 to 1.1 THB (revenue model); price an estimate."},
insert:{v:12,u:"per copy",t:"Printed insert",k:"est",src:"Estimate."},
@@ -378,7 +385,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; /* Diggers Factory: dance music ~11:27 against 19:23 standard on a 12" LP */
const JNAME={none:"Jacket supplied by you",plain:"Plain jacket",printed:"Printed jacket",gatefold:"Printed gatefold jacket"};
-const ART={jacket:{t:"Jacket front",mm:{12:320,10:266,7:190}},label:{t:"Label",mm:{12:106,10:106,7:96}}};
+const ART={jacket:{t:"Jacket",mm:{12:320,10:266,7:190}},label:{t:"Labels",mm:{12:106,10:106,7:96}}};
/* stages, working days [low, high]: mature column of the production timeline */
const STG=[
{id:"intake",t:"Audio intake and technical QA",d:[1,2]},
@@ -412,7 +419,7 @@ const UPD={
const TODAY=new Date(); TODAY.setHours(12,0,0,0);
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,
+ qty:300,inner:"paper",jacket:"printed",spine:"thin",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},
@@ -461,6 +468,7 @@ function quote(q){
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(thickOn())C.push({t:"Thick 5 mm spine",n:1,u:"",r:R.spine.v[sz]});
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});
@@ -576,18 +584,188 @@ async function jpegComponents(file){
const len=(b[i+2]<<8)|b[i+3];if(m>=0xC0&&m<=0xCF&&m!==0xC4&&m!==0xC8&&m!==0xCC)return b[i+9];i+=2+len;}
return null;
}
-async function readImage(file){
+/* ---- artwork: spine, templates, PDF and AI files (October 2026) ----
+ 12" panels 315 mm: 633 x 315 mm with a 3 mm spine, 635 x 315 with 5 mm (Mighty Media Discs templates); 3 mm holds one
+ record, 5 mm two (Implant, Jungle Record Press); gatefold 7 mm (RPM Records), 9 mm for two (XVinylX), all checked October 2026.
+ 10" and 7" panels and the labels follow SIZE_MM; 3 mm bleed and 5 mm safe area are this demo's choice; hole 7.24 mm, dinked 38.2 mm. */
+const TPL={panel:{12:315,10:260,7:184},bleed:3,safe:5,hole:7.24,dink:38.2};
+const singleJ=()=>S.jacket==="printed"||S.jacket==="plain";
+const spineForced=()=>singleJ()&&S.discs>1&&S.size!=="7";
+const thickOn=()=>singleJ()&&S.size!=="7"&&(S.spine==="thick"||S.discs>1);
+function spineMM(){if(S.jacket==="none")return 0;if(S.jacket==="gatefold")return S.discs>1?9:7;return thickOn()?5:3;}
+function spineState(){
+ const off=!singleJ()||S.size==="7";
+ return {off,forced:spineForced(),
+ why:S.jacket==="gatefold"?"A gatefold has its own "+spineMM()+" mm spine":S.jacket==="none"?"You supply the jacket":S.size==="7"?"7\" jackets come with a 3 mm spine":"",
+ hint:S.jacket==="none"?"you supply the jacket":S.jacket==="gatefold"?"the gatefold has a "+spineMM()+" mm spine":S.size==="7"?"3 mm on a 7\" jacket":thickOn()?(spineForced()?"5 mm: needed for two records":"5 mm: room for a thick insert or a poster"):"3 mm: one record in its inner sleeve"};
+}
+function tplDims(k){const B=TPL.bleed;
+ if(k==="label"){const d=SIZE_MM[S.size].lab;return {d,txt:d+" mm label, "+(d+2*B)+" mm with bleed, a page per side ("+sideNames().join(", ")+")"};}
+ const P=TPL.panel[S.size],sp=spineMM(),w=2*P+sp;
+ return {P,sp,w,h:P,txt:(w+2*B)+" × "+(P+2*B)+" mm with bleed, "+sp+" mm spine"+(S.jacket==="gatefold"?", outside and inside":"")};}
+const tplOff=k=>k==="jacket"?(S.jacket==="printed"||S.jacket==="gatefold"?"":S.jacket==="none"?"You supply the jacket":"A plain jacket has no print"):(S.labels==="printed"?"":"Plain labels have no print");
+
+/* templates as vector PDF, written here: trim and bleed boxes set, guides on the page */
+function pdfStr(s){return "("+String(s).replace(/[^\x20-\x7e]/g,"?").replace(/[\\()]/g,m=>"\\"+m)+")";}
+function pdfBlob(pages,title){
+ const objs=[],add=s=>objs.push(s);
+ add("");add("");add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
+ add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>");
+ add("<< /Title "+pdfStr(title)+" /Creator (CNX Vinyl demo template, working name) >>");
+ const kids=[],bx=a=>"["+a.map(v=>v.toFixed(2)).join(" ")+"]";
+ for(const p of pages){const c=add("<< /Length "+p.ops.length+" >>\nstream\n"+p.ops+"\nendstream");
+ kids.push(add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 "+p.w.toFixed(2)+" "+p.h.toFixed(2)+"] /TrimBox "+bx(p.trim)+" /BleedBox "+bx(p.bleed)+" /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents "+c+" 0 R >>"));}
+ objs[0]="<< /Type /Catalog /Pages 2 0 R >>";objs[1]="<< /Type /Pages /Kids ["+kids.map(k=>k+" 0 R").join(" ")+"] /Count "+kids.length+" >>";
+ let out="%PDF-1.4\n";const off=[];
+ objs.forEach((o,i)=>{off.push(out.length);out+=(i+1)+" 0 obj\n"+o+"\nendobj\n";});
+ const xr=out.length;
+ out+="xref\n0 "+(objs.length+1)+"\n0000000000 65535 f \n"+off.map(o=>String(o).padStart(10,"0")+" 00000 n \n").join("")+"trailer\n<< /Size "+(objs.length+1)+" /Root 1 0 R /Info 5 0 R >>\nstartxref\n"+xr+"\n%%EOF\n";
+ return new Blob([out],{type:"application/pdf"});
+}
+function tplPages(k){
+ const K=72/25.4,B=TPL.bleed,SF=TPL.safe,M=16,MB=30,TOP=14,f=v=>(v*K).toFixed(2),pages=[];
+ const ST={trim:"0 0 0 RG 0.7 w [] 0 d",bleed:"0.85 0.12 0.12 RG 0.6 w [3 2] 0 d",safe:"0.1 0.45 0.85 RG 0.6 w [1.5 1.5] 0 d",fold:"0.55 0.2 0.7 RG 0.6 w [4 2] 0 d",mark:"0 0 0 RG 0.3 w [] 0 d"};
+ const wrap=(s,mm,size)=>{const per=Math.max(20,Math.floor(mm*K/(size*0.5))),L=[];let ln="";for(const w of s.split(" ")){if((ln+" "+w).trim().length>per){L.push(ln);ln=w;}else ln=(ln+" "+w).trim();}if(ln)L.push(ln);return L;};
+ function page(tw,th,draw,head,foot){
+ const W=tw+2*B+2*M,H=th+2*B+M+MB+TOP,ox=M+B,oy=MB+B;let o="";
+ const P=(x,y)=>f(ox+x)+" "+f(oy+y);
+ const g={
+ rect:(x,y,w,h,s)=>{o+="q "+ST[s]+" "+P(x,y)+" "+f(w)+" "+f(h)+" re S Q\n";},
+ line:(x1,y1,x2,y2,s)=>{o+="q "+ST[s]+" "+P(x1,y1)+" m "+P(x2,y2)+" l S Q\n";},
+ circ:(cx,cy,r,s,fill)=>{const c=0.5523*r;o+="q "+(fill?fill+" g ":"")+ST[s]+" "+P(cx+r,cy)+" m "+P(cx+r,cy+c)+" "+P(cx+c,cy+r)+" "+P(cx,cy+r)+" c "+P(cx-c,cy+r)+" "+P(cx-r,cy+c)+" "+P(cx-r,cy)+" c "+P(cx-r,cy-c)+" "+P(cx-c,cy-r)+" "+P(cx,cy-r)+" c "+P(cx+c,cy-r)+" "+P(cx+r,cy-c)+" "+P(cx+r,cy)+" c "+(fill?"B":"S")+" Q\n";},
+ text:(x,y,size,s,op)=>{op=op||{};const x0=op.c?x-String(s).length*size*0.25/K:x;o+="q "+(op.grey||"0.25")+" g BT /"+(op.b?"F2":"F1")+" "+size+" Tf 1 0 0 1 "+P(x0,y)+" Tm "+pdfStr(s)+" Tj ET Q\n";}
+ };
+ draw(g);
+ [[0,0],[tw,0],[0,th],[tw,th]].forEach(([x,y])=>{const sx=x?1:-1,sy=y?1:-1;g.line(x+sx*(B+2),y,x+sx*(B+9),y,"mark");g.line(x,y+sy*(B+2),x,y+sy*(B+9),"mark");});
+ let y=th+B+M+TOP-7;const tm=tw+2*B+2*M-12;
+ head.forEach((s,i)=>wrap(s,tm,i?6.5:9).forEach(l=>{g.text(-B-M+6,y,i?6.5:9,l,{b:!i,grey:i?"0.3":"0.1"});y-=i?3:4.2;}));
+ y=-B-14;foot.forEach(s=>wrap(s,tm,6).forEach(l=>{g.text(-B-M+6,y,6,l,{grey:"0.35"});y-=2.8;}));
+ pages.push({w:W*K,h:H*K,trim:[ox*K,oy*K,(ox+tw)*K,(oy+th)*K],bleed:[(ox-B)*K,(oy-B)*K,(ox+tw+B)*K,(oy+th+B)*K],ops:o});
+ }
+ const foot=["Put your artwork on a layer under these guides and hide the guides before you export. Deliver a PDF (PDF/X-4 preferred) or an Illustrator file saved with PDF compatibility; CMYK; images 300 dpi at print size; text outlined or fonts embedded.",
+ "Demo template: sizes follow published plant templates (3 mm spine for one record, 5 mm for two). The production template comes from the packaging partner. CNX Vinyl is a working name."];
+ if(k==="jacket"){
+ const D=tplDims("jacket"),P=D.P,sp=D.sp,w=D.w,h=D.h,gf=S.jacket==="gatefold",nm=S.size+"\" "+(gf?"gatefold jacket":"jacket");
+ const spread=inside=>page(w,h,g=>{
+ g.rect(-B,-B,w+2*B,h+2*B,"bleed");g.rect(0,0,w,h,"trim");g.line(P,-B,P,h+B,"fold");g.line(P+sp,-B,P+sp,h+B,"fold");
+ g.rect(SF,SF,P-2*SF,h-2*SF,"safe");g.rect(P+sp+SF,SF,P-2*SF,h-2*SF,"safe");
+ g.text(P/2,h/2,16,inside?"INSIDE LEFT":"BACK",{c:1,b:1,grey:"0.78"});g.text(P+sp+P/2,h/2,16,inside?"INSIDE RIGHT":"FRONT",{c:1,b:1,grey:"0.78"});
+ g.text(P+sp/2,h+B+3,6,"spine "+sp+" mm",{c:1});
+ },["CNX Vinyl demo template: "+nm+", "+sp+" mm spine"+(gf?(inside?", page 2: inside":", page 1: outside"):""),
+ "Trim "+w+" x "+h+" mm: "+(inside?"inside left "+P+", spine "+sp+", inside right "+P:"back "+P+", spine "+sp+", front "+P)+" mm. Bleed "+B+" mm (red), safe area "+SF+" mm inside the trim (blue), folds (purple)."],foot);
+ spread(false);if(gf)spread(true);
+ }else{
+ const d=SIZE_MM[S.size].lab,r=d/2,dk=dinkOn(),hole=dk?TPL.dink:TPL.hole,keep=dk?3:8;
+ sideNames().forEach(sd=>page(d,d,g=>{
+ g.circ(r,r,r+B,"bleed");g.circ(r,r,r,"trim");g.circ(r,r,r-SF,"safe");g.circ(r,r,hole/2+keep,"safe");g.circ(r,r,hole/2,"trim","0.85");
+ g.text(r,r+hole/2+keep+4,8,"SIDE "+sd,{c:1,b:1,grey:"0.7"});
+ },["CNX Vinyl demo template: "+S.size+"\" label, side "+sd,
+ "Label "+d+" mm across, "+(d+2*B)+" mm with bleed (red). Centre hole "+hole+" mm"+(dk?" (dinked)":"")+": keep text "+keep+" mm clear of it and "+SF+" mm inside the edge (blue). One page per side."],foot));
+ }
+ return pages;
+}
+function downloadTpl(k){
+ if(tplOff(k))return;
+ const name="cnx-demo-template-"+S.size+"in-"+(k==="label"?"labels-"+sideNames().join(""):(S.jacket==="gatefold"?"gatefold":"jacket")+"-"+spineMM()+"mm-spine")+".pdf";
+ const a=document.createElement("a");a.href=URL.createObjectURL(pdfBlob(tplPages(k),name.slice(0,-4)));a.download=name;document.body.appendChild(a);a.click();
+ setTimeout(()=>{URL.revokeObjectURL(a.href);a.remove();},5000);toast("Template downloaded: "+name);
+}
+
+/* PDF and Illustrator files: read in this browser. Page boxes, colour spaces and fonts come from the file
+ (object streams inflated with DecompressionStream); the preview is rendered with pdf.js, loaded only when a PDF arrives. */
+const lat=u=>new TextDecoder("latin1").decode(u);
+async function inflate(u){const rd=new Blob([u]).stream().pipeThrough(new DecompressionStream("deflate")).getReader(),ch=[];let n=0;
+ try{for(;;){const x=await rd.read();if(x.done)break;ch.push(x.value);n+=x.value.length;}}catch(_){}
+ const o=new Uint8Array(n);let p=0;for(const c of ch){o.set(c,p);p+=c.length;}return o;}
+async function pdfFacts(file){
+ const u8=new Uint8Array(await file.arrayBuffer()),head=lat(u8.subarray(0,4096)),a={name:file.name,kind:"pdf",ai:/\.ai$/i.test(file.name)};
+ if(/^%!PS-Adobe/.test(head)){a.kind="ps";return a;}
+ const vm=/%PDF-(\d\.\d)/.exec(head);if(!vm){a.kind="bad";return a;}
+ a.ver=vm[1];let T=lat(u8);const re=/\/Type\s*\/ObjStm/g,extra=[];let m,n=0;
+ while((m=re.exec(T))&&n<500){n++;let s=T.indexOf("stream",m.index);if(s<0)break;const d0=T.slice(Math.max(0,m.index-400),s);s+=6;if(T.charCodeAt(s)===13)s++;if(T.charCodeAt(s)===10)s++;
+ let e=T.indexOf("endstream",s);if(e<0)break;re.lastIndex=e;while(e>s&&(T.charCodeAt(e-1)===10||T.charCodeAt(e-1)===13))e--;
+ if(/FlateDecode/.test(d0))extra.push(lat(await inflate(u8.subarray(s,e))));}
+ T+="\n"+extra.join("\n");
+ const box=(src,nm)=>{const x=new RegExp("\\/"+nm+"\\s*\\[([^\\]]+)\\]").exec(src);if(!x)return null;const v=x[1].trim().split(/[\s,]+/).map(Number);return v.length===4&&v.every(isFinite)?v:null;};
+ const pm=/\/Type\s*\/Page(?![a-zA-Z])/.exec(T),win=pm?T.slice(Math.max(0,pm.index-1500),pm.index+1500):"";
+ a.media=box(win,"MediaBox")||box(T,"MediaBox");a.trimB=box(win,"TrimBox");a.bleedB=box(win,"BleedBox");
+ let c=0;for(const x of T.matchAll(/\/Count\s+(\d+)/g))c=Math.max(c,+x[1]);a.pages=c||1;
+ a.pdfx=/GTS_PDFX/.test(T);
+ const cr=/([^<]{1,80})1.5?crop(cv,R-h,T,h,h,800):crop(cv,L,T,R-L,h,800);doc.destroy();
+ }catch(e){a.noPrev=true;}
+}
+const mmOf=b=>b?[(b[2]-b[0])*25.4/72,(b[3]-b[1])*25.4/72]:null;
+function pdfChecks(k,a){
+ const out=[],B=TPL.bleed,r1=v=>Math.round(v*10)/10,near=(x,y)=>Math.abs(x-y)<=1;
+ out.push(["ok",(a.ai?"Illustrator file, saved PDF-compatible":"PDF "+a.ver)+(a.pdfx?", PDF/X":"")]);
+ const tr=mmOf(a.trimB),md=mmOf(a.media),sz=tr||md||[0,0],tw=sz[0],th=sz[1];let bl=null;
+ if(tr){const ref=a.bleedB||a.media;bl=Math.min(a.trimB[0]-ref[0],a.trimB[1]-ref[1],ref[2]-a.trimB[2],ref[3]-a.trimB[3])*25.4/72;}
+ const fit=(W,H)=>{if(tr)return near(tw,W)&&near(th,H)?"trim":"";if(near(tw,W+2*B)&&near(th,H+2*B)){bl=B;return "bleed";}if(near(tw,W)&&near(th,H)){bl=0;return "trim";}return "";};
+ if(k==="label"){const d=SIZE_MM[S.size].lab,ns=sideNames().length,ok=fit(d,d);
+ out.push(ok?["ok",(ok==="trim"?"Trim ":"Page ")+r1(tw)+" mm: the "+S.size+"\" label"+(ok==="bleed"?" with 3 mm bleed":"")]:["bad",(tr?"Trim ":"Page ")+r1(tw)+" × "+r1(th)+" mm; the "+S.size+"\" label is "+d+" mm ("+(d+2*B)+" with bleed)"]);
+ out.push(a.pages>=ns?["ok",a.pages+" page"+(a.pages>1?"s":"")+": side"+(ns>1?"s ":" ")+sideNames().join(", ")]:["warn",a.pages+" page"+(a.pages>1?"s":"")+" for "+ns+" sides: side "+sideNames().slice(a.pages).join(", ")+" missing"]);
+ }else{const D=tplDims("jacket"),ok=fit(D.w,D.h),hh=ok==="bleed"?th-2*B:th,ww=ok==="bleed"?tw-2*B:tw;
+ if(ok)out.push(["ok",r1(ww)+" × "+r1(hh)+" mm: back, "+D.sp+" mm spine, front"]);
+ else if(fit(D.P,D.P))out.push(["warn","Front only: the back and the "+D.sp+" mm spine are needed before print (use the template)"]);
+ else{const H=near(th,D.h)?th:near(th,D.h+2*B)?th-2*B:0,W=H===th?tw:tw-2*B,sp=W-2*D.P;
+ out.push(H&&sp>0.5&&sp<12?["bad","The spread fits a "+Math.round(sp)+" mm spine; this order has "+D.sp+" mm. Use the template"]:["bad",r1(tw)+" × "+r1(th)+" mm does not match the template ("+D.w+" × "+D.h+" mm trim)"]);}
+ if(S.jacket==="gatefold")out.push(a.pages>=2?["ok","Page 2: the inside spread"]:["warn","Gatefold: the inside spread is needed as page 2"]);
+ }
+ out.push(bl==null?["warn","No trim box: the bleed could not be checked"]:bl>=B-0.2?["ok",r1(bl)+" mm bleed"]:["bad",(bl<=0.2?"No bleed":r1(bl)+" mm bleed")+": 3 mm needed"]);
+ out.push(a.cmyk&&!a.rgb?["ok","CMYK"]:a.cmyk?["warn","RGB and CMYK mixed: convert to CMYK"]:a.rgb?["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]:["pend","Colour checked on the proof: no colour space declared in the file"]);
+ out.push(!a.fonts?["ok","No live text: outlined"]:a.emb?["ok","Fonts embedded"]:["bad","Fonts not embedded: outline the text or embed the fonts"]);
+ return out;
+}
+function rasterChecks(k,a){
+ const out=[],B=TPL.bleed;let mmH;
+ if(k==="label"){mmH=SIZE_MM[S.size].lab+2*B;out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square, for side A"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);}
+ else{const D=tplDims("jacket"),r=a.w/a.h;mmH=D.h+2*B;
+ if(r>1.5){const wmm=a.w/(a.h/mmH)-2*B,sp=wmm-2*D.P;
+ out.push(Math.abs(wmm-D.w)<=1.2?["ok","Spread: back, "+D.sp+" mm spine, front"]:sp>0.5&&sp<12?["bad","The spread fits a "+Math.round(sp)+" mm spine; this order has "+D.sp+" mm. Use the template"]:["bad","Spread "+Math.round(wmm)+" mm wide; the template is "+D.w+" mm plus bleed"]);
+ if(S.jacket==="gatefold")out.push(["warn","Gatefold: the inside spread is needed too (page 2 of a PDF)"]);}
+ else if(Math.abs(r-1)<0.01)out.push(["warn","Front only: the back and the "+D.sp+" mm spine are needed before print (use the template)"]);
+ else out.push(["bad","Neither the spread nor a square front ("+a.w+" × "+a.h+" px)"]);}
+ const need=Math.round(mmH/25.4*300),dpi=Math.round(a.h/(mmH/25.4));
+ out.push(a.h>=need?["ok",dpi+" dpi at "+mmH+" mm high"]:a.h>=need*0.8?["warn",dpi+" dpi, a little under 300 (needs "+need+" px high)"]:["bad",dpi+" dpi: too low for print (needs "+need+" px high)"]);
+ out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]);
+ return out;
+}
+async function readImage(file,k){
const url=URL.createObjectURL(file),img=new Image();img.src=url;await img.decode();
const comps=(file.type==="image/jpeg"||/\.jpe?g$/i.test(file.name))?await jpegComponents(file):null;
- return {url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
+ const a={kind:"img",url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
+ if(k==="jacket"&&a.w/a.h>1.5)a.url=crop(img,a.w-a.h,0,a.h,a.h,800);
+ return a;
}
function artChecks(k){
const a=S.art[k];if(!a)return null;if(a.err)return [["pend",a.err]];
- const mm=ART[k].mm[S.size],need=Math.round(mm/25.4*300),px=Math.min(a.w,a.h),dpi=Math.round(px/(mm/25.4)),out=[];
- out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);
- out.push(px>=need?["ok",a.w+" × "+a.h+" px: "+dpi+" dpi at "+mm+" mm"+(k==="jacket"?" incl. 3 mm bleed":"")]:px>=need*0.8?["warn",dpi+" dpi at "+mm+" mm, under 300 (needs "+need+" px)"]:["bad",dpi+" dpi at "+mm+" mm: too low for print (needs "+need+" px)"]);
- out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]);
- return out;
+ if(a.kind==="ps")return [["bad","Illustrator file saved without PDF content: save it with Create PDF Compatible File, or export a PDF"]];
+ if(a.kind==="bad")return [["bad","Not a PDF, AI or image file"]];
+ return a.kind==="pdf"?pdfChecks(k,a):rasterChecks(k,a);
+}
+const artThumb=a=>a.url?``:`${a.kind==="ps"||a.ai?"AI":a.kind==="pdf"?"PDF":"?"}`;
+const artNote=a=>a.loading?`
○ Rendering a preview
`:a.noPrev?`
○ No preview: pdf.js could not be loaded. The checks above read the file itself.
`:"";
+async function handleArt(k,files){
+ const f=files.find(f=>/^image\//.test(f.type)||/\.(jpe?g|png|tiff?|webp|pdf|ai)$/i.test(f.name)||/pdf|postscript|illustrator/.test(f.type));if(!f)return;
+ const vec=/\.(pdf|ai)$/i.test(f.name)||/pdf|postscript|illustrator/.test(f.type);
+ let a;try{a=vec?await pdfFacts(f):await readImage(f,k);}catch(err){a={name:f.name,err:"This browser cannot read the file; the real platform checks it after upload."};}
+ const u8=a.u8;delete a.u8;S.art[k]=a;if(a.kind==="pdf")a.loading=true;
+ if(S.mode)resetCampaign();renderAll();
+ if(a.kind==="pdf"){await pdfPreview(a,u8,k);a.loading=false;if(S.art[k]===a)renderAll();}
}
/* ---- checks before gate 1 ---- */
@@ -651,6 +829,7 @@ function mockSVG(id){
else if(S.art.jacket&&S.art.jacket.url&&S.jacket!=="plain")jk+=``;
else if(S.jacket==="plain")jk+=``;
else jk+=`${esc(S.title.slice(0,22))}${esc(S.artist.slice(0,28))}`;
+ if(singleJ()){const sw=spineMM()*J.w/Z.jk;jk+=``;}
if(S.jacket!=="none"){
if(S.shrink)jk+=``;
if(S.sticker)jk+=`NEW`;
@@ -658,7 +837,7 @@ function mockSVG(id){
}
/* a 12" jacket outline for scale when a smaller format is chosen */
const ghost=S.size==="12"?"":`12" jacket, for scale`;
- const cap=`${Z.name}: record ${Z.rec} mm, jacket ${Z.jk} mm`;
+ const cap=`${Z.name}: record ${Z.rec} mm, jacket ${Z.jk} mm${spineMM()?", spine "+spineMM()+" mm":""}`;
return ``;
}
@@ -690,6 +869,9 @@ function syncSegs(){
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 sps=spineState();
+ $$("#rowSpine button").forEach(x=>{const thin=x.dataset.v==="thin",dis=sps.off||(sps.forced&&thin);x.disabled=dis;x.title=sps.off?sps.why:sps.forced&&thin?"Two records need the thick 5 mm spine":"";x.classList.toggle("on",S.jacket!=="gatefold"&&S.jacket!=="none"&&(thickOn()?!thin:thin));});
+ $("#rowSpine").classList.toggle("off",sps.off);$("#spineHint").textContent=sps.hint;
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":"";
@@ -721,8 +903,9 @@ function renderSides(){
}
function renderArt(){
const ic={ok:"✓",warn:"!",bad:"✕",pend:"○"};
+ $("#tplRow").innerHTML=["jacket","label"].map(k=>{const off=tplOff(k);return `${off?esc(off):esc(tplDims(k).txt)}`;}).join("");
$("#artRes").innerHTML=["jacket","label"].map(k=>{const a=S.art[k];if(!a)return "";const cs=artChecks(k);
- return `