Files
cnx_vinyl/docs/make-a-record.html
T
grabowski f7e20e1015
Deploy test site / deploy (push) Successful in 1m45s
Make a record: USD / THB / EUR selector and English / Thai language switch
User (October 2026): currency selector on the first page (USD / THB /
EUR) and a language selector for Thai on the make-a-record demo.

- Top bar: one currency control (replaces the THB/USD switch on the
  price step) and an EN / Thai control, visible from step 1; both persist
  (localStorage, #cur= / #lang= in the URL, hash wins). Prices stay
  computed in THB: USD = THB / 33, EUR = THB / 38.115 (1 EUR = 1.155
  USD); the fan price is entered in the chosen currency and stored in
  THB, the campaign rule max(100, break-even) unchanged.
- Thai: every visible string, tooltip, aria label, file check, quote
  line, production update, passport, investor note and tour caption
  (dictionary T = {en, th}, t(key, vars), data-i18n attributes; 636
  keys), system Thai fonts, Thai dates with Gregorian years, and a footer
  note that the Thai is a draft translation for review. Template PDFs
  stay English (Helvetica has no Thai glyphs).
- Top bar wraps to two rows below 1000 px (steps were cut off between 761
  and 990 px before).
- README and landing entries mention the currencies and Thai.

Checks (headless Edge): doc example unchanged in THB (37,836 per title,
232 per copy at 100, break-even 41, goal 100, ~548 kept, 640 hint);
conversions exact; 390 / 768 / 1024 / 1440 px, light and dark, EN and
TH, all currencies, steps 1 to 5: no page errors, overflow or untranslated
leftovers; tour in Thai; node --check; no em dashes.
2026-10-16 22:03:50 +07:00

2120 lines
236 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="Design a vinyl record, check your music, get a transparent price, and let your fans fund the pressing. Investor demo, indicative prices.">
<title>Make a record · CNX Vinyl (demo)</title>
<script>
(function(){var t=null;try{t=localStorage.getItem("cnx-theme")}catch(e){}
if(t!=="light"&&t!=="dark")t=window.matchMedia&&matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";
document.documentElement.setAttribute("data-theme",t);})();
</script>
<style>
:root {
--bg: #0c0c0f; --bg2: #111116; --card: #16161c; --card2: #1e1e26; --line: #25252e; --stroke: #363642;
--ink: #f3f0e8; --mute: #a3a0ad; --faint: #6f6c79;
--acc: #ff6b3d; --acc-s: rgba(255,107,61,0.13); --onacc: #1d0c04;
--ok: #3ecf8e; --warn: #f5b941; --bad: #f2706b; --inv: #96a2ff; --inv-s: rgba(150,162,255,0.09);
--glass: rgba(12,12,15,0.74); --shadow: 0 18px 50px rgba(0,0,0,0.5); --kraft: #d8c3a0;
color-scheme: dark;
}
html[data-theme=light] {
--bg: #f6f4ef; --bg2: #efebe3; --card: #ffffff; --card2: #f4f1ea; --line: #e8e3d9; --stroke: #d8d2c5;
--ink: #16151a; --mute: #65616d; --faint: #9a96a1;
--acc: #e4521f; --acc-s: rgba(228,82,31,0.09); --onacc: #ffffff;
--ok: #0f9468; --warn: #b5770f; --bad: #d0453f; --inv: #4a55c8; --inv-s: rgba(74,85,200,0.07);
--glass: rgba(246,244,239,0.82); --shadow: 0 18px 44px rgba(60,40,10,0.11);
color-scheme: light;
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; }
button { font: inherit; color: inherit; }
a { color: var(--acc); }
.mute { color: var(--mute); }
.hint { color: var(--mute); font-size: 13px; }
small { font-size: 12.5px; }
/* top bar */
.top { position: sticky; top: 0; z-index: 30; background: var(--glass); backdrop-filter: saturate(1.4) blur(14px); -webkit-backdrop-filter: saturate(1.4) blur(14px); border-bottom: 1px solid var(--line); }
.top .in { max-width: 1320px; margin: 0 auto; padding: 10px 24px; display: flex; align-items: center; gap: 16px; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 16px; white-space: nowrap; letter-spacing: 0.01em; }
.brand svg { width: 28px; height: 28px; flex: none; }
.brand .pill { font-size: 10.5px; font-weight: 700; color: var(--mute); border: 1px solid var(--stroke); border-radius: 99px; padding: 1px 8px; letter-spacing: 0.08em; text-transform: uppercase; }
nav.steps { flex: 1; display: flex; justify-content: flex-start; gap: 2px; overflow-x: auto; scrollbar-width: none; min-width: 0; }
nav.steps::-webkit-scrollbar { display: none; }
nav.steps > button:first-child { margin-left: auto; } nav.steps > button:last-child { margin-right: auto; }
nav.steps button { display: flex; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--mute); padding: 6px 12px 6px 7px; border-radius: 99px; cursor: pointer; white-space: nowrap; font-size: 14px; flex: none; }
nav.steps button:hover { color: var(--ink); }
nav.steps .d { width: 23px; height: 23px; border-radius: 50%; display: grid; place-items: center; font-size: 12px; font-weight: 700; border: 1.5px solid var(--stroke); transition: background .2s, border-color .2s; }
nav.steps button.on { color: var(--ink); background: var(--card2); }
nav.steps button.on .d { border-color: var(--acc); color: var(--acc); }
nav.steps button.done .d { background: var(--acc); border-color: var(--acc); color: var(--onacc); }
.tools { display: flex; align-items: center; gap: 10px; flex: none; }
.progress { height: 2px; }
.progress i { display: block; height: 100%; width: 20%; background: linear-gradient(90deg, var(--acc), var(--warn)); transition: width .45s ease; }
.sw { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--mute); cursor: pointer; white-space: nowrap; user-select: none; position: relative; }
.sw input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.sw .tr { width: 34px; height: 20px; border-radius: 99px; background: var(--stroke); position: relative; transition: background .2s; flex: none; }
.sw .tr::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .2s; }
.sw input:checked + .tr { background: var(--inv); }
.sw input:checked + .tr::after { transform: translateX(14px); }
.sw input:focus-visible + .tr { outline: 2px solid var(--inv); outline-offset: 2px; }
.ib { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--stroke); background: var(--card); cursor: pointer; display: grid; place-items: center; font-size: 15px; flex: none; }
.ib:hover { border-color: var(--acc); }
/* layout */
.app { max-width: 1320px; margin: 0 auto; padding: 34px 24px 70px; display: grid; grid-template-columns: minmax(0, 1fr) 400px; gap: 44px; align-items: start; }
main { min-width: 0; }
.app.full { grid-template-columns: minmax(0, 1fr); }
.app.full .stage { display: none; }
.stage { position: sticky; top: 82px; }
.step { animation: fade .35s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.eyebrow { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--acc); font-weight: 700; }
h1 { font-size: 42px; line-height: 1.06; letter-spacing: -0.025em; margin: 8px 0 12px; font-weight: 760; }
.lead { color: var(--mute); font-size: 17px; max-width: 640px; margin: 0 0 28px; }
.block { margin: 0 0 30px; scroll-margin-top: 90px; }
h2 { font-size: 15px; font-weight: 680; margin: 0 0 12px; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
h2 small { font-weight: 400; color: var(--mute); }
h3 { margin: 0 0 6px; font-size: 18px; letter-spacing: -0.01em; }
.mt { margin-top: 16px; }
/* investor notes */
.inv { display: none; border: 1px solid var(--inv); border-color: color-mix(in srgb, var(--inv) 38%, transparent); background: var(--inv-s); border-radius: 14px; padding: 12px 16px; margin: 0 0 28px; font-size: 14px; }
body.inv-on .inv { display: block; }
.inv .il { display: block; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--inv); font-weight: 800; margin-bottom: 3px; }
.inv p { margin: 4px 0; }
/* choices */
.cards { display: grid; gap: 10px; }
.c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.c3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.c4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 640px) { .c3, .c4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.choice { position: relative; text-align: left; background: var(--card); border: 1.5px solid var(--line); border-radius: 14px; padding: 13px 14px; cursor: pointer; transition: border-color .15s, transform .15s, background .15s; display: flex; flex-direction: column; gap: 3px; }
.choice:hover:not(:disabled) { border-color: var(--stroke); transform: translateY(-1px); }
.choice.on { border-color: var(--acc); background: var(--acc-s); }
.choice.on::after { content: "\2713"; position: absolute; top: 10px; right: 11px; width: 20px; height: 20px; border-radius: 50%; background: var(--acc); color: var(--onacc); font-size: 12px; font-weight: 800; display: grid; place-items: center; }
.choice:disabled { opacity: 0.36; cursor: not-allowed; }
.choice b { font-size: 15px; font-weight: 680; }
.choice > span:not(.ic) { font-size: 12.5px; color: var(--mute); }
.choice .ic { height: 60px; display: flex; align-items: center; margin-bottom: 4px; }
.fi-d { fill: #151515; stroke: var(--stroke); stroke-width: 1; } .fi-g { fill: none; stroke: rgba(255,255,255,0.10); stroke-width: 0.8; } .fi-l { fill: var(--acc); }
.jic { display: inline-block; width: 46px; height: 46px; border-radius: 3px; box-shadow: 0 4px 10px rgba(0,0,0,0.25); }
.jic.none { border: 1.5px dashed var(--faint); box-shadow: none; }
.jic.plain { background: var(--kraft); }
.jic.printed { background: linear-gradient(135deg, #ff7a45, #7a2a8c 60%, #1b1d3a); }
.jic.gf { width: 30px; border-radius: 3px 0 0 3px; } .jic.gf + .jic.gf { border-radius: 0 3px 3px 0; margin-left: 1px; background: linear-gradient(135deg, #7a2a8c, #1b1d3a); }
.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line); border-radius: 99px; padding: 3px; gap: 2px; flex-wrap: wrap; }
.seg button { border: 0; background: transparent; color: var(--mute); border-radius: 99px; padding: 6px 14px; cursor: pointer; font-size: 14px; white-space: nowrap; }
.seg button:hover:not(:disabled) { color: var(--ink); }
.seg button.on { background: var(--ink); color: var(--bg); }
.seg button:disabled { opacity: 0.32; cursor: not-allowed; }
.rowc { display: flex; flex-wrap: wrap; gap: 14px 28px; align-items: flex-end; }
.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.fl { font-size: 12.5px; color: var(--mute); font-weight: 600; }
.field.off .fl { opacity: 0.45; }
.colours { display: flex; flex-wrap: wrap; gap: 16px 36px; margin-top: 16px; }
.sw-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.swatch { width: 30px; height: 30px; border-radius: 50%; border: 2.5px solid var(--bg); box-shadow: 0 0 0 1.5px var(--stroke); cursor: pointer; padding: 0; transition: transform .12s; }
.swatch:hover:not(:disabled) { transform: scale(1.1); }
.swatch.on { box-shadow: 0 0 0 2px var(--acc); }
.swatch:disabled { opacity: 0.2; cursor: not-allowed; }
.pal { position: relative; display: inline-flex; align-items: center; gap: 8px; margin-left: 6px; padding: 3px 12px 3px 3px; border: 1.5px solid var(--line); border-radius: 99px; background: var(--card); font-size: 13px; font-weight: 600; color: var(--ink); cursor: pointer; transition: border-color .15s; }
.pal:hover { border-color: var(--stroke); }
.pal input[type=color] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; border: 0; padding: 0; z-index: 2; }
.pal .cw { width: 26px; height: 26px; border-radius: 50%; flex: none; position: relative; background: conic-gradient(#ff4d4d, #ffb23b, #f2ee3b, #3bd36a, #3bc4f2, #5361ff, #c03bff, #ff4d4d); }
.pal .cw::after { content: "+"; position: absolute; inset: 5px; border-radius: 50%; background: var(--card); color: var(--ink); display: grid; place-items: center; font-size: 14px; font-weight: 800; line-height: 1; }
.pal.on { border-color: var(--acc); background: var(--acc-s); }
.pal.on .cw::after { content: ""; background: var(--cc); inset: 4px; }
.pal.off { opacity: 0.3; cursor: not-allowed; } .pal.off input { cursor: not-allowed; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip { border: 1.5px solid var(--line); background: var(--card); border-radius: 99px; padding: 7px 14px; cursor: pointer; font-size: 14px; transition: border-color .15s, background .15s; }
.chip:hover { border-color: var(--stroke); }
.chip.on { border-color: var(--acc); background: var(--acc-s); }
.chip.on::before { content: "\2713 "; color: var(--acc); font-weight: 800; }
.stepper { display: inline-flex; align-items: center; border: 1px solid var(--line); border-radius: 99px; background: var(--card); }
.stepper button { width: 36px; height: 36px; border: 0; background: transparent; cursor: pointer; font-size: 18px; border-radius: 50%; }
.stepper button:hover { color: var(--acc); }
.stepper b { min-width: 30px; text-align: center; font-variant-numeric: tabular-nums; }
.panel { background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 20px 22px; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: var(--acc); color: var(--onacc); border: 1px solid var(--acc); border-radius: 99px; padding: 11px 22px; font-weight: 680; cursor: pointer; font-size: 15px; transition: filter .15s, transform .1s; white-space: nowrap; }
.btn:hover:not(:disabled) { filter: brightness(1.08); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: 0.38; cursor: not-allowed; }
.btn.ghost { background: transparent; color: var(--ink); border-color: var(--stroke); }
.btn.ghost:hover:not(:disabled) { border-color: var(--acc); filter: none; }
.btn.sm { padding: 6px 14px; font-size: 13.5px; }
.btn.wide { width: 100%; }
.lnk { background: none; border: 0; color: var(--mute); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; padding: 4px; font-size: 14px; }
.lnk:hover { color: var(--ink); }
input[type=text], input[type=number] { background: var(--card); color: var(--ink); border: 1px solid var(--stroke); border-radius: 10px; padding: 9px 12px; font: inherit; min-width: 0; }
input[type=number] { font-variant-numeric: tabular-nums; }
input[type=text]:focus, input[type=number]:focus, select:focus { outline: none; border-color: var(--acc); }
input.err { border-color: var(--bad); }
input[type=range] { accent-color: var(--acc); width: 100%; margin: 6px 0; }
select { background: var(--card); color: var(--ink); border: 1px solid var(--stroke); border-radius: 8px; padding: 6px 6px; font: inherit; }
.g2f { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 560px) { .g2f { grid-template-columns: minmax(0, 1fr); } }
/* drop strips */
.drop { display: flex; align-items: center; gap: 12px; border: 1.5px dashed var(--stroke); border-radius: 12px; padding: 10px 14px; color: var(--mute); font-size: 14px; cursor: pointer; transition: border-color .15s, background .15s, color .15s; margin-bottom: 12px; }
.drop:hover, .drop.over { border-color: var(--acc); color: var(--ink); background: var(--acc-s); }
.drop b { color: var(--ink); }
.drop .dic { width: 30px; height: 30px; border-radius: 50%; background: var(--acc-s); color: var(--acc); display: grid; place-items: center; font-size: 15px; flex: none; }
.drop.off { opacity: 0.4; cursor: not-allowed; }
.drop.off:hover { border-color: var(--stroke); background: none; color: var(--mute); }
/* sides and tracks */
.sides { display: grid; gap: 10px; margin-bottom: 10px; }
.side .sh { display: flex; align-items: baseline; gap: 10px; font-size: 14px; margin-bottom: 5px; }
.side .sh span { color: var(--mute); font-size: 13px; }
.side .sh .pill { margin-left: auto; }
.pill { font-size: 12px; font-weight: 700; border-radius: 99px; padding: 1px 9px; border: 1px solid var(--stroke); color: var(--mute); white-space: nowrap; font-variant-numeric: tabular-nums; }
.pill.ok { color: var(--ok); border-color: var(--ok); } .pill.warn { color: var(--warn); border-color: var(--warn); } .pill.bad { color: var(--bad); border-color: var(--bad); }
.sbar { position: relative; height: 26px; border-radius: 8px; background: var(--card2); border: 1px solid var(--line); overflow: hidden; }
.sbar i { position: absolute; top: 3px; bottom: 3px; border-radius: 5px; background: var(--acc); border-right: 2px solid var(--card2); transition: width .25s, left .25s; }
.sbar i.s1 { background: color-mix(in srgb, var(--acc) 62%, var(--card2)); }
.side.bad .sbar i { background: var(--bad); } .side.bad .sbar i.s1 { background: color-mix(in srgb, var(--bad) 62%, var(--card2)); }
.sbar .mk { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--ink); opacity: 0.45; }
.trk { margin: 6px 0 12px; }
.tr { display: grid; grid-template-columns: 22px 58px minmax(0, 1fr) 70px minmax(0, max-content) 28px; grid-template-areas: "no sd tt du fi x"; gap: 8px; align-items: center; padding: 5px 0; border-bottom: 1px solid var(--line); }
.tr .no { grid-area: no; color: var(--faint); font-size: 13px; text-align: right; font-variant-numeric: tabular-nums; }
.tr select { grid-area: sd; } .tr .tt { grid-area: tt; width: 100%; padding: 7px 10px; } .tr .tdur { grid-area: du; width: 100%; text-align: right; padding: 7px 8px; }
.tr .fi { grid-area: fi; font-size: 12px; color: var(--mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tr .x { grid-area: x; background: none; border: 0; color: var(--faint); font-size: 18px; cursor: pointer; }
.tr .x:hover { color: var(--bad); }
@media (max-width: 600px) { .tr { grid-template-columns: 18px 54px minmax(0, 1fr) 62px 24px; grid-template-areas: "no sd tt du x" ". . fi fi ."; } .tr .fi:empty { display: none; } }
.bad-t { color: var(--bad); }
/* artwork */
.art { display: flex; gap: 12px; align-items: flex-start; }
.art img { width: 72px; height: 72px; object-fit: cover; border-radius: 6px; border: 1px solid var(--line); flex: none; }
.art .aph { width: 72px; height: 72px; border-radius: 6px; border: 1px solid var(--line); flex: none; display: grid; place-items: center; font-weight: 700; color: var(--mute); }
.tplk { margin: 6px 2px 10px; font-size: 13px; color: var(--mute); line-height: 1.45; }
.tplk .lnk { padding: 0; font-size: 13px; color: var(--acc); }
.tplk .lnk:disabled { color: var(--faint); cursor: not-allowed; text-decoration: none; }
.art ul { list-style: none; padding: 0; margin: 2px 0 6px; font-size: 13px; }
.art li { display: flex; gap: 6px; }
/* checks */
.chk { list-style: none; padding: 0; margin: 10px 0 14px; }
.chk li { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line); align-items: flex-start; }
.chk li:last-child { border-bottom: 0; }
.chk li small { display: block; color: var(--mute); }
.ci { width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; font-size: 12px; font-weight: 800; flex: none; margin-top: 1px; }
.ci.ok { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
.ci.warn { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
.ci.bad { background: color-mix(in srgb, var(--bad) 18%, transparent); color: var(--bad); }
.ci.pend { border: 1.5px solid var(--stroke); color: var(--faint); }
.chkhead { display: flex; align-items: center; gap: 12px; }
.chkhead b { font-size: 17px; }
.rights { display: flex; gap: 10px; align-items: flex-start; font-size: 14px; margin: 6px 0 14px; cursor: pointer; }
.rights input { margin-top: 3px; accent-color: var(--acc); width: 16px; height: 16px; flex: none; }
/* price */
.qtyhead { display: flex; justify-content: space-between; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.qbig { display: flex; align-items: baseline; gap: 8px; }
.qbig input { font-size: 30px; font-weight: 760; width: 140px; padding: 4px 10px; letter-spacing: -0.02em; }
.qbig span { color: var(--mute); }
.qchips { display: flex; gap: 6px; flex-wrap: wrap; }
.qchips .chip { padding: 4px 12px; font-size: 13px; }
.qchips .chip.on::before { content: ""; }
.qchips .chip:disabled { opacity: 0.35; cursor: not-allowed; }
.sw.acc { color: var(--ink); font-size: 14px; } .sw.acc input:checked + .tr { background: var(--acc); }
input:disabled { opacity: 0.4; cursor: not-allowed; }
.need { font-size: 15px; margin: 4px 0 2px; } .need b { color: var(--acc); font-size: 19px; }
.kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 16px 0 10px; }
.kpis.k4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 640px) { .kpis, .kpis.k4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.kpi { background: var(--card2); border-radius: 14px; padding: 12px 14px; min-width: 0; }
.kpi small { display: block; color: var(--mute); font-size: 12px; }
.kpi b { display: block; font-size: 21px; letter-spacing: -0.015em; font-variant-numeric: tabular-nums; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kpi.hl { background: var(--acc-s); } .kpi.hl b { color: var(--acc); }
.kpi em { font-style: normal; color: var(--mute); font-size: 12px; }
.cv svg { width: 100%; height: auto; display: block; }
.cv-grid { stroke: var(--line); } .cv-t { fill: var(--faint); font-size: 11px; }
.cv-line { stroke: var(--acc); fill: none; stroke-width: 2.5; } .cv-area { fill: var(--acc); opacity: 0.08; }
.cv-dot { fill: var(--acc); stroke: var(--card); stroke-width: 3; } .cv-m { fill: var(--ink); font-size: 12.5px; font-weight: 700; }
details summary { cursor: pointer; font-weight: 680; display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap; list-style: none; }
details summary::-webkit-details-marker { display: none; }
details summary::before { content: "\25B8"; color: var(--acc); margin-right: -2px; transition: transform .2s; }
details[open] summary::before { transform: rotate(90deg); }
details summary .sumr { margin-left: auto; color: var(--mute); font-weight: 400; font-size: 14px; }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
td, th { padding: 7px 4px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
th { color: var(--mute); font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; padding-top: 16px; }
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
tr.sub td { color: var(--mute); }
tr.tot td { font-weight: 760; font-size: 16px; border-bottom: 0; }
.pass { color: var(--mute); font-size: 12px; }
.tscroll { overflow-x: auto; }
.tl2 { display: flex; border-radius: 10px; overflow: hidden; margin: 12px 0 8px; font-size: 12.5px; }
.tl2 > div { padding: 9px 12px; min-width: 0; }
.tl2 .a { background: var(--card2); color: var(--mute); }
.tl2 .b { background: var(--acc-s); color: var(--ink); }
.tl2 b { display: block; font-size: 14px; color: var(--ink); }
.paths { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 640px) { .paths { grid-template-columns: minmax(0, 1fr); } }
.path { background: var(--card); border: 1.5px solid var(--line); border-radius: 18px; padding: 18px 20px; display: flex; flex-direction: column; gap: 6px; }
.path.hi { border-color: var(--acc); background: linear-gradient(160deg, var(--acc-s), transparent 70%), var(--card); }
.path p { color: var(--mute); margin: 0 0 6px; font-size: 14px; flex: 1; }
.ptag { align-self: flex-start; font-size: 11px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mute); border: 1px solid var(--stroke); border-radius: 99px; padding: 1px 9px; }
.ptag.acc { color: var(--acc); border-color: var(--acc); }
.pm { font-size: 14px; margin-bottom: 8px; }
.pm b { font-variant-numeric: tabular-nums; }
.note { border-left: 3px solid var(--warn); padding: 8px 12px; background: var(--card2); border-radius: 6px; font-size: 14px; margin: 10px 0; }
.note.bad { border-left-color: var(--bad); }
/* fund */
.fgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 28px; }
@media (max-width: 640px) { .fgrid { grid-template-columns: minmax(0, 1fr); } }
.inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.inline input[type=number] { width: 110px; }
.inline b { font-size: 20px; }
.keep { display: grid; gap: 8px; margin-top: 6px; }
.krow { display: grid; grid-template-columns: 86px minmax(0, 1fr) minmax(0, max-content); gap: 12px; align-items: center; font-size: 14px; }
.krow .kb { height: 12px; border-radius: 6px; background: var(--card2); overflow: hidden; }
.krow .kb i { display: block; height: 100%; border-radius: 6px; background: linear-gradient(90deg, var(--acc), var(--warn)); transition: width .3s; }
.krow .kv { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.krow .kv small { color: var(--mute); display: block; }
.krow.goal > span:first-child { font-weight: 700; color: var(--acc); }
.safe { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
@media (max-width: 640px) { .safe { grid-template-columns: minmax(0, 1fr); } }
.safe > div { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px; font-size: 13.5px; color: var(--mute); }
.safe b { display: block; color: var(--ink); font-size: 14.5px; margin: 6px 0 2px; }
.safe .si { width: 34px; height: 34px; border-radius: 10px; background: var(--acc-s); color: var(--acc); display: grid; place-items: center; font-size: 17px; }
/* stage */
.stagecard { background: var(--card); border: 1px solid var(--line); border-radius: 24px; padding: 18px 20px 20px; box-shadow: var(--shadow); }
.mock { border-radius: 16px; background: radial-gradient(120% 100% at 30% 0%, var(--card2), transparent 75%); cursor: default; }
.mock svg { width: 100%; height: auto; display: block; }
.spin { transform-box: fill-box; transform-origin: center; animation: spin 1.8s linear infinite; animation-play-state: paused; }
.spin.r45 { animation-duration: 1.333s; }
.mock:hover .spin, .mock.play .spin { animation-play-state: running; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } .step { animation: none; } }
.hole { fill: var(--card); }
.st-t { font-size: 21px; font-weight: 760; letter-spacing: -0.015em; margin-top: 8px; }
.st-a { color: var(--mute); }
.specs { display: flex; flex-wrap: wrap; gap: 5px; margin: 10px 0 14px; }
.specs span { font-size: 12px; background: var(--card2); border-radius: 99px; padding: 2px 9px; color: var(--mute); }
.sp { display: flex; justify-content: space-between; align-items: flex-end; gap: 10px; border-top: 1px solid var(--line); padding-top: 14px; }
.sp small { display: block; color: var(--mute); font-size: 12px; }
.sp b { font-variant-numeric: tabular-nums; font-size: 16px; }
.sp .pc { font-size: 30px; letter-spacing: -0.025em; }
.sp .r { text-align: right; }
.sl { display: flex; justify-content: space-between; gap: 10px; font-size: 13.5px; color: var(--mute); margin: 8px 0 16px; }
.sl b { color: var(--ink); font-weight: 650; text-align: right; }
.st-hint { font-size: 12.5px; color: var(--mute); margin-top: 8px; text-align: center; min-height: 1em; }
.mbar { display: none; }
/* campaign and production */
.s5grid { display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr); gap: 20px; align-items: start; margin-bottom: 24px; }
@media (max-width: 1180px) { .s5grid { grid-template-columns: minmax(0, 1fr); } }
.phone { max-width: 340px; width: 100%; margin: 0 auto; border: 1px solid var(--stroke); border-radius: 34px; padding: 12px; background: var(--bg2); box-shadow: var(--shadow); }
.phone .scr { border-radius: 24px; background: var(--card); padding: 14px 14px 16px; overflow: hidden; }
.phone .notch { width: 90px; height: 6px; border-radius: 99px; background: var(--stroke); margin: 0 auto 10px; }
.fp-t { font-size: 19px; font-weight: 760; margin-top: 6px; letter-spacing: -0.01em; }
.fp-p { font-size: 22px; font-weight: 760; margin: 8px 0 6px; }
.fp-p small { font-weight: 400; color: var(--mute); }
.bar { height: 10px; background: var(--card2); border-radius: 99px; overflow: hidden; }
.bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--acc), var(--warn)); border-radius: 99px; transition: width .12s; }
.bar i.bad { background: var(--bad); }
.fp-n { display: flex; justify-content: space-between; font-size: 13.5px; margin: 6px 0 8px; }
.fp-s { font-size: 13.5px; margin: 8px 0; }
.fp-tr { list-style: none; padding: 0; margin: 6px 0 0; font-size: 13.5px; }
.fp-tr li { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.fp-tr .tn { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fp-tr .sd { color: var(--faint); font-size: 11px; width: 12px; }
.pv { background: transparent; border: 1px solid var(--stroke); border-radius: 50%; width: 24px; height: 24px; font-size: 9px; cursor: pointer; flex: none; }
.pv:disabled { opacity: 0.35; cursor: not-allowed; } .pv.on { border-color: var(--acc); color: var(--acc); }
.fp-f { font-size: 11.5px; color: var(--mute); margin: 10px 0 0; }
.share { display: flex; gap: 6px; margin: 10px 0 4px; flex-wrap: wrap; }
.simrow { display: flex; align-items: center; gap: 12px; margin: 12px 0 4px; flex-wrap: wrap; }
.simrow input { flex: 1; min-width: 140px; width: auto; }
.ticker { list-style: none; padding: 0; margin: 4px 0 0; font-size: 13.5px; }
.ticker li { display: flex; justify-content: space-between; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--line); }
.ticker li:last-child { border-bottom: 0; }
.ticker small { color: var(--mute); }
.mini { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--mute); margin: 14px 0 2px; font-weight: 700; }
.ok-card { border-color: var(--ok); } .warn-card { border-color: var(--warn); }
.opts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 10px; }
@media (max-width: 640px) { .opts { grid-template-columns: minmax(0, 1fr); } }
.opt { background: var(--card2); border-radius: 14px; padding: 14px; display: flex; flex-direction: column; gap: 6px; }
.opt p { flex: 1; margin: 0 0 4px; color: var(--mute); font-size: 13.5px; }
.kvl { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px; font-size: 14px; margin: 10px 0; }
.kvl > span { color: var(--mute); } .kvl > b { text-align: right; font-variant-numeric: tabular-nums; }
.pgrid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 20px; align-items: start; scroll-margin-top: 90px; }
@media (max-width: 1180px) { .pgrid { grid-template-columns: minmax(0, 1fr); } }
.ph { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.ph h2 { margin: 0; }
ol.tl { list-style: none; padding: 0; margin: 0; position: relative; }
ol.tl li { display: flex; gap: 12px; align-items: flex-start; padding: 8px 0; position: relative; }
ol.tl li::before { content: ""; position: absolute; left: 10px; top: 30px; bottom: -8px; width: 2px; background: var(--line); }
ol.tl li:last-child::before { display: none; }
ol.tl li.done::before { background: var(--acc); }
ol.tl .dot { width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--stroke); background: var(--card); flex: none; display: grid; place-items: center; font-size: 11px; font-weight: 800; position: relative; z-index: 1; }
ol.tl li.done .dot { background: var(--acc); border-color: var(--acc); color: var(--onacc); }
ol.tl li.now .dot { border-color: var(--acc); box-shadow: 0 0 0 5px var(--acc-s); }
ol.tl li.gate .dot { border-radius: 3px; transform: rotate(45deg); width: 18px; height: 18px; margin: 2px; }
ol.tl li.gate .dot span { transform: rotate(-45deg); }
ol.tl li > div { flex: 1; min-width: 0; }
ol.tl li small { display: block; color: var(--mute); }
ol.tl li.next b { color: var(--mute); font-weight: 600; }
.gtag { font-size: 10.5px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--acc); margin-left: 6px; white-space: nowrap; display: inline-block; }
ul.feed { list-style: none; padding: 0; margin: 0; max-height: 420px; overflow: auto; }
ul.feed li { padding: 9px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
ul.feed small { display: block; color: var(--mute); }
/* passport */
.passport { margin-top: 24px; border-radius: 22px; scroll-margin-top: 90px; background: linear-gradient(170deg, var(--acc-s), transparent 45%), var(--card); border: 1.5px solid var(--acc); padding: 22px 24px; }
.pp-head { display: flex; gap: 18px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; }
.pp-t { font-size: 26px; margin: 4px 0 2px; letter-spacing: -0.02em; font-weight: 760; display: block; }
.qr { width: 96px; height: 96px; border-radius: 8px; shape-rendering: crispEdges; flex: none; }
.q0 { fill: #fff; } .q1 { fill: #111; }
.pp-key { background: var(--card2); border-radius: 12px; padding: 10px 14px; margin: 16px 0 8px; font-size: 13px; }
.pp-key span { display: block; color: var(--mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; font-weight: 700; }
.pp-key code { font: 600 14px/1.5 ui-monospace, "Cascadia Mono", Consolas, monospace; word-break: break-word; }
.pp-chain { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 26px; }
@media (max-width: 760px) { .pp-chain { grid-template-columns: minmax(0, 1fr); } }
.pp-chain li { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--line); font-size: 14px; }
.pp-chain .ci { margin-top: 2px; }
.pp-chain small { display: block; color: var(--mute); }
.pp-chain .dt { margin-left: auto; color: var(--mute); font-size: 12.5px; white-space: nowrap; padding-left: 8px; }
.repress { display: flex; justify-content: space-between; align-items: center; gap: 14px; flex-wrap: wrap; margin-top: 18px; }
.repress p { margin: 0; flex: 1; min-width: 220px; color: var(--mute); font-size: 14px; }
.repress b { color: var(--ink); }
.empty { text-align: center; padding: 40px 20px; }
.empty .rowc { justify-content: center; margin-top: 16px; }
/* tour caption, toast, footer */
.tour { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 60; width: min(760px, calc(100% - 28px)); display: flex; align-items: center; gap: 14px; background: var(--glass); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid var(--stroke); border-radius: 18px; padding: 12px 14px 12px 18px; box-shadow: var(--shadow); font-size: 14.5px; }
.tour .tn { font-size: 11px; font-weight: 800; color: var(--acc); letter-spacing: 0.08em; white-space: nowrap; }
.tour #tourT { flex: 1; }
.toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: var(--card); border: 1px solid var(--acc); border-radius: 12px; padding: 9px 16px; font-size: 14px; z-index: 70; box-shadow: var(--shadow); max-width: calc(100% - 28px); }
footer { max-width: 1320px; margin: 0 auto; padding: 0 24px 50px; color: var(--mute); font-size: 13px; }
footer .draft { font-size: 12px; color: var(--faint); }
footer p { max-width: 900px; margin: 6px 0; }
/* narrow screens */
/* top bar: currency and language next to the theme button */
.prefs { display: flex; align-items: center; gap: 6px; }
.seg.cmp { padding: 2px; gap: 1px; flex-wrap: nowrap; }
.seg.cmp button { padding: 4px 9px; font-size: 12.5px; font-weight: 650; }
.kpi b.wr { white-space: normal; }
@media (max-width: 640px) { .kpi.span2 { grid-column: 1 / -1; } }
.seg.cmp button[lang=th], html[lang=th] body { font-family: system-ui, -apple-system, "Segoe UI", "Leelawadee UI", "Noto Sans Thai", Thonburi, Tahoma, sans-serif; }
/* Thai: system Thai faces, more line height for tone marks, no wide tracking */
html[lang=th] body { line-height: 1.6; }
html[lang=th] h1 { line-height: 1.28; letter-spacing: 0; }
html[lang=th] .eyebrow, html[lang=th] .inv .il, html[lang=th] .ptag, html[lang=th] th, html[lang=th] .gtag, html[lang=th] .mini, html[lang=th] .pp-key span, html[lang=th] .brand .pill { letter-spacing: 0.02em; }
html[lang=th] .mock text[font-family^="system-ui"] { font-family: system-ui, "Leelawadee UI", "Noto Sans Thai", Thonburi, Tahoma, sans-serif; }
@media (max-width: 1300px) {
nav.steps .lb { display: none; }
nav.steps button { padding: 6px; }
nav.steps button.on .lb { display: inline; }
nav.steps button.on { padding-right: 12px; }
}
@media (max-width: 1000px) {
.top .in { flex-wrap: wrap; row-gap: 4px; }
.tools { flex: 1 1 100%; }
.prefs { margin-left: auto; }
.block, .pgrid, .passport { scroll-margin-top: 118px; }
}
@media (max-width: 1080px) {
#tourBtn .tl { display: none; }
.app { grid-template-columns: minmax(0, 1fr); gap: 20px; padding-top: 18px; padding-bottom: 110px; }
.stage { position: static; order: -1; }
.stagecard { display: grid; grid-template-columns: minmax(0, 220px) minmax(0, 1fr); gap: 4px 16px; align-items: center; padding: 12px 14px; border-radius: 18px; }
.stagecard .mock { grid-row: span 3; }
.stagecard .st-t { margin-top: 0; font-size: 18px; }
.stagecard .specs { margin: 4px 0 0; }
#stPrice, #stCta, .st-hint { display: none; }
.mbar { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); background: var(--glass); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border-top: 1px solid var(--line); }
.mbar small { display: block; color: var(--mute); font-size: 12px; }
.mbar b { font-size: 18px; font-variant-numeric: tabular-nums; }
.mbar .btn { padding: 10px 16px; font-size: 14px; }
.tour, .toast { bottom: 84px; }
h1 { font-size: 32px; }
}
@media (max-width: 760px) {
nav.steps .lb { display: none; }
.sw .lbl { display: none; }
.top .in { padding: 8px 14px; gap: 10px; }
.app { padding-left: 14px; padding-right: 14px; }
.brand .nm { display: none; }
}
@media (max-width: 520px) {
.stagecard { grid-template-columns: minmax(0, 1fr); }
.stagecard .mock { grid-row: auto; max-width: 300px; margin: 0 auto; width: 100%; }
.brand .pill { display: none; }
.lead { font-size: 15.5px; }
.panel { padding: 16px; }
.tour { flex-wrap: wrap; }
nav.steps button.on .lb { display: inline-block; max-width: 11em; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
.top .in { gap: 8px; }
.seg.cmp button { padding: 4px 8px; }
}
@media (max-width: 400px) {
.tools, .top .in { gap: 6px; }
.prefs { gap: 4px; }
.seg.cmp button { padding: 4px 6px; }
#tourBtn { padding: 6px 10px; }
.tools .ib { width: 32px; height: 32px; }
nav.steps button.on .lb { max-width: 7.5em; }
}
@media (max-width: 350px) { nav.steps button.on .lb { display: none; } }
</style>
</head>
<body>
<header class="top">
<div class="in">
<div class="brand"><svg viewBox="0 0 32 32" aria-hidden="true"><circle cx="16" cy="16" r="15" fill="#141414" stroke="var(--stroke)"/><circle cx="16" cy="16" r="11" fill="none" stroke="rgba(255,255,255,0.12)"/><circle cx="16" cy="16" r="7.5" fill="none" stroke="rgba(255,255,255,0.12)"/><circle cx="16" cy="16" r="5" fill="var(--acc)"/><circle cx="16" cy="16" r="1.3" fill="#141414"/></svg><span class="nm">CNX Vinyl</span><span class="pill" data-i18n="pill">Demo</span></div>
<nav class="steps" id="steps" aria-label="Steps" data-i18n-aria="navAria">
<button type="button" data-act="go" data-s="1"><span class="d">1</span><span class="lb" data-i18n="nav1">Design</span></button>
<button type="button" data-act="go" data-s="2"><span class="d">2</span><span class="lb" data-i18n="nav2">Music &amp; art</span></button>
<button type="button" data-act="go" data-s="3"><span class="d">3</span><span class="lb" data-i18n="nav3">Price</span></button>
<button type="button" data-act="go" data-s="4"><span class="d">4</span><span class="lb" data-i18n="nav4">Fan funding</span></button>
<button type="button" data-act="go" data-s="5"><span class="d">5</span><span class="lb" data-i18n="nav5">Track</span></button>
</nav>
<div class="tools">
<label class="sw" title="Show the business behind each step" data-i18n-title="invSwT"><input type="checkbox" id="invT"><span class="tr"></span><span class="lbl" data-i18n="invSw">Investor notes</span></label>
<button type="button" class="btn ghost sm" data-act="tour" id="tourBtn" title="A guided walk through the whole flow, about a minute" data-i18n-title="tourT">&#x25B6;<span class="tl"> Tour</span></button>
<div class="prefs">
<div class="seg cmp" data-k="cur" role="group" aria-label="Currency" data-i18n-aria="curAria"><button type="button" data-v="USD" title="Prices in US dollars (1 USD = 33 THB, planning rate)" data-i18n-title="cur_USD">USD</button><button type="button" data-v="THB" title="Prices in Thai baht" data-i18n-title="cur_THB">THB</button><button type="button" data-v="EUR" title="Prices in euros (1 EUR = 1.155 USD = 38.115 THB, planning rate)" data-i18n-title="cur_EUR">EUR</button></div>
<div class="seg cmp" data-k="lang" role="group" aria-label="Language" data-i18n-aria="langAria"><button type="button" data-v="en" lang="en" title="English">EN</button><button type="button" data-v="th" lang="th" title="ภาษาไทย">ไทย</button></div>
</div>
<button type="button" class="ib" id="themeBtn">&#x2600;</button>
</div>
</div>
<div class="progress"><i id="prog"></i></div>
</header>
<div class="app">
<main>
<!-- STEP 1 -->
<section class="step" data-step="1">
<div class="eyebrow" data-i18n="eb1">Step 1 of 5</div>
<h1 data-i18n="h1_1">Design your record</h1>
<p class="lead" data-i18n="lead1">Cut, plated and pressed under one roof near Chiang Mai, from 100 copies. Pay for the run yourself, or let your fans fund it.</p>
<div class="inv"><span class="il" data-i18n="invL">Investor note</span><p data-i18n="inv1"><b>The configurator is also market research.</b> Every configuration, logged anonymously, is a data point on run size, colour, packaging and the price at which people walk away: the run-size question the revenue model leaves open. It goes public with indicative prices and an interest list long before the plant opens.</p></div>
<div class="block">
<h2 data-i18n="fmtH">Format</h2>
<div class="cards c3">
<button type="button" class="choice" data-k="size" data-v="12"><span class="ic"><svg viewBox="0 0 60 60" width="60" height="60" aria-hidden="true"><circle cx="30" cy="30" r="26" class="fi-d"/><circle cx="30" cy="30" r="21" class="fi-g"/><circle cx="30" cy="30" r="15.5" class="fi-g"/><circle cx="30" cy="30" r="8.6" class="fi-l"/></svg></span><b>12" LP</b><span class="sub" data-sub="12"></span></button>
<button type="button" class="choice" data-k="size" data-v="10"><span class="ic"><svg viewBox="0 0 60 60" width="60" height="60" aria-hidden="true"><circle cx="30" cy="30" r="21.5" class="fi-d"/><circle cx="30" cy="30" r="17.2" class="fi-g"/><circle cx="30" cy="30" r="12.9" class="fi-g"/><circle cx="30" cy="30" r="8.6" class="fi-l"/></svg></span><b>10"</b><span class="sub" data-sub="10"></span></button>
<button type="button" class="choice" data-k="size" data-v="7"><span class="ic"><svg viewBox="0 0 60 60" width="60" height="60" aria-hidden="true"><circle cx="30" cy="30" r="15.1" class="fi-d"/><circle cx="30" cy="30" r="11.5" class="fi-g"/><circle cx="30" cy="30" r="7.8" class="fi-l"/></svg></span><b data-i18n="f7">7" single</b><span class="sub" data-sub="7"></span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl" data-i18n="fSpeed">Speed</span><div class="seg" data-k="rpm"><button type="button" data-v="33" data-i18n="sp33">33&#x2153; rpm</button><button type="button" data-v="45" data-i18n="sp45">45 rpm</button></div></div>
<div class="field"><span class="fl" data-i18n="fDiscs">Discs</span><div class="seg" data-k="discs" data-t="n"><button type="button" data-v="1" data-i18n="dSingle">Single</button><button type="button" data-v="2" data-i18n="dDouble">Double</button></div></div>
<div class="field" id="fWeight"><span class="fl" data-i18n="fWeight">Weight</span><div class="seg" data-k="weight" data-t="n"><button type="button" data-v="140" data-i18n="w140">140 g</button><button type="button" data-v="180" data-i18n="w180">180 g heavyweight</button></div></div>
<div class="field" id="fDink"><span class="fl" data-i18n="fHole">Centre hole</span><div class="seg" data-k="dinked" data-t="b"><button type="button" data-v="false" data-i18n="hSmall">Small</button><button type="button" data-v="true" data-i18n="hLarge">Large (dinked)</button></div></div>
</div>
</div>
<div class="block">
<h2 data-i18n="vinylH">Vinyl</h2>
<div class="cards c4">
<button type="button" class="choice" data-k="colour" data-v="black"><span class="ic" data-mini="black"></span><b data-i18n="cBlack">Classic black</b><span data-i18n="cBlackS">Timeless</span></button>
<button type="button" class="choice" data-k="colour" data-v="solid"><span class="ic" data-mini="solid"></span><b data-i18n="cSolid">Solid colour</b><span data-i18n="cSolidS">Any colour you pick</span></button>
<button type="button" class="choice" data-k="colour" data-v="marbled"><span class="ic" data-mini="marbled"></span><b data-i18n="cMarb">Marbled</b><span data-i18n="cMarbS">Two colours swirled</span></button>
<button type="button" class="choice" data-k="colour" data-v="splatter"><span class="ic" data-mini="splatter"></span><b data-i18n="cSplat">Splatter</b><span data-i18n="cSplatS">Flecks on a base</span></button>
</div>
<div class="colours">
<div class="field" id="fC1"><span class="fl" data-i18n="fCol">Colour</span><div class="sw-row" data-c="c1"></div></div>
<div class="field" id="fC2"><span class="fl" data-i18n="fCol2">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" data-i18n="fClean">Clean the press before your colour</span><div class="seg" data-k="cleaning" data-t="b"><button type="button" data-v="true" data-i18n="clYes">Yes, recommended</button><button type="button" data-v="false" data-i18n="clNo2">No</button></div></div>
<p class="hint" id="cleanHint" style="max-width:400px;margin:0"></p>
</div>
</div>
<div class="block">
<h2 data-i18n="packH">Packaging</h2>
<div class="cards c4">
<button type="button" class="choice" data-k="jacket" data-v="printed"><span class="ic"><span class="jic printed"></span></span><b data-i18n="jPr">Printed jacket</b><span data-i18n="jPrS">Your artwork</span></button>
<button type="button" class="choice" data-k="jacket" data-v="gatefold" id="bGate"><span class="ic"><span class="jic printed gf"></span><span class="jic printed gf"></span></span><b data-i18n="jGf">Gatefold</b><span data-i18n="jGfS">Opens like a book</span></button>
<button type="button" class="choice" data-k="jacket" data-v="plain"><span class="ic"><span class="jic plain"></span></span><b data-i18n="jPl">Plain jacket</b><span data-i18n="jPlS">Unprinted card</span></button>
<button type="button" class="choice" data-k="jacket" data-v="none"><span class="ic"><span class="jic none"></span></span><b data-i18n="jOwn">My own jacket</b><span data-i18n="jOwnS">You supply it</span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl" data-i18n="fLabels">Labels</span><div class="seg" data-k="labels"><button type="button" data-v="printed" data-i18n="lPr">Printed</button><button type="button" data-v="white" data-i18n="lWh">Plain white</button><button type="button" data-v="black" data-i18n="lBl">Plain black</button></div></div>
<div class="field"><span class="fl" data-i18n="fInner">Inner sleeve</span><div class="seg" data-k="inner"><button type="button" data-v="paper" data-i18n="iPaper">Paper</button><button type="button" data-v="poly" data-i18n="iPoly">Poly-lined</button></div></div>
<div class="field" id="fSpine"><span class="fl"><span data-i18n="fSpine">Spine</span> <small class="mute" id="spineHint"></small></span><div class="seg" data-k="spine"><button type="button" data-v="thin" data-i18n="spThin">Thin, 3 mm</button><button type="button" data-v="thick" data-i18n="spThick">Thick, 5 mm</button></div></div>
<div class="field"><span class="fl" data-i18n="fExtras">Extras</span><div class="chips"><button type="button" class="chip" data-b="shrink" data-i18n="xShrink">Shrink wrap</button><button type="button" class="chip" data-b="insert" data-i18n="xInsert">Printed insert</button><button type="button" class="chip" data-b="sticker" data-i18n="xSticker">Hype sticker</button><button type="button" class="chip" data-b="numbered" data-i18n="xNum">Numbered edition</button><button type="button" class="chip" data-b="dlcard" data-i18n="xDl">Download card</button><button type="button" class="chip" data-b="barcode" data-i18n="xBar">Barcode sticker</button></div></div>
</div>
</div>
<div class="block">
<h2 data-i18n="soundH">Sound</h2>
<div class="field" style="margin-bottom:14px;align-items:flex-start"><span class="fl" data-i18n="fSource">Your audio arrives as</span><div class="seg" data-k="source"><button type="button" data-v="digital" data-i18n="srcD">Digital files</button><button type="button" data-v="tape" data-i18n="srcT">Analog tape</button><button type="button" data-v="lacquer" data-i18n="srcL">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 data-i18n="mYes">Vinyl mastering</b><span data-i18n="mYesS">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 data-i18n="mNo">My master is vinyl-ready</b><span data-i18n="mNoS">Cut straight from your files</span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl" data-i18n="fTps">Test pressings for you</span><div class="stepper"><button type="button" data-act="tps" data-d="-1" aria-label="Fewer" data-i18n-aria="tpsLess">&#x2212;</button><b id="tpsV">5</b><button type="button" data-act="tps" data-d="1" aria-label="More" data-i18n-aria="tpsMore">+</button></div></div>
<p class="hint" id="srcHint" style="max-width:380px;margin:0">You hear and approve a real pressing before the run. The father and mother are archived, so a repress later skips cutting.</p>
</div>
</div>
</section>
<!-- STEP 2 -->
<section class="step" data-step="2" hidden>
<div class="eyebrow" data-i18n="eb2">Step 2 of 5</div>
<h1 data-i18n="h1_2">Your music and artwork</h1>
<p class="lead" data-i18n="lead2">Drop your masters and artwork. They are checked right here, before you pay anything. In this demo nothing leaves your browser.</p>
<div class="inv"><span class="il" data-i18n="invL">Investor note</span><p data-i18n="inv2"><b>Nothing reaches fans that the plant cannot make.</b> Files are checked before anyone pays, then a cutting engineer confirms. In the real platform, masters land in the plant's encrypted client vault and previews are rendered at the plant, so a full master never rests outside it.</p></div>
<div class="block g2f">
<div class="field"><span class="fl" data-i18n="fArtist">Artist</span><input type="text" id="artist" maxlength="60"></div>
<div class="field"><span class="fl" data-i18n="fTitle">Release title</span><input type="text" id="title" maxlength="80"></div>
<div class="field" style="grid-column:1/-1"><span class="fl" data-i18n="fCatno">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><span data-i18n="tracksH">Tracks and sides</span> <small id="sideHint"></small></h2>
<label class="drop" id="dropAudio"><input type="file" id="fileAudio" accept=".wav,.flac,.aif,.aiff,.aifc,audio/*" multiple hidden><span class="dic">&#x266B;</span><span data-i18n="dropA"><b>Drop WAV, FLAC or AIFF masters</b> or click to browse</span></label>
<div class="sides" id="sides"></div>
<div class="trk" id="trk"></div>
<div class="rowc">
<button type="button" class="btn ghost sm" data-act="add" data-i18n="addTrack">+ Add a track</button>
<button type="button" class="btn ghost sm" data-act="split" data-i18n="balance">Balance the sides</button>
<div class="seg" data-k="loud" data-t="b"><button type="button" data-v="false" data-i18n="loudStd">Standard</button><button type="button" data-v="true" data-i18n="loudLoud">Loud, bass-heavy</button></div>
</div>
</div>
<div class="block">
<h2 data-i18n="artH">Artwork</h2>
<div class="g2f">
<div><label class="drop" id="dropJacket"><input type="file" id="fileJacket" accept=".pdf,.ai,application/pdf,application/postscript,application/illustrator,image/*" hidden><span class="dic">&#x25A3;</span><span data-i18n="dropJ"><b>Jacket</b>, PDF, AI or a 300 dpi image</span></label><div class="tplk" id="tplJacket"></div><div id="artJacket"></div></div>
<div><label class="drop" id="dropLabel"><input type="file" id="fileLabel" accept=".pdf,.ai,application/pdf,application/postscript,application/illustrator,image/*" hidden><span class="dic">&#x25CE;</span><span data-i18n="dropL"><b>Labels</b>, PDF, AI or a 300 dpi image</span></label><div class="tplk" id="tplLabel"></div><div id="artLabel"></div></div>
</div>
</div>
<div class="block panel" id="checkPanel">
<div class="chkhead" id="checkHead"></div>
<ul class="chk" id="checks"></ul>
<label class="rights"><input type="checkbox" id="rights"> <span data-i18n="rightsL">I own or have licensed these recordings and the songs on them. Cover versions need a mechanical licence.</span></label>
<div class="rowc" style="align-items:center"><button type="button" class="btn" data-act="approve" id="bApprove">Approve and see my price &#x2192;</button><button type="button" class="lnk" data-act="go" data-s="3" data-i18n="justPrice">Just show me the price</button></div>
<p class="hint" id="gateMsg"></p>
</div>
</section>
<!-- STEP 3 -->
<section class="step" data-step="3" hidden>
<div class="eyebrow" data-i18n="eb3">Step 3 of 5</div>
<h1 data-i18n="h1_3">Your price</h1>
<p class="lead" data-i18n="lead3">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" data-i18n="invL">Investor note</span><p data-i18n="inv3a"><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 data-i18n="inv3b">Prices on this page are indicative, at Western benchmark rates. The Thai market pays <span data-thb="650" data-bare="1">650</span> to <span data-thb="950">950 THB</span> a copy at 300 copies today; the real price list follows market validation.</p><p data-i18n="inv3c"><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" data-i18n="fCopies">Copies</span><div class="qbig"><input type="number" id="qty" min="100" max="5000" step="10" aria-label="Copies" data-i18n-aria="copiesAria"><span data-i18n="copiesU">copies</span></div></div>
</div>
<input type="range" id="qtyR" min="100" max="2000" step="10" aria-label="Copies" data-i18n-aria="copiesAria">
<div class="qchips"><button type="button" class="chip" data-q="100">100</button><button type="button" class="chip" data-q="200">200</button><button type="button" class="chip" data-q="300">300</button><button type="button" class="chip" data-q="500">500</button><button type="button" class="chip" data-q="1000">1,000</button></div>
<div class="kpis" id="kpis"></div>
<div class="cv" id="curve"></div>
</div>
<div class="panel block"><details open><summary><span data-i18n="incl">What is included</span> <span class="sumr" id="bdSum"></span></summary><div class="tscroll" id="offer"></div></details></div>
<div class="panel block" id="leadP"></div>
<div class="block">
<h2 data-i18n="payH">How do you want to pay for it?</h2>
<div class="paths">
<div class="path"><span class="ptag" data-i18n="pClassic">Classic</span><h3 data-i18n="pOrderNow">Order now</h3><p data-i18n="pOrderP">Pay a deposit, approve the master and the test pressing, and the run ships to you or straight to your buyers.</p><div class="pm" id="pDirect"></div><button type="button" class="btn ghost" data-act="direct" id="bDirect" data-i18n="bDirect">Order directly</button></div>
<div class="path hi"><span class="ptag acc" data-i18n="pNoUp">No upfront cost</span><h3 data-i18n="pFanH">Let your fans fund it</h3><p data-i18n="pFanP">Fans pre-order on your campaign page. Pressed if enough of them do before the deadline; every fan refunded if not.</p><div class="pm" id="pFan"></div><button type="button" class="btn" data-act="go" data-s="4" data-i18n="pSetUp">Set up a campaign &#x2192;</button></div>
</div>
<p class="hint" id="choiceMsg"></p>
</div>
</section>
<!-- STEP 4 -->
<section class="step" data-step="4" hidden>
<div class="eyebrow" data-i18n="eb4">Step 4 of 5</div>
<h1 data-i18n="h1_4">Let your fans fund it</h1>
<p class="lead" data-i18n="lead4">Set a price and a deadline. If enough fans pre-order, we press and ship a copy to each of them. If not, every fan gets their money back.</p>
<div class="inv"><span class="il" data-i18n="invL">Investor note</span><p data-i18n="inv4"><b>A plant-owned platform, not a broker.</b> Comparable platforms broker other people's plants; CNX controls price, schedule and quality and keeps the manufacturing margin instead of a commission. Fan money is ring-fenced: the lesson of Qrates, whose service is suspended with funded projects caught in the wind-down.</p></div>
<div class="panel block">
<div class="fgrid">
<div class="field" style="grid-column:1/-1"><span class="fl" data-i18n="fFanPrice">Your price to fans, per copy</span><div class="inline"><input type="number" id="fanPrice" min="300" max="5000" step="50" aria-label="Fan price" data-i18n-aria="fanAria"><span id="fpCur">THB</span></div><input type="range" id="fanR" min="300" max="3000" step="50" aria-label="Fan price" data-i18n-aria="fanAria"><div class="qchips" id="fpChips"><button type="button" class="chip" data-fp="800">800</button><button type="button" class="chip" data-fp="1000">1,000</button><button type="button" class="chip" data-fp="1200">1,200</button><button type="button" class="chip" data-fp="1500">1,500</button><button type="button" class="chip" data-fp="2000">2,000</button></div><div class="need" id="need"></div><small class="mute" id="needHint"></small></div>
<div class="field"><span class="fl" data-i18n="fLength">Campaign length</span><div class="inline"><b id="daysV"></b><span data-i18n="daysU">days</span></div><input type="range" id="days" min="14" max="45" step="1" aria-label="Days" data-i18n-aria="daysAria"></div>
<div class="field" id="fCap"><span class="fl" data-i18n="fLimited">Limited edition</span><div class="inline"><label class="sw acc"><input type="checkbox" id="limT"><span class="tr"></span><span data-i18n="stopAt">Stop at</span></label><input type="number" id="cap" min="100" max="5000" step="50" aria-label="Edition size" data-i18n-aria="capAria"><small class="mute" data-i18n="numCopies">numbered copies</small></div></div>
<div class="field"><span class="fl" data-i18n="fAcop">Copies for yourself</span><div class="inline"><input type="number" id="acop" min="0" max="200" step="5" aria-label="Copies for yourself" data-i18n-aria="acopAria"><small class="mute" data-i18n="acopU">pressed with the run</small></div></div>
</div>
<label class="rights" style="margin:16px 0 0"><input type="checkbox" id="vat"> <span data-i18n="vatL">Show the numbers with 7 % VAT taken out of the fan price</span></label>
</div>
<div class="block" id="fundEcon"></div>
<div class="block">
<h2 data-i18n="safeH">Your fans' money is safe</h2>
<div class="safe">
<div><span class="si">&#x26C1;</span><b data-i18n="safe1t">Held apart</b><span data-i18n="safe1">Pre-orders sit in a separate account and are never used to run the plant until the campaign succeeds.</span></div>
<div><span class="si">&#x21BA;</span><b data-i18n="safe2t">Refunded automatically</b><span data-i18n="safe2">If the goal is missed, every fan gets their money back. Nobody ends up with a garage full of records.</span></div>
<div><span class="si">&#x2713;</span><b data-i18n="safe3t">Pay the way fans pay</b><span data-i18n="safe3">PromptPay QR, cards and wallets. Card details never touch our servers.</span></div>
</div>
</div>
<div class="rowc" style="align-items:center"><button type="button" class="btn" data-act="launch" id="bLaunch" data-i18n="launchBtn">Launch the campaign &#x2192;</button><span class="hint" id="launchMsg"></span></div>
</section>
<!-- STEP 5 -->
<section class="step" data-step="5" hidden>
<div class="eyebrow" data-i18n="eb5">Step 5 of 5</div>
<h1 id="s5h">Campaign and production</h1>
<p class="lead" id="s5l"></p>
<div class="inv"><span class="il" data-i18n="invL">Investor note</span><p data-i18n="inv5a"><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 data-i18n="inv5b"><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>
<aside class="stage">
<div class="stagecard">
<div class="mock" id="mock" title="Hover to spin" data-i18n-title="mockT"></div>
<div class="st-t" id="stT"></div>
<div class="st-a" id="stA"></div>
<div class="specs" id="stSpecs"></div>
<div id="stPrice"></div>
<button type="button" class="btn wide" data-act="cta" id="stCta"></button>
<div class="st-hint" id="stHint"></div>
</div>
</aside>
</div>
<div class="mbar" id="mbar"><div id="mbP"></div><button type="button" class="btn" data-act="cta" id="mbCta"></button></div>
<div class="tour" id="tourCap" hidden><span class="tn" id="tourN"></span><span id="tourT"></span><button type="button" class="btn ghost sm" data-act="tour" data-i18n="tourStop">Stop</button></div>
<footer>
<p data-i18n="foot1">Investor demo of the CNX Vinyl artist platform. Prices are indicative (price list demo-0.1, planning estimates) and are not an offer. Nothing is uploaded, sent or charged; the campaign, the production and the record passport are simulated. CNX Vinyl is a working name; the platform's own name is still open.</p>
<p data-i18n="foot2">The detailed version lists every rate and where it comes from: <a href="../artist-order/">artist ordering demo</a>.</p>
<p class="draft" id="footDraft" data-i18n="footDraft" hidden>The Thai text on this page is a draft translation for review.</p>
</footer>
<script>
(function(){
"use strict";
const $=s=>document.querySelector(s), $$=s=>Array.from(document.querySelectorAll(s));
const esc=s=>String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const THB_USD=33, USD_EUR=1.155, THB_EUR=THB_USD*USD_EUR; /* 1 EUR = 1.155 USD = 38.115 THB */
const RATE={THB:1,USD:THB_USD,EUR:THB_EUR},CURS=["USD","THB","EUR"],LANGS=["en","th"];
/* ---- words: T.en / T.th. Static markup carries data-i18n (innerHTML), data-i18n-title, data-i18n-aria; its English
text is read from the markup into T.en at start, so T.en below holds only the strings built in code.
t(key, vars) fills {name} slots; an entry may be a function of vars (plurals, word order). A key missing in T.th
falls back to English and is listed in window.__mr.missing. ---- */
const T={en:{
docTitle:"Make a record · CNX Vinyl (demo)",
docDesc:"Design a vinyl record, check your music, get a transparent price, and let your fans fund the pressing. Investor demo, indicative prices.",
range:"{a} to {b}",rpm33:"33⅓ rpm",rpm45:"45 rpm",rpmS33:"33⅓",rpmS45:"45",
sz12:"12\" LP",sz10:"10\"",sz7:"7\" single",dinkedP:" (dinked)",
cn_black:"Black vinyl",cn_solid:"Solid colour",cn_marbled:"Marbled",cn_splatter:"Splatter",
ct_black:"black",ct_solid:"solid colour",ct_marbled:"marbled",ct_splatter:"splatter",
descT:v=>v.d+v.n+v.k+", "+v.r+", "+(v.w?v.w+" g ":"")+v.c+" vinyl",
jn_none:"Your own jacket",jn_plain:"Plain jacket",jn_printed:"Printed jacket",jn_gatefold:"Gatefold jacket",
xn_shrink:"Shrink wrap",xn_insert:"Insert",xn_sticker:"Hype sticker",xn_numbered:"Numbered",xn_dlcard:"Download card",xn_barcode:"Barcode sticker",
c_Ruby:"Ruby",c_Tangerine:"Tangerine",c_Sunflower:"Sunflower",c_Jade:"Jade",c_Ocean:"Ocean",c_Violet:"Violet",c_Bone:"Bone",c_Smoke:"Smoke",
c_Gold:"Gold",c_White:"White",c_Black:"Black",c_Pink:"Pink",c_Teal:"Teal",
cities:["Chiang Mai","Bangkok","Chiang Rai","Khon Kaen","Lampang","Phuket","Hat Yai","Nakhon Ratchasima","Pai","Udon Thani","Tokyo","Singapore","Taipei","Seoul","Berlin","London","Melbourne","Kuala Lumpur"],
stg_intake:"Audio intake and technical QA",stg_g1:"You approve the master plan",stg_mast:"Vinyl mastering",stg_g2:"You approve the vinyl master",
stg_cut:"Cutting the lacquer",stg_plate:"Plating: father, mother, stampers",stg_prep:"Stamper finishing",stg_tp:"Test pressings",
stg_g3:"You approve the test pressing",stg_queue:"Press slot booked",stg_run:"Pressing",stg_fin:"Packing",stg_ship:"Shipping",
gate_g1:"the master plan",gate_g2:"the vinyl master",gate_g3:"the test pressing",
ev_mast:"Mastered for vinyl. The artist is listening to the master.",ev_g2:"The artist approved the vinyl master.",
ev_cut:"Cut to lacquer in the cutting room.",ev_plate:"Plated in-house: father, mother and stampers. Father and mother are archived for represses.",
ev_prep:"Stampers punched, back-finished and measured.",ev_tp:"First test pressings made and sent to the artist.",
ev_g3:"The artist approved the test pressing.",ev_queue:"Pressing slot booked.",
ev_run:"Pressed: {n} copies, every one optically inspected and logged.",
ev_fin:v=>"Packed: sleeves"+(v.j?", jackets":"")+(v.s?" and shrink wrap":"")+".",
ev_shipD:"Dispatched; tracking sent.",ev_ship:"Shipped: one parcel per fan, tracking sent to all {n} fans.",
ev_startD:"Order confirmed and deposit received.",ev_startM:"Going ahead at the {min}-copy minimum with {n} fans.",
ev_startC:"Funded by {n} fans. The campaign is now a production order.",
u_sides:"sides",u_disc:"disc",u_discs:"discs",u_copies:"copies",
q_master:"Vinyl mastering",q_tape:"Analog tape transfer",q_blank:"Lacquer blanks",q_cut:"Lacquer cutting",
q_plate:"Plating: father, mother (kept for represses), stamper",q_setup:"Press setup",q_tp:"Test pressings for you",
q_clean:"Press cleaned before your colour",q_press:v=>"Pressing, "+v.s+"\""+(v.w?" 140 g":"")+", printed labels",
q_g180:"180 g heavyweight",q_colour:"Colour vinyl, {c}",q_inner:"Inner sleeve, {x}",q_paper:"paper",q_poly:"poly-lined",
q_assy:"Assembly and packing",q_pic:" (picture sleeve)",q_spine:"Thick 5 mm spine",q_plainL:v=>"Plain "+v.c+" labels instead of printed",
lc_white:"white",lc_black:"black",
q_shrink:"Shrink wrap",q_dlcard:"Download card",q_barcode:"Barcode sticker",q_insert:"Printed insert",q_sticker:"Hype sticker",q_numbered:"Hand-numbered",
spWhyGf:"A gatefold has its own {mm} mm spine",spWhyNone:"You supply the jacket",spWhy7:"7\" jackets come with a 3 mm spine",
spHNone:"you supply the jacket",spHGf:"the gatefold has a {mm} mm spine",spH7:"3 mm on a 7\" jacket",
spH5two:"5 mm: needed for two records",spH5:"5 mm: room for a thick insert or a poster",spH3:"3 mm: one record in its inner sleeve",
spTwo:"Two records need the thick 5 mm spine",
tplLabel:v=>v.d+" mm label, "+v.e+" mm with bleed, a page per side ("+v.s+")",
tplJacket:v=>v.w+" × "+v.h+" mm with bleed, "+v.sp+" mm spine"+(v.gf?", outside and inside":""),
tplOffJ:"You supply the jacket",tplOffP:"A plain jacket has no print",tplOffL:"Plain labels have no print",
tplDl:"Template downloaded: {n}",tplLink:v=>"Download the "+(v.k==="label"?"label":v.gf?"gatefold":"jacket")+" template",
artRender:"Rendering a preview",artNoPrev:"No preview: pdf.js could not be loaded. The checks above read the file itself.",
artErr:"This browser cannot read the file; the real platform checks it after upload.",
artPS:"Illustrator file saved without PDF content: save it with Create PDF Compatible File, or export a PDF",artBad:"Not a PDF, AI or image file",
pc_ai:"Illustrator file, saved PDF-compatible",pc_pdf:"PDF {v}",pc_x:", PDF/X",pc_trim:"Trim",pc_page:"Page",
pc_lblOk:v=>v.tp+" "+v.w+" mm: the "+v.s+"\" label"+(v.b?" with 3 mm bleed":""),
pc_lblBad:v=>v.tp+" "+v.w+" × "+v.h+" mm; the "+v.s+"\" label is "+v.d+" mm ("+v.e+" with bleed)",
pc_pagesOk:v=>v.n+" page"+(v.n>1?"s":"")+": side"+(v.ns>1?"s ":" ")+v.l,
pc_pagesWarn:v=>v.n+" page"+(v.n>1?"s":"")+" for "+v.ns+" sides: side "+v.l+" missing",
pc_jOk:"{w} × {h} mm: back, {sp} mm spine, front",pc_front:"Front only: the back and the {sp} mm spine are needed before print (use the template)",
pc_spine:"The spread fits a {n} mm spine; this order has {sp} mm. Use the template",pc_size:"{w} × {h} mm does not match the template ({W} × {H} mm trim)",
pc_gfOk:"Page 2: the inside spread",pc_gfWarn:"Gatefold: the inside spread is needed as page 2",
pc_noTrim:"No trim box: the bleed could not be checked",pc_bleed:"{b} mm bleed",pc_noBleed:"No bleed",pc_bleedBad:"{b}: 3 mm needed",
pc_cmyk:"CMYK",pc_mixed:"RGB and CMYK mixed: convert to CMYK",pc_rgb:"RGB: the printer converts it to CMYK, and bright colours can shift",
pc_noCs:"Colour checked on the proof: no colour space declared in the file",pc_outl:"No live text: outlined",pc_emb:"Fonts embedded",
pc_noEmb:"Fonts not embedded: outline the text or embed the fonts",
rc_sq:"Square, for side A",rc_nsq:"Not square ({w} × {h} px)",rc_spread:"Spread: back, {sp} mm spine, front",
rc_wide:"Spread {w} mm wide; the template is {W} mm plus bleed",rc_gf:"Gatefold: the inside spread is needed too (page 2 of a PDF)",
rc_neither:"Neither the spread nor a square front ({w} × {h} px)",rc_dpiOk:"{d} dpi at {mm} mm high",
rc_dpiLow:"{d} dpi, a little under 300 (needs {px} px high)",rc_dpiBad:"{d} dpi: too low for print (needs {px} px high)",
ck_tracks:"Track list",ck_tracksD:"Add at least one track.",ck_lac:"Your lacquer",
ck_lacD:"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.",
ck_tape:"Your tape",ck_tapeD:"Analog tape is transferred and checked on arrival; format, speed and EQ curve are confirmed with you first.",
ck_files:"Master files",ck_filesD:"Not added yet: these checks use the lengths typed in. Drop WAV, FLAC or AIFF files to check the files themselves.",
ck_fmt:"File format",ck_lossy:"{l}: not WAV, FLAC or AIFF. Send the lossless master.",ck_lossless:v=>v.n+" lossless file"+(v.n===1?"":"s"),
ck_sr:"Sample rate",ck_srBad:"{n} file(s) below 44.1 kHz",ck_srOk:"44.1 kHz or more",
ck_bits:"Bit depth",ck_bitsBad:"{n} file(s) below 16 bit",ck_bits16:"{n} file(s) at 16 bit: fine, 24 bit is preferred for cutting",ck_bitsOk:"24 bit or more",
ck_clipPk:"Clipping and peak",ck_scan:"Scanning {n} file(s)…",ck_clip:"Clipping",ck_clipD:"{l}: runs of full-scale samples. The engineer will ask about it.",
ck_peak:"Peak level",ck_peakD:"{n} file(s) peak above -0.3 dBFS",ck_noClip:"No clipping found",
ck_f:"Files",ck_fD:"{n} track(s) without a file yet",
ck_side:"Side length",sideN:"Side {n}",
ck_sideBad:v=>v.l+": over the "+v.max+" a "+v.s+"\" side holds at "+v.r+". Move tracks, add a disc or change the speed.",
ck_sideWarn:"{l}: past the {a} that sounds best, so the cut gets quieter",ck_sideEmpty:"Side {l} has no music",ck_sideOk:"Every side within the {a} that sounds best",
ck_jArt:"Jacket artwork",ck_lArt:"Label artwork",ck_notYet:"Not added yet",ck_artOk:"Size, resolution and colour space fine",
ck_eng:"Engineer check",ck_engD:"After upload a cutting engineer confirms the sides are cuttable. This decides, not the bars above.",
apHintBad:"Fix the items marked ✕ in step 2 first.",apHintRights:"Confirm the rights in step 2 first.",
mk_own:"your own jacket",mk_scale:"12\" jacket, for scale",mk_side:"SIDE A · {r}",mk_new:"NEW",
mk_cap:v=>v.n+": record "+v.r+" mm, jacket "+v.j+" mm"+(v.s?", spine "+v.s+" mm":""),mk_aria:"Mock-up of the {n} record",
qrAria:"Code printed on the sleeve (demo pattern)",
cv_mark:"{q} copies: {m} each",cv_axis:"price per copy, all in, {c} · copies on a log scale",cv_aria:"Price per copy against the number of copies",
noWeight:"Only 12\" records come in 140 or 180 g",noGate:"Gatefold jackets are for 12\" records",
fmtSub:v=>"Up to "+v.m+" min a side at "+v.r+", about "+v.n+" song"+(v.n===1?"":"s"),
noC1:"Black vinyl: no colour to choose",noC2:"Only for marbled and splatter",palTitle:"Open the palette and pick any colour",
palYours:"Your colour",palName:"Palette",palAria:"Palette: pick any colour",
noDink:"Only 7\" singles come with a large centre hole",noClean:"Black vinyl needs no cleaning",
clOff:"Only for colour vinyl.",clOn:"The press is purged before your run, so no trace of the previous colour gets in. Light colours can still show the odd speck.",
clNo:"Saves the purge, but some records can carry streaks of the previous colour.",noMast:"Your lacquer is already cut and mastered",
srcLac:"Your lacquer goes straight to plating. You still hear and approve a test pressing, and the father and mother are archived for represses.",
srcTape:"Your tape is transferred in-house, then cut. You hear and approve a real pressing before the run.",
srcDig:"You hear and approve a real pressing before the run. The father and mother are archived, so a repress later skips cutting.",
noLabelArt:"Plain labels have no print",noJacketOwn:"You supply your own jacket",noJacketPlain:"A plain jacket has no print",
fiReading:"reading…",fiLossy:"{f}: not lossless",fiScan:" · scanning",fiPeak:" · peak {p} dB",fiClips:" · clips",
aSide:"Side",aTitle:"Track title",aLen:"Length",fromFile:"Read from the file",rm:"Remove",
sideHint:v=>v.s+"\" at "+v.r+(v.loud?", loud music":"")+": up to "+v.a+" a side sounds best, "+v.b+" at most (about "+v.n+" songs of 4:32)",
nTracks:v=>v.n+" track"+(v.n===1?"":"s"),sb_ok:"fits",sb_warn:"long",sb_bad:"too long",sb_empty:"empty",sbMark:"Sounds best up to here",
chFix:v=>v.n+" thing"+(v.n===1?"":"s")+" to fix",chLook:v=>"Ready, with "+v.n+" thing"+(v.n===1?"":"s")+" to look at",chReady:"Ready for the engineer",
apSee:"See my price &#x2192;",apApprove:"Approve and see my price &#x2192;",
gateDone:"Master plan approved: approval 1 of 3. Change the tracks or the format and it needs approving again.",gateBad:"Fix the items marked ✕ first.",
gateRights:"Confirm the rights to approve. This is approval 1 of 3: the plan now, the master and the test pressing later.",
gateReady:"This is approval 1 of 3: the plan now, the master and the test pressing later.",
kPer:"Per copy, all in",kTotal:"Order total",kShips:"Ordered today, ships",bdSum:"{a} once + {b} per copy",
partner:"partner price {p} + {h} % handling",thOnce:"One-off, for this release",thAmt:"Amount",onceTot:"One-off total",thPer:"Per copy",
perRow:"{c} × {q} copies",offerHint:"One-off costs are paid once per release, however many copies. They count sides, not songs: mastering, cutting and plating are done side by side, so 8 songs or 12 cost the same and a double LP has four sides. Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.",
whenShips:"When it ships",weeks:"{a} to {b} weeks",lpA:"your approvals and the metal work",weeksQ:"{a} to {b} weeks, quoted",lpB:"from your test-pressing approval to dispatch",
leadHint:"Ordered today, it ships about <b>{r}</b>. The quote counts from your last approval, because the plant cannot run a clock that is waiting for you. ",
leadOwn:"Supplying your own jacket saves 1 to 2 weeks.",leadNoJ:"No printed jacket to wait for, which saves 1 to 2 weeks.",
deposit:"Deposit today <b>{m}</b>",pFan:"Goal <b>{n} fans</b> at {p}, you keep about <b>{k}</b> a copy",pFanNo:"Raise the fan price in the next step",
needGate:"Approve the master plan in step 2 first",choiceMsg:"Ordering and launching need your approval of the master plan. ",goStep2:"Go to step 2",
lhGate:"Approve the master plan in step 2 first.",lhPrice:"Raise the fan price: it does not cover the cost per copy.",lhCap:"The edition cap is below the goal.",
fe_bad:v=>"At "+v.p+" a copy, what is left after payment fees"+(v.vat?" and VAT":"")+" does not cover the cost per copy. Raise the fan price.",
fe_need:"Fans needed",nFans:"{n} fans",fe_pre:"{m} in pre-orders",fe_be:"Your break-even",fe_keepG:"You keep at the goal",aCopy:"a copy",fe_that:"That is",fe_tot:"in total",
fe_cap:"The edition is capped at {c}, below the goal of {g}: this campaign cannot succeed.",fe_more:"What you keep if more fans join",perCopyX:"{m} a copy",
fe_hint:v=>"What you keep is after payment fees of about "+v.f+" %"+(v.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.",
needBad:"This price does not cover the cost per copy.",need:"You need <b>{n} fans</b> to press it, {m} in pre-orders",
nhRaise:"Raise the price.",nhBe:"Your break-even is {be} fans at this price, above the plant's {min}-copy minimum. From {p} a copy, {min} fans are enough.",
nhMin:"The plant presses from {min} copies. Below about {p} a copy you would need more fans to cover the costs.",
capOff:"Switch on to make it a limited edition",launchSim:"Simulated: no page goes live and nobody is charged.",
stDone:"Delivered. Every copy has a passport.",stWait:"Waiting for your approval: {s}.",stNow:"Now: {s}.",stDay:"Day {d} of {n} · {f} of {g} fans",
s5Dir:"Your order",s5Camp:"Your campaign",s5None:"Campaign and production",
s5lDir:"Follow your record through the plant. The diamonds are your approvals: the plant waits for them.",
s5lCamp:"Your campaign page, live. Pick how it goes and press play: 30 days in about 8 seconds.",
emptyH:"Nothing running yet",emptyP:"Choose in step 3 how to pay for the record, or skip ahead with the demo record.",
quickCamp:"Run the demo campaign",quickDone:"Jump to a delivered record",runCamp:"Run the campaign",simulated:"simulated",
scSell:"Sells out",scMake:"Just makes it",scMiss:"Falls short",aDay:"Campaign day",latest:"Latest pre-orders",
dayOf:"Day {d} of {n}",pause:"&#x275A;&#x275A; Pause",replay:"&#x21BA; Replay",play:"&#x25B6; Play",
stRefund:v=>"If fewer than "+v.g+" fans pre-order by "+v.d+", everyone is refunded automatically.",
stFunded:v=>"<b style=\"color:var(--ok)\">&#x2713; Funded by "+v.n+" fans"+(v.so?", sold out":"")+".</b> Pressing starts now.",
stMin:"<b>Going ahead at the {min}-copy minimum.</b>",stRefunded:"<b>Goal missed.</b> Every fan has been refunded.",stMissed:"<b>Goal missed.</b> Refunds go out unless the artist covers the gap.",
pvPlay:"Play a 30 second preview",pvNone:"Previews are rendered by the plant from your master",shipX:"+ shipping",limitedOf:"Limited, numbered edition of {n}",
ofFans:"<b>{n}</b> of {g} fans",closed:"closed",daysLeft:"{n} days left",preClosed:"Pre-orders closed",preorder:"Pre-order",estShip:"Estimated to ship",
fpTracks:"Tracks",copyLink:"Copy link",lineT:"Shares to LINE at launch",qrB:"QR code",qrT:"A QR code for gigs and merch tables",
fpFine:"Made and sold by CNX Vinyl, Thailand. Your payment is held in a separate account until the campaign succeeds, and refunded automatically if it does not.",
mFans:"Fans",mHeld:"Held apart",mClosed:"Closed",mDays:"Days left",fanN:"Fan #{i} · {c}",minAgo:"{n} min ago",justNow:"just now",noPre:"No pre-orders yet. Press play.",
ocFunded:"Funded",ocFundedP:"{n} fans pre-ordered. Their money leaves the separate account for production.",
ocNet:v=>"Fan money after fees"+(v.vat?" and VAT":""),ocSetup:"One-off setup",ocPress:"Pressing and packaging, {n} copies",ocKeep:"You keep",
ocRefunded:"Refunded",ocRefundedP:"{n} refunds of {p} went back to each fan's original payment method. Nothing was pressed; your master and artwork stay in your vault for another try.",
ocRetry:"Run it again",ocMinH:"Going ahead at the minimum",ocPaid:"You paid the gap of <b>{m}</b>",ocCovered:"The fans' money covered the minimum run; you keep <b>{m}</b>",
ocSpare:", and you receive the {n} copies no fan claimed.",ocMissH:"Goal missed: {n} of {g}",ocTwo:"Two ways out. The artist chooses.",
ocRefundAll:"Refund everyone",ocRefundAllP:"Automatic, from the separate account. Nothing is pressed; your files stay in your vault.",ocRefundBtn:"Refund all {n} fans",
ocGoMin:"Go ahead at the {min}-copy minimum",ocPay:"You pay the gap of <b>{m}</b>",ocFree:"Costs you nothing: the fans' money covers it and you keep {m}",
ocUnclaimed:". You receive the {n} unclaimed copies.",ocGo:"Go ahead",
dirH:"Direct order",dirCopies:"{n} copies, {d}",dirPaid:"Deposit paid",dirBal:"Balance before dispatch",
dirHint:"Deposit terms are a placeholder. Labels with an account get their own rates and approve in the customer portal.",
trApprove:"Approve",trNext:"Next &#x25B8;",trGate:"approval {g} of 3",trDone:"done {d}",trWait:"waiting for you · planned {d}",trPlan:"planned {d}",
trProd:"Production",trCopies:"{n} copies",trDelivered:"delivered",trPlayNext:"&#x25B6; Play to the next approval",trUpd:"Your order updates",trUpdFans:"Updates your fans see",
trLog:"from the factory log",trHint:"These come from the plant's own event log: the progress is the factory's record, not a status someone remembers to update.",
ppNo:"No. {c} of {n}",ppCopy:"Copy {c} of {n}",ppH:"Record passport",ppKey:"Record key: stamper · press · cycle time · side",ppSide:"side A",
pp_tape:"Tape received",pp_master:"Master received",pp_tapeD:"Transferred in-house, stored in the client vault",pp_masterD:"Checksums verified at both ends, stored in the client vault",
pp_mast:"Mastered for vinyl",pp_mastD:"Master approved by the artist",pp_cut:"Cut to lacquer",pp_cutD:v=>"Lacquer "+v.l+(v.c?", \u201C"+v.c+" A\u201D":", serial")+" scribed in the run-out",
pp_lac:"Lacquer received",pp_lacD:"Cut by the artist's studio, inspected under the microscope on arrival",pp_plate:"Plated in-house",
pp_plateD:"Father {f} → mother {m} (both archived for represses) → stamper {s}",pp_tp:"Test pressing approved",pp_tpD:"Heard and approved by the artist",
pp_press:"Pressed",pp_pressD:"Press D1-B (Newbilt Duplex) at {t}, compound lot {l}, press data logged for this cycle",
pp_insp:"Inspected",pp_inspD:"Optical inspection passed: every record is checked. The stamper's first records and about 1 in 50 after are compared with the master by audio analysis",
pp_pack:"Packed",pp_packD:v=>"Sleeve"+(v.j?" and jacket":"")+(v.s?", shrink wrap":"")+"; the jacket code is checked against this record",
pp_ship:"Shipped",pp_shipD:"Dispatched with tracking",pp_shipC:"Tracked parcel to the fan",
ppHint:"What a fan sees after scanning the code printed on the sleeve: the plant's traceability record for this one copy. IDs and times here are simulated.",
ppRepress:"<b>Sold out? Open a repress.</b> Father and mother are archived, so a repress skips cutting and goes straight to new stampers: about 1.5 to 2.5 weeks of plant time, pressed when 100 more fans are in.",
ppRepressB:"Open repress pre-orders",
ctaS1:"Continue to music & art →",ctaM1:"Music & art →",ctaS2a:"See my price →",ctaM2a:"My price →",ctaS2:"Approve and see my price →",ctaM2:"Approve →",
ctaS3:"Let my fans fund it →",ctaM3:"Fan funding →",ctaH3:"Or order directly, below.",ctaS4:"Launch the campaign →",ctaM4:"Launch →",
untitled:"Untitled",spW:"{w} g",spPlain:v=>"Plain "+v.c+" labels",spRunout:"Run-out {c}",spTape:"From tape",spLac:"Your lacquer",
stFan:"Fan price",stNeeded:"Fans needed",stNot:"not reachable",stKeep:"You keep at the goal",na:"n/a",mbFan:"Fan price · fans needed",
nCopies:"{n} copies",mbCopies:"{n} copies · per copy",status:"Status",
toastFormat:"Format changed: approve the master plan again in step 2.",toastApprove:"Approve the master plan first.",
toastPre:"Demo: fans pay by PromptPay QR, card or wallet, held apart until the goal is met.",toastRepress:"Simulated: the campaign page reopens as a repress pre-order.",
toastCopied:"Copied (example link): {u}",toastNumbered:"Numbered edition added to the record (step 1).",toastTour:"Tour finished. Everything on the page is clickable.",
tourOn:"&#x25A0;<span class=\"tl\"> Stop tour</span>",tourOff:"&#x25B6;<span class=\"tl\"> Tour</span>",
tour1:"An artist designs a record. The mock-up is drawn to scale and follows every choice.",
tour2:"Masters and artwork are checked before anyone pays: file format, side length against the format, print resolution.",
tour3:"The artist approves the master plan, approval 1 of 3. Nothing goes in front of fans that the plant cannot make.",
tour4:"An itemised price, every line visible. The price per copy falls fast with the run, because cutting and plating are in-house.",
tour5:"No money up front: the artist sets the price for fans, and the page works out how many fans are needed: the plant's 100-copy minimum, more if the price is low.",
tour6:"A 30-day campaign, simulated. Fan money sits in a separate account until the goal is met; if it is missed, everyone is refunded.",
tour7:"Funded: the campaign becomes a production order. Fans follow the factory's own event log, and the plant waits for the artist's approvals.",
tour8:"Delivered. Every copy has a passport: stamper, press, cycle time and side, with its inspection results.",
themeTo:v=>"Switch to "+v.m+" mode",themeDark:"dark",themeLight:"light"
},
th:{
/* static markup (English lives in the markup) */
pill:"เดโม",navAria:"ขั้นตอน",nav1:"ออกแบบ",nav2:"เพลงและอาร์ตเวิร์ก",nav3:"ราคา",nav4:"ระดมทุนจากแฟน",nav5:"ติดตาม",
invSwT:"แสดงมุมธุรกิจเบื้องหลังแต่ละขั้นตอน",invSw:"สำหรับนักลงทุน",tourT:"พาชมทุกขั้นตอนแบบอัตโนมัติ ประมาณหนึ่งนาที",tourStop:"หยุด",
curAria:"สกุลเงิน",cur_THB:"แสดงราคาเป็นบาท",cur_USD:"แสดงราคาเป็นดอลลาร์สหรัฐ (1 USD = 33 THB อัตราที่ใช้วางแผน)",cur_EUR:"แสดงราคาเป็นยูโร (1 EUR = 1.155 USD = 38.115 THB อัตราที่ใช้วางแผน)",
langAria:"ภาษา",invL:"โน้ตสำหรับนักลงทุน",
eb1:"ขั้นตอนที่ 1 จาก 5",eb2:"ขั้นตอนที่ 2 จาก 5",eb3:"ขั้นตอนที่ 3 จาก 5",eb4:"ขั้นตอนที่ 4 จาก 5",eb5:"ขั้นตอนที่ 5 จาก 5",
h1_1:"ออกแบบแผ่นเสียงของคุณ",lead1:"ตัดแผ่น ชุบโลหะ และปั๊มแผ่น ครบจบในโรงงานเดียวใกล้เชียงใหม่ เริ่มต้นที่ 100 แผ่น จะจ่ายค่าผลิตเองหรือให้แฟนเพลงช่วยระดมทุนก็ได้",
inv1:"<b>ตัวออกแบบนี้เป็นการวิจัยตลาดไปในตัว</b> ทุกสเปกที่มีคนเลือกจะถูกบันทึกแบบไม่ระบุตัวตน เป็นข้อมูลเรื่องจำนวนผลิตต่อรอบ สี แพ็กเกจ และราคาที่ทำให้คนเลิกสนใจ ซึ่งตอบคำถามเรื่องจำนวนผลิตต่อรอบที่โมเดลรายได้ยังเปิดค้างไว้ หน้านี้จะเปิดสู่สาธารณะพร้อมราคาประมาณการและรายชื่อผู้สนใจ ตั้งแต่ก่อนโรงงานเปิดนาน",
fmtH:"ขนาดแผ่น",f7:"7\" ซิงเกิล",fSpeed:"ความเร็ว",sp33:"33&#x2153; รอบ",sp45:"45 รอบ",fDiscs:"แผ่นต่อชุด",dSingle:"แผ่นเดียว",dDouble:"แผ่นคู่",
fWeight:"น้ำหนัก",w140:"140 กรัม",w180:"180 กรัม (แผ่นหนา)",fHole:"รูกลางแผ่น",hSmall:"เล็ก",hLarge:"ใหญ่ (แบบตู้เพลง)",
vinylH:"สีแผ่น",cBlack:"ดำคลาสสิก",cBlackS:"ไม่มีวันตกยุค",cSolid:"สีล้วน",cSolidS:"เลือกสีไหนก็ได้",cMarb:"ลายหินอ่อน",cMarbS:"สองสีวนผสมกัน",
cSplat:"Splatter (สีสาด)",cSplatS:"จุดสีกระเซ็นบนสีพื้น",fCol:"สี",fCol2:"สีที่สอง",fClean:"ล้างเครื่องปั๊มก่อนรันสีของคุณ",clYes:"ล้าง (แนะนำ)",clNo2:"ไม่ล้าง",
packH:"แพ็กเกจ",jPr:"ปกพิมพ์ลาย",jPrS:"ใช้อาร์ตเวิร์กของคุณ",jGf:"ปก Gatefold",jGfS:"เปิดได้เหมือนหนังสือ",jPl:"ปกเปล่า",jPlS:"กระดาษแข็งไม่พิมพ์",
jOwn:"ใช้ปกของฉันเอง",jOwnS:"คุณจัดหาปกมาเอง",fLabels:"ฉลากกลางแผ่น",lPr:"พิมพ์ลาย",lWh:"ขาวเปล่า",lBl:"ดำเปล่า",fInner:"ซองใน",iPaper:"กระดาษ",iPoly:"บุพลาสติก",
fSpine:"สันปก",spThin:"บาง 3 มม.",spThick:"หนา 5 มม.",fExtras:"ของเสริม",xShrink:"ซีลพลาสติก",xInsert:"ใบแทรกพิมพ์ลาย",xSticker:"สติกเกอร์โปรโมต",
xNum:"ใส่หมายเลขกำกับ",xDl:"การ์ดดาวน์โหลด",xBar:"สติกเกอร์บาร์โค้ด",soundH:"เสียง",fSource:"ต้นฉบับเสียงของคุณเป็น",srcD:"ไฟล์ดิจิทัล",srcT:"เทปอนาล็อก",
srcL:"แผ่นแล็กเกอร์ที่ตัดแล้ว",mYes:"มาสเตอร์สำหรับแผ่นเสียง",mYesS:"วิศวกรของเรามาสเตอร์มิกซ์ของคุณให้เหมาะกับร่องแผ่น",mNo:"มาสเตอร์ของฉันพร้อมตัดแผ่นแล้ว",
mNoS:"ตัดแผ่นจากไฟล์ของคุณโดยตรง",fTps:"แผ่นทดสอบ (test pressing) สำหรับคุณ",tpsLess:"ลด",tpsMore:"เพิ่ม",
h1_2:"เพลงและอาร์ตเวิร์กของคุณ",lead2:"วางไฟล์มาสเตอร์และอาร์ตเวิร์กได้เลย ระบบตรวจให้ทันทีตรงนี้ ก่อนที่คุณจะจ่ายเงินใดๆ ในเดโมนี้ไม่มีไฟล์ใดออกจากเบราว์เซอร์ของคุณ",
inv2:"<b>ไม่มีอะไรไปถึงแฟนเพลงที่โรงงานผลิตไม่ได้</b> ไฟล์ถูกตรวจก่อนมีใครจ่ายเงิน จากนั้นวิศวกรตัดแผ่นยืนยันอีกครั้ง บนแพลตฟอร์มจริง ไฟล์มาสเตอร์จะเก็บในคลังลูกค้าที่เข้ารหัสของโรงงาน และเสียงตัวอย่างสร้างที่โรงงาน มาสเตอร์ฉบับเต็มจึงไม่เคยถูกเก็บไว้นอกโรงงาน",
fArtist:"ศิลปิน",fTitle:"ชื่อผลงาน",fCatno:"ข้อความในร่องท้ายแผ่น (run-out) <span class=\"mute\" style=\"font-weight:400\">สลักด้วยมือข้างร่องท้ายแผ่นทุกหน้า ใช้ตัวอักษรอังกฤษและตัวเลข ไม่เกิน 15 ตัว</span>",
tracksH:"เพลงและหน้าแผ่น",dropA:"<b>วางไฟล์มาสเตอร์ WAV, FLAC หรือ AIFF</b> หรือคลิกเพื่อเลือกไฟล์",addTrack:"+ เพิ่มเพลง",balance:"จัดหน้าให้สมดุล",
loudStd:"ปกติ",loudLoud:"ดัง เบสหนัก",artH:"อาร์ตเวิร์ก",dropJ:"<b>ปก</b> ไฟล์ PDF, AI หรือภาพ 300 dpi",dropL:"<b>ฉลาก</b> ไฟล์ PDF, AI หรือภาพ 300 dpi",
rightsL:"ฉันเป็นเจ้าของหรือได้รับอนุญาตให้ใช้งานบันทึกเสียงและเพลงเหล่านี้แล้ว เพลงคัฟเวอร์ต้องมีใบอนุญาตทำซ้ำ (mechanical licence)",justPrice:"ขอดูราคาก่อน",
h1_3:"ราคาของคุณ",lead3:"แจกแจงทุกรายการ ไม่มีค่าธรรมเนียมแอบแฝงมาทีหลัง ยิ่งผลิตมาก ราคาต่อแผ่นยิ่งลดลงเร็ว",
inv3a:"<b>ทำไมการผลิตจำนวนน้อยจึงเป็นไปได้ที่นี่</b> โรงงานไทยแห่งเดียวที่เรามีข้อมูลเสนอขั้นต่ำ 150 แผ่น ซึ่ง “แทบไม่มีใครรับ เพราะต้นทุนสูงเกินไป” ต้นทุนครั้งเดียวต่อผลงานของโรงงานนั้น (สแตมเปอร์นำเข้า จ้างตัดแผ่นภายนอก) ไม่ลดลงตามจำนวนผลิต การตัดแผ่นและชุบโลหะในโรงงานเองทำให้ต้นทุนส่วนนี้ลดลงมาก",
inv3b:"ราคาในหน้านี้เป็นราคาประมาณการ อิงอัตราอ้างอิงของตลาดตะวันตก ปัจจุบันตลาดไทยจ่าย <span data-thb=\"650\" data-bare=\"1\"></span> ถึง <span data-thb=\"950\"></span> ต่อแผ่นที่ 300 แผ่น รายการราคาจริงจะกำหนดหลังการทดสอบตลาด",
inv3c:"<b>เร็วกว่าโรงงานที่มีชื่อในยุโรป</b> Record Industry (เนเธอร์แลนด์) เสนอเวลา 9 ถึง 11 สัปดาห์ นับจากอนุมัติแผ่นทดสอบจนออร์เดอร์เสร็จสมบูรณ์ (จากเครื่องคำนวณของบริษัท ตุลาคม 2026) ส่วน CNX เสนอ 7 ถึง 9 สัปดาห์",
fCopies:"จำนวนแผ่น",copiesU:"แผ่น",copiesAria:"จำนวนแผ่น",incl:"รวมอะไรบ้าง",payH:"คุณอยากจ่ายค่าผลิตแบบไหน",pClassic:"แบบปกติ",pOrderNow:"สั่งผลิตเลย",
pOrderP:"จ่ายมัดจำ อนุมัติมาสเตอร์และแผ่นทดสอบ แล้วแผ่นทั้งหมดจะส่งถึงคุณหรือส่งตรงถึงผู้ซื้อ",bDirect:"สั่งผลิตโดยตรง",pNoUp:"ไม่ต้องจ่ายล่วงหน้า",
pFanH:"ให้แฟนเพลงช่วยระดมทุน",pFanP:"แฟนเพลงพรีออร์เดอร์ผ่านหน้าแคมเปญของคุณ ถ้ามีคนสั่งครบก่อนกำหนดจะเริ่มปั๊มแผ่น ถ้าไม่ครบ แฟนทุกคนได้เงินคืน",pSetUp:"ตั้งค่าแคมเปญ &#x2192;",
h1_4:"ให้แฟนเพลงช่วยระดมทุน",lead4:"ตั้งราคาและกำหนดเวลา ถ้าแฟนเพลงพรีออร์เดอร์ครบ เราจะปั๊มแผ่นและส่งให้ทุกคน ถ้าไม่ครบ แฟนทุกคนได้เงินคืน",
inv4:"<b>แพลตฟอร์มของโรงงานเอง ไม่ใช่นายหน้า</b> แพลตฟอร์มที่คล้ายกันเป็นนายหน้าให้โรงงานของคนอื่น แต่ CNX คุมราคา ตารางผลิต และคุณภาพเอง และได้กำไรจากการผลิตแทนค่าคอมมิชชัน เงินของแฟนถูกแยกเก็บต่างหาก ซึ่งเป็นบทเรียนจาก Qrates ที่บริการถูกระงับ ทั้งที่มีโปรเจกต์ที่ระดมทุนสำเร็จแล้วติดค้างอยู่ระหว่างปิดกิจการ",
fFanPrice:"ราคาสำหรับแฟน ต่อแผ่น",fanAria:"ราคาสำหรับแฟน",fLength:"ระยะเวลาแคมเปญ",daysU:"วัน",daysAria:"จำนวนวัน",fLimited:"จำกัดจำนวน",stopAt:"ปิดที่",
numCopies:"แผ่น มีหมายเลขกำกับ",capAria:"จำนวนที่จำกัด",fAcop:"แผ่นสำหรับตัวคุณเอง",acopU:"ปั๊มไปพร้อมกับรอบผลิต",acopAria:"แผ่นสำหรับตัวคุณเอง",
vatL:"แสดงตัวเลขโดยหัก VAT 7 % ออกจากราคาสำหรับแฟน",safeH:"เงินของแฟนเพลงปลอดภัย",safe1t:"แยกเก็บต่างหาก",
safe1:"เงินพรีออร์เดอร์อยู่ในบัญชีแยก และไม่ถูกนำไปใช้ดำเนินงานของโรงงานจนกว่าแคมเปญจะสำเร็จ",safe2t:"คืนเงินอัตโนมัติ",
safe2:"ถ้าไม่ถึงเป้า แฟนทุกคนได้เงินคืน ไม่มีใครต้องเหลือแผ่นเสียงกองเต็มโรงรถ",safe3t:"จ่ายแบบที่แฟนถนัด",safe3:"พร้อมเพย์ QR บัตร และวอลเล็ต ข้อมูลบัตรไม่ผ่านเซิร์ฟเวอร์ของเราเลย",
launchBtn:"เปิดแคมเปญ &#x2192;",
inv5a:"<b>การตรวจสอบย้อนกลับคือตัวสินค้า</b> อัปเดตที่แฟนเห็นมาจากบันทึกเหตุการณ์ของโรงงานเอง และทุกแผ่นสืบย้อนได้ถึงสแตมเปอร์ เครื่องปั๊ม รอบการปั๊ม และหน้าแผ่น ที่งาน Making Vinyl Europe 2026 เรื่องนี้คือสิ่งที่วงการตอบรับมากที่สุด โรงงานที่มีชื่อในยุโรปแห่งหนึ่งขอให้ลูกค้ายอมรับแผ่นเสียประมาณ 2 % (FAQ ของ Record Industry ตุลาคม 2026) ส่วน CNX ตรวจด้วยระบบออปติคัลทุกแผ่น",
inv5b:"<b>ผลงานเดียวกลายเป็นรายได้ที่กลับมาซ้ำ</b> แม่พิมพ์ mother ที่เก็บไว้ทำให้การปั๊มซ้ำใช้เวลาโรงงานเพียงประมาณ 1.5 ถึง 2.5 สัปดาห์ และเป็นออร์เดอร์ที่มีมาร์จิ้นสูงที่สุดของโรงงาน",
mockT:"ชี้เมาส์ที่แผ่นเพื่อให้หมุน",
foot1:"เดโมสำหรับนักลงทุนของแพลตฟอร์มศิลปิน CNX Vinyl ราคาเป็นราคาประมาณการ (รายการราคา demo-0.1 ตัวเลขประมาณการเพื่อการวางแผน) และไม่ถือเป็นข้อเสนอขาย ไม่มีการอัปโหลด ส่ง หรือเรียกเก็บเงินใดๆ แคมเปญ การผลิต และพาสปอร์ตแผ่นเสียงเป็นการจำลองทั้งหมด CNX Vinyl เป็นชื่อชั่วคราว ชื่อของแพลตฟอร์มยังไม่ได้กำหนด",
foot2:"เวอร์ชันละเอียดแสดงทุกอัตราพร้อมแหล่งที่มา: <a href=\"../artist-order/\">เดโมการสั่งผลิตสำหรับศิลปิน</a> (ภาษาอังกฤษ)",
footDraft:"ข้อความภาษาไทยในหน้านี้เป็นคำแปลฉบับร่าง อยู่ระหว่างตรวจทาน",
/* built in code */
docTitle:"ทำแผ่นเสียง · CNX Vinyl (เดโม)",
docDesc:"ออกแบบแผ่นเสียง ตรวจไฟล์เพลง ดูราคาที่โปร่งใส และให้แฟนเพลงช่วยระดมทุนค่าปั๊มแผ่น เดโมสำหรับนักลงทุน ราคาประมาณการ",
range:"{a} ถึง {b}",rpm33:"33⅓ รอบ",rpm45:"45 รอบ",rpmS33:"33⅓",rpmS45:"45",
sz12:"12\" LP",sz10:"10\"",sz7:"7\" ซิงเกิล",dinkedP:" (รูใหญ่)",
cn_black:"แผ่นสีดำ",cn_solid:"สีล้วน",cn_marbled:"ลายหินอ่อน",cn_splatter:"Splatter (สีสาด)",
ct_black:"สีดำ",ct_solid:"สีล้วน",ct_marbled:"ลายหินอ่อน",ct_splatter:"สี splatter",
descT:v=>v.d+v.n+v.k+", "+v.r+", แผ่น"+v.c+(v.w?" "+v.w+" กรัม":""),
jn_none:"ปกของคุณเอง",jn_plain:"ปกเปล่า",jn_printed:"ปกพิมพ์ลาย",jn_gatefold:"ปก Gatefold",
xn_shrink:"ซีลพลาสติก",xn_insert:"ใบแทรก",xn_sticker:"สติกเกอร์โปรโมต",xn_numbered:"มีหมายเลขกำกับ",xn_dlcard:"การ์ดดาวน์โหลด",xn_barcode:"สติกเกอร์บาร์โค้ด",
c_Ruby:"แดงทับทิม",c_Tangerine:"ส้มแทนเจอรีน",c_Sunflower:"เหลืองทานตะวัน",c_Jade:"เขียวหยก",c_Ocean:"น้ำเงินทะเล",c_Violet:"ม่วง",c_Bone:"ขาวงาช้าง",c_Smoke:"เทาควัน",
c_Gold:"ทอง",c_White:"ขาว",c_Black:"ดำ",c_Pink:"ชมพู",c_Teal:"เขียวอมฟ้า",
cities:["เชียงใหม่","กรุงเทพฯ","เชียงราย","ขอนแก่น","ลำปาง","ภูเก็ต","หาดใหญ่","นครราชสีมา","ปาย","อุดรธานี","โตเกียว","สิงคโปร์","ไทเป","โซล","เบอร์ลิน","ลอนดอน","เมลเบิร์น","กัวลาลัมเปอร์"],
stg_intake:"รับไฟล์เสียงและตรวจทางเทคนิค",stg_g1:"คุณอนุมัติแผนมาสเตอร์",stg_mast:"มาสเตอร์สำหรับแผ่นเสียง",stg_g2:"คุณอนุมัติมาสเตอร์แผ่นเสียง",
stg_cut:"ตัดแผ่นแล็กเกอร์",stg_plate:"ชุบโลหะ: father, mother, สแตมเปอร์",stg_prep:"เก็บงานสแตมเปอร์",stg_tp:"ทำแผ่นทดสอบ",
stg_g3:"คุณอนุมัติแผ่นทดสอบ",stg_queue:"จองคิวเครื่องปั๊มแล้ว",stg_run:"ปั๊มแผ่น",stg_fin:"บรรจุ",stg_ship:"จัดส่ง",
gate_g1:"แผนมาสเตอร์",gate_g2:"มาสเตอร์แผ่นเสียง",gate_g3:"แผ่นทดสอบ",
ev_mast:"มาสเตอร์สำหรับแผ่นเสียงแล้ว ศิลปินกำลังฟังมาสเตอร์",ev_g2:"ศิลปินอนุมัติมาสเตอร์แผ่นเสียงแล้ว",
ev_cut:"ตัดลงแผ่นแล็กเกอร์ในห้องตัดแผ่นแล้ว",ev_plate:"ชุบโลหะในโรงงาน: father, mother และสแตมเปอร์ โดยเก็บ father และ mother ไว้สำหรับปั๊มซ้ำ",
ev_prep:"เจาะรูสแตมเปอร์ ขัดด้านหลัง และวัดขนาดแล้ว",ev_tp:"ทำแผ่นทดสอบชุดแรกและส่งให้ศิลปินแล้ว",
ev_g3:"ศิลปินอนุมัติแผ่นทดสอบแล้ว",ev_queue:"จองคิวปั๊มแผ่นแล้ว",
ev_run:"ปั๊มแล้ว {n} แผ่น ทุกแผ่นผ่านการตรวจด้วยระบบออปติคัลและบันทึกไว้",
ev_fin:v=>"บรรจุแล้ว: ซองใน"+(v.j?" ปก":"")+(v.s?" และซีลพลาสติก":""),
ev_shipD:"จัดส่งแล้ว ส่งเลขพัสดุให้แล้ว",ev_ship:"จัดส่งแล้ว พัสดุคนละหนึ่งกล่อง ส่งเลขพัสดุให้แฟนทั้ง {n} คนแล้ว",
ev_startD:"ยืนยันออร์เดอร์และได้รับเงินมัดจำแล้ว",ev_startM:"เดินหน้าผลิตที่ขั้นต่ำ {min} แผ่น ด้วยแฟน {n} คน",
ev_startC:"ระดมทุนสำเร็จจากแฟน {n} คน แคมเปญกลายเป็นออร์เดอร์ผลิตแล้ว",
u_sides:"หน้า",u_disc:"แผ่น",u_discs:"แผ่น",u_copies:"แผ่น",
q_master:"มาสเตอร์สำหรับแผ่นเสียง",q_tape:"แปลงสัญญาณจากเทปอนาล็อก",q_blank:"แผ่นแล็กเกอร์เปล่า",q_cut:"ตัดแผ่นแล็กเกอร์",
q_plate:"ชุบโลหะ: father, mother (เก็บไว้สำหรับปั๊มซ้ำ), สแตมเปอร์",q_setup:"ตั้งเครื่องปั๊ม",q_tp:"แผ่นทดสอบสำหรับคุณ",
q_clean:"ล้างเครื่องปั๊มก่อนรันสีของคุณ",q_press:v=>"ปั๊มแผ่น "+v.s+"\""+(v.w?" 140 กรัม":"")+" ฉลากพิมพ์ลาย",
q_g180:"แผ่นหนา 180 กรัม",q_colour:"แผ่นสี: {c}",q_inner:"ซองใน: {x}",q_paper:"กระดาษ",q_poly:"บุพลาสติก",
q_assy:"ประกอบและแพ็ก",q_pic:" (ซองพิมพ์ภาพ)",q_spine:"สันหนา 5 มม.",q_plainL:v=>"ฉลาก"+v.c+"เปล่าแทนฉลากพิมพ์ลาย",
lc_white:"ขาว",lc_black:"ดำ",
q_shrink:"ซีลพลาสติก",q_dlcard:"การ์ดดาวน์โหลด",q_barcode:"สติกเกอร์บาร์โค้ด",q_insert:"ใบแทรกพิมพ์ลาย",q_sticker:"สติกเกอร์โปรโมต",q_numbered:"เขียนหมายเลขกำกับด้วยมือ",
spWhyGf:"ปก Gatefold มีสัน {mm} มม. ในตัว",spWhyNone:"คุณจัดหาปกเอง",spWhy7:"ปก 7\" มีสัน 3 มม. อยู่แล้ว",
spHNone:"คุณจัดหาปกเอง",spHGf:"ปก Gatefold มีสัน {mm} มม.",spH7:"สัน 3 มม. สำหรับปก 7\"",
spH5two:"5 มม. จำเป็นสำหรับสองแผ่น",spH5:"5 มม. มีที่ใส่ใบแทรกหนาหรือโปสเตอร์",spH3:"3 มม. ใส่แผ่นเดียวพร้อมซองใน",
spTwo:"สองแผ่นต้องใช้สันหนา 5 มม.",
tplLabel:v=>"ฉลาก "+v.d+" มม. รวมระยะตัดตก (bleed) "+v.e+" มม. หน้าละหนึ่งเพจ ("+v.s+")",
tplJacket:v=>v.w+" × "+v.h+" มม. รวมระยะตัดตก (bleed) สัน "+v.sp+" มม."+(v.gf?" ด้านนอกและด้านใน":""),
tplOffJ:"คุณจัดหาปกเอง",tplOffP:"ปกเปล่าไม่มีงานพิมพ์",tplOffL:"ฉลากเปล่าไม่มีงานพิมพ์",
tplDl:"ดาวน์โหลดเทมเพลตแล้ว: {n}",tplLink:v=>"ดาวน์โหลดเทมเพลต"+(v.k==="label"?"ฉลาก":v.gf?"ปก Gatefold":"ปก"),
artRender:"กำลังสร้างภาพตัวอย่าง",artNoPrev:"ไม่มีภาพตัวอย่าง: โหลด pdf.js ไม่ได้ ผลตรวจด้านบนอ่านจากตัวไฟล์โดยตรง",
artErr:"เบราว์เซอร์นี้อ่านไฟล์ไม่ได้ แพลตฟอร์มจริงจะตรวจหลังอัปโหลด",
artPS:"ไฟล์ Illustrator บันทึกโดยไม่มีข้อมูล PDF: บันทึกใหม่โดยเลือก Create PDF Compatible File หรือส่งออกเป็น PDF",artBad:"ไม่ใช่ไฟล์ PDF, AI หรือรูปภาพ",
pc_ai:"ไฟล์ Illustrator บันทึกแบบเข้ากันได้กับ PDF",pc_pdf:"PDF {v}",pc_x:", PDF/X",pc_trim:"ขนาดตัด",pc_page:"ขนาดหน้า",
pc_lblOk:v=>v.tp+" "+v.w+" มม.: ฉลาก "+v.s+"\""+(v.b?" รวมระยะตัดตก 3 มม.":""),
pc_lblBad:v=>v.tp+" "+v.w+" × "+v.h+" มม. แต่ฉลาก "+v.s+"\" มีขนาด "+v.d+" มม. ("+v.e+" มม. รวมระยะตัดตก)",
pc_pagesOk:v=>v.n+" เพจ: หน้า "+v.l,
pc_pagesWarn:v=>v.n+" เพจ สำหรับ "+v.ns+" หน้า: ขาดหน้า "+v.l,
pc_jOk:"{w} × {h} มม.: ปกหลัง สัน {sp} มม. ปกหน้า",pc_front:"มีแต่ปกหน้า: ต้องมีปกหลังและสัน {sp} มม. ก่อนพิมพ์ (ใช้เทมเพลต)",
pc_spine:"ไฟล์นี้ทำไว้สำหรับสัน {n} มม. แต่ออร์เดอร์นี้ใช้สัน {sp} มม. กรุณาใช้เทมเพลต",pc_size:"{w} × {h} มม. ไม่ตรงกับเทมเพลต (ขนาดตัด {W} × {H} มม.)",
pc_gfOk:"เพจ 2: ด้านในของปก",pc_gfWarn:"Gatefold: ต้องมีด้านในของปกเป็นเพจ 2",
pc_noTrim:"ไม่มี trim box จึงตรวจระยะตัดตกไม่ได้",pc_bleed:"ระยะตัดตก {b} มม.",pc_noBleed:"ไม่มีระยะตัดตก",pc_bleedBad:"{b}: ต้องมี 3 มม.",
pc_cmyk:"CMYK",pc_mixed:"RGB ปนกับ CMYK: แปลงเป็น CMYK",pc_rgb:"RGB: โรงพิมพ์จะแปลงเป็น CMYK และสีสดอาจเพี้ยนได้",
pc_noCs:"ตรวจสีจากปรู๊ฟ: ไฟล์ไม่ได้ระบุโหมดสี",pc_outl:"ไม่มีตัวอักษรที่แก้ไขได้: แปลงเป็นเส้นแล้ว",pc_emb:"ฝังฟอนต์แล้ว",
pc_noEmb:"ไม่ได้ฝังฟอนต์: แปลงตัวอักษรเป็นเส้นหรือฝังฟอนต์",
rc_sq:"สี่เหลี่ยมจัตุรัส สำหรับหน้า A",rc_nsq:"ไม่ใช่สี่เหลี่ยมจัตุรัส ({w} × {h} px)",rc_spread:"ภาพกางเต็ม: ปกหลัง สัน {sp} มม. ปกหน้า",
rc_wide:"ภาพกางกว้าง {w} มม. แต่เทมเพลตคือ {W} มม. บวกระยะตัดตก",rc_gf:"Gatefold: ต้องมีด้านในของปกด้วย (เพจ 2 ของ PDF)",
rc_neither:"ไม่ใช่ทั้งภาพกางเต็มและปกหน้าสี่เหลี่ยมจัตุรัส ({w} × {h} px)",rc_dpiOk:"{d} dpi ที่ความสูง {mm} มม.",
rc_dpiLow:"{d} dpi ต่ำกว่า 300 เล็กน้อย (ต้องสูง {px} px)",rc_dpiBad:"{d} dpi ต่ำเกินไปสำหรับงานพิมพ์ (ต้องสูง {px} px)",
ck_tracks:"รายชื่อเพลง",ck_tracksD:"เพิ่มอย่างน้อยหนึ่งเพลง",ck_lac:"แล็กเกอร์ของคุณ",
ck_lacD:"ตัดโดยสตูดิโอของคุณและส่งด่วนในวันที่ตัด เพราะแล็กเกอร์ต้องชุบโลหะภายในไม่กี่วัน ตรวจด้วยกล้องจุลทรรศน์เมื่อมาถึง ไฟล์ด้านล่างใช้สำหรับแผนหน้าแผ่นและเสียงตัวอย่าง",
ck_tape:"เทปของคุณ",ck_tapeD:"เทปอนาล็อกจะถูกแปลงสัญญาณและตรวจเมื่อมาถึง โดยยืนยันฟอร์แมต ความเร็ว และ EQ curve กับคุณก่อน",
ck_files:"ไฟล์มาสเตอร์",ck_filesD:"ยังไม่ได้เพิ่ม: ตอนนี้ตรวจจากความยาวที่พิมพ์ไว้ วางไฟล์ WAV, FLAC หรือ AIFF เพื่อตรวจตัวไฟล์จริง",
ck_fmt:"ฟอร์แมตไฟล์",ck_lossy:"{l}: ไม่ใช่ WAV, FLAC หรือ AIFF กรุณาส่งมาสเตอร์แบบ lossless",ck_lossless:v=>"ไฟล์ lossless "+v.n+" ไฟล์",
ck_sr:"แซมเปิลเรต",ck_srBad:"{n} ไฟล์ต่ำกว่า 44.1 kHz",ck_srOk:"44.1 kHz ขึ้นไป",
ck_bits:"บิตเดปท์",ck_bitsBad:"{n} ไฟล์ต่ำกว่า 16 bit",ck_bits16:"{n} ไฟล์เป็น 16 bit ใช้ได้ แต่ 24 bit ดีกว่าสำหรับการตัดแผ่น",ck_bitsOk:"24 bit ขึ้นไป",
ck_clipPk:"คลิปปิ้งและพีค",ck_scan:"กำลังสแกน {n} ไฟล์…",ck_clip:"คลิปปิ้ง",ck_clipD:"{l}: มีแซมเปิลเต็มสเกลต่อเนื่องกัน วิศวกรจะสอบถามเรื่องนี้",
ck_peak:"ระดับพีค",ck_peakD:"{n} ไฟล์มีพีคเกิน -0.3 dBFS",ck_noClip:"ไม่พบคลิปปิ้ง",
ck_f:"ไฟล์",ck_fD:"{n} เพลงยังไม่มีไฟล์",
ck_side:"ความยาวต่อหน้า",sideN:"หน้า {n}",
ck_sideBad:v=>v.l+": เกิน "+v.max+" ที่หน้าแผ่น "+v.s+"\" รองรับได้ที่ "+v.r+" ย้ายเพลง เพิ่มแผ่น หรือเปลี่ยนความเร็ว",
ck_sideWarn:"{l}: เกิน {a} ที่ให้เสียงดีที่สุด เสียงที่ตัดได้จึงเบาลง",ck_sideEmpty:"หน้า {l} ยังไม่มีเพลง",ck_sideOk:"ทุกหน้าอยู่ในช่วง {a} ที่ให้เสียงดีที่สุด",
ck_jArt:"อาร์ตเวิร์กปก",ck_lArt:"อาร์ตเวิร์กฉลาก",ck_notYet:"ยังไม่ได้เพิ่ม",ck_artOk:"ขนาด ความละเอียด และโหมดสีถูกต้อง",
ck_eng:"วิศวกรตรวจ",ck_engD:"หลังอัปโหลด วิศวกรตัดแผ่นจะยืนยันว่าแต่ละหน้าตัดได้จริง ผลนี้คือตัวตัดสิน ไม่ใช่แถบด้านบน",
apHintBad:"แก้รายการที่มีเครื่องหมาย ✕ ในขั้นตอนที่ 2 ก่อน",apHintRights:"ยืนยันสิทธิ์ในขั้นตอนที่ 2 ก่อน",
mk_own:"ปกของคุณเอง",mk_scale:"ปก 12\" เพื่อเทียบขนาด",mk_side:"หน้า A · {r}",mk_new:"ใหม่",
mk_cap:v=>v.n+": แผ่น "+v.r+" มม. ปก "+v.j+" มม."+(v.s?" สัน "+v.s+" มม.":""),mk_aria:"ภาพจำลองแผ่น {n}",
qrAria:"โค้ดที่พิมพ์บนซอง (ลายตัวอย่าง)",
cv_mark:"{q} แผ่น: แผ่นละ {m}",cv_axis:"ราคาต่อแผ่น รวมทุกอย่าง ({c}) · จำนวนแผ่นแบบสเกลลอการิทึม",cv_aria:"ราคาต่อแผ่นเทียบกับจำนวนแผ่น",
noWeight:"มีแบบ 140 หรือ 180 กรัมเฉพาะแผ่น 12\"",noGate:"ปก Gatefold มีเฉพาะแผ่น 12\"",
fmtSub:v=>"ที่ "+v.r+" รอบ หน้าละไม่เกิน "+v.m+"\u00a0นาที ประมาณ "+v.n+"\u00a0เพลง",
noC1:"แผ่นสีดำ ไม่ต้องเลือกสี",noC2:"ใช้กับลายหินอ่อนและ splatter เท่านั้น",palTitle:"เปิดจานสีแล้วเลือกสีที่ต้องการ",
palYours:"สีของคุณ",palName:"จานสี",palAria:"จานสี: เลือกสีใดก็ได้",
noDink:"รูกลางใหญ่มีเฉพาะซิงเกิล 7\"",noClean:"แผ่นสีดำไม่ต้องล้างเครื่อง",
clOff:"ใช้กับแผ่นสีเท่านั้น",clOn:"เครื่องปั๊มจะถูกไล่เนื้อเก่าออกก่อนรันของคุณ จึงไม่มีสีจากงานก่อนปนเข้ามา แต่สีอ่อนอาจยังเห็นจุดเล็กๆ ได้บ้าง",
clNo:"ประหยัดค่าไล่เครื่อง แต่บางแผ่นอาจมีริ้วสีจากงานก่อนติดมา",noMast:"แล็กเกอร์ของคุณตัดและมาสเตอร์มาแล้ว",
srcLac:"แล็กเกอร์ของคุณเข้าสู่การชุบโลหะทันที คุณยังได้ฟังและอนุมัติแผ่นทดสอบ และ father กับ mother จะถูกเก็บไว้สำหรับปั๊มซ้ำ",
srcTape:"เทปของคุณถูกแปลงสัญญาณในโรงงาน แล้วจึงตัดแผ่น คุณได้ฟังและอนุมัติแผ่นที่ปั๊มจริงก่อนผลิตทั้งรอบ",
srcDig:"คุณได้ฟังและอนุมัติแผ่นที่ปั๊มจริงก่อนผลิตทั้งรอบ father และ mother ถูกเก็บไว้ การปั๊มซ้ำครั้งต่อไปจึงไม่ต้องตัดแผ่นใหม่",
noLabelArt:"ฉลากเปล่าไม่มีงานพิมพ์",noJacketOwn:"คุณจัดหาปกเอง",noJacketPlain:"ปกเปล่าไม่มีงานพิมพ์",
fiReading:"กำลังอ่าน…",fiLossy:"{f}: ไม่ใช่ lossless",fiScan:" · กำลังสแกน",fiPeak:" · พีค {p} dB",fiClips:" · คลิป",
aSide:"หน้า",aTitle:"ชื่อเพลง",aLen:"ความยาว",fromFile:"อ่านจากไฟล์",rm:"ลบ",
sideHint:v=>v.s+"\" ที่ "+v.r+(v.loud?" เพลงเสียงดัง":"")+": เสียงดีที่สุดเมื่อหน้าละไม่เกิน "+v.a+" ยาวสุด "+v.b+" (ประมาณ "+v.n+"\u00a0เพลง เพลงละ 4:32)",
nTracks:v=>v.n+" เพลง",sb_ok:"พอดี",sb_warn:"ยาว",sb_bad:"ยาวเกิน",sb_empty:"ว่าง",sbMark:"เสียงดีที่สุดถึงตรงนี้",
chFix:v=>"ต้องแก้ "+v.n+" รายการ",chLook:v=>"พร้อมแล้ว มี "+v.n+" รายการที่ควรดู",chReady:"พร้อมส่งให้วิศวกร",
apSee:"ดูราคา &#x2192;",apApprove:"อนุมัติและดูราคา &#x2192;",
gateDone:"อนุมัติแผนมาสเตอร์แล้ว: การอนุมัติครั้งที่ 1 จาก 3 ถ้าเปลี่ยนเพลงหรือขนาดแผ่น ต้องอนุมัติใหม่",gateBad:"แก้รายการที่มีเครื่องหมาย ✕ ก่อน",
gateRights:"ยืนยันสิทธิ์ก่อนจึงอนุมัติได้ นี่คือการอนุมัติครั้งที่ 1 จาก 3: แผนตอนนี้ ส่วนมาสเตอร์และแผ่นทดสอบภายหลัง",
gateReady:"นี่คือการอนุมัติครั้งที่ 1 จาก 3: แผนตอนนี้ ส่วนมาสเตอร์และแผ่นทดสอบภายหลัง",
kPer:"ต่อแผ่น รวมทุกอย่าง",kTotal:"ยอดรวมออร์เดอร์",kShips:"สั่งวันนี้ จัดส่ง",bdSum:"{a} ครั้งเดียว + {b} ต่อแผ่น",
partner:"ราคาพาร์ทเนอร์ {p} + ค่าดำเนินการ {h} %",thOnce:"ค่าใช้จ่ายครั้งเดียว สำหรับผลงานนี้",thAmt:"จำนวนเงิน",onceTot:"รวมค่าใช้จ่ายครั้งเดียว",thPer:"ต่อแผ่น",
perRow:"{c} × {q} แผ่น",offerHint:"ค่าใช้จ่ายครั้งเดียวจ่ายครั้งเดียวต่อผลงาน ไม่ว่าจะผลิตกี่แผ่น และคิดตามจำนวนหน้าแผ่น ไม่ใช่จำนวนเพลง เพราะการมาสเตอร์ ตัดแผ่น และชุบโลหะทำทีละหน้า 8 เพลงหรือ 12 เพลงจึงราคาเท่ากัน และแผ่นคู่มี 4 หน้า ราคายังไม่รวม VAT 7 % และค่าจัดส่ง ปกพิมพ์ลายมาจากพาร์ทเนอร์ด้านบรรจุภัณฑ์ในท้องถิ่น ตามราคาของพาร์ทเนอร์บวกค่าดำเนินการที่แสดงไว้",
whenShips:"จัดส่งเมื่อไร",weeks:"{a} ถึง {b} สัปดาห์",lpA:"การอนุมัติของคุณและงานแม่พิมพ์โลหะ",weeksQ:"{a} ถึง {b} สัปดาห์ ตามที่เสนอ",lpB:"นับจากคุณอนุมัติแผ่นทดสอบจนถึงวันส่งของ",
leadHint:"สั่งวันนี้ จัดส่งประมาณ <b>{r}</b> ระยะเวลาที่เสนอนับจากการอนุมัติครั้งสุดท้ายของคุณ เพราะโรงงานเดินเวลาไม่ได้ระหว่างที่รอคุณอยู่ ",
leadOwn:"ถ้าใช้ปกของคุณเองจะเร็วขึ้น 1 ถึง 2 สัปดาห์",leadNoJ:"ไม่ต้องรอปกพิมพ์ จึงเร็วขึ้น 1 ถึง 2 สัปดาห์",
deposit:"มัดจำวันนี้ <b>{m}</b>",pFan:"เป้าหมาย <b>{n} คน</b> ที่ราคา {p} คุณได้รับประมาณ <b>{k}</b> ต่อแผ่น",pFanNo:"ขึ้นราคาสำหรับแฟนในขั้นตอนถัดไป",
needGate:"อนุมัติแผนมาสเตอร์ในขั้นตอนที่ 2 ก่อน",choiceMsg:"การสั่งผลิตและการเปิดแคมเปญต้องให้คุณอนุมัติแผนมาสเตอร์ก่อน ",goStep2:"ไปขั้นตอนที่ 2",
lhGate:"อนุมัติแผนมาสเตอร์ในขั้นตอนที่ 2 ก่อน",lhPrice:"ขึ้นราคาสำหรับแฟน เพราะยังไม่พอกับต้นทุนต่อแผ่น",lhCap:"จำนวนที่จำกัดต่ำกว่าเป้าหมาย",
fe_bad:v=>"ที่ราคา "+v.p+" ต่อแผ่น เงินที่เหลือหลังหักค่าธรรมเนียมการชำระเงิน"+(v.vat?"และ VAT":"")+" ไม่พอกับต้นทุนต่อแผ่น กรุณาขึ้นราคาสำหรับแฟน",
fe_need:"แฟนที่ต้องการ",nFans:"{n} คน",fe_pre:"ยอดพรีออร์เดอร์ {m}",fe_be:"จุดคุ้มทุนของคุณ",fe_keepG:"คุณได้รับเมื่อถึงเป้า",aCopy:"ต่อแผ่น",fe_that:"รวมเป็น",fe_tot:"ทั้งหมด",
fe_cap:"จำกัดจำนวนไว้ที่ {c} แผ่น ต่ำกว่าเป้าหมาย {g} คน แคมเปญนี้จึงสำเร็จไม่ได้",fe_more:"รายได้ของคุณถ้ามีแฟนร่วมมากขึ้น",perCopyX:"{m} ต่อแผ่น",
fe_hint:v=>"รายได้ของคุณคิดหลังหักค่าธรรมเนียมการชำระเงินประมาณ "+v.f+" %"+(v.vat?" และ VAT":"")+" ค่าเตรียมงานครั้งเดียว และค่าปั๊มแผ่น ยิ่งมีแฟนเกินเป้ายิ่งดี เพราะค่าเตรียมงานครั้งเดียวกระจายไปหลายแผ่นขึ้น ค่าจัดส่งแฟนแต่ละคนจ่ายเองตามจริง",
needBad:"ราคานี้ไม่พอกับต้นทุนต่อแผ่น",need:"ต้องมีแฟน <b>{n} คน</b> จึงจะปั๊มได้ ยอดพรีออร์เดอร์ {m}",
nhRaise:"กรุณาขึ้นราคา",nhBe:"ที่ราคานี้ จุดคุ้มทุนของคุณคือ {be} คน สูงกว่าขั้นต่ำ {min} แผ่นของโรงงาน ถ้าตั้งราคาตั้งแต่ {p} ต่อแผ่น แฟน {min} คนก็พอ",
nhMin:"โรงงานปั๊มขั้นต่ำ {min} แผ่น ถ้าราคาต่ำกว่าประมาณ {p} ต่อแผ่น คุณจะต้องมีแฟนมากขึ้นจึงจะคุ้มต้นทุน",
capOff:"เปิดสวิตช์เพื่อทำเป็นรุ่นจำกัดจำนวน",launchSim:"จำลองเท่านั้น: ไม่มีหน้าใดเปิดจริงและไม่มีใครถูกเรียกเก็บเงิน",
stDone:"ส่งถึงแล้ว ทุกแผ่นมีพาสปอร์ต",stWait:"รอคุณอนุมัติ: {s}",stNow:"ตอนนี้: {s}",stDay:"วันที่ {d} จาก {n} · แฟน {f} จาก {g} คน",
s5Dir:"ออร์เดอร์ของคุณ",s5Camp:"แคมเปญของคุณ",s5None:"แคมเปญและการผลิต",
s5lDir:"ติดตามแผ่นเสียงของคุณในโรงงาน จุดรูปข้าวหลามตัดคือการอนุมัติของคุณ โรงงานจะรอจนกว่าคุณอนุมัติ",
s5lCamp:"หน้าแคมเปญของคุณแบบสด เลือกสถานการณ์แล้วกดเล่น: 30 วันในเวลาประมาณ 8 วินาที",
emptyH:"ยังไม่มีอะไรดำเนินอยู่",emptyP:"เลือกวิธีจ่ายค่าผลิตในขั้นตอนที่ 3 หรือข้ามไปดูด้วยแผ่นตัวอย่าง",
quickCamp:"รันแคมเปญตัวอย่าง",quickDone:"ข้ามไปที่แผ่นที่ส่งถึงแล้ว",runCamp:"รันแคมเปญ",simulated:"จำลอง",
scSell:"ขายหมด",scMake:"ถึงเป้าพอดี",scMiss:"ไม่ถึงเป้า",aDay:"วันของแคมเปญ",latest:"พรีออร์เดอร์ล่าสุด",
dayOf:"วันที่ {d} จาก {n}",pause:"&#x275A;&#x275A; หยุดชั่วคราว",replay:"&#x21BA; เล่นอีกครั้ง",play:"&#x25B6; เล่น",
stRefund:v=>"ถ้ามีแฟนพรีออร์เดอร์น้อยกว่า "+v.g+" คนภายใน "+v.d+" ทุกคนจะได้เงินคืนอัตโนมัติ",
stFunded:v=>"<b style=\"color:var(--ok)\">&#x2713; ระดมทุนสำเร็จจากแฟน "+v.n+" คน"+(v.so?" ขายหมดแล้ว":"")+"</b> เริ่มปั๊มแผ่นแล้ว",
stMin:"<b>เดินหน้าผลิตที่ขั้นต่ำ {min} แผ่น</b>",stRefunded:"<b>ไม่ถึงเป้า</b> คืนเงินแฟนทุกคนแล้ว",stMissed:"<b>ไม่ถึงเป้า</b> จะคืนเงินให้แฟน เว้นแต่ศิลปินจ่ายส่วนต่างเอง",
pvPlay:"เล่นตัวอย่าง 30 วินาที",pvNone:"เสียงตัวอย่างสร้างโดยโรงงานจากมาสเตอร์ของคุณ",shipX:"+ ค่าส่ง",limitedOf:"จำกัดจำนวน {n} แผ่น มีหมายเลขกำกับ",
ofFans:"<b>{n}</b> จาก {g} คน",closed:"ปิดแล้ว",daysLeft:"เหลือ {n} วัน",preClosed:"ปิดรับพรีออร์เดอร์แล้ว",preorder:"พรีออร์เดอร์",estShip:"คาดว่าจะจัดส่ง",
fpTracks:"รายชื่อเพลง",copyLink:"คัดลอกลิงก์",lineT:"แชร์ไปยัง LINE เมื่อเปิดตัว",qrB:"คิวอาร์โค้ด",qrT:"คิวอาร์โค้ดสำหรับหน้างานคอนเสิร์ตและโต๊ะขายสินค้า",
fpFine:"ผลิตและจำหน่ายโดย CNX Vinyl ประเทศไทย เงินของคุณถูกเก็บในบัญชีแยกจนกว่าแคมเปญจะสำเร็จ และคืนให้อัตโนมัติหากไม่สำเร็จ",
mFans:"แฟน",mHeld:"เงินที่แยกเก็บ",mClosed:"ปิดเมื่อ",mDays:"วันที่เหลือ",fanN:"แฟนคนที่ {i} · {c}",minAgo:"{n} นาทีที่แล้ว",justNow:"เมื่อสักครู่",noPre:"ยังไม่มีพรีออร์เดอร์ กดเล่นได้เลย",
ocFunded:"ระดมทุนสำเร็จ",ocFundedP:"แฟน {n} คนพรีออร์เดอร์แล้ว เงินถูกโอนออกจากบัญชีแยกเพื่อใช้ผลิต",
ocNet:v=>"เงินจากแฟนหลังหักค่าธรรมเนียม"+(v.vat?"และ VAT":""),ocSetup:"ค่าเตรียมงานครั้งเดียว",ocPress:"ปั๊มแผ่นและบรรจุ {n} แผ่น",ocKeep:"คุณได้รับ",
ocRefunded:"คืนเงินแล้ว",ocRefundedP:"คืนเงิน {n} รายการ รายการละ {p} กลับไปยังช่องทางชำระเงินเดิมของแฟนแต่ละคน ไม่มีการปั๊มแผ่น มาสเตอร์และอาร์ตเวิร์กของคุณยังอยู่ในคลังสำหรับลองใหม่",
ocRetry:"รันอีกครั้ง",ocMinH:"เดินหน้าผลิตที่ขั้นต่ำ",ocPaid:"คุณจ่ายส่วนต่าง <b>{m}</b>",ocCovered:"เงินจากแฟนครอบคลุมการผลิตขั้นต่ำ คุณได้รับ <b>{m}</b>",
ocSpare:" และคุณได้รับ {n} แผ่นที่ไม่มีแฟนสั่ง",ocMissH:"ไม่ถึงเป้า: {n} จาก {g}",ocTwo:"มีสองทางเลือก ศิลปินเป็นคนตัดสินใจ",
ocRefundAll:"คืนเงินทุกคน",ocRefundAllP:"คืนอัตโนมัติจากบัญชีแยก ไม่มีการปั๊มแผ่น ไฟล์ของคุณยังอยู่ในคลัง",ocRefundBtn:"คืนเงินแฟนทั้ง {n} คน",
ocGoMin:"เดินหน้าผลิตที่ขั้นต่ำ {min} แผ่น",ocPay:"คุณจ่ายส่วนต่าง <b>{m}</b>",ocFree:"ไม่เสียเงินเพิ่ม: เงินจากแฟนครอบคลุมแล้ว และคุณได้รับ {m}",
ocUnclaimed:" คุณได้รับ {n} แผ่นที่ไม่มีคนสั่ง",ocGo:"เดินหน้า",
dirH:"สั่งผลิตโดยตรง",dirCopies:"{n} แผ่น, {d}",dirPaid:"จ่ายมัดจำแล้ว",dirBal:"ยอดคงเหลือก่อนจัดส่ง",
dirHint:"เงื่อนไขมัดจำเป็นเพียงตัวอย่าง ค่ายเพลงที่มีบัญชีจะได้อัตราเฉพาะและอนุมัติผ่านพอร์ทัลลูกค้า",
trApprove:"อนุมัติ",trNext:"ถัดไป &#x25B8;",trGate:"อนุมัติครั้งที่ {g} จาก 3",trDone:"เสร็จ {d}",trWait:"รอคุณอยู่ · ตามแผน {d}",trPlan:"ตามแผน {d}",
trProd:"การผลิต",trCopies:"{n} แผ่น",trDelivered:"ส่งถึงแล้ว",trPlayNext:"&#x25B6; เล่นถึงการอนุมัติครั้งถัดไป",trUpd:"อัปเดตออร์เดอร์ของคุณ",trUpdFans:"อัปเดตที่แฟนเห็น",
trLog:"จากบันทึกของโรงงาน",trHint:"อัปเดตเหล่านี้มาจากบันทึกเหตุการณ์ของโรงงานเอง ความคืบหน้าคือข้อมูลจริงของโรงงาน ไม่ใช่สถานะที่ต้องมีคนคอยจำมาอัปเดต",
ppNo:"หมายเลข {c} จาก {n}",ppCopy:"แผ่นที่ {c} จาก {n}",ppH:"พาสปอร์ตแผ่นเสียง",ppKey:"รหัสแผ่น: สแตมเปอร์ · เครื่องปั๊ม · เวลารอบปั๊ม · หน้า",ppSide:"หน้า A",
pp_tape:"รับเทปแล้ว",pp_master:"รับมาสเตอร์แล้ว",pp_tapeD:"แปลงสัญญาณในโรงงาน เก็บในคลังลูกค้า",pp_masterD:"ตรวจ checksum ตรงกันทั้งสองฝั่ง เก็บในคลังลูกค้า",
pp_mast:"มาสเตอร์สำหรับแผ่นเสียงแล้ว",pp_mastD:"ศิลปินอนุมัติมาสเตอร์แล้ว",pp_cut:"ตัดลงแผ่นแล็กเกอร์",pp_cutD:v=>"แล็กเกอร์ "+v.l+(v.c?" สลัก \u201C"+v.c+" A\u201D":" สลักหมายเลข")+" ไว้ในร่องท้ายแผ่น",
pp_lac:"รับแล็กเกอร์แล้ว",pp_lacD:"ตัดโดยสตูดิโอของศิลปิน ตรวจด้วยกล้องจุลทรรศน์เมื่อมาถึง",pp_plate:"ชุบโลหะในโรงงาน",
pp_plateD:"Father {f} → mother {m} (เก็บไว้ทั้งคู่สำหรับปั๊มซ้ำ) → สแตมเปอร์ {s}",pp_tp:"อนุมัติแผ่นทดสอบแล้ว",pp_tpD:"ศิลปินฟังและอนุมัติแล้ว",
pp_press:"ปั๊มแล้ว",pp_pressD:"เครื่องปั๊ม D1-B (Newbilt Duplex) เวลา {t} ล็อตเม็ดพลาสติก {l} บันทึกข้อมูลเครื่องปั๊มของรอบนี้แล้ว",
pp_insp:"ตรวจแล้ว",pp_inspD:"ผ่านการตรวจด้วยระบบออปติคัล ซึ่งตรวจทุกแผ่น แผ่นแรกๆ ของสแตมเปอร์และประมาณ 1 ใน 50 แผ่นหลังจากนั้นถูกเทียบกับมาสเตอร์ด้วยการวิเคราะห์เสียง",
pp_pack:"บรรจุแล้ว",pp_packD:v=>"ซองใน"+(v.j?"และปก":"")+(v.s?" ซีลพลาสติก":"")+" ตรวจรหัสบนปกให้ตรงกับแผ่นนี้แล้ว",
pp_ship:"จัดส่งแล้ว",pp_shipD:"ส่งพร้อมเลขพัสดุ",pp_shipC:"ส่งพัสดุพร้อมเลขติดตามถึงแฟน",
ppHint:"สิ่งที่แฟนเห็นหลังสแกนโค้ดที่พิมพ์บนซอง: ข้อมูลการตรวจสอบย้อนกลับของโรงงานสำหรับแผ่นนี้แผ่นเดียว รหัสและเวลาในหน้านี้เป็นข้อมูลจำลอง",
ppRepress:"<b>ขายหมดแล้ว? เปิดปั๊มซ้ำได้</b> father และ mother ถูกเก็บไว้ การปั๊มซ้ำจึงไม่ต้องตัดแผ่นใหม่ ทำสแตมเปอร์ชุดใหม่ได้ทันที ใช้เวลาโรงงานประมาณ 1.5 ถึง 2.5 สัปดาห์ และปั๊มเมื่อมีแฟนสั่งเพิ่มอีก 100\u00a0คน",
ppRepressB:"เปิดพรีออร์เดอร์ปั๊มซ้ำ",
ctaS1:"ต่อไป: เพลงและอาร์ตเวิร์ก →",ctaM1:"เพลงและอาร์ต →",ctaS2a:"ดูราคา →",ctaM2a:"ราคา →",ctaS2:"อนุมัติและดูราคา →",ctaM2:"อนุมัติ →",
ctaS3:"ให้แฟนช่วยระดมทุน →",ctaM3:"ระดมทุน →",ctaH3:"หรือสั่งผลิตโดยตรงได้ด้านล่าง",ctaS4:"เปิดแคมเปญ →",ctaM4:"เปิดแคมเปญ →",
untitled:"ไม่มีชื่อ",spW:"{w} กรัม",spPlain:v=>"ฉลาก"+v.c+"เปล่า",spRunout:"ร่องท้ายแผ่น {c}",spTape:"จากเทป",spLac:"แล็กเกอร์ของคุณ",
stFan:"ราคาสำหรับแฟน",stNeeded:"แฟนที่ต้องการ",stNot:"ไปไม่ถึง",stKeep:"คุณได้รับเมื่อถึงเป้า",na:"ไม่มี",mbFan:"ราคาสำหรับแฟน · แฟนที่ต้องการ",
nCopies:"{n} แผ่น",mbCopies:"{n} แผ่น · ต่อแผ่น",status:"สถานะ",
toastFormat:"เปลี่ยนขนาดแผ่นแล้ว: อนุมัติแผนมาสเตอร์อีกครั้งในขั้นตอนที่ 2",toastApprove:"อนุมัติแผนมาสเตอร์ก่อน",
toastPre:"เดโม: แฟนจ่ายผ่านพร้อมเพย์ QR บัตร หรือวอลเล็ต เงินถูกแยกเก็บจนกว่าจะถึงเป้า",toastRepress:"จำลอง: หน้าแคมเปญเปิดอีกครั้งเป็นพรีออร์เดอร์ปั๊มซ้ำ",
toastCopied:"คัดลอกแล้ว (ลิงก์ตัวอย่าง): {u}",toastNumbered:"เพิ่มหมายเลขกำกับให้แผ่นแล้ว (ขั้นตอนที่ 1)",toastTour:"จบการพาชมแล้ว ทุกอย่างในหน้านี้คลิกได้",
tourOn:"&#x25A0;<span class=\"tl\"> หยุดพาชม</span>",tourOff:"&#x25B6;<span class=\"tl\"> พาชม</span>",
tour1:"ศิลปินออกแบบแผ่นเสียง ภาพจำลองวาดตามสัดส่วนจริงและเปลี่ยนตามทุกตัวเลือก",
tour2:"ไฟล์มาสเตอร์และอาร์ตเวิร์กถูกตรวจก่อนมีใครจ่ายเงิน: ฟอร์แมตไฟล์ ความยาวต่อหน้าเทียบกับขนาดแผ่น และความละเอียดงานพิมพ์",
tour3:"ศิลปินอนุมัติแผนมาสเตอร์ เป็นการอนุมัติครั้งที่ 1 จาก 3 ไม่มีอะไรไปถึงแฟนเพลงที่โรงงานผลิตไม่ได้",
tour4:"ราคาแจกแจงครบทุกรายการ ราคาต่อแผ่นลดลงเร็วตามจำนวนผลิต เพราะตัดแผ่นและชุบโลหะเองในโรงงาน",
tour5:"ไม่ต้องจ่ายล่วงหน้า: ศิลปินตั้งราคาสำหรับแฟน แล้วหน้านี้คำนวณว่าต้องมีแฟนกี่คน คือขั้นต่ำ 100 แผ่นของโรงงาน หรือมากกว่านั้นถ้าราคาต่ำ",
tour6:"แคมเปญ 30 วันแบบจำลอง เงินของแฟนอยู่ในบัญชีแยกจนกว่าจะถึงเป้า ถ้าไม่ถึง ทุกคนได้เงินคืน",
tour7:"ระดมทุนสำเร็จ: แคมเปญกลายเป็นออร์เดอร์ผลิต แฟนติดตามได้จากบันทึกเหตุการณ์ของโรงงานเอง และโรงงานรอการอนุมัติของศิลปิน",
tour8:"ส่งถึงแล้ว ทุกแผ่นมีพาสปอร์ต: สแตมเปอร์ เครื่องปั๊ม เวลารอบปั๊ม และหน้าแผ่น พร้อมผลการตรวจ",
themeTo:v=>"สลับเป็นโหมด"+v.m,themeDark:"มืด",themeLight:"สว่าง"
}};
const MISSING=new Set(),I18N_ATTR=[["data-i18n-title","title"],["data-i18n-aria","aria-label"]];
function t(k,v){
let s=T[S.lang]&&T[S.lang][k];
if(s==null){if(S.lang!=="en")MISSING.add(k);s=T.en[k];}
if(s==null){MISSING.add(k);return k;}
if(typeof s==="function")return s(v||{});
return v&&typeof s==="string"?s.replace(/\{(\w+)\}/g,(m,x)=>v[x]!=null?v[x]:m):s;
}
$$("[data-i18n]").forEach(el=>{const k=el.dataset.i18n;if(T.en[k]==null)T.en[k]=el.innerHTML;});
I18N_ATTR.forEach(([d,a])=>$$("["+d+"]").forEach(el=>{const k=el.getAttribute(d);if(T.en[k]==null)T.en[k]=el.getAttribute(a)||"";}));
/* ---- price list demo-0.1: the same rates as artist-order.html, which lists each one with its basis.
Change both pages together (and artist-platform.md, revenue-model.md, production-timeline.md). ---- */
const R={
master:{v:5000},blank:{v:Math.round(70*USD_EUR*THB_USD)},cut:{v:2000},metal:{v:7000},setup:{v:3000},tp:{v:300},
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]}}},
spine:{v:{12:12,10:10}},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;
at most = the team's table (October 2026, from recordplayerexpert.com), see artist-platform.md "Side length per format" */
const LIM={"12":{"33":[18,22],"45":[12,15]},"10":{"33":[12,15],"45":[8,12]},"7":{"33":[5,7],"45":[4.5,5]}};
const SONG=272; /* seconds: the 4:32 average song the at-most table counts songs with */
const LOUD=0.6;
const jname=k=>t("jn_"+k);
const XN=["shrink","insert","sticker","numbered","dlcard","barcode"];
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=[
{id:"intake",t:"Audio intake and technical QA",d:[1,2]},
{id:"g1",t:"You approve the master plan",d:[2,10],gate:1},
{id:"mast",t:"Vinyl mastering",d:[2,4],opt:"mastering"},
{id:"g2",t:"You approve the vinyl master",d:[2,10],gate:2,opt:"mastering"},
{id:"cut",t:"Cutting the lacquer",d:[1,2]},
{id:"plate",t:"Plating: father, mother, stampers",d:[2,4]},
{id:"prep",t:"Stamper finishing",d:[1,1]},
{id:"tp",t:"Test pressings",d:[0.5,1]},
{id:"g3",t:"You approve the test pressing",d:[5,15],gate:3},
{id:"queue",t:"Press slot booked",d:null},
{id:"run",t:"Pressing",d:[1,3]},
{id:"fin",t:"Packing",d:[2,3]},
{id:"ship",t:"Shipping",d:[1,2]}
];
const stgT=s=>t("stg_"+s.id);
/* production updates are stored as stage ids and worded when shown, so they follow the language */
function evText(e,P){
switch(e.id){
case "start":return P.origin==="direct"?t("ev_startD"):P.origin==="minimum"?t("ev_startM",{min:R.minRun.v,n:fmtInt(P.fans)}):t("ev_startC",{n:fmtInt(P.fans)});
case "run":return t("ev_run",{n:fmtInt(P.run)});
case "fin":return t("ev_fin",{j:S.jacket!=="none",s:S.shrink});
case "ship":return P.origin==="direct"?t("ev_shipD"):t("ev_ship",{n:fmtInt(P.fans)});
default:return t("ev_"+e.id);
}
}
const C1=[["#b3202a","Ruby"],["#f26b1d","Tangerine"],["#f2c14e","Sunflower"],["#1f8a5b","Jade"],["#1f5fa8","Ocean"],["#6b3fa0","Violet"],["#e9e1cf","Bone"],["#2b2b2b","Smoke"]];
const C2=[["#f2c14e","Gold"],["#f5f5f0","White"],["#151515","Black"],["#ff7eb6","Pink"],["#22b8b0","Teal"],["#b3202a","Ruby"]];
const TODAY=new Date(); TODAY.setHours(12,0,0,0);
const S={
step:1,cur:"THB",lang:"en",size:"12",rpm:"33",discs:1,weight:140,colour:"black",c1:"#b3202a",c2:"#f2c14e",
qty:300,inner:"paper",jacket:"printed",spine:"thin",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,
visited:{1:true}
};
let tid=0;
[["Doi Suthep at Dawn",262],["Ping River",241],["Nimman Rain",198],["Songthaew",225],["Old City Walls",284],["Lanna Lights",233],["Mae Rim Drive",256],["Last Train North",301]]
.forEach(([t,s],i)=>S.tracks.push({id:++tid,title:t,sec:s,side:i<4?"A":"B",file:null,info:null,url:null}));
const S0=JSON.stringify(S);
/* ---- formatting ---- */
const fmtInt=n=>Math.round(n).toLocaleString("en-US");
const rate=()=>RATE[S.cur]||1,toTHB=v=>Math.round(v*rate()*100)/100;
/* the fan price is kept in THB; it is typed and shown in the chosen currency */
const fanIn=()=>S.cur==="THB"?Math.round(S.fanPrice):Math.round(S.fanPrice/rate()*100)/100;
const FAN_IN={THB:[300,5000,50],USD:[10,150,1],EUR:[8,130,1]},FAN_RNG={THB:[300,3000,50],USD:[10,90,1],EUR:[8,80,1]},FP={THB:[800,1000,1200,1500,2000],USD:[25,30,35,45,60],EUR:[20,25,30,40,50]};
function money(thb,o){o=o||{};
if(S.cur!=="THB"){const u=thb/rate(),d=o.dec!=null?o.dec:(Math.abs(u)<1000?2:0);return u.toLocaleString("en-US",{minimumFractionDigits:d,maximumFractionDigits:d})+(o.bare?"":" "+S.cur);}
return Math.round(thb).toLocaleString("en-US")+(o.bare?"":" THB");}
function mmss(s){s=Math.round(s||0);return Math.floor(s/60)+":"+String(s%60).padStart(2,"0");}
const LOC=()=>S.lang==="th"?"th-TH-u-ca-gregory":"en-GB";
const fmtD=d=>d.toLocaleDateString(LOC(),{day:"numeric",month:"short",year:"numeric"});
const fmtM=d=>d.toLocaleDateString(LOC(),{month:"short",year:"numeric"});
function monthRange(a,b){const A=fmtM(a),B=fmtM(b);if(A===B)return A;return t("range",{a:a.getFullYear()===b.getFullYear()?a.toLocaleDateString(LOC(),{month:"short"}):A,b:B});}
const addDays=(d,n)=>{const x=new Date(d);x.setDate(x.getDate()+Math.round(n));return x;};
function addWD(d,wd){const x=new Date(d);let n=Math.ceil(wd);while(n>0){x.setDate(x.getDate()+1);const g=x.getDay();if(g!==0&&g!==6)n--;}return x;}
const sideNames=()=>S.discs>1?["A","B","C","D"]:["A","B"];
const rpmT=()=>t("rpm"+S.rpm);
const colourName=()=>t("cn_"+S.colour);
const colourT=()=>t("ct_"+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"}};
const szName=()=>t("sz"+S.size);
function descT(){return t("descT",{d:S.discs>1?"2 × ":"",n:szName(),k:dinkOn()?t("dinkedP"):"",r:rpmT(),w:S.size==="12"?S.weight:"",c:colourT()});}
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);}
const toastT=(k,v)=>toast(t(k,v));
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);
function rnd(seed){return function(){seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.imul(seed^seed>>>15,1|seed);t=t+Math.imul(t^t>>>7,61|t)^t;return ((t^t>>>14)>>>0)/4294967296;};}
/* ---- pricing (same model as artist-order.html) ---- */
function quote(q){
const sz=S.size,d=S.discs,sides=2*d,T=[],C=[],du=t(d>1?"u_discs":"u_disc"),us=t("u_sides");
if(mastOn())T.push({t:t("q_master"),n:sides,u:us,r:R.master.v});
if(S.source==="tape")T.push({t:t("q_tape"),n:1,u:"",r:R.tape.v});
if(S.source!=="lacquer"){T.push({t:t("q_blank"),n:sides,u:us,r:R.blank.v});T.push({t:t("q_cut"),n:sides,u:us,r:R.cut.v});}
T.push({t:t("q_plate"),n:sides,u:us,r:R.metal.v});
T.push({t:t("q_setup"),n:d,u:du,r:R.setup.v});
if(S.tps>0)T.push({t:t("q_tp"),n:S.tps,u:t("u_copies"),r:R.tp.v});
if(cleanOn())T.push({t:t("q_clean"),n:1,u:"",r:R.clean.v});
C.push({t:t("q_press",{s:sz,w:sz==="12"}),n:d,u:du,r:R.press.v[sz]});
if(sz==="12"&&S.weight===180)C.push({t:t("q_g180"),n:d,u:du,r:R.g180.v});
if(S.colour!=="black")C.push({t:t("q_colour",{c:colourT()}),n:d,u:du,r:R.colour.v[S.colour]});
C.push({t:t("q_inner",{x:t(S.inner==="poly"?"q_poly":"q_paper")}),n:d,u:du,r:R.inner.v[S.inner][sz]});
C.push({t:t("q_assy"),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"?t("q_pic"):""),n:1,u:"",r:Math.round(p*(1+R.handling.v)),pass:p});}
if(thickOn())C.push({t:t("q_spine"),n:1,u:"",r:R.spine.v[sz]});
if(S.labels!=="printed")C.push({t:t("q_plainL",{c:t("lc_"+S.labels)}),n:d,u:du,r:R.plainLabel.v});
if(S.shrink)C.push({t:t("q_shrink"),n:1,u:"",r:R.shrink.v});
if(S.dlcard)C.push({t:t("q_dlcard"),n:1,u:"",r:R.dlcard.v});
if(S.barcode)C.push({t:t("q_barcode"),n:1,u:"",r:R.barcode.v});
if(S.insert)C.push({t:t("q_insert"),n:1,u:"",r:R.insert.v});
if(S.sticker)C.push({t:t("q_sticker"),n:1,u:"",r:R.sticker.v});
if(S.numbered)C.push({t:t("q_numbered"),n:1,u:"",r:R.numbered.v});
T.forEach(x=>x.a=x.n*x.r);C.forEach(x=>x.a=x.n*x.r);
const tSum=T.reduce((s,x)=>s+x.a,0),cSum=C.reduce((s,x)=>s+x.a,0);
return {T,C,tSum,cSum,q,total:tSum+q*cSum,per:tSum/q+cSum};
}
function leadInfo(campaign){
const g3=STG.findIndex(s=>s.id==="g3");
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";
const qw=printed?[7,9]:[5,8];
return {pre,pw,qw,tw:[pw[0]+qw[0],pw[1]+qw[1]],printed};
}
function campEcon(){
const p=S.fanPrice,fee=R.fee.v,vat=S.vat?p*7/107:0,net1=p-p*fee-vat,tSum=quote(R.minRun.v).tSum,ac=S.artistCopies;
const cAt=n=>quote(Math.max(1,n)).cSum;
let be=null;
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
/* the artist sets the fan price; the fans needed follow: the plant minimum, or the break-even if that is higher */
const min=R.minRun.v,goal=be==null?null:Math.max(min,be);
const at=n=>{const run=n+ac,c=cAt(run),net=n*net1,cost=tSum+run*c;return {n,run,c,net,cost,keep:net-cost,per:(net-cost)/n};};
return {p,fee,vat,net1,tSum,be,min,goal,at,cAt};
}
function finalPledges(E){
if(!E.goal)return 0;const g=E.goal;
let P=S.scen==="sell"?Math.ceil(g*1.6):S.scen==="make"?Math.ceil(g*1.08):Math.floor(g*0.68);
if(S.cap>0)P=Math.min(P,S.cap);return P;
}
/* share of the final pledges in by time t (0..1): launch spike, steady middle, last-days push */
const frac=t=>0.38*(1-Math.exp(-t/0.07))/(1-Math.exp(-1/0.07))+0.37*t+0.25*Math.pow(t,6);
/* ---- tracks and sides ---- */
function parseDur(s){s=String(s).trim();let m=s.match(/^(\d+):(\d{1,2})$/);if(m)return +m[1]*60+ +m[2];m=s.match(/^\d+(\.\d+)?$/);if(m)return Math.round(parseFloat(s)*60);return null;}
function sideStats(){
const L=LIM[S.size][S.rpm].map(m=>m*60*(S.loud?LOUD:1));
return sideNames().map(n=>{const tr=S.tracks.filter(t=>t.side===n),sec=tr.reduce((a,t)=>a+(t.sec||0),0);
return {n,sec,count:tr.length,st:!tr.length?"warn":sec<=L[0]?"ok":sec<=L[1]?"warn":"bad",lim:L};});
}
function autoSplit(){
const names=sideNames(),k=names.length,a=S.tracks.map(t=>t.sec||0),n=a.length;if(!n)return;
const pre=[0];a.forEach(x=>pre.push(pre[pre.length-1]+x));
const dp=[],cut=[];for(let j=0;j<=k;j++){dp.push(Array(n+1).fill(Infinity));cut.push(Array(n+1).fill(0));}
dp[0][0]=0;
for(let j=1;j<=k;j++)for(let i=0;i<=n;i++)for(let p=0;p<=i;p++){const v=Math.max(dp[j-1][p],pre[i]-pre[p]);if(v<dp[j][i]){dp[j][i]=v;cut[j][i]=p;}}
let i=n;for(let j=k;j>=1;j--){const p=cut[j][i];for(let x=p;x<i;x++)S.tracks[x].side=names[j-1];i=p;}
}
const trkOf=el=>S.tracks.find(t=>t.id===+el.closest(".tr").dataset.id);
/* ---- reading files in the browser ---- */
const str=(dv,o,n)=>{let s="";for(let i=0;i<n&&o+i<dv.byteLength;i++)s+=String.fromCharCode(dv.getUint8(o+i));return s;};
async function dvAt(file,o,n){return new DataView(await file.slice(o,o+n).arrayBuffer());}
async function audioInfo(file){
let off=0,h=await dvAt(file,0,12);if(h.byteLength<12)return {fmt:"?",lossless:false};
if(str(h,0,3)==="ID3"){const b=await dvAt(file,6,4);off=10+((b.getUint8(0)<<21)|(b.getUint8(1)<<14)|(b.getUint8(2)<<7)|b.getUint8(3));h=await dvAt(file,off,12);}
const tag=str(h,0,4),sub=str(h,8,4);
if(tag==="RIFF"&&sub==="WAVE")return wavInfo(file);
if(tag==="fLaC")return flacInfo(file,off);
if(tag==="FORM"&&(sub==="AIFF"||sub==="AIFC"))return aiffInfo(file,sub==="AIFC");
return {fmt:(file.name.split(".").pop()||"?").toUpperCase(),lossless:false};
}
async function wavInfo(file){
let o=12,f=null,data=null;
while(o+8<=file.size&&(!f||data==null)){
const h=await dvAt(file,o,8),id=str(h,0,4),sz=h.getUint32(4,true);
if(id==="fmt "){const d=await dvAt(file,o+8,Math.min(sz,40));let tg=d.getUint16(0,true);if(tg===0xFFFE&&sz>=26)tg=d.getUint16(24,true);f={tg,ch:d.getUint16(2,true),sr:d.getUint32(4,true),bits:d.getUint16(14,true)};}
else if(id==="data")data=Math.min(sz,file.size-o-8);
o+=8+sz+(sz&1);
}
if(!f||data==null)return {fmt:"WAV",lossless:false,err:"unreadable header"};
const bps=f.sr*f.ch*f.bits/8;
return {fmt:"WAV",lossless:f.tg===1||f.tg===3,ch:f.ch,sr:f.sr,bits:f.bits,sec:bps?data/bps:0};
}
async function flacInfo(file,off){
const d=await dvAt(file,off+8,18),b=i=>d.getUint8(i);
const sr=(b(10)<<12)|(b(11)<<4)|(b(12)>>4),ch=((b(12)>>1)&7)+1,bits=(((b(12)&1)<<4)|(b(13)>>4))+1;
const total=(b(13)&15)*4294967296+((b(14)<<24)>>>0)+(b(15)<<16)+(b(16)<<8)+b(17);
return {fmt:"FLAC",lossless:true,ch,sr,bits,sec:sr?total/sr:0};
}
async function aiffInfo(file,isC){
let o=12;
while(o+8<=file.size){
const h=await dvAt(file,o,8),id=str(h,0,4),sz=h.getUint32(4,false);
if(id==="COMM"){
const d=await dvAt(file,o+8,Math.min(sz,22)),ch=d.getUint16(0),frames=d.getUint32(2),bits=d.getUint16(6);
const e=d.getUint16(8)&0x7FFF,hi=d.getUint32(10),lo=d.getUint32(14),sr=(hi*4294967296+lo)*Math.pow(2,e-16383-63);
const comp=isC&&sz>=22?str(d,18,4):"NONE";
return {fmt:isC?"AIFF-C":"AIFF",lossless:["NONE","sowt","twos","fl32","FL32","in24","in32"].includes(comp),ch,sr:Math.round(sr),bits,sec:sr?frames/sr:0};
}
o+=8+sz+(sz&1);
}
return {fmt:"AIFF",lossless:false,err:"no COMM chunk"};
}
async function scanPeak(t){
if(!t.info.lossless||t.file.size>150e6){t.info.peak="skip";return;}
try{
const ab=await t.file.arrayBuffer(),ctx=new OfflineAudioContext(1,1,Math.min(192000,Math.max(8000,t.info.sr||44100)));
const buf=await ctx.decodeAudioData(ab);let pk=0,runs=0;
for(let c=0;c<buf.numberOfChannels;c++){const d=buf.getChannelData(c);let run=0;for(let i=0;i<d.length;i++){const a=Math.abs(d[i]);if(a>pk)pk=a;if(a>=0.9999){run++;if(run===3)runs++;}else run=0;}}
t.info.peak=pk;t.info.clips=runs;
}catch(e){t.info.peak="skip";}
}
async function jpegComponents(file){
const b=new Uint8Array(await file.slice(0,262144).arrayBuffer());let i=2;
while(i+9<b.length){if(b[i]!==0xFF){i++;continue;}const m=b[i+1];
if(m===0xD8||m===0x01||m===0xFF||(m>=0xD0&&m<=0xD7)){i+=(m===0xFF?1:2);continue;}
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;
}
/* ---- 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"?t("spWhyGf",{mm:spineMM()}):S.jacket==="none"?t("spWhyNone"):S.size==="7"?t("spWhy7"):"",
hint:S.jacket==="none"?t("spHNone"):S.jacket==="gatefold"?t("spHGf",{mm:spineMM()}):S.size==="7"?t("spH7"):thickOn()?(spineForced()?t("spH5two"):t("spH5")):t("spH3")};
}
function tplDims(k){const B=TPL.bleed;
if(k==="label"){const d=SIZE_MM[S.size].lab;return {d,txt:t("tplLabel",{d,e:d+2*B,s:sideNames().join(", ")})};}
const P=TPL.panel[S.size],sp=spineMM(),w=2*P+sp;
return {P,sp,w,h:P,txt:t("tplJacket",{w:w+2*B,h:P+2*B,sp,gf:S.jacket==="gatefold"})};}
const tplOff=k=>k==="jacket"?(S.jacket==="printed"||S.jacket==="gatefold"?"":S.jacket==="none"?t("tplOffJ"):t("tplOffP")):(S.labels==="printed"?"":t("tplOffL"));
/* 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);toastT("tplDl",{n: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=/<xmp:CreatorTool>([^<]{1,80})</.exec(T)||/\/Creator\s*\(([^)]{1,80})\)/.exec(T);a.creator=cr?cr[1]:"";
a.ai=a.ai||/Illustrator/i.test(a.creator)||/AIPrivateData/.test(T);
a.cmyk=/\/DeviceCMYK|\/Separation|\/DeviceN/.test(T)||/\/N\s+4\b/.test(T);a.rgb=/\/DeviceRGB/.test(T)||/\/N\s+3\b/.test(T);
a.fonts=(T.match(/\/Type\s*\/Font\b/g)||[]).length;a.emb=(T.match(/\/FontFile[23]?\b/g)||[]).length;
a.u8=u8;return a;
}
let PDFJS=null;
async function pdfjs(){if(!PDFJS){const u="https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/build/",m=await import(u+"pdf.min.mjs");m.GlobalWorkerOptions.workerSrc=u+"pdf.worker.min.mjs";PDFJS=m;}return PDFJS;}
function crop(src,sx,sy,sw,sh,max){const s=Math.min(1,max/Math.max(sw,sh)),c=document.createElement("canvas");c.width=Math.max(1,Math.round(sw*s));c.height=Math.max(1,Math.round(sh*s));c.getContext("2d").drawImage(src,sx,sy,sw,sh,0,0,c.width,c.height);return c.toDataURL("image/jpeg",0.88);}
async function pdfPreview(a,u8,k){
try{const m=await pdfjs(),doc=await m.getDocument({data:u8,isEvalSupported:false}).promise,pg=await doc.getPage(1),v1=pg.getViewport({scale:1}),vp=pg.getViewport({scale:Math.min(4,1600/Math.max(v1.width,v1.height))});
const cv=document.createElement("canvas");cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);const cx=cv.getContext("2d");cx.fillStyle="#fff";cx.fillRect(0,0,cv.width,cv.height);
await pg.render({canvasContext:cx,viewport:vp}).promise;
const q=vp.convertToViewportRectangle(a.trimB||a.media||pg.view),L=Math.min(q[0],q[2]),R=Math.max(q[0],q[2]),T=Math.min(q[1],q[3]),h=Math.max(q[1],q[3])-T;
a.url=k==="jacket"&&(R-L)/h>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?t("pc_ai"):t("pc_pdf",{v:a.ver}))+(a.pdfx?t("pc_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",t("pc_lblOk",{tp:t(ok==="trim"?"pc_trim":"pc_page"),w:r1(tw),s:S.size,b:ok==="bleed"})]:["bad",t("pc_lblBad",{tp:t(tr?"pc_trim":"pc_page"),w:r1(tw),h:r1(th),s:S.size,d,e:d+2*B})]);
out.push(a.pages>=ns?["ok",t("pc_pagesOk",{n:a.pages,ns,l:sideNames().join(", ")})]:["warn",t("pc_pagesWarn",{n:a.pages,ns,l:sideNames().slice(a.pages).join(", ")})]);
}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",t("pc_jOk",{w:r1(ww),h:r1(hh),sp:D.sp})]);
else if(fit(D.P,D.P))out.push(["warn",t("pc_front",{sp:D.sp})]);
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",t("pc_spine",{n:Math.round(sp),sp:D.sp})]:["bad",t("pc_size",{w:r1(tw),h:r1(th),W:D.w,H:D.h})]);}
if(S.jacket==="gatefold")out.push(a.pages>=2?["ok",t("pc_gfOk")]:["warn",t("pc_gfWarn")]);
}
out.push(bl==null?["warn",t("pc_noTrim")]:bl>=B-0.2?["ok",t("pc_bleed",{b:r1(bl)})]:["bad",t("pc_bleedBad",{b:bl<=0.2?t("pc_noBleed"):t("pc_bleed",{b:r1(bl)})})]);
out.push(a.cmyk&&!a.rgb?["ok",t("pc_cmyk")]:a.cmyk?["warn",t("pc_mixed")]:a.rgb?["warn",t("pc_rgb")]:["pend",t("pc_noCs")]);
out.push(!a.fonts?["ok",t("pc_outl")]:a.emb?["ok",t("pc_emb")]:["bad",t("pc_noEmb")]);
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",t("rc_sq")]:["bad",t("rc_nsq",{w:a.w,h:a.h})]);}
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",t("rc_spread",{sp:D.sp})]:sp>0.5&&sp<12?["bad",t("pc_spine",{n:Math.round(sp),sp:D.sp})]:["bad",t("rc_wide",{w:Math.round(wmm),W:D.w})]);
if(S.jacket==="gatefold")out.push(["warn",t("rc_gf")]);}
else if(Math.abs(r-1)<0.01)out.push(["warn",t("pc_front",{sp:D.sp})]);
else out.push(["bad",t("rc_neither",{w:a.w,h:a.h})]);}
const need=Math.round(mmH/25.4*300),dpi=Math.round(a.h/(mmH/25.4));
out.push(a.h>=need?["ok",t("rc_dpiOk",{d:dpi,mm:mmH})]:a.h>=need*0.8?["warn",t("rc_dpiLow",{d:dpi,px:need})]:["bad",t("rc_dpiBad",{d:dpi,px:need})]);
out.push(a.cmyk?["ok",t("pc_cmyk")]:["warn",t("pc_rgb")]);
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;
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",t(a.err)]];
if(a.kind==="ps")return [["bad",t("artPS")]];
if(a.kind==="bad")return [["bad",t("artBad")]];
return a.kind==="pdf"?pdfChecks(k,a):rasterChecks(k,a);
}
const artThumb=a=>a.url?`<img src="${a.url}" alt="">`:`<span class="aph">${a.kind==="ps"||a.ai?"AI":a.kind==="pdf"?"PDF":"?"}</span>`;
const artNote=a=>a.loading?`<li><span class="ci pend" style="width:18px;height:18px;font-size:10px">&#x25CB;</span> ${t("artRender")}</li>`:a.noPrev?`<li><span class="ci pend" style="width:18px;height:18px;font-size:10px">&#x25CB;</span> ${t("artNoPrev")}</li>`:"";
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:"artErr"};}
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();}
}
const jacketArt=()=>S.jacket==="printed"||S.jacket==="gatefold";
/* ---- checks before approval 1 ---- */
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",t("ck_tracks"),t("ck_tracksD")]);
if(S.source==="lacquer")L.push(["pend",t("ck_lac"),t("ck_lacD")]);
else if(S.source==="tape")L.push(["pend",t("ck_tape"),t("ck_tapeD")]);
if(S.source!=="digital"){}else if(!files.length)L.push(["pend",t("ck_files"),t("ck_filesD")]);
else{
const lossy=ready.filter(x=>!x.info.lossless);
L.push(lossy.length?["bad",t("ck_fmt"),t("ck_lossy",{l:lossy.map(x=>esc(x.file.name)).join(", ")})]:["ok",t("ck_fmt"),t("ck_lossless",{n:ready.length})]);
const ok=ready.filter(x=>x.info.lossless),lowSr=ok.filter(x=>x.info.sr<44100),lowB=ok.filter(x=>x.info.bits<16),b16=ok.filter(x=>x.info.bits===16);
L.push(lowSr.length?["bad",t("ck_sr"),t("ck_srBad",{n:lowSr.length})]:["ok",t("ck_sr"),t("ck_srOk")]);
L.push(lowB.length?["bad",t("ck_bits"),t("ck_bitsBad",{n:lowB.length})]:b16.length?["warn",t("ck_bits"),t("ck_bits16",{n:b16.length})]:["ok",t("ck_bits"),t("ck_bitsOk")]);
const scanning=ok.filter(x=>x.info.scanning),clipped=ok.filter(x=>x.info.clips>0),hot=ok.filter(x=>typeof x.info.peak==="number"&&x.info.peak>=0.966&&!(x.info.clips>0));
L.push(scanning.length?["pend",t("ck_clipPk"),t("ck_scan",{n:scanning.length})]:clipped.length?["warn",t("ck_clip"),t("ck_clipD",{l:clipped.map(x=>esc(x.title)).join(", ")})]:hot.length?["warn",t("ck_peak"),t("ck_peakD",{n:hot.length})]:["ok",t("ck_clipPk"),t("ck_noClip")]);
const miss=S.tracks.filter(x=>!x.file).length;if(miss)L.push(["warn",t("ck_f"),t("ck_fD",{n:miss})]);
}
const ss=sideStats(),bad=ss.filter(s=>s.st==="bad"),warn=ss.filter(s=>s.st==="warn"&&s.count),empty=ss.filter(s=>!s.count),lim=ss[0].lim;
const sl=a=>a.map(s=>t("sideN",{n:s.n})+" "+mmss(s.sec)).join(", ");
L.push(bad.length?["bad",t("ck_side"),t("ck_sideBad",{l:sl(bad),max:mmss(lim[1]),s:S.size,r:rpmT()})]
:warn.length?["warn",t("ck_side"),t("ck_sideWarn",{l:sl(warn),a:mmss(lim[0])})]
:empty.length?["warn",t("ck_side"),t("ck_sideEmpty",{l:empty.map(s=>s.n).join(", ")})]:["ok",t("ck_side"),t("ck_sideOk",{a:mmss(lim[0])})]);
const artRow=(k,h)=>{const c=artChecks(k);return !c?["pend",h,t("ck_notYet")]:c.some(x=>x[0]==="bad")?["bad",h,c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn",h,c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok",h,t("ck_artOk")];};
if(jacketArt())L.push(artRow("jacket",t("ck_jArt")));
if(S.labels==="printed")L.push(artRow("label",t("ck_lArt")));
L.push(["pend",t("ck_eng"),t("ck_engD")]);
return L;
}
const canApprove=()=>!checkList().some(x=>x[0]==="bad")&&S.rights&&S.tracks.length>0;
function approveHint(){const bad=checkList().filter(x=>x[0]==="bad").length;return bad?t("apHintBad"):!S.rights?t("apHintRights"):"";}
/* ---- mock-up, drawn to scale ---- */
function mockSVG(id){
const dark=S.colour==="black",base=dark?"#141414":S.c1,groove=dark?"rgba(255,255,255,0.06)":"rgba(0,0,0,0.10)";
const Z=SIZE_MM[S.size],PX=236/314,JW=Z.jk*PX,dR=Z.rec*PX/2,s=dR/105,rl=105*Z.lab/Z.rec,fs=rl/34.8;
const GY=262,JX=18,JY=GY-JW,cy=JY+JW/2,cx=JX+JW+rl*s+10;
const pt=a=>[(105*Math.cos(a*Math.PI/180)).toFixed(1),(105*Math.sin(a*Math.PI/180)).toFixed(1)];
const wedge=(a1,a2)=>{const p=pt(a1),q=pt(a2);return `<path d="M0 0 L${p[0]} ${p[1]} A105 105 0 0 1 ${q[0]} ${q[1]} Z" fill="#fff" opacity="${dark?0.07:0.12}"/>`;};
const defs=`<clipPath id="${id}c"><circle r="105"/></clipPath><clipPath id="${id}l"><circle r="${rl.toFixed(1)}"/></clipPath>`+
`<radialGradient id="${id}sh" cx="0.35" cy="0.3" r="0.9"><stop offset="0" stop-color="#fff" stop-opacity="0.12"/><stop offset="0.55" stop-color="#fff" stop-opacity="0"/></radialGradient>`+
`<filter id="${id}m" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence type="fractalNoise" baseFrequency="0.008 0.03" numOctaves="3" seed="4" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 0 0 0 -3.3" result="m"/><feFlood flood-color="${S.c2}" result="f"/><feComposite in="f" in2="m" operator="in"/></filter>`+
`<linearGradient id="${id}g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${dark?"#ff7a45":S.c1}"/><stop offset="0.55" stop-color="${dark?"#7a2a8c":"#3a2a55"}"/><stop offset="1" stop-color="#14162b"/></linearGradient>`+
`<filter id="${id}sd" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="0" dy="8" stdDeviation="7" flood-color="#000" flood-opacity="0.35"/></filter>`+
`<radialGradient id="${id}fl" cx="0.5" cy="0.5" r="0.5"><stop offset="0" stop-color="#000" stop-opacity="0.35"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient>`;
function disc(x){
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+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.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">${esc(t("mk_side",{r:t("rpmS"+S.rpm)}))}</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*(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;
if(S.jacket==="gatefold")jk+=`<rect x="-6" y="-6" width="${J}" height="${J}" fill="#24183a"/><rect x="-6" y="-6" width="4" height="${J}" fill="#000" opacity="0.3"/>`;
if(S.jacket==="none")jk+=`<rect x="0" y="0" width="${J}" height="${J}" rx="2" fill="none" style="stroke:var(--faint)" stroke-dasharray="6 5"/><text x="${J/2}" y="${J/2+4}" text-anchor="middle" font-size="13" style="fill:var(--mute)" font-family="system-ui,sans-serif">${esc(t("mk_own"))}</text>`;
else if(S.art.jacket&&S.art.jacket.url&&S.jacket!=="plain")jk+=`<image href="${S.art.jacket.url}" x="0" y="0" width="${J}" height="${J}" preserveAspectRatio="xMidYMid slice"/>`;
else if(S.jacket==="plain")jk+=`<rect x="0" y="0" width="${J}" height="${J}" fill="#d8c3a0"/><circle cx="${J/2}" cy="${J/2}" r="44" fill="none" stroke="#b9a27c" stroke-width="2"/><circle cx="${J/2}" cy="${J/2}" r="3" fill="#b9a27c"/>`;
else jk+=`<rect x="0" y="0" width="${J}" height="${J}" fill="url(#${id}g)"/><circle cx="168" cy="72" r="52" fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="12"/><circle cx="168" cy="72" r="20" fill="rgba(255,255,255,0.16)"/><text x="18" y="${J-44}" font-size="19" font-weight="800" fill="#fff" font-family="system-ui,sans-serif" letter-spacing="-0.3">${esc(S.title.slice(0,20))}</text><text x="18" y="${J-22}" font-size="12.5" fill="rgba(255,255,255,0.82)" font-family="system-ui,sans-serif">${esc(S.artist.slice(0,30))}</text>`;
if(singleJ()){const sw=spineMM()*J/Z.jk;jk+=`<rect class="spn" x="${(-sw).toFixed(2)}" y="0" width="${sw.toFixed(2)}" height="${J}" fill="${S.jacket==="plain"?"#a88f68":"#070b14"}"/>`;}
if(S.jacket!=="none"){
jk+=`<rect x="0" y="0" width="${J}" height="${J}" fill="none" stroke="rgba(255,255,255,0.10)"/>`;
if(S.shrink)jk+=`<polygon points="0,0 100,0 0,100" fill="#fff" opacity="0.08"/><polygon points="${J},${J} ${J-60},${J} ${J},${J-60}" fill="#fff" opacity="0.05"/>`;
if(S.sticker)jk+=`<circle cx="${J-32}" cy="30" r="21" fill="#ffd23f"/><text x="${J-32}" y="34" text-anchor="middle" font-size="10.5" font-weight="800" fill="#222" font-family="system-ui,sans-serif">${esc(t("mk_new"))}</text>`;
if(S.numbered)jk+=`<text x="${J-9}" y="${J-9}" text-anchor="end" font-size="9.5" fill="#fff" opacity="0.88" font-family="ui-monospace,monospace">No. 001</text>`;
}
const ghost=S.size==="12"?"":`<rect x="${JX}" y="${GY-236}" width="236" height="236" fill="none" style="stroke:var(--stroke)" stroke-dasharray="5 5"/><text x="${JX+8}" y="${GY-222}" font-size="10" style="fill:var(--faint)" font-family="system-ui,sans-serif">${esc(t("mk_scale"))}</text>`;
const floor=`<ellipse cx="${(JX+cx+dR)/2}" cy="${GY+6}" rx="${((cx+dR-JX)/2+10).toFixed(1)}" ry="9" fill="url(#${id}fl)"/>`;
const cap=`<text x="452" y="286" text-anchor="end" font-size="10" style="fill:var(--faint)" font-family="system-ui,sans-serif">${esc(t("mk_cap",{n:szName(),r:Z.rec,j:Z.jk,s:spineMM()}))}</text>`;
return `<svg viewBox="0 0 460 292" role="img" aria-label="${esc(t("mk_aria",{n:szName()}))}"><defs>${defs}</defs>${floor}${ghost}${recs}<g transform="translate(${JX} ${JY.toFixed(1)}) scale(${(JW/236).toFixed(3)})"${S.jacket!=="none"?` filter="url(#${id}sd)"`:""}>${jk}</g>${cap}</svg>`;
}
function miniDisc(st){
const base=st==="black"?"#141414":S.c1;let p="";
if(st==="marbled")p=`<path d="M-19 -5 C-9 -17 4 7 19 -7" stroke="${S.c2}" stroke-width="5" fill="none" opacity="0.9"/><path d="M-16 10 C-6 2 6 18 17 8" stroke="${S.c2}" stroke-width="3.5" fill="none" opacity="0.8"/>`;
if(st==="splatter"){const r=rnd(3);for(let i=0;i<16;i++){const a=r()*6.283,d=8+r()*12;p+=`<circle cx="${(Math.cos(a)*d).toFixed(1)}" cy="${(Math.sin(a)*d).toFixed(1)}" r="${(0.8+r()*1.8).toFixed(1)}" fill="${i%4?S.c2:"#fff"}"/>`;}}
return `<svg viewBox="-23 -23 46 46" width="52" height="52" aria-hidden="true"><circle r="22" fill="${base}"/>${p}<circle r="19" fill="none" stroke="rgba(255,255,255,0.1)"/><circle r="7" fill="#efe6d2"/><circle r="1.1" fill="#111"/></svg>`;
}
function codeSVG(seed){
const n=21,r=rnd(seed);let s="";
const fin=(x,y)=>`<rect x="${x}" y="${y}" width="7" height="7" class="q1"/><rect x="${x+1}" y="${y+1}" width="5" height="5" class="q0"/><rect x="${x+2}" y="${y+2}" width="3" height="3" class="q1"/>`;
for(let y=0;y<n;y++)for(let x=0;x<n;x++){if((x<8&&y<8)||(x>=n-8&&y<8)||(x<8&&y>=n-8))continue;if(r()<0.47)s+=`<rect x="${x}" y="${y}" width="1" height="1" class="q1"/>`;}
return `<svg viewBox="-2 -2 ${n+4} ${n+4}" class="qr" role="img" aria-label="${esc(t("qrAria"))}"><rect x="-2" y="-2" width="${n+4}" height="${n+4}" class="q0"/>${fin(0,0)}${fin(n-7,0)}${fin(0,n-7)}${s}</svg>`;
}
/* ---- price curve ---- */
function curveSVG(){
const W=640,H=210,pl=56,pr=14,pt=18,pb=30,lo=100,hi=2000,qs=[];for(let q=lo;q<=hi;q+=10)qs.push(q);
const ys=qs.map(q=>quote(q).per),top=Math.max(...ys),unit=rate(),step=[5,10,20,25,50,100,200,250,500].find(s=>top/unit/s<=5)||1000,ymax=Math.ceil(top/unit/step)*step*unit;
const X=q=>pl+(Math.log(q)-Math.log(lo))/(Math.log(hi)-Math.log(lo))*(W-pl-pr),Y=v=>pt+(1-v/ymax)*(H-pt-pb);
let g="";for(let v=0;v<=ymax+1e-6;v+=step*unit)g+=`<line class="cv-grid" x1="${pl}" x2="${W-pr}" y1="${Y(v).toFixed(1)}" y2="${Y(v).toFixed(1)}"/><text class="cv-t" x="${pl-8}" y="${(Y(v)+4).toFixed(1)}" text-anchor="end">${money(v,{bare:true,dec:0})}</text>`;
[100,200,300,500,1000,2000].forEach(q=>g+=`<text class="cv-t" x="${X(q).toFixed(1)}" y="${H-pb+17}" text-anchor="middle">${fmtInt(q)}</text>`);
const d=qs.map((q,i)=>(i?"L":"M")+X(q).toFixed(1)+" "+Y(ys[i]).toFixed(1)).join(" ");
const area=d+` L${X(hi).toFixed(1)} ${Y(0).toFixed(1)} L${X(lo).toFixed(1)} ${Y(0).toFixed(1)} Z`;
const q=Math.min(hi,Math.max(lo,S.qty)),v=quote(q).per,mx=X(q),my=Y(v),right=mx>W-190;
return `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(t("cv_aria"))}">${g}<path class="cv-area" d="${area}"/><path class="cv-line" d="${d}"/><circle class="cv-dot" cx="${mx.toFixed(1)}" cy="${my.toFixed(1)}" r="6"/><text class="cv-m" x="${(mx+(right?-12:12)).toFixed(1)}" y="${(my-12).toFixed(1)}" text-anchor="${right?"end":"start"}">${t("cv_mark",{q:fmtInt(q),m:money(v)})}</text><text class="cv-t" x="${W-pr}" y="${pt-5}" text-anchor="end">${t("cv_axis",{c:S.cur})}</text></svg>`;
}
/* ---- renderers ---- */
function syncControls(){
$$("[data-k]").forEach(g=>{const k=g.dataset.k,btns=g.matches("button")?[g]:Array.from(g.querySelectorAll("button[data-v]"));
btns.forEach(b=>{const on=String(S[k])===b.dataset.v;b.classList.toggle("on",on);b.setAttribute("aria-pressed",on?"true":"false");});});
$$(".chip[data-b]").forEach(c=>{const on=!!S[c.dataset.b];c.classList.toggle("on",on);c.setAttribute("aria-pressed",on?"true":"false");});
const w12=S.size==="12",blk=S.colour==="black",two=S.colour==="marbled"||S.colour==="splatter";
$$("#fWeight button").forEach(x=>{x.disabled=!w12;x.title=w12?"":t("noWeight");});
$("#fWeight").classList.toggle("off",!w12);
const g=$("#bGate");g.disabled=!w12;g.title=w12?"":t("noGate");
$$("[data-sub]").forEach(el=>{const m=LIM[el.dataset.sub][S.rpm][1],n=Math.max(1,Math.round(m*60/SONG));el.textContent=t("fmtSub",{m,r:t("rpmS"+S.rpm),n});});
$$("[data-mini]").forEach(el=>el.innerHTML=miniDisc(el.dataset.mini));
[["c1",blk,t("noC1")],["c2",!two,t("noC2")]].forEach(([c,off,why])=>{
const row=$(`.sw-row[data-c="${c}"]`);$(c==="c1"?"#fC1":"#fC2").classList.toggle("off",off);
row.querySelectorAll(".swatch").forEach(b=>{b.disabled=off;b.classList.toggle("on",!off&&b.dataset.col.toLowerCase()===S[c].toLowerCase());b.title=off?why:t("c_"+b.dataset.name);b.setAttribute("aria-label",t("c_"+b.dataset.name));});
const inp=row.querySelector("input[type=color]"),pal=row.querySelector(".pal"),custom=!Array.from(row.querySelectorAll(".swatch")).some(b=>b.dataset.col.toLowerCase()===S[c].toLowerCase());
inp.disabled=off;pal.title=off?why:t("palTitle");inp.setAttribute("aria-label",t("palAria"));pal.classList.toggle("off",off);pal.classList.toggle("on",!off&&custom);pal.style.setProperty("--cc",S[c]);
pal.querySelector(".pt").textContent=!off&&custom?t("palYours"):t("palName");if(inp.value.toLowerCase()!==S[c].toLowerCase())inp.value=S[c];});
$("#tpsV").textContent=S.tps;
const sps=spineState();
$$("#fSpine 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?t("spTwo"):"";const on=S.jacket!=="gatefold"&&S.jacket!=="none"&&(thickOn()?!thin:thin);x.classList.toggle("on",on);x.setAttribute("aria-pressed",on?"true":"false");});
$("#fSpine").classList.toggle("off",sps.off);$("#spineHint").textContent="· "+sps.hint;
const s7=S.size==="7";$$("#fDink button").forEach(x=>{x.disabled=!s7;x.title=s7?"":t("noDink");});$("#fDink").classList.toggle("off",!s7);
$$("#fClean button").forEach(x=>{x.disabled=blk;x.title=blk?t("noClean"):"";});$("#fClean").classList.toggle("off",blk);
$("#cleanHint").textContent=t(blk?"clOff":S.cleaning?"clOn":"clNo");
const lq=S.source==="lacquer";$$(".choice[data-k=mastering]").forEach(x=>{x.disabled=lq;x.title=lq?t("noMast"):"";if(lq){x.classList.remove("on");x.setAttribute("aria-pressed","false");}});
$("#srcHint").textContent=t(lq?"srcLac":S.source==="tape"?"srcTape":"srcDig");
const dl=$("#dropLabel"),offL=S.labels!=="printed";dl.classList.toggle("off",offL);$("#fileLabel").disabled=offL;dl.title=offL?t("noLabelArt"):"";
const dj=$("#dropJacket"),offJ=!jacketArt();dj.classList.toggle("off",offJ);$("#fileJacket").disabled=offJ;dj.title=offJ?t(S.jacket==="none"?"noJacketOwn":"noJacketPlain"):"";
$$(".qchips .chip").forEach(c=>c.classList.toggle("on",+c.dataset.q===S.qty));
}
function renderNav(){
const delivered=!!(S.prod&&S.prod.idx>=S.prod.rows.length);
const done={1:!!S.visited[2],2:S.gate1,3:!!S.mode,4:S.launched,5:delivered};
$$("#steps button").forEach(b=>{const s=+b.dataset.s;b.classList.toggle("on",s===S.step);b.classList.toggle("done",!!done[s]&&s!==S.step);if(s===S.step)b.setAttribute("aria-current","step");else b.removeAttribute("aria-current");});
$$("section.step").forEach(x=>x.hidden=+x.dataset.step!==S.step);
$("#prog").style.width=(S.step/5*100)+"%";
$(".app").classList.toggle("full",S.step===5);
}
function fileInfo(x){
if(!x.file)return "";
const i=x.info||{};if(i.reading)return t("fiReading");
if(!i.lossless)return `<span class="bad-t">${esc(t("fiLossy",{f:i.fmt||"?"}))}</span>`;
let pk="";if(i.scanning)pk=t("fiScan");else if(typeof i.peak==="number")pk=t("fiPeak",{p:i.peak>0?(20*Math.log10(i.peak)).toFixed(1):"-inf"})+(i.clips?t("fiClips"):"");
return `${i.fmt} ${i.bits}-bit ${(i.sr/1000).toFixed(i.sr%1000?1:0)} kHz${pk}`;
}
function renderTracks(){
const names=sideNames();
$("#trk").innerHTML=S.tracks.map((x,i)=>`<div class="tr" data-id="${x.id}"><span class="no">${i+1}</span><select class="tside" aria-label="${t("aSide")}">${names.map(n=>`<option${n===x.side?" selected":""}>${n}</option>`).join("")}</select><input type="text" class="tt" value="${esc(x.title)}" aria-label="${t("aTitle")}"><input type="text" class="tdur" value="${x.sec?mmss(x.sec):""}" placeholder="m:ss" aria-label="${t("aLen")}"${x.file&&x.info&&x.info.sec?` readonly title="${t("fromFile")}"`:""}><span class="fi">${fileInfo(x)}</span><button type="button" class="x" data-act="rm" title="${t("rm")}" aria-label="${t("rm")}">&#xD7;</button></div>`).join("");
}
function renderSides(){
const ss=sideStats(),L=ss[0].lim;
$("#sideHint").textContent=t("sideHint",{s:S.size,r:rpmT(),loud:S.loud,a:mmss(L[0]),b:mmss(L[1]),n:Math.max(1,Math.round(L[1]/SONG))});
const lbl={ok:t("sb_ok"),warn:t("sb_warn"),bad:t("sb_bad")};
$("#sides").innerHTML=ss.map(s=>{const tr=S.tracks.filter(t=>t.side===s.n);let x=0;
const segs=tr.map((t,i)=>{const w=Math.max(0,Math.min(100-x,(t.sec||0)/L[1]*100)),el=`<i class="s${i%2}" style="left:${x.toFixed(2)}%;width:${w.toFixed(2)}%" title="${esc(t.title)} ${mmss(t.sec)}"></i>`;x+=w;return el;}).join("");
return `<div class="side ${s.st}"><div class="sh"><b>${t("sideN",{n:s.n})}</b><span>${t("nTracks",{n:s.count})}</span><span class="pill ${s.count?s.st:"warn"}">${mmss(s.sec)} · ${s.count?lbl[s.st]:t("sb_empty")}</span></div><div class="sbar">${segs}<b class="mk" style="left:${(L[0]/L[1]*100).toFixed(1)}%" title="${t("sbMark")}"></b></div></div>`;}).join("");
}
function renderArt(){
const ic={ok:"&#x2713;",warn:"!",bad:"&#x2715;",pend:"&#x25CB;"};
[["jacket","#tplJacket"],["label","#tplLabel"]].forEach(([k,sel])=>{const off=tplOff(k);$(sel).innerHTML=`<button type="button" class="lnk" data-act="tpl" data-k="${k}"${off?` disabled title="${esc(off)}"`:""}>${t("tplLink",{k,gf:S.jacket==="gatefold"})}</button> (PDF): ${off?esc(S.lang==="en"?off.toLowerCase():off):esc(tplDims(k).txt)}`;});
[["jacket","#artJacket"],["label","#artLabel"]].forEach(([k,sel])=>{const a=S.art[k];if(!a){$(sel).innerHTML="";return;}const cs=artChecks(k);
$(sel).innerHTML=`<div class="art">${artThumb(a)}<div><small class="mute">${esc(a.name)}</small><ul>${cs.map(([s,m])=>`<li><span class="ci ${s}" style="width:18px;height:18px;font-size:10px">${ic[s]}</span>${esc(m)}</li>`).join("")}${artNote(a)}</ul><button type="button" class="lnk" data-act="artRm" data-k="${k}">${t("rm")}</button></div></div>`;});
}
function renderChecks(){
const ic={ok:"&#x2713;",warn:"!",bad:"&#x2715;",pend:"&#x25CB;"},L=checkList(),bad=L.filter(x=>x[0]==="bad").length,warn=L.filter(x=>x[0]==="warn").length;
$("#checkHead").innerHTML=bad?`<span class="ci bad">&#x2715;</span><b>${t("chFix",{n:bad})}</b>`:warn?`<span class="ci warn">!</span><b>${t("chLook",{n:warn})}</b>`:`<span class="ci ok">&#x2713;</span><b>${t("chReady")}</b>`;
$("#checks").innerHTML=L.map(([s,h,d])=>`<li><span class="ci ${s}">${ic[s]}</span><div><b>${h}</b><small>${d}</small></div></li>`).join("");
const b=$("#bApprove"),can=canApprove();
b.disabled=!can&&!S.gate1;
b.innerHTML=t(S.gate1?"apSee":"apApprove");
$("#gateMsg").textContent=S.gate1?t("gateDone"):bad?t("gateBad"):!S.rights?t("gateRights"):t("gateReady");
$("#rights").checked=S.rights;
}
function renderPrice(){
const Q=quote(S.qty),L=leadInfo(false),ship=[addDays(TODAY,L.tw[0]*7),addDays(TODAY,L.tw[1]*7)];
if(document.activeElement!==$("#qty"))$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
$("#kpis").innerHTML=`<div class="kpi hl"><small>${t("kPer")}</small><b>${money(Q.per)}</b></div><div class="kpi"><small>${t("kTotal")}</small><b>${money(Q.total)}</b></div><div class="kpi span2"><small>${t("kShips")}</small><b class="wr">${monthRange(ship[0],ship[1])}</b></div>`;
$("#curve").innerHTML=curveSVG();
$("#bdSum").textContent=t("bdSum",{a:money(Q.tSum),b:money(Q.cSum)});
const tr=x=>`<tr><td>${x.t}${x.pass!=null?`<br><span class="pass">${t("partner",{p:money(x.pass),h:Math.round(R.handling.v*100)})}</span>`:""}</td><td class="n">${x.u?x.n+" "+x.u:""}</td><td class="n">${money(x.a)}</td></tr>`;
$("#offer").innerHTML=`<table><tbody><tr><th>${t("thOnce")}</th><th class="n"></th><th class="n">${t("thAmt")}</th></tr>${Q.T.map(tr).join("")}<tr class="sub"><td colspan="2">${t("onceTot")}</td><td class="n">${money(Q.tSum)}</td></tr>
<tr><th>${t("thPer")}</th><th class="n"></th><th class="n">${t("thPer")}</th></tr>${Q.C.map(tr).join("")}<tr class="sub"><td colspan="2">${t("perRow",{c:money(Q.cSum),q:fmtInt(S.qty)})}</td><td class="n">${money(Q.cSum*S.qty)}</td></tr>
<tr class="tot"><td colspan="2">${t("kTotal")}</td><td class="n">${money(Q.total)}</td></tr></tbody></table><p class="hint">${t("offerHint")}</p>`;
$("#leadP").innerHTML=`<h2>${t("whenShips")}</h2><div class="tl2"><div class="a" style="flex:${L.pw[0]+L.pw[1]}"><b>${t("weeks",{a:L.pw[0],b:L.pw[1]})}</b>${t("lpA")}</div><div class="b" style="flex:${L.qw[0]+L.qw[1]}"><b>${t("weeksQ",{a:L.qw[0],b:L.qw[1]})}</b>${t("lpB")}</div></div><p class="hint">${t("leadHint",{r:monthRange(ship[0],ship[1])})}${t(L.printed?"leadOwn":"leadNoJ")}</p>`;
const E=campEcon();
$("#pDirect").innerHTML=t("deposit",{m:money(Q.total*R.deposit.v)});
$("#pFan").innerHTML=E.goal?t("pFan",{n:E.goal,p:money(S.fanPrice),k:money(E.at(E.goal).per)}):t("pFanNo");
$("#bDirect").title=S.gate1?"":t("needGate");
$("#choiceMsg").innerHTML=S.gate1?"":`${t("choiceMsg")}<button type="button" class="lnk" data-act="go" data-s="2">${t("goStep2")}</button>`;
}
const canLaunch=()=>{const E=campEcon();return S.gate1&&E.goal!=null&&!(S.cap&&S.cap<E.goal);};
function launchHint(){const E=campEcon();return !S.gate1?t("lhGate"):E.goal==null?t("lhPrice"):S.cap&&S.cap<E.goal?t("lhCap"):"";}
function renderFund(){
const E=campEcon();$("#daysV").textContent=S.days;
const fi=$("#fanPrice"),frg=$("#fanR"),fr=FAN_RNG[S.cur];
if(fi.dataset.cur!==S.cur){const a=FAN_IN[S.cur];fi.dataset.cur=S.cur;fi.min=a[0];fi.max=a[1];fi.step=a[2];frg.min=fr[0];frg.max=fr[1];frg.step=fr[2];$("#fpCur").textContent=S.cur;
$("#fpChips").innerHTML=FP[S.cur].map(v=>`<button type="button" class="chip" data-fp="${v}">${fmtInt(v)}</button>`).join("");}
if(document.activeElement!==fi)fi.value=fanIn();frg.value=Math.min(fr[1],Math.max(fr[0],fanIn()));
let h="";
if(E.goal==null)h=`<div class="note bad">${t("fe_bad",{p:money(E.p),vat:S.vat})}</div>`;
else{
const g=E.at(E.goal),ns=[...new Set([E.goal,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b),rows=ns.map(E.at),mx=Math.max(...rows.map(r=>r.keep),1);
h=`<div class="kpis k4"><div class="kpi hl"><small>${t("fe_need")}</small><b>${t("nFans",{n:E.goal})}</b><em>${t("fe_pre",{m:money(E.goal*S.fanPrice)})}</em></div><div class="kpi"><small>${t("fe_be")}</small><b>${t("nFans",{n:E.be})}</b></div><div class="kpi"><small>${t("fe_keepG")}</small><b>${money(g.per)}</b><em>${t("aCopy")}</em></div><div class="kpi"><small>${t("fe_that")}</small><b>${money(g.keep)}</b><em>${t("fe_tot")}</em></div></div>
${S.cap&&S.cap<E.goal?`<div class="note bad">${t("fe_cap",{c:S.cap,g:E.goal})}</div>`:""}
<div class="panel"><h2>${t("fe_more")}</h2><div class="keep">${rows.map(r=>`<div class="krow${r.n===E.goal?" goal":""}"><span>${t("nFans",{n:fmtInt(r.n)})}</span><div class="kb"><i style="width:${Math.max(2,r.keep/mx*100).toFixed(1)}%"></i></div><div class="kv"><b>${money(r.keep)}</b><small>${t("perCopyX",{m:money(r.per)})}</small></div></div>`).join("")}</div>
<p class="hint">${t("fe_hint",{f:(R.fee.v*100).toFixed(1),vat:S.vat})}</p></div>`;
}
$("#fundEcon").innerHTML=h;
$$("#fpChips .chip").forEach(c=>c.classList.toggle("on",Math.abs(toTHB(+c.dataset.fp)-S.fanPrice)<0.01));
/* the lowest fan price at which the plant minimum covers the costs; below it the break-even sets the fans needed */
const ac=S.artistCopies,keepShare=1-R.fee.v-(S.vat?7/107:0),pRaw=(E.tSum+(E.min+ac)*E.cAt(E.min+ac))/E.min/keepShare;
const pMin=S.cur==="THB"?Math.ceil(pRaw/10)*10:Math.ceil(pRaw/rate())*rate(),pMinT=money(pMin,{dec:0});
$("#need").innerHTML=E.goal==null?`<span class="bad-t">${t("needBad")}</span>`:t("need",{n:fmtInt(E.goal),m:money(E.goal*S.fanPrice)});
$("#needHint").textContent=E.goal==null?t("nhRaise"):E.be>E.min?t("nhBe",{be:E.be,min:E.min,p:pMinT}):t("nhMin",{min:E.min,p:pMinT});
$("#fCap").classList.toggle("off",!S.limited);$("#cap").disabled=!S.limited;$("#cap").title=S.limited?"":t("capOff");
$("#bLaunch").disabled=!canLaunch();
$("#launchMsg").textContent=launchHint()||t("launchSim");
}
/* ---- step 5: campaign simulation and production ---- */
let s5mode=null,TIMER=null,PRODT=null,AU=null,AUid=null,AUt=null;
function statusLine(){
if(S.prod){const P=S.prod;if(P.idx>=P.rows.length)return t("stDone");const r=P.rows[P.idx];return r.gate?t("stWait",{s:t("gate_"+r.id)}):t("stNow",{s:stgT(r)});}
if(S.mode==="campaign"){const E=campEcon(),n=pledgesNow(E);return t("stDay",{d:Math.floor(S.simDay),n:S.days,f:fmtInt(n),g:E.goal||0});}
return "";
}
function pledgesNow(E){const P=finalPledges(E);return S.simDay>=S.days?P:Math.round(P*frac(S.simDay/S.days));}
function renderS5(){
const el=$("#s5"),m=S.mode||"none";
$("#s5h").textContent=t(m==="direct"?"s5Dir":m==="campaign"?"s5Camp":"s5None");
$("#s5l").textContent=m==="none"?"":t(m==="direct"?"s5lDir":"s5lCamp");
if(m!==s5mode){s5mode=m;
if(m==="none")el.innerHTML=`<div class="panel empty"><h3>${t("emptyH")}</h3><p class="mute">${t("emptyP")}</p><div class="rowc"><button type="button" class="btn" data-act="quickCamp">${t("quickCamp")}</button><button type="button" class="btn ghost" data-act="quickDone">${t("quickDone")}</button></div></div>`;
else if(m==="direct")el.innerHTML=`<div class="panel block" id="dirSum"></div><div id="tracker"></div>`;
else el.innerHTML=`<div class="s5grid"><div class="phone"><div class="notch"></div><div class="scr" id="fanPage"></div></div>
<div><div class="panel block"><h2>${t("runCamp")} <small>${t("simulated")}</small></h2><div class="seg" data-k="scen"><button type="button" data-v="sell">${t("scSell")}</button><button type="button" data-v="make">${t("scMake")}</button><button type="button" data-v="miss">${t("scMiss")}</button></div>
<div class="simrow"><button type="button" class="btn ghost sm" data-act="play" id="playBtn"></button><input type="range" id="simDay" min="0" step="0.1" aria-label="${t("aDay")}"><span id="dayLbl" class="hint"></span></div><div class="kpis" id="money"></div>
<div class="mini">${t("latest")}</div><ul class="ticker" id="ticker"></ul></div><div id="outcome"></div></div></div><div id="tracker"></div>`;
}
updateS5();
}
function trackerHTML(){
const P=S.prod;if(!P)return "";
const rows=P.rows.map((s,i)=>{const st=i<P.idx?"done":i===P.idx?"now":"next";
const act=i===P.idx?(s.gate?`<button type="button" class="btn sm" data-act="adv">${t("trApprove")}</button>`:`<button type="button" class="btn ghost sm" data-act="adv">${t("trNext")}</button>`):"";
return `<li class="${st}${s.gate?" gate":""}"><span class="dot"><span>${i<P.idx?"&#x2713;":""}</span></span><div><b>${stgT(s)}</b>${s.gate?`<span class="gtag">${t("trGate",{g:s.gate})}</span>`:""}<small>${i<P.idx?t("trDone",{d:fmtD(s.date)}):t(s.gate&&i===P.idx?"trWait":"trPlan",{d:fmtD(s.date)})}</small></div>${act}</li>`;}).join("");
const feed=P.events.slice().reverse().map(e=>`<li><small>${fmtD(e.date)}</small>${evText(e,P)}</li>`).join("");
const fin=P.idx>=P.rows.length;
return `<div class="pgrid" id="prodGrid"><div class="panel"><div class="ph"><h2>${t("trProd")} <small>${t("trCopies",{n:fmtInt(P.run)})}</small></h2>${fin?`<span class="pill ok">${t("trDelivered")}</span>`:`<button type="button" class="btn ghost sm" data-act="prodPlay">${PRODT?t("pause"):t("trPlayNext")}</button>`}</div><ol class="tl">${rows}</ol></div>
<div class="panel"><h2>${t(P.origin==="direct"?"trUpd":"trUpdFans")} <small>${t("trLog")}</small></h2><ul class="feed">${feed}</ul><p class="hint">${t("trHint")}</p></div></div>${fin?passportHTML():""}`;
}
function newProd(origin,start,fans){
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)});});
const run=origin==="direct"?S.qty:origin==="minimum"?Math.max(fans,R.minRun.v)+S.artistCopies:fans+S.artistCopies;
S.prod={origin,fans,run,rows,idx:0,start:new Date(start),events:[{date:new Date(start),id:"start"}]};
}
function advance(){const P=S.prod;if(!P||P.idx>=P.rows.length)return;const s=P.rows[P.idx];P.events.push({date:s.date,id:s.id});P.idx++;if(P.idx>=P.rows.length)stopProd();renderAll();}
function stopProd(){if(PRODT){clearInterval(PRODT);PRODT=null;}}
function toggleProd(){
if(PRODT){stopProd();renderAll();return;}
const P=S.prod;if(!P)return;
if(P.idx<P.rows.length&&P.rows[P.idx].gate)advance();
PRODT=setInterval(()=>{const Q=S.prod;if(!Q||Q.idx>=Q.rows.length||Q.rows[Q.idx].gate){stopProd();renderAll();return;}advance();},800);
renderAll();
}
function stopSim(){if(TIMER){clearInterval(TIMER);TIMER=null;}}
function resetSim(){stopSim();stopProd();S.simDay=0;S.outcome=null;if(S.mode==="campaign")S.prod=null;S.outcomeAct=null;}
function resetCampaign(){stopSim();stopProd();S.mode=null;S.launched=false;S.simDay=0;S.outcome=null;S.outcomeAct=null;S.prod=null;}
function revokePlan(){const was=S.gate1||S.mode;S.gate1=false;resetCampaign();return was;}
function checkEnd(){
if(S.simDay>=S.days){if(!S.outcome){const E=campEcon(),P=finalPledges(E);S.finalFans=P;S.outcome=P>=E.goal?"funded":"missed";if(S.outcome==="funded")newProd("campaign",addDays(S.launchDate,S.days),P);}}
else if(S.outcome){S.outcome=null;S.prod=null;S.outcomeAct=null;stopProd();}
}
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();renderAll();},100);
}
function toggleSim(){if(TIMER){stopSim();renderAll();return;}startSim();renderAll();}
function updateS5(){
if(S.mode==="direct"){
const Q=quote(S.qty);
$("#dirSum").innerHTML=`<h2>${t("dirH")}</h2><div class="kvl"><span>${t("dirCopies",{n:fmtInt(S.qty),d:esc(descT())})}</span><b>${money(Q.total)}</b><span>${t("dirPaid")}</span><b>${money(Q.total*R.deposit.v)}</b><span>${t("dirBal")}</span><b>${money(Q.total*(1-R.deposit.v))}</b></div><p class="hint">${t("dirHint")}</p>`;
$("#tracker").innerHTML=trackerHTML();return;
}
if(S.mode!=="campaign")return;
const E=campEcon(),P=finalPledges(E),ended=S.simDay>=S.days,n=pledgesNow(E),goal=E.goal||0;
const close=addDays(S.launchDate,S.days),L=leadInfo(true),ship=[addDays(close,L.tw[0]*7),addDays(close,L.tw[1]*7)];
const sl=$("#simDay");sl.max=S.days;if(document.activeElement!==sl)sl.value=S.simDay;
$("#dayLbl").textContent=t("dayOf",{d:Math.floor(S.simDay),n:S.days});
$("#playBtn").innerHTML=TIMER?t("pause"):ended?t("replay"):t("play");
const funded=ended&&n>=goal,soldOut=S.cap&&n>=S.cap;
const status=!ended?t("stRefund",{g:goal,d:fmtD(close)})
:funded?t("stFunded",{n:fmtInt(n),so:soldOut})
:S.outcomeAct==="min"?t("stMin",{min:E.min}):S.outcomeAct==="refund"?t("stRefunded"):t("stMissed");
const tracks=sideNames().map(sd=>S.tracks.filter(x=>x.side===sd).map(x=>`<li><span class="sd">${sd}</span><button type="button" class="pv${AUid===x.id?" on":""}" data-act="pv" data-id="${x.id}"${x.url?` title="${t("pvPlay")}"`:` disabled title="${t("pvNone")}"`}>${AUid===x.id?"&#x25A0;":"&#x25B6;"}</button><span class="tn">${esc(x.title)}</span><small class="mute">${mmss(x.sec)}</small></li>`).join("")).join("");
$("#fanPage").innerHTML=`<div class="mock">${mockSVG("mf")}</div><div class="fp-t">${esc(S.title)}</div><div class="mute">${esc(S.artist)}</div><div class="hint">${esc(descT())}</div>
<div class="fp-p">${money(S.fanPrice)} <small>${t("shipX")}</small></div>${S.cap?`<div class="hint">${t("limitedOf",{n:fmtInt(S.cap)})}</div>`:""}
<div class="bar"><i class="${ended&&!funded?"bad":""}" style="width:${goal?Math.min(100,n/goal*100).toFixed(1):0}%"></i></div>
<div class="fp-n"><span>${t("ofFans",{n:fmtInt(n),g:fmtInt(goal)})}${n>goal?` · ${Math.round(n/goal*100)} %`:""}</span><span class="mute">${ended?t("closed"):t("daysLeft",{n:Math.max(0,Math.ceil(S.days-S.simDay))})}</span></div>
<button type="button" class="btn wide" data-act="preorder"${ended?" disabled":""}>${ended?t("preClosed"):t("preorder")}</button>
<p class="fp-s">${status}</p><div class="kvl" style="margin:4px 0"><span>${t("estShip")}</span><b>${monthRange(ship[0],ship[1])}</b></div>
<div class="mini">${t("fpTracks")}</div><ol class="fp-tr">${tracks}</ol>
<div class="share"><button type="button" class="btn ghost sm" data-act="copy">${t("copyLink")}</button><button type="button" class="btn ghost sm" disabled title="${t("lineT")}">LINE</button><button type="button" class="btn ghost sm" disabled title="${t("qrT")}">${t("qrB")}</button></div>
<p class="fp-f">${t("fpFine")}</p>`;
const held=n*S.fanPrice;
$("#money").innerHTML=`<div class="kpi hl"><small>${t("mFans")}</small><b>${fmtInt(n)} / ${fmtInt(goal)}</b></div><div class="kpi"><small>${t("mHeld")}</small><b>${money(held)}</b></div><div class="kpi"><small>${ended?t("mClosed"):t("mDays")}</small><b>${ended?close.toLocaleDateString(LOC(),{day:"numeric",month:"short"}):Math.max(0,Math.ceil(S.days-S.simDay))}</b></div>`;
const tk=[],cities=t("cities");let ago=0;for(let i=n;i>Math.max(0,n-4);i--){const r=rnd(i*7919+13);tk.push(`<li><span>${t("fanN",{i,c:cities[Math.floor(r()*cities.length)]})}</span><small>${ago?t("minAgo",{n:ago}):t("justNow")}</small></li>`);ago+=2+Math.floor(r()*6);}
$("#ticker").innerHTML=tk.join("")||`<li><small>${t("noPre")}</small></li>`;
let oc="";
if(ended&&goal){
if(funded){const A=E.at(n);
oc=`<div class="panel ok-card block"><h2>${t("ocFunded")}</h2><p class="mute" style="margin-top:0">${t("ocFundedP",{n:fmtInt(n)})}</p><div class="kvl"><span>${t("ocNet",{vat:S.vat})}</span><b>${money(A.net)}</b><span>${t("ocSetup")}</span><b>${money(-E.tSum)}</b><span>${t("ocPress",{n:fmtInt(A.run)})}</span><b>${money(-A.run*A.c)}</b><span><b style="color:var(--ink)">${t("ocKeep")}</b></span><b style="color:var(--acc);font-size:17px">${money(A.keep)}</b></div></div>`;}
else{const run=Math.max(n,E.min)+S.artistCopies,c=E.cAt(run),cost=E.tSum+run*c,net=n*E.net1,diff=net-cost,spare=Math.max(0,E.min-n);
if(S.outcomeAct==="refund")oc=`<div class="panel block"><h2>${t("ocRefunded")}</h2><p class="mute">${t("ocRefundedP",{n:fmtInt(n),p:money(S.fanPrice)})}</p><button type="button" class="btn ghost" data-act="retry">${t("ocRetry")}</button></div>`;
else if(S.outcomeAct==="min")oc=`<div class="panel ok-card block"><h2>${t("ocMinH")}</h2><p class="mute">${diff<0?t("ocPaid",{m:money(-diff)}):t("ocCovered",{m:money(diff)})}${t("ocSpare",{n:spare})}</p></div>`;
else oc=`<div class="panel warn-card block"><h2>${t("ocMissH",{n:fmtInt(n),g:fmtInt(goal)})}</h2><p class="mute" style="margin:0">${t("ocTwo")}</p><div class="opts"><div class="opt"><b>${t("ocRefundAll")}</b><p>${t("ocRefundAllP")}</p><button type="button" class="btn ghost sm" data-act="refund">${t("ocRefundBtn",{n:fmtInt(n)})}</button></div><div class="opt"><b>${t("ocGoMin",{min:E.min})}</b><p>${diff<0?t("ocPay",{m:money(-diff)}):t("ocFree",{m:money(diff)})}${t("ocUnclaimed",{n:spare})}</p><button type="button" class="btn sm" data-act="goMin">${t("ocGo")}</button></div></div></div>`;
}
}
$("#outcome").innerHTML=oc;
$("#tracker").innerHTML=trackerHTML();
}
function passportHTML(){
const P=S.prod,by=id=>P.rows.find(r=>r.id===id),seed=S.title+"|"+S.artist+"|"+P.run;
const copy=Math.min(P.run,42),num=S.numbered?t("ppNo",{c:String(copy).padStart(3,"0"),n:fmtInt(P.run)}):t("ppCopy",{c:copy,n:fmtInt(P.run)});
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=S.source==="lacquer"?[]:[[t(S.source==="tape"?"pp_tape":"pp_master"),t(S.source==="tape"?"pp_tapeD":"pp_masterD"),P.start]];
if(by("g2"))it.push([t("pp_mast"),t("pp_mastD"),by("g2").date]);
if(by("cut"))it.push([t("pp_cut"),t("pp_cutD",{l:lac,c:S.catno}),by("cut").date]);
else it.push([t("pp_lac"),t("pp_lacD"),P.start]);
it.push([t("pp_plate"),t("pp_plateD",{f:fa,m:mo,s:st}),by("plate").date]);
it.push([t("pp_tp"),t("pp_tpD"),by("g3").date]);
it.push([t("pp_press"),t("pp_pressD",{t:ts.slice(11),l:lot}),run.date]);
it.push([t("pp_insp"),t("pp_inspD"),run.date]);
it.push([t("pp_pack"),t("pp_packD",{j:S.jacket!=="none",s:S.shrink}),by("fin").date]);
it.push([t("pp_ship"),t(P.origin==="direct"?"pp_shipD":"pp_shipC"),by("ship").date]);
return `<div class="passport" id="passport"><div class="pp-head"><div><div class="eyebrow">${t("ppH")}</div><span class="pp-t">${num}</span><div class="mute">${esc(S.title)} · ${esc(S.artist)}</div></div>${codeSVG(hsh(seed))}</div>
<div class="pp-key"><span>${t("ppKey")}</span><code>${st}-A1 · D1-B · ${ts} · ${t("ppSide")}</code></div>
<ol class="pp-chain">${it.map(([a,d,dt])=>`<li><span class="ci ok">&#x2713;</span><div><b>${a}</b><small>${esc(d)}</small></div><span class="dt">${fmtD(dt)}</span></li>`).join("")}</ol>
<p class="hint">${t("ppHint")}</p>
<div class="repress"><p>${t("ppRepress")}</p><button type="button" class="btn ghost" data-act="repress">${t("ppRepressB")}</button></div></div>`;
}
function preview(id){
const t=S.tracks.find(x=>x.id===id);
if(AU){AU.pause();clearTimeout(AUt);const was=AUid;AU=null;AUid=null;if(was===id){renderAll();return;}}
if(!t||!t.url)return;
AU=new Audio(t.url);AUid=id;AU.play().catch(()=>{});AUt=setTimeout(()=>{if(AU){AU.pause();AU=null;AUid=null;renderAll();}},30000);
AU.addEventListener("ended",()=>{AU=null;AUid=null;renderAll();});renderAll();
}
/* ---- the stage (sticky summary) and its call to action ---- */
let CUR=null;
function cta(){
switch(S.step){
case 1:return {t:t("ctaS1"),m:t("ctaM1"),f:()=>go(2)};
case 2:return S.gate1?{t:t("ctaS2a"),m:t("ctaM2a"),f:()=>go(3)}:{t:t("ctaS2"),m:t("ctaM2"),f:approve,dis:!canApprove(),hint:approveHint()};
case 3:return {t:t("ctaS3"),m:t("ctaM3"),f:()=>go(4),hint:t("ctaH3")};
case 4:return {t:t("ctaS4"),m:t("ctaM4"),f:launch,dis:!canLaunch(),hint:launchHint()};
default:return null;
}
}
function renderStage(){
const Q=quote(S.qty),L=leadInfo(false),ship=[addDays(TODAY,L.tw[0]*7),addDays(TODAY,L.tw[1]*7)];
$("#mock").innerHTML=mockSVG("ms");
$("#stT").textContent=S.title||t("untitled");$("#stA").textContent=S.artist||"";
const sp=[(S.discs>1?"2 × ":"")+szName(),rpmT()];if(S.size==="12")sp.push(t("spW",{w:S.weight}));sp.push(colourName(),jname(S.jacket));
if(S.labels!=="printed")sp.push(t("spPlain",{c:t("lc_"+S.labels)}));if(S.catno)sp.push(t("spRunout",{c:S.catno}));if(S.source!=="digital")sp.push(t(S.source==="tape"?"spTape":"spLac"));
XN.forEach(k=>{if(S[k])sp.push(t("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;
if(camp){const E=campEcon();
pr=`<div class="sp"><div><small>${t("stFan")}</small><b class="pc">${money(S.fanPrice)}</b></div><div class="r"><small>${t("stNeeded")}</small><b>${E.goal?t("nFans",{n:E.goal}):t("stNot")}</b></div></div><div class="sl"><span>${t("stKeep")}</span><b>${E.goal?t("perCopyX",{m:money(E.at(E.goal).per)}):t("na")}</b></div>`;
mb=`<small>${t("mbFan")}</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?t("nFans",{n:E.goal}):""}</small>`;}
else{pr=`<div class="sp"><div><small>${t("kPer")}</small><b class="pc">${money(Q.per)}</b></div><div class="r"><small>${t("nCopies",{n:fmtInt(S.qty)})}</small><b>${money(Q.total)}</b></div></div><div class="sl"><span>${t("kShips")}</span><b>${monthRange(ship[0],ship[1])}</b></div>`;
mb=`<small>${t("mbCopies",{n:fmtInt(S.qty)})}</small><b>${money(Q.per)}</b>`;}
$("#stPrice").innerHTML=pr;
const c=cta();CUR=c;
[$("#stCta"),$("#mbCta")].forEach(b=>{b.hidden=!c;if(c){b.textContent=c.t;b.disabled=!!c.dis;}});
$("#stHint").textContent=c?(c.hint||""):statusLine();
$("#mbP").innerHTML=c?mb:`<small>${t("status")}</small><b style="font-size:14px">${esc(statusLine())}</b>`;
if(c)$("#mbCta").textContent=c.m;
}
function renderAll(){syncControls();renderNav();renderSides();renderArt();renderChecks();renderPrice();renderFund();renderS5();renderStage();
$$("[data-thb]").forEach(el=>{el.textContent=money(+el.dataset.thb,{bare:!!el.dataset.bare});});}
function go(s){
if(s<1||s>5)return;const changed=S.step!==s;S.step=s;S.visited[s]=true;renderAll();
if(changed){const y=window.scrollY,hh=Math.max(90,$(".top").offsetHeight+12),top=$("main").getBoundingClientRect().top+y-hh;if(y>top)window.scrollTo({top:Math.max(0,top),behavior:TOUR?"smooth":"auto"});}
}
function approve(){if(!canApprove())return;S.gate1=true;go(3);}
function launch(){if(!canLaunch()){if(!S.gate1)go(2);return;}resetCampaign();S.mode="campaign";S.launched=true;S.launchDate=new Date(TODAY);go(5);renderAll();}
function setFanPrice(v){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}
function setQty(q){S.qty=Math.min(5000,Math.max(100,Math.round(q)));if(S.mode==="direct")resetCampaign();renderAll();}
function syncInputs(){
$("#artist").value=S.artist;$("#title").value=S.title;$("#catno").value=S.catno;$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
$("#fanPrice").value=fanIn();$("#days").value=S.days;$("#cap").value=S.capN;$("#limT").checked=S.limited;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
}
function resetDemo(){
stopSim();stopProd();if(AU){AU.pause();AU=null;AUid=null;}
const o=JSON.parse(S0),keep={cur:S.cur,lang:S.lang};Object.keys(o).forEach(k=>{S[k]=o[k];});Object.assign(S,keep);s5mode=null;syncInputs();renderTracks();renderAll();
}
/* ---- events ---- */
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","spine","mastering","labels","cleaning","dinked"];
function setK(k,v){
if(S[k]===v)return;S[k]=v;
if(k==="cur"||k==="lang"){savePref(k,v);if(k==="lang")applyLang();else renderAll();return;}
if(k==="size"&&v!=="12"&&S.jacket==="gatefold")S.jacket="printed";
if(k==="scen"){resetSim();renderAll();return;}
if(k==="discs")autoSplit();
if(PLAN_KEYS.includes(k)){if(revokePlan()&&!TOUR)toastT("toastFormat");}
else if(RUN_KEYS.includes(k)&&S.mode)resetCampaign();
if(k==="discs"||k==="size")renderTracks();
renderAll();
}
const ACT={
go:b=>go(+b.dataset.s),
cta:()=>{if(CUR&&!CUR.dis)CUR.f();},
tps:b=>{const v=Math.min(10,Math.max(0,S.tps+ +b.dataset.d));if(v!==S.tps){S.tps=v;if(S.mode)resetCampaign();renderAll();}},
rm:b=>{const t=trkOf(b);S.tracks=S.tracks.filter(x=>x!==t);S.demoTracks=false;revokePlan();renderTracks();renderAll();},
add:()=>{S.tracks.push({id:++tid,title:"New track",sec:180,side:sideNames().slice(-1)[0],file:null,info:null,url:null});S.demoTracks=false;revokePlan();renderTracks();renderAll();},
split:()=>{autoSplit();revokePlan();renderTracks();renderAll();},
approve:()=>{if(S.gate1)go(3);else approve();},
artRm:b=>{S.art[b.dataset.k]=null;renderAll();},
tpl:b=>downloadTpl(b.dataset.k),
direct:()=>{if(!S.gate1){toastT("toastApprove");return go(2);}resetCampaign();S.mode="direct";newProd("direct",TODAY,0);go(5);},
launch:()=>launch(),
play:()=>toggleSim(),
adv:()=>{stopProd();advance();},
prodPlay:()=>toggleProd(),
refund:()=>{S.outcomeAct="refund";renderAll();},
goMin:()=>{S.outcomeAct="min";newProd("minimum",addDays(S.launchDate,S.days),S.finalFans);renderAll();},
retry:()=>{resetSim();renderAll();},
pv:b=>preview(+b.dataset.id),
quickCamp:()=>{S.rights=true;S.gate1=true;syncInputs();launch();},
quickDone:()=>{S.rights=true;S.gate1=true;syncInputs();resetCampaign();S.mode="campaign";S.launched=true;S.scen="sell";S.launchDate=new Date(TODAY);S.simDay=S.days;checkEnd();while(S.prod&&S.prod.idx<S.prod.rows.length){const s=S.prod.rows[S.prod.idx];S.prod.events.push({date:s.date,id:s.id});S.prod.idx++;}go(5);setTimeout(()=>{const p=$("#passport");if(p)p.scrollIntoView({behavior:"smooth",block:"start"});},60);},
preorder:()=>toastT("toastPre"),
repress:()=>toastT("toastRepress"),
copy:()=>{const slug=S.title.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");const u="https://vinyl.example/c/"+slug;(navigator.clipboard?navigator.clipboard.writeText(u):Promise.reject()).then(()=>toastT("toastCopied",{u}),()=>toast(u));},
tour:()=>tour()
};
document.addEventListener("click",e=>{
const sw=e.target.closest(".swatch");if(sw){if(sw.disabled)return;const c=sw.closest("[data-c]").dataset.c;S[c]=sw.dataset.col;renderAll();return;}
const chip=e.target.closest(".chip[data-b]");if(chip){S[chip.dataset.b]=!S[chip.dataset.b];if(S.mode)resetCampaign();renderAll();return;}
const qc=e.target.closest(".chip[data-q]");if(qc){setQty(+qc.dataset.q);return;}
const fc=e.target.closest(".chip[data-fp]");if(fc){setFanPrice(toTHB(+fc.dataset.fp));return;}
const b=e.target.closest("button");if(!b||b.disabled)return;
const a=b.dataset.act;if(a&&ACT[a]){ACT[a](b);return;}
const g=b.closest("[data-k]");
if(g&&b.dataset.v!=null){const v=b.dataset.v,t=g.dataset.t;setK(g.dataset.k,t==="n"?Number(v):t==="b"?v==="true":v);}
});
document.addEventListener("input",e=>{
const t=e.target,id=t.id;
if(id==="qtyR")setQty(+t.value);
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=toTHB(+t.value);if(v>=100&&v<=20000){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="fanR"){S.fanPrice=toTHB(+t.value);if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="cap"){const v=Math.round(+t.value)||0;if(v>=1){S.capN=v;if(S.limited)S.cap=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="acop"){S.artistCopies=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="simDay"){stopSim();S.simDay=+t.value;checkEnd();renderAll();}
});
document.addEventListener("change",e=>{
const t=e.target;
if(t.id==="qty"){setQty(Math.round((+t.value||100)/10)*10);t.value=S.qty;}
else if(t.id==="limT"){S.limited=t.checked;S.cap=S.limited?S.capN:0;if(S.limited&&!S.numbered){S.numbered=true;toastT("toastNumbered");}if(S.mode)resetCampaign();renderAll();}
else if(t.id==="fanPrice"){const v=toTHB(+t.value);S.fanPrice=Math.min(20000,Math.max(100,v>0?(S.cur==="THB"?Math.round(v):v):1200));t.value=fanIn();renderAll();}
else if(t.classList.contains("tdur")){const v=parseDur(t.value);if(v==null||v<=0){t.classList.add("err");return;}t.classList.remove("err");trkOf(t).sec=v;t.value=mmss(v);S.demoTracks=false;revokePlan();renderAll();}
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
else if(t.id==="rights"){S.rights=t.checked;if(!t.checked)revokePlan();renderAll();}
else if(t.id==="vat"){S.vat=t.checked;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(t.id==="invT"){document.body.classList.toggle("inv-on",t.checked);}
});
document.addEventListener("keydown",e=>{if(e.key==="Escape"&&TOUR)stopTour();});
/* ---- drag and drop ---- */
window.addEventListener("dragover",e=>e.preventDefault());
window.addEventListener("drop",e=>e.preventDefault());
function dropZone(el,input,handler){
["dragenter","dragover"].forEach(ev=>el.addEventListener(ev,e=>{e.preventDefault();if(!el.classList.contains("off"))el.classList.add("over");}));
["dragleave","drop"].forEach(ev=>el.addEventListener(ev,e=>{e.preventDefault();el.classList.remove("over");}));
el.addEventListener("drop",e=>{if(!el.classList.contains("off"))handler(Array.from(e.dataTransfer.files||[]));});
input.addEventListener("change",()=>{handler(Array.from(input.files||[]));input.value="";});
}
async function handleAudio(files){
if(!files.length)return;
files.sort((a,b)=>a.name.localeCompare(b.name,undefined,{numeric:true}));
if(S.demoTracks){S.tracks=[];S.demoTracks=false;}
const added=files.map(f=>{const t={id:++tid,title:f.name.replace(/\.[^.]+$/,"").replace(/^\d+\s*[-._ ]\s*/,""),sec:0,side:"A",file:f,info:{reading:true},url:URL.createObjectURL(f)};S.tracks.push(t);return t;});
revokePlan();renderTracks();renderAll();
for(const t of added){try{t.info=await audioInfo(t.file);}catch(err){t.info={fmt:"?",lossless:false,err:String(err)};}if(t.info.sec)t.sec=t.info.sec;if(!t.info.lossless){URL.revokeObjectURL(t.url);t.url=null;}}
autoSplit();renderTracks();renderAll();
for(const t of added){if(!t.info.lossless)continue;t.info.scanning=true;renderTracks();renderChecks();await scanPeak(t);t.info.scanning=false;renderTracks();renderAll();}
}
dropZone($("#dropAudio"),$("#fileAudio"),handleAudio);
dropZone($("#dropJacket"),$("#fileJacket"),f=>handleArt("jacket",f));
dropZone($("#dropLabel"),$("#fileLabel"),f=>handleArt("label",f));
/* ---- guided tour: about a minute, hands-free, for presenting ---- */
let TOUR=null,TOURCAP=0;
function renderTourBtn(){$("#tourBtn").innerHTML=t(TOUR?"tourOn":"tourOff");}
function stopTour(){if(TOUR)TOUR.alive=false;TOUR=null;$("#tourCap").hidden=true;$("#mock").classList.remove("play");renderTourBtn();}
async function tour(){
if(TOUR){stopTour();return;}
const T={alive:true};TOUR=T;renderTourBtn();
const w=ms=>new Promise((res,rej)=>setTimeout(()=>T.alive?res():rej(new Error("stop")),ms));
const N=8,cap=i=>{TOURCAP=i;$("#tourCap").hidden=false;$("#tourN").textContent=i+" / "+N;$("#tourT").textContent=t("tour"+i);};
const into=sel=>{const el=$(sel);if(el)el.scrollIntoView({behavior:"smooth",block:"start"});};
try{
resetDemo();go(1);window.scrollTo({top:0});$("#mock").classList.add("play");
cap(1);await w(2800);
setK("colour","marbled");await w(900);S.c1="#b3202a";S.c2="#f2c14e";renderAll();await w(900);
S.numbered=true;renderAll();await w(1600);
go(2);cap(2);await w(3800);
S.rights=true;syncInputs();renderAll();into("#checkPanel");await w(1200);
cap(3);await w(2600);
approve();
cap(4);
await w(900);for(const q of [100,150,200,300,500,700,1000,700,500,300]){setQty(q);await w(280);}await w(2000);
go(4);cap(5);await w(2600);
for(const v of [1000,800,650,550,500,550,650,800,1000,1200]){setFanPrice(v);await w(330);}await w(1800);
S.scen="sell";launch();await w(1200);startSim();renderAll();
cap(6);
while(!S.outcome)await w(250);await w(1800);
into("#prodGrid");
cap(7);await w(1400);
while(S.prod&&S.prod.idx<S.prod.rows.length){const g=S.prod.rows[S.prod.idx].gate;advance();await w(g?1500:700);}
await w(500);into("#passport");
cap(8);await w(6000);
stopTour();toastT("toastTour");
}catch(e){if(TOUR===T)stopTour();}
}
/* ---- theme ---- */
let themeRefresh=()=>{};
(function(){
const b=$("#themeBtn"),de=document.documentElement;
function set(th,save){de.setAttribute("data-theme",th);if(save){try{localStorage.setItem("cnx-theme",th)}catch(e){}}b.textContent=th==="light"?"☾":"☀";b.title=t("themeTo",{m:t(th==="light"?"themeDark":"themeLight")});b.setAttribute("aria-label",b.title);}
set(de.getAttribute("data-theme")||"dark",false);themeRefresh=()=>set(de.getAttribute("data-theme")||"dark",false);
b.addEventListener("click",()=>{set(de.getAttribute("data-theme")==="light"?"dark":"light",true);b.blur();});
const mq=window.matchMedia&&matchMedia("(prefers-color-scheme: light)");
if(mq&&mq.addEventListener)mq.addEventListener("change",e=>{let s=null;try{s=localStorage.getItem("cnx-theme")}catch(_){}if(!s)set(e.matches?"light":"dark",false);});
})();
/* ---- init ---- */
[["c1",C1],["c2",C2]].forEach(([c,list])=>{$(`.sw-row[data-c="${c}"]`).innerHTML=list.map(([h,n])=>`<button type="button" class="swatch" data-col="${h}" data-name="${n}" style="background:${h}" aria-label="${t("c_"+n)}"></button>`).join("")+`<label class="pal"><input type="color" aria-label="${t("palAria")}"><span class="cw"></span><span class="pt">${t("palName")}</span></label>`;});
/* ---- language and currency: one state each, kept in localStorage and the URL hash (#cur=EUR, #lang=th) ---- */
const PREF={cur:"cnx-mr-cur",lang:"cnx-mr-lang"},PREF_OK={cur:CURS,lang:LANGS};
const hashTokens=()=>location.hash.slice(1).split(/[&,]/).filter(Boolean);
function hashPref(k){const x=hashTokens().find(s=>s.toLowerCase().startsWith(k+"="));if(!x)return null;let v=x.slice(k.length+1);try{v=decodeURIComponent(v);}catch(e){}return k==="cur"?v.toUpperCase():v.toLowerCase();}
function savePref(k,v){try{localStorage.setItem(PREF[k],v);}catch(e){}
const tk=hashTokens().filter(s=>!s.toLowerCase().startsWith(k+"="));tk.push(k+"="+v);
try{history.replaceState(null,"","#"+tk.join("&"));}catch(e){}}
function readPrefs(){["cur","lang"].forEach(k=>{let v=hashPref(k);
if(PREF_OK[k].includes(v)){try{localStorage.setItem(PREF[k],v);}catch(e){}}else{v=null;try{v=localStorage.getItem(PREF[k]);}catch(e){}}
if(PREF_OK[k].includes(v))S[k]=v;});}
function applyLang(){
document.documentElement.lang=S.lang;
$$("[data-i18n]").forEach(el=>{const s=t(el.dataset.i18n);if(el.innerHTML!==s)el.innerHTML=s;});
I18N_ATTR.forEach(([d,a])=>$$("["+d+"]").forEach(el=>el.setAttribute(a,t(el.getAttribute(d)))));
document.title=t("docTitle");const md=$("meta[name=description]");if(md)md.setAttribute("content",t("docDesc"));
$("#footDraft").hidden=S.lang!=="th";
s5mode=null;renderTracks();renderAll();syncControls();renderTourBtn();themeRefresh();
if(TOUR&&TOURCAP)$("#tourT").textContent=t("tour"+TOURCAP);
}
readPrefs();syncInputs();applyLang();
window.addEventListener("hashchange",()=>{const c=hashPref("cur"),l=hashPref("lang");let ch=false;
if(CURS.includes(c)&&c!==S.cur){S.cur=c;ch=true;try{localStorage.setItem(PREF.cur,c);}catch(e){}}
if(LANGS.includes(l)&&l!==S.lang){S.lang=l;try{localStorage.setItem(PREF.lang,l);}catch(e){}applyLang();}else if(ch)renderAll();
if(hashTokens().includes("notes")){$("#invT").checked=true;document.body.classList.add("inv-on");}});
const H=location.hash.slice(1).split(/[&,]/);
if(H.includes("notes")){$("#invT").checked=true;document.body.classList.add("inv-on");}
if(H.includes("tour"))setTimeout(tour,700);
window.__mr={S,T,t,missing:MISSING,applyLang,money,rate,fanIn,setFanPrice,tplPages,spineMM,artChecks,quote,campEcon,leadInfo,sideStats,checkList,go,ACT,setK,setQty,tour,stopTour,resetDemo};
})();
</script>
</body>
</html>