diff --git a/README.md b/README.md index 9037c06..7db4a92 100644 --- a/README.md +++ b/README.md @@ -211,8 +211,9 @@ The detail lives in dedicated documents: enough have pre-ordered, with refunds if not - [Order a record](docs/order.html): the order tool as a step-by-step configurator after the Mobineko pattern (step rail, choices, a large live - record picture, running price and ship date, review with what is - missing); same price list as the artist demos + record picture, running price and ship date, a "how to pay" step with + direct payment or a fan-funded campaign on make-a-record's calculator, + review with what is missing); same price list as the artist demos - [Artist ordering demo](docs/artist-order.html): the artist platform as a 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 @@ -322,9 +323,10 @@ The detail lives in dedicated documents: caveats behind the numbers - [Investor deck](docs/pitch/index.html) (October 2026): the plant in 16 web slides for BOI, bank and institutional readers, proposing Plan B - (~215M THB) with Plan A as the comparison, with a - presenter view (press P) and a - [PDF](docs/pitch/CNX-Vinyl-Investor-Deck.pdf); every figure carries its + (~215M THB) with Plan A as the comparison, in a light and a dark theme + (press T), with a presenter view (press P) and a PDF of each theme + ([dark](docs/pitch/CNX-Vinyl-Investor-Deck.pdf), + [light](docs/pitch/CNX-Vinyl-Investor-Deck-light.pdf)); every figure carries its source in the business plan or the owning doc - Business plan, draft 0.3 (October 2026), in two versions: [Plan A, ~180M THB](docs/business-plan/CNX-Vinyl-Business-Plan-A.pdf) diff --git a/docs/artist-platform.md b/docs/artist-platform.md index 10a68dc..f16b2ea 100644 --- a/docs/artist-platform.md +++ b/docs/artist-platform.md @@ -57,11 +57,27 @@ demo, so a rate change goes into both pages. plant's online order form: a rail of numbered steps (order type, size, discs, vinyl colour and effects, labels, inner sleeves, jacket and finish, extras, mastering and cutting, test pressings, shipping, track -list, artwork, review), each step's choices beside a large live picture -of the record, and a running price and ship date. The review lists what -is still missing before an order can be placed and links to fan funding -in the detailed demo. It carries the same price list, so a rate change -goes into all three pages. +list, artwork, how to pay, review), each step's choices beside a large +live picture of the record, and a running price and ship date. The review +lists what is still missing before an order can be placed. It carries the +same price list, so a rate change goes into all three pages. + +**Fan funding in the order tool** (user, 20 October 2026: the new tool +offers fan funding too, so the investor deck's platform pictures come +from one tool and one calculator). Step 15 "How to pay" chooses between +paying the plant directly (the deposit, the rest before dispatch) and a +fan-funded campaign with the same inputs and the same campaign maths as +[make-a-record](make-a-record.html): the artist sets the fan price +(default 1,200 THB) and the campaign length, the fans needed are the +larger of break-even and the 100-copy minimum run, and the tool shows +what the artist keeps at the goal and with more fans, after the payment +fees (~3.5 %, to confirm with the provider), the one-off setup and the +pressing. Step 16 becomes "Review & launch" and simulates the campaign's +outcome. For the same record both pages give the same numbers (checked +headless over 40 combinations, October 2026): the default record at +1,200 THB needs 100 fans (break-even 41) and keeps ~548 THB a copy; the +Splatter 12" preset needs 100 fans (break-even 46) and keeps ~480 THB a +copy. Indicative demo prices. **Mastering as a choice** (user, October 2026): the order tool lets the customer pick the mastering: none (the master is final and only prepared diff --git a/docs/order.html b/docs/order.html index 117c99e..d7f3128 100644 --- a/docs/order.html +++ b/docs/order.html @@ -60,12 +60,12 @@ document.documentElement.setAttribute("data-theme",t);})(); .app.wide { grid-template-columns: 250px minmax(0, 1fr); } .app.wide .stage { display: none; } .app.wide .px { visibility: hidden; } - .rail { overflow-y: auto; padding: 10px 10px 16px; background: var(--panel); border-right: 1px solid var(--stroke); display: flex; flex-direction: column; gap: 3px; } - .ri { display: flex; align-items: center; gap: 9px; width: 100%; background: transparent; border: 1px solid transparent; border-radius: 9px; padding: 7px 8px; color: var(--mute); text-align: left; cursor: pointer; min-height: 46px; flex: none; } + .rail { overflow-y: auto; padding: 10px 10px 12px; background: var(--panel); border-right: 1px solid var(--stroke); display: flex; flex-direction: column; gap: 2px; } + .ri { display: flex; align-items: center; gap: 9px; width: 100%; background: transparent; border: 1px solid transparent; border-radius: 9px; padding: 5px 8px; color: var(--mute); text-align: left; cursor: pointer; min-height: 42px; flex: none; } .ri:hover { background: var(--panel2); color: var(--fg); } .ri .rn { flex: none; display: inline-grid; place-items: center; width: 23px; height: 23px; border-radius: 50%; background: var(--line); color: var(--mute); font-size: 12px; font-weight: 700; } .ri .ric { flex: none; width: 18px; height: 18px; color: var(--mute); } - .ri .rt { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; } + .ri .rt { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.2; } .ri .rt b { font-weight: 500; font-size: 14px; } .ri .rt small { font-size: 11.5px; color: var(--accent); opacity: 0.55; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ri .rch { flex: none; color: var(--mute); opacity: 0.6; font-size: 15px; } @@ -297,6 +297,7 @@ document.documentElement.setAttribute("data-theme",t);})(); .sumcard { border: 1.5px solid var(--stroke); border-radius: 14px; padding: 16px 18px; background: var(--panel2); } .sumhd { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-bottom: 8px; } .sumhd h3 { margin: 0; font-size: 22px; font-weight: 500; letter-spacing: 0.01em; } + .sumhd > div { flex: 1; min-width: 180px; } .chips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; } .chips span { background: var(--panel); border: 1px solid var(--stroke); border-radius: 5px; padding: 1px 8px; font-size: 12px; color: var(--mute); } .rvgrid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 16px; align-items: start; } @@ -316,6 +317,82 @@ document.documentElement.setAttribute("data-theme",t);})(); .conf code { font-size: 12.5px; } details.pl summary { cursor: pointer; font-weight: 600; margin: 16px 0 6px; } .toast { position: fixed; top: 64px; left: 50%; transform: translateX(-50%); background: var(--panel); border: 1px solid var(--act); border-radius: 8px; padding: 8px 14px; font-size: 13.5px; z-index: 80; max-width: calc(100% - 30px); } + /* how to pay: route cards and the fan campaign (same maths as make-a-record) */ + .tiles.route .tile { min-height: 112px; padding-left: 9px; padding-right: 9px; } + .tiles.route .tile .pr { font-weight: 600; } + fieldset.camp { border: 0; padding: 0; margin: 2px 0 0; } + fieldset.camp:disabled { opacity: 0.5; } + fieldset.camp:disabled .chip, fieldset.camp:disabled input { cursor: not-allowed; } + input[type=range] { -webkit-appearance: none; appearance: none; height: 6px; border-radius: 3px; background: linear-gradient(to right, var(--act) var(--p, 50%), var(--stroke) var(--p, 50%)); outline: none; cursor: pointer; margin: 8px 0; } + input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--act); border: 3px solid var(--panel); box-shadow: 0 0 0 1px var(--act), 0 1px 3px rgba(0,0,0,0.3); } + input[type=range]::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: var(--act); border: 3px solid var(--panel); box-shadow: 0 0 0 1px var(--act); } + input[type=range]:focus-visible { box-shadow: 0 0 0 3px var(--actring); } + input[type=range]:disabled { cursor: not-allowed; } + .fprow { display: flex; align-items: center; gap: 12px; margin: 4px 0 6px; } + .fprow input[type=range] { flex: 1; min-width: 0; } + .stepper.fp input { width: 70px; } + .chip.t { padding: 3px 11px; font-variant-numeric: tabular-nums; } + .chip.on { border-color: var(--act); background: var(--actbg); box-shadow: 0 0 0 2px var(--actring); } + .chip:disabled { opacity: 0.6; cursor: not-allowed; } + .need { font-size: 14.5px; margin: 8px 0 0; } + .need b { font-size: 16px; background: var(--actbg); border-bottom: 2px solid var(--act); padding: 0 4px; border-radius: 3px; } + .bad-t { color: var(--bad); font-weight: 600; } + .camprow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0 14px; margin: 4px 0 0; } + .camprow .fld { margin: 6px 0; } + .rngrow { display: flex; align-items: center; gap: 10px; min-height: 40px; } + .rngrow input[type=range] { flex: 1; min-width: 0; } + .daysv { font-weight: 700; color: var(--fg); font-size: 15px; white-space: nowrap; font-variant-numeric: tabular-nums; } + .limrow { display: flex; align-items: center; gap: 6px 12px; flex-wrap: wrap; margin: 4px 0; } + .limrow .chk { margin: 4px 0; } + .capf { display: inline-flex; align-items: center; gap: 6px; font-size: 13.5px; color: var(--fg); } + .capf input { width: 80px; padding: 4px 7px; } + .capf.off { opacity: 0.45; } + .kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 12px 0 4px; } + .kpis.k3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .kpi { background: var(--panel2); border: 1.5px solid var(--stroke); border-radius: 11px; padding: 8px 11px 9px; min-width: 0; } + .kpi small { display: block; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .kpi b { display: block; font-size: 21px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .kpi em { display: block; font-style: normal; font-size: 11.5px; color: var(--mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .kpi.hl { border-color: var(--act); background: var(--actbg); box-shadow: 0 0 0 3px var(--actring); } + .keep { margin: 2px 0 4px; } + .krow { display: grid; grid-template-columns: 70px minmax(0, 1fr) 118px; gap: 10px; align-items: center; padding: 4px 0; font-size: 13px; border-bottom: 1px solid var(--line); } + .krow:last-child { border-bottom: 0; } + .krow > span { color: var(--mute); white-space: nowrap; font-variant-numeric: tabular-nums; } + .krow .kb { height: 10px; background: var(--line); border-radius: 5px; overflow: hidden; } + .krow .kb i { display: block; height: 100%; background: var(--accent); border-radius: 5px; } + .krow.goal > span { color: var(--fg); font-weight: 700; } + .krow.goal .kb i { background: var(--act); } + .krow .kv2 { text-align: right; line-height: 1.2; } + .krow .kv2 b { display: block; font-variant-numeric: tabular-nums; } + .krow .kv2 small { font-size: 11px; white-space: nowrap; } + .safe { display: grid; gap: 9px; margin: 6px 0 4px; } + .safe > div { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; line-height: 1.35; } + .safe .si { flex: none; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; background: var(--okbg); color: var(--accent); border: 1px solid var(--line); } + .safe b { display: block; font-weight: 600; } + .safe small { display: block; font-size: 12.5px; } + .launch { display: flex; flex-direction: column; gap: 4px; margin: 14px 0 2px; } + .launch .btn { width: 100%; padding: 10px 14px; font-size: 15px; } + .launch .hint { margin: 2px 0 0; text-align: center; } + /* quote card on the fan route */ + .qn.info { color: var(--mute); } + /* review: the launched campaign */ + .cgrid { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: 16px; align-items: start; margin-bottom: 14px; } + .cgrid .conf { margin-bottom: 0; } + .simhd { display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; flex-wrap: wrap; } + .simhd h3 { margin: 0; font-size: 18px; } + .simhd h3 small { font-weight: 400; } + .simrow { display: flex; align-items: center; gap: 10px; margin: 12px 0 6px; } + .simrow input[type=range] { flex: 1; min-width: 0; } + .simrow .hint { white-space: nowrap; margin: 0; } + .bar.big { height: 12px; border-radius: 6px; } + .ocard { border: 1.5px solid var(--accent); background: var(--okbg); border-radius: 12px; padding: 12px 14px; margin-top: 12px; } + .ocard.warn { border-color: var(--warn); background: var(--warnbg); } + .ocard.plain { border-color: var(--stroke); background: var(--panel); } + .ocard h3 { margin: 0 0 4px; font-size: 16px; } + .ocard p { font-size: 13.5px; margin: 4px 0; } + .opts { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; margin-top: 8px; } + .opt { border: 1px solid var(--stroke); border-radius: 10px; padding: 10px 12px; background: var(--panel); display: flex; flex-direction: column; align-items: flex-start; } + .opt p { font-size: 13px; color: var(--mute); margin: 4px 0 8px; flex: 1; } /* phone bottom bar */ .pbar { display: none; } /* tablet: compact rail */ @@ -330,7 +407,7 @@ document.documentElement.setAttribute("data-theme",t);})(); .qt { font-size: 21px; } .sinfo { max-width: calc(100% - 240px); } } - @media (max-width: 1100px) and (min-width: 900px) { .rvgrid { grid-template-columns: minmax(0, 1fr); } } + @media (max-width: 1100px) and (min-width: 900px) { .rvgrid, .cgrid { grid-template-columns: minmax(0, 1fr); } } /* phone */ @media (max-width: 899px) { body { overflow: visible; display: block; height: auto; padding-bottom: 74px; } @@ -363,6 +440,10 @@ document.documentElement.setAttribute("data-theme",t);})(); .sumcard { padding: 12px 12px; } .grid2f { grid-template-columns: minmax(0, 1fr); } .tiles.c4 { --cols: 2; } + .cgrid, .opts { grid-template-columns: minmax(0, 1fr); } + .kpi b { font-size: 18px; } + .krow { grid-template-columns: 62px minmax(0, 1fr) 112px; gap: 8px; } + .simrow { flex-wrap: wrap; } .pbar { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; background: var(--panel); border-top: 1px solid var(--stroke); padding: 8px 10px; gap: 8px; align-items: center; box-shadow: 0 -6px 20px rgba(0,0,0,0.2); } .pbar .pt { flex: 1; min-width: 0; line-height: 1.2; } .pbar .pt b { font-size: 16px; display: block; white-space: nowrap; font-variant-numeric: tabular-nums; } @@ -389,7 +470,7 @@ document.documentElement.setAttribute("data-theme",t);})();
A repress skips mastering, cutting and all three approvals (production timeline): about 7 to 11 working days of work, quoted with queue and buffer.
` - :`The quote counts from your last approval: the plant cannot be held to a clock that waits for approvals. ${L.printed?"A printed jacket is on the critical path; supplying your own saves 1 to 2 weeks.":"No printed jacket to wait for, which saves 1 to 2 weeks."} Stage times: mature-plant column of the production timeline.
`)+ + const closeT=(S.launched?"Closes ":"If launched today, closes "); + $("#rvLead").innerHTML=(F?(L.repress?`A repress skips mastering, cutting and all three approvals (production timeline); it starts when the campaign succeeds.
` + :`Cutting starts only when the campaign succeeds, because a lacquer has to be plated within one to three days (artist platform). The quote counts from your last approval. Stage times: mature-plant column of the production timeline.
`) + :(L.repress?`A repress skips mastering, cutting and all three approvals (production timeline): about 7 to 11 working days of work, quoted with queue and buffer.
` + :`The quote counts from your last approval: the plant cannot be held to a clock that waits for approvals. ${L.printed?"A printed jacket is on the critical path; supplying your own saves 1 to 2 weeks.":"No printed jacket to wait for, which saves 1 to 2 weeks."} Stage times: mature-plant column of the production timeline.
`))+ `| ${s.gate?"◆ ":""}${esc(s.t)} | ${s.d[0]} to ${s.d[1]} days |
Set in step ${PAY}.
` + :`Or let your fans pre-order it: the record is pressed when enough of them have, and every fan is refunded if not.
`; + if(F&&S.launched){const no=orderNo(),yr=S.launchDate.getFullYear(),nm=S.addr.b.name||"the artist"; + $("#rvConf").innerHTML=`Pick how it goes and press play: ${S.days} days in about 8 seconds, or drag the slider to any day.
+No page went live and nobody was charged. In the real platform the campaign is written to ERPNext, the plant's only system of record, through the plant's integration layer and its outbox; the public side never talks to ERPNext directly (artist platform, digital factory).
+${esc(nm)}: the artist, matched by e-mail or created, with the billing address.CMP-${yr}-0${no}, a custom document from launch: ${money(S.fanPrice)} a copy, goal ${fmtInt(E.goal||0)} fans, closes ${fmtD(W.close)}${cap?", limited to "+fmtInt(cap):""}.cnx_erp app) and three Artist Gate records: master plan, master, test pressing.Nothing was sent or charged. In the real platform the order is written to ERPNext, the plant's only system of record, through the plant's integration layer and its outbox; the public side never talks to ERPNext directly (digital factory, artist platform).
${esc(nm)}: matched by e-mail or created, with the billing${S.ship!=="pickup"?" and shipping":""} address${S.split?" and the second address":""} as Address records.SAL-QTN-${yr}-0${no}: this configuration priced from the ERPNext price list ${PLV} (this page's price list is its prototype), ${soItems.length} items, valid 30 days.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.
`;} + $("#fundKpi").innerHTML=hk;$("#fundEcon").innerHTML=h; + const b=$("#bLaunch"),n=missing().length;b.disabled=!!cw;b.title=cw; + $("#launchMsg").textContent=cw&&F?cw+".":n?"Launching opens the review first: "+n+" item"+(n>1?"s":"")+" to complete there.":"Simulated: no page goes live and nobody is charged."; +} + +/* ---- the campaign simulation (make-a-record): launch, play, outcome ---- */ +let TIMER=null; +function stopSim(){if(TIMER){clearInterval(TIMER);TIMER=null;}} +function resetSim(){stopSim();S.simDay=0;S.outcome=null;S.outcomeAct=null;S.finalFans=0;} +function resetCampaign(){resetSim();S.launched=false;S.launchDate=null;} +function checkEnd(){ + if(S.simDay>=S.days){if(!S.outcome){const E=econ(),P=finalPledges(E);S.finalFans=P;S.outcome=P>=E.goal?"funded":"missed";}} + else if(S.outcome){S.outcome=null;S.outcomeAct=null;} +} +function startSim(){ + stopSim();if(S.simDay>=S.days)resetSim(); + TIMER=setInterval(()=>{S.simDay=Math.min(S.days,S.simDay+S.days/80);checkEnd();if(S.simDay>=S.days)stopSim();updateSim();},100); +} +function toggleSim(){if(TIMER)stopSim();else startSim();updateSim();} +function launch(){ + if(campWhy(econ()))return; + if(missing().length){S.visited[RV]=true;go(RV);toast("Complete the items in the yellow box first");return;} + resetCampaign();S.launched=true;S.launchDate=new Date(TODAY);S.visited[RV]=true;go(RV);$("#pBody").scrollTop=0;window.scrollTo(0,0); +} +function campChanged(){if(S.launched)resetCampaign();renderAll();} +function setFanPrice(v){S.fanPrice=v;campChanged();} +function updateSim(){ + if(!S.launched||!$("#simKpis"))return; + const E=econ(),ended=S.simDay>=S.days,n=pledgesNow(E),goal=E.goal||0,W=campWindow(),cap=capOn(); + $$('#rvConf .seg[data-k="scen"] button').forEach(b=>b.classList.toggle("on",b.dataset.v===S.scen)); + const sl=$("#simDay");sl.max=S.days;if(document.activeElement!==sl)sl.value=S.simDay;rngP(sl); + $("#dayLbl").textContent="Day "+Math.floor(S.simDay)+" of "+S.days; + $("#playBtn").innerHTML=TIMER?"❚❚ Pause":ended?"↺ Replay":"▶ Play"; + const funded=ended&&n>=goal,soldOut=cap&&n>=cap,bi=$("#simBar"); + bi.style.width=(goal?Math.min(100,n/goal*100):0).toFixed(1)+"%";bi.className=ended&&!funded?"bad":""; + $("#simKpis").innerHTML=`${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production: the campaign becomes a production order and ships about ${dRange(W.ship[0],W.ship[1])}.
${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.
${diff<0?"You paid the gap of "+money(-diff)+"":"The fans' money covered the minimum run; you keep "+money(diff)+""}, and you receive the ${fmtInt(spare)} copies no fan claimed.
Two ways out. The artist chooses.
Automatic, from the separate account. Nothing is pressed; your files stay in your vault.
${diff<0?"You pay the gap of "+money(-diff)+"":"Costs you nothing: the fans' money covers it and you keep "+money(diff)}. You receive the ${fmtInt(spare)} unclaimed copies.