Files
cnx_vinyl/docs/make-a-record.html
T
grabowski 607f6631a7
Deploy test site / deploy (push) Successful in 53s
Make a record: investor demo of the artist platform, end-user flow
New static page docs/make-a-record.html (test site /demos/make-a-record/):
the artist platform as a customer would see it, polished for presenting.

- five steps (design, music and art, price, fan funding, track) with a
  sticky stage: to-scale mock-up (record spins at 33 1/3 or 45 rpm on
  hover), spec chips, price per copy, ship month and the next action
- design as option cards and swatches; options that do not apply stay
  visible and disabled (weight and gatefold off 12", colour pickers for
  black vinyl)
- masters and artwork read in the browser as in the detailed demo, side
  bars per side, checks in plain words, approval 1 of 3
- price with the curve, itemised lines folded under one summary, lead time
  as two bars (approvals, then the 7 to 9 week quote)
- fan funding: goal, break-even and what the artist keeps, money rules;
  the campaign on a phone with a pre-order ticker, funded or missed ends,
  production timeline with the artist's approvals, updates feed
- record passport for one delivered copy (stamper, press, cycle time and
  side; stage dates; optical inspection and sampled audio), per
  traceability.md and quality-automation.md, IDs simulated; repress card
- "Investor notes" switch with the business case per step (market
  research, vault, short-run economics and the Thai price gap, plant-owned
  vs broker and the Qrates lesson, traceability and represses); guided
  tour of about a minute (#tour, #notes in the URL)
- light/dark theme shared with the other pages; phone layout with a
  bottom bar

Same price list (demo-0.1) and stage table as artist-order.html, so the
defaults reproduce the artist platform example: 37,836 THB per title,
232 THB per copy at 100, break-even 41, goal 100, 548 THB kept a copy.

artist-platform.md: investor demo paragraph. traceability.md: the record
passport as the per-copy customer view. README bullet and layout line,
site DEMOS line and landing-page entry.
2026-10-15 16:13:29 +07:00

1395 lines
123 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; }
input[type=color] { width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%; background: none; cursor: pointer; }
input[type=color]::-webkit-color-swatch-wrapper { padding: 0; }
input[type=color]::-webkit-color-swatch { border: 1.5px dashed var(--faint); border-radius: 50%; }
input[type=color]:disabled { opacity: 0.2; 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 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: ""; }
.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 p { max-width: 900px; margin: 6px 0; }
/* narrow screens */
@media (max-width: 1080px) {
.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; }
nav.steps button { padding: 6px; }
nav.steps button.on .lb { display: inline; }
.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, #tourBtn .tl { display: none; }
.top .in { gap: 8px; }
}
</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">Demo</span></div>
<nav class="steps" id="steps" aria-label="Steps">
<button type="button" data-act="go" data-s="1"><span class="d">1</span><span class="lb">Design</span></button>
<button type="button" data-act="go" data-s="2"><span class="d">2</span><span class="lb">Music &amp; art</span></button>
<button type="button" data-act="go" data-s="3"><span class="d">3</span><span class="lb">Price</span></button>
<button type="button" data-act="go" data-s="4"><span class="d">4</span><span class="lb">Fan funding</span></button>
<button type="button" data-act="go" data-s="5"><span class="d">5</span><span class="lb">Track</span></button>
</nav>
<div class="tools">
<label class="sw" title="Show the business behind each step"><input type="checkbox" id="invT"><span class="tr"></span><span class="lbl">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">&#x25B6;<span class="tl"> Tour</span></button>
<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">Step 1 of 5</div>
<h1>Design your record</h1>
<p class="lead">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">Investor note</span><p><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>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>7" single</b><span class="sub" data-sub="7"></span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl">Speed</span><div class="seg" data-k="rpm"><button type="button" data-v="33">33&#x2153; rpm</button><button type="button" data-v="45">45 rpm</button></div></div>
<div class="field"><span class="fl">Discs</span><div class="seg" data-k="discs" data-t="n"><button type="button" data-v="1">Single</button><button type="button" data-v="2">Double</button></div></div>
<div class="field" id="fWeight"><span class="fl">Weight</span><div class="seg" data-k="weight" data-t="n"><button type="button" data-v="140">140 g</button><button type="button" data-v="180">180 g heavyweight</button></div></div>
</div>
</div>
<div class="block">
<h2>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>Classic black</b><span>Timeless</span></button>
<button type="button" class="choice" data-k="colour" data-v="solid"><span class="ic" data-mini="solid"></span><b>Solid colour</b><span>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>Marbled</b><span>Two colours swirled</span></button>
<button type="button" class="choice" data-k="colour" data-v="splatter"><span class="ic" data-mini="splatter"></span><b>Splatter</b><span>Flecks on a base</span></button>
</div>
<div class="colours">
<div class="field" id="fC1"><span class="fl">Colour</span><div class="sw-row" data-c="c1"></div></div>
<div class="field" id="fC2"><span class="fl">Second colour</span><div class="sw-row" data-c="c2"></div></div>
</div>
</div>
<div class="block">
<h2>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>Printed jacket</b><span>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>Gatefold</b><span>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>Plain jacket</b><span>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>My own jacket</b><span>You supply it</span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl">Inner sleeve</span><div class="seg" data-k="inner"><button type="button" data-v="paper">Paper</button><button type="button" data-v="poly">Poly-lined</button></div></div>
<div class="field"><span class="fl">Extras</span><div class="chips"><button type="button" class="chip" data-b="shrink">Shrink wrap</button><button type="button" class="chip" data-b="insert">Printed insert</button><button type="button" class="chip" data-b="sticker">Hype sticker</button><button type="button" class="chip" data-b="numbered">Numbered edition</button></div></div>
</div>
</div>
<div class="block">
<h2>Sound</h2>
<div class="cards c2">
<button type="button" class="choice" data-k="mastering" data-v="true" data-t="b"><b>Vinyl mastering</b><span>Our engineer masters your mix for the groove</span></button>
<button type="button" class="choice" data-k="mastering" data-v="false" data-t="b"><b>My master is vinyl-ready</b><span>Cut straight from your files</span></button>
</div>
<div class="rowc mt">
<div class="field"><span class="fl">Test pressings for you</span><div class="stepper"><button type="button" data-act="tps" data-d="-1" aria-label="Fewer">&#x2212;</button><b id="tpsV">5</b><button type="button" data-act="tps" data-d="1" aria-label="More">+</button></div></div>
<p class="hint" style="max-width:380px;margin:0">You hear and approve a real pressing before the run. The mother plate is archived, so a repress later skips cutting and plating.</p>
</div>
</div>
</section>
<!-- STEP 2 -->
<section class="step" data-step="2" hidden>
<div class="eyebrow">Step 2 of 5</div>
<h1>Your music and artwork</h1>
<p class="lead">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">Investor note</span><p><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">Artist</span><input type="text" id="artist" maxlength="60"></div>
<div class="field"><span class="fl">Release title</span><input type="text" id="title" maxlength="80"></div>
</div>
<div class="block">
<h2>Tracks and sides <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><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">+ Add a track</button>
<button type="button" class="btn ghost sm" data-act="split">Balance the sides</button>
<div class="seg" data-k="loud" data-t="b"><button type="button" data-v="false">Standard</button><button type="button" data-v="true">Loud, bass-heavy</button></div>
</div>
</div>
<div class="block">
<h2>Artwork</h2>
<div class="g2f">
<div><label class="drop" id="dropJacket"><input type="file" id="fileJacket" accept="image/*" hidden><span class="dic">&#x25A3;</span><span><b>Jacket front</b>, square, 300 dpi</span></label><div id="artJacket"></div></div>
<div><label class="drop" id="dropLabel"><input type="file" id="fileLabel" accept="image/*" hidden><span class="dic">&#x25CE;</span><span><b>Label</b>, square, 300 dpi</span></label><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>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">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">Step 3 of 5</div>
<h1>Your price</h1>
<p class="lead">Every line itemised, with no fees that appear later. The price per copy falls fast as the run grows.</p>
<div class="inv"><span class="il">Investor note</span><p><b>Why short runs work here.</b> The one Thai plant on file quotes a 150-copy minimum that is "rarely accepted because the cost is way too high": its per-title cost (imported stampers, outsourced cutting) does not shrink with the run. In-house cutting and plating collapses that cost.</p><p>Prices on this page are indicative, at Western benchmark rates. The Thai market pays 650 to 950 THB a copy at 300 copies today; the real price list follows market validation.</p></div>
<div class="panel block">
<div class="qtyhead">
<div class="field"><span class="fl">Copies</span><div class="qbig"><input type="number" id="qty" min="100" max="5000" step="10" aria-label="Copies"><span>copies</span></div></div>
<div class="seg" data-k="cur"><button type="button" data-v="THB">THB</button><button type="button" data-v="USD">USD</button></div>
</div>
<input type="range" id="qtyR" min="100" max="2000" step="10" aria-label="Copies">
<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>What is included <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>How do you want to pay for it?</h2>
<div class="paths">
<div class="path"><span class="ptag">Classic</span><h3>Order now</h3><p>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">Order directly</button></div>
<div class="path hi"><span class="ptag acc">No upfront cost</span><h3>Let your fans fund it</h3><p>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">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">Step 4 of 5</div>
<h1>Let your fans fund it</h1>
<p class="lead">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">Investor note</span><p><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"><span class="fl">Fan price per copy</span><div class="inline"><input type="number" id="fanPrice" min="300" max="5000" step="50" aria-label="Fan price"><span>THB</span></div><input type="range" id="fanR" min="500" max="3000" step="50" aria-label="Fan price"></div>
<div class="field"><span class="fl">Campaign length</span><div class="inline"><b id="daysV"></b><span>days</span></div><input type="range" id="days" min="14" max="45" step="1" aria-label="Days"></div>
<div class="field"><span class="fl">Limited edition</span><div class="inline"><input type="number" id="cap" min="0" max="5000" step="50" aria-label="Edition cap"><small class="mute">copies at most, 0 = no limit</small></div></div>
<div class="field"><span class="fl">Copies for yourself</span><div class="inline"><input type="number" id="acop" min="0" max="200" step="5" aria-label="Copies for yourself"><small class="mute">pressed with the run</small></div></div>
</div>
<label class="rights" style="margin:16px 0 0"><input type="checkbox" id="vat"> <span>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>Your fans' money is safe</h2>
<div class="safe">
<div><span class="si">&#x26C1;</span><b>Held apart</b>Pre-orders sit in a separate account and are never used to run the plant until the campaign succeeds.</div>
<div><span class="si">&#x21BA;</span><b>Refunded automatically</b>If the goal is missed, every fan gets their money back. Nobody ends up with a garage full of records.</div>
<div><span class="si">&#x2713;</span><b>Pay the way fans pay</b>PromptPay QR, cards and wallets. Card details never touch our servers.</div>
</div>
</div>
<div class="rowc" style="align-items:center"><button type="button" class="btn" data-act="launch" id="bLaunch">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">Step 5 of 5</div>
<h1 id="s5h">Campaign and production</h1>
<p class="lead" id="s5l"></p>
<div class="inv"><span class="il">Investor note</span><p><b>Traceability is the product.</b> Fan updates come from the factory's own event log, and every copy resolves to its stamper, press, cycle and side. At Making Vinyl Europe 2026 this is what the industry responded to most.</p><p><b>One release becomes a recurring one.</b> The archived mother makes a repress about 1.5 to 2.5 weeks of plant time: the highest-margin order the plant will take.</p></div>
<div id="s5"></div>
</section>
</main>
<aside class="stage">
<div class="stagecard">
<div class="mock" id="mock" title="Hover to spin"></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">Stop</button></div>
<footer>
<p>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>The detailed version lists every rate and where it comes from: <a href="../artist-order/">artist ordering demo</a>.</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;
/* ---- 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]}}},
handling:{v:0.10},shrink:{v:5},insert:{v:12},sticker:{v:6},numbered:{v:5},fee:{v:0.035},minRun:{v:100},deposit:{v:0.5}
};
const tierIdx=q=>q>=1000?3:q>=500?2:q>=300?1:0;
/* side length per format, minutes [comfortable, at most]: microcut.studio format guide, checked October 2026 */
const LIM={"12":{"33":[18,22],"45":[12,15]},"10":{"33":[12,16],"45":[8,11]},"7":{"33":[5,7],"45":[4.5,6]}};
const LOUD=0.6;
const JNAME={none:"Your own jacket",plain:"Plain jacket",printed:"Printed jacket",gatefold:"Gatefold jacket"};
const XN={shrink:"Shrink wrap",insert:"Insert",sticker:"Hype sticker",numbered:"Numbered"};
const 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 UPD={
mast:"Mastered for vinyl. The artist is listening to the master.",
g2:"The artist approved the vinyl master.",
cut:"Cut to lacquer in the cutting room.",
plate:"Plated in-house: father, mother and stampers. The mother is archived for represses.",
prep:"Stampers punched, back-finished and measured.",
tp:"First test pressings made and sent to the artist.",
g3:"The artist approved the test pressing.",
queue:"Pressing slot booked.",
run:P=>"Pressed: "+fmtInt(P.run)+" copies, every one optically inspected and logged.",
fin:()=>"Packed: sleeves"+(S.jacket!=="none"?", jackets":"")+(S.shrink?" and shrink wrap":"")+".",
ship:P=>P.origin==="direct"?"Dispatched; tracking sent.":"Shipped: one parcel per fan, tracking sent to all "+fmtInt(P.fans)+" fans."
};
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 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"];
const TODAY=new Date(); TODAY.setHours(12,0,0,0);
const S={
step:1,cur:"THB",size:"12",rpm:"33",discs:1,weight:140,colour:"black",c1:"#b3202a",c2:"#f2c14e",
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
mastering:true,tps:5,loud:false,artist:"Mae Ping Collective",title:"Night Market Songs",
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
fanPrice:1200,days:30,cap:0,artistCopies:0,vat:false,
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
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");
function money(thb,o){o=o||{};
if(S.cur==="USD"){const u=thb/THB_USD,d=o.dec!=null?o.dec:(Math.abs(u)<1000?2:0);return u.toLocaleString("en-US",{minimumFractionDigits:d,maximumFractionDigits:d})+(o.bare?"":" USD");}
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 fmtD=d=>d.toLocaleDateString("en-GB",{day:"numeric",month:"short",year:"numeric"});
const fmtM=d=>d.toLocaleDateString("en-GB",{month:"short",year:"numeric"});
function monthRange(a,b){const A=fmtM(a),B=fmtM(b);if(A===B)return A;return a.getFullYear()===b.getFullYear()?a.toLocaleDateString("en-GB",{month:"short"})+" to "+B:A+" to "+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=()=>S.rpm==="33"?"33⅓ rpm":"45 rpm";
const colourName=()=>({black:"Black vinyl",solid:"Solid colour",marbled:"Marbled",splatter:"Splatter"})[S.colour];
const colourT=()=>({black:"black",solid:"solid colour",marbled:"marbled",splatter:"splatter"})[S.colour];
const SIZE_MM={"12":{rec:302,jk:314,lab:100,name:"12\" LP"},"10":{rec:250,jk:260,lab:100,name:"10\""},"7":{rec:175,jk:184,lab:90,name:"7\" single"}};
function descT(){return (S.discs>1?"2 × ":"")+SIZE_MM[S.size].name+", "+rpmT()+", "+(S.size==="12"?S.weight+" g ":"")+colourT()+" vinyl";}
function toast(t){$$(".toast").forEach(x=>x.remove());const el=document.createElement("div");el.className="toast";el.textContent=t;document.body.appendChild(el);setTimeout(()=>el.remove(),2600);}
function hsh(s){let h=2166136261;for(const c of String(s)){h^=c.charCodeAt(0);h=Math.imul(h,16777619);}return h>>>0;}
const idOf=(p,s,n)=>p+"-"+hsh(s).toString(36).toUpperCase().padStart(7,"0").slice(0,n||4);
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=d>1?"discs":"disc";
if(S.mastering)T.push({t:"Vinyl mastering",n:sides,u:"sides",r:R.master.v});
T.push({t:"Lacquer blanks",n:sides,u:"sides",r:R.blank.v});
T.push({t:"Lacquer cutting",n:sides,u:"sides",r:R.cut.v});
T.push({t:"Plating: father, mother (kept for represses), stamper",n:sides,u:"sides",r:R.metal.v});
T.push({t:"Press setup",n:d,u:du,r:R.setup.v});
if(S.tps>0)T.push({t:"Test pressings for you",n:S.tps,u:"copies",r:R.tp.v});
C.push({t:"Pressing, "+sz+"\""+(sz==="12"?" 140 g":"")+", printed labels",n:d,u:du,r:R.press.v[sz]});
if(sz==="12"&&S.weight===180)C.push({t:"180 g heavyweight",n:d,u:du,r:R.g180.v});
if(S.colour!=="black")C.push({t:"Colour vinyl, "+colourT(),n:d,u:du,r:R.colour.v[S.colour]});
C.push({t:"Inner sleeve, "+(S.inner==="poly"?"poly-lined":"paper"),n:d,u:du,r:R.inner.v[S.inner][sz]});
C.push({t:"Assembly and packing",n:1,u:"",r:R.assy.v});
if(S.jacket!=="none"){const p=R.jacket.v[S.jacket][sz][tierIdx(q)];C.push({t:JNAME[S.jacket]+(sz==="7"&&S.jacket!=="plain"?" (picture sleeve)":""),n:1,u:"",r:Math.round(p*(1+R.handling.v)),pass:p});}
if(S.shrink)C.push({t:"Shrink wrap",n:1,u:"",r:R.shrink.v});
if(S.insert)C.push({t:"Printed insert",n:1,u:"",r:R.insert.v});
if(S.sticker)C.push({t:"Hype sticker",n:1,u:"",r:R.sticker.v});
if(S.numbered)C.push({t:"Hand-numbered",n:1,u:"",r:R.numbered.v});
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=>(!s.opt||S[s.opt])&&!((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;}}
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;
}
async function readImage(file){
const url=URL.createObjectURL(file),img=new Image();img.src=url;await img.decode();
const comps=(file.type==="image/jpeg"||/\.jpe?g$/i.test(file.name))?await jpegComponents(file):null;
return {url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
}
function artChecks(k){
const a=S.art[k];if(!a)return null;if(a.err)return [["pend",a.err]];
const mm=ART[k].mm[S.size],need=Math.round(mm/25.4*300),px=Math.min(a.w,a.h),dpi=Math.round(px/(mm/25.4)),out=[];
out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);
out.push(px>=need?["ok",dpi+" dpi at "+mm+" mm"+(k==="jacket"?" incl. 3 mm bleed":"")]:px>=need*0.8?["warn",dpi+" dpi, a little under 300 (needs "+need+" px)"]:["bad",dpi+" dpi: too low for print (needs "+need+" px)"]);
out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, bright colours can shift"]);
return out;
}
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","Track list","Add at least one track."]);
if(!files.length)L.push(["pend","Master files","Not added yet: these checks use the lengths typed in. Drop WAV, FLAC or AIFF files to check the files themselves."]);
else{
const lossy=ready.filter(t=>!t.info.lossless);
L.push(lossy.length?["bad","File format",lossy.map(t=>esc(t.file.name)).join(", ")+": not WAV, FLAC or AIFF. Send the lossless master."]:["ok","File format",ready.length+" lossless file"+(ready.length===1?"":"s")]);
const ok=ready.filter(t=>t.info.lossless),lowSr=ok.filter(t=>t.info.sr<44100),lowB=ok.filter(t=>t.info.bits<16),b16=ok.filter(t=>t.info.bits===16);
L.push(lowSr.length?["bad","Sample rate",lowSr.length+" file(s) below 44.1 kHz"]:["ok","Sample rate","44.1 kHz or more"]);
L.push(lowB.length?["bad","Bit depth",lowB.length+" file(s) below 16 bit"]:b16.length?["warn","Bit depth",b16.length+" file(s) at 16 bit: fine, 24 bit is preferred for cutting"]:["ok","Bit depth","24 bit or more"]);
const scanning=ok.filter(t=>t.info.scanning),clipped=ok.filter(t=>t.info.clips>0),hot=ok.filter(t=>typeof t.info.peak==="number"&&t.info.peak>=0.966&&!(t.info.clips>0));
L.push(scanning.length?["pend","Clipping and peak","Scanning "+scanning.length+" file(s)…"]:clipped.length?["warn","Clipping",clipped.map(t=>esc(t.title)).join(", ")+": runs of full-scale samples. The engineer will ask about it."]:hot.length?["warn","Peak level",hot.length+" file(s) peak above -0.3 dBFS"]:["ok","Clipping and peak","No clipping found"]);
const miss=S.tracks.filter(t=>!t.file).length;if(miss)L.push(["warn","Files",miss+" track(s) without a file yet"]);
}
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;
L.push(bad.length?["bad","Side length",bad.map(s=>"Side "+s.n+" "+mmss(s.sec)).join(", ")+": over the "+mmss(lim[1])+" a "+S.size+"\" side holds at "+rpmT()+". Move tracks, add a disc or change the speed."]
:warn.length?["warn","Side length",warn.map(s=>"Side "+s.n+" "+mmss(s.sec)).join(", ")+": past the "+mmss(lim[0])+" that sounds best, so the cut gets quieter"]
:empty.length?["warn","Side length","Side "+empty.map(s=>s.n).join(", ")+" has no music"]:["ok","Side length","Every side within the "+mmss(lim[0])+" that sounds best"]);
if(jacketArt()){const c=artChecks("jacket");L.push(!c?["pend","Jacket artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Jacket artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Jacket artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Jacket artwork","Size, resolution and colour space fine"]);}
{const c=artChecks("label");L.push(!c?["pend","Label artwork","Not added yet"]:c.some(x=>x[0]==="bad")?["bad","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:c.some(x=>x[0]!=="ok")?["warn","Label artwork",c.filter(x=>x[0]!=="ok").map(x=>x[1]).join("; ")]:["ok","Label artwork","Size, resolution and colour space fine"]);}
L.push(["pend","Engineer check","After upload a cutting engineer confirms the sides are cuttable. This decides, not the bars above."]);
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?"Fix the items marked ✕ in step 2 first.":!S.rights?"Confirm the rights in step 2 first.":"";}
/* ---- 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+5;r-=3.2)g+=`<circle r="${r.toFixed(1)}" fill="none" stroke="${groove}" stroke-width="${(0.5/s).toFixed(2)}"/>`;
const L=rl.toFixed(1);
if(S.art.label&&S.art.label.url)g+=`<image href="${S.art.label.url}" x="-${L}" y="-${L}" width="${(2*rl).toFixed(1)}" height="${(2*rl).toFixed(1)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${id}l)"/>`;
else g+=`<circle r="${L}" fill="#efe6d2"/><circle r="${(rl*0.86).toFixed(1)}" fill="none" stroke="#d6c7a6" stroke-width="0.8"/><text y="${(-8*fs).toFixed(1)}" text-anchor="middle" font-size="${(6.6*fs).toFixed(1)}" font-weight="700" fill="#2a2a2a" font-family="system-ui,sans-serif">${esc(S.artist.slice(0,22))}</text><text y="${(13*fs).toFixed(1)}" text-anchor="middle" font-size="${(5.4*fs).toFixed(1)}" fill="#555" font-family="system-ui,sans-serif">${esc(S.title.slice(0,26))}</text><text y="${(24*fs).toFixed(1)}" text-anchor="middle" font-size="${(4.4*fs).toFixed(1)}" fill="#8a7d62" font-family="system-ui,sans-serif" letter-spacing="1">SIDE A · ${S.rpm==="33"?"33⅓":"45"}</text>`;
g+=`</g><circle r="105" fill="url(#${id}sh)"/>${wedge(-128,-104)}${wedge(52,76)}<circle r="105" fill="none" stroke="rgba(255,255,255,0.10)" stroke-width="${(1/s).toFixed(2)}"/>`;
return g+`<circle r="${(105*3.6/Z.rec).toFixed(2)}" class="hole"/></g>`;
}
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">your own jacket</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(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">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">12" jacket, for 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(Z.name)}: record ${Z.rec} mm, jacket ${Z.jk} mm</text>`;
return `<svg viewBox="0 0 460 292" role="img" aria-label="${esc("Mock-up of the "+Z.name+" record")}"><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="Code printed on the sleeve (demo pattern)"><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=S.cur==="USD"?THB_USD:1,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="Price per copy against the number of copies">${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"}">${fmtInt(q)} copies: ${money(v)} each</text><text class="cv-t" x="${W-pr}" y="${pt-5}" text-anchor="end">price per copy, all in, ${S.cur} · copies on a log scale</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?"":"Only 12\" records come in 140 or 180 g";});
$("#fWeight").classList.toggle("off",!w12);
const g=$("#bGate");g.disabled=!w12;g.title=w12?"":"Gatefold jackets are for 12\" records";
$$("[data-sub]").forEach(el=>{const L=LIM[el.dataset.sub][S.rpm];el.textContent="Up to "+L[0]+" min a side at "+(S.rpm==="33"?"33⅓":"45");});
$$("[data-mini]").forEach(el=>el.innerHTML=miniDisc(el.dataset.mini));
[["c1",blk,"Black vinyl: no colour to choose"],["c2",!two,"Only for marbled and splatter"]].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:b.dataset.name;});
const inp=row.querySelector("input[type=color]");inp.disabled=off;inp.title=off?why:"Any colour";if(inp.value.toLowerCase()!==S[c].toLowerCase())inp.value=S[c];});
$("#tpsV").textContent=S.tps;
const dj=$("#dropJacket"),offJ=!jacketArt();dj.classList.toggle("off",offJ);$("#fileJacket").disabled=offJ;dj.title=offJ?(S.jacket==="none"?"You supply your own jacket":"A plain jacket has no print"):"";
$$(".qchips .chip").forEach(c=>c.classList.toggle("on",+c.dataset.q===S.qty));
}
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(t){
if(!t.file)return "";
const i=t.info||{};if(i.reading)return "reading…";
if(!i.lossless)return `<span class="bad-t">${esc(i.fmt||"?")}: not lossless</span>`;
let pk="";if(i.scanning)pk=" · scanning";else if(typeof i.peak==="number")pk=" · peak "+(i.peak>0?(20*Math.log10(i.peak)).toFixed(1):"-inf")+" dB"+(i.clips?" · clips":"");
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((t,i)=>`<div class="tr" data-id="${t.id}"><span class="no">${i+1}</span><select class="tside" aria-label="Side">${names.map(n=>`<option${n===t.side?" selected":""}>${n}</option>`).join("")}</select><input type="text" class="tt" value="${esc(t.title)}" aria-label="Track title"><input type="text" class="tdur" value="${t.sec?mmss(t.sec):""}" placeholder="m:ss" aria-label="Length"${t.file&&t.info&&t.info.sec?' readonly title="Read from the file"':""}><span class="fi">${fileInfo(t)}</span><button type="button" class="x" data-act="rm" title="Remove" aria-label="Remove">&#xD7;</button></div>`).join("");
}
function renderSides(){
const ss=sideStats(),L=ss[0].lim;
$("#sideHint").textContent=`${S.size}" at ${rpmT()}${S.loud?", loud music":""}: up to ${mmss(L[0])} a side sounds best, ${mmss(L[1])} at most`;
const lbl={ok:"fits",warn:"long",bad:"too long"};
$("#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>Side ${s.n}</b><span>${s.count} track${s.count===1?"":"s"}</span><span class="pill ${s.count?s.st:"warn"}">${mmss(s.sec)} · ${s.count?lbl[s.st]:"empty"}</span></div><div class="sbar">${segs}<b class="mk" style="left:${(L[0]/L[1]*100).toFixed(1)}%" title="Sounds best up to here"></b></div></div>`;}).join("");
}
function renderArt(){
const ic={ok:"&#x2713;",warn:"!",bad:"&#x2715;",pend:"&#x25CB;"};
[["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">${a.url?`<img src="${a.url}" alt="">`:""}<div><small class="mute">${esc(a.name)}</small><ul>${cs.map(([s,t])=>`<li><span class="ci ${s}" style="width:18px;height:18px;font-size:10px">${ic[s]}</span>${t}</li>`).join("")}</ul><button type="button" class="lnk" data-act="artRm" data-k="${k}">Remove</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>${bad} thing${bad===1?"":"s"} to fix</b>`:warn?`<span class="ci warn">!</span><b>Ready, with ${warn} thing${warn===1?"":"s"} to look at</b>`:`<span class="ci ok">&#x2713;</span><b>Ready for the engineer</b>`;
$("#checks").innerHTML=L.map(([s,t,d])=>`<li><span class="ci ${s}">${ic[s]}</span><div><b>${t}</b><small>${d}</small></div></li>`).join("");
const b=$("#bApprove"),can=canApprove();
b.disabled=!can&&!S.gate1;
b.innerHTML=S.gate1?"See my price &#x2192;":"Approve and see my price &#x2192;";
$("#gateMsg").textContent=S.gate1?"Master plan approved: approval 1 of 3. Change the tracks or the format and it needs approving again.":bad?"Fix the items marked ✕ first.":!S.rights?"Confirm the rights to approve. This is approval 1 of 3: the plan now, the master and the test pressing later.":"This is approval 1 of 3: the plan now, the master and the test pressing later.";
$("#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>Per copy, all in</small><b>${money(Q.per)}</b></div><div class="kpi"><small>Order total</small><b>${money(Q.total)}</b></div><div class="kpi"><small>Ordered today, ships</small><b>${monthRange(ship[0],ship[1])}</b></div>`;
$("#curve").innerHTML=curveSVG();
$("#bdSum").textContent=`${money(Q.tSum)} per title + ${money(Q.cSum)} per copy`;
const tr=x=>`<tr><td>${x.t}${x.pass!=null?`<br><span class="pass">partner price ${money(x.pass)} + ${Math.round(R.handling.v*100)} % handling</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>Once per title</th><th class="n"></th><th class="n">Amount</th></tr>${Q.T.map(tr).join("")}<tr class="sub"><td colspan="2">Per title</td><td class="n">${money(Q.tSum)}</td></tr>
<tr><th>Per copy</th><th class="n"></th><th class="n">Per copy</th></tr>${Q.C.map(tr).join("")}<tr class="sub"><td colspan="2">${money(Q.cSum)} × ${fmtInt(S.qty)} copies</td><td class="n">${money(Q.cSum*S.qty)}</td></tr>
<tr class="tot"><td colspan="2">Order total</td><td class="n">${money(Q.total)}</td></tr></tbody></table><p class="hint">Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.</p>`;
$("#leadP").innerHTML=`<h2>When it ships</h2><div class="tl2"><div class="a" style="flex:${L.pw[0]+L.pw[1]}"><b>${L.pw[0]} to ${L.pw[1]} weeks</b>your approvals and the metal work</div><div class="b" style="flex:${L.qw[0]+L.qw[1]}"><b>${L.qw[0]} to ${L.qw[1]} weeks, quoted</b>from your test-pressing approval to dispatch</div></div><p class="hint">Ordered today, it ships about <b>${monthRange(ship[0],ship[1])}</b>. The quote counts from your last approval, because the plant cannot run a clock that is waiting for you. ${L.printed?"Supplying your own jacket saves 1 to 2 weeks.":"No printed jacket to wait for, which saves 1 to 2 weeks."}</p>`;
const E=campEcon();
$("#pDirect").innerHTML=`Deposit today <b>${money(Q.total*R.deposit.v)}</b>`;
$("#pFan").innerHTML=E.goal?`Goal <b>${E.goal} fans</b> at ${money(S.fanPrice)}, you keep about <b>${money(E.at(E.goal).per)}</b> a copy`:"Raise the fan price in the next step";
$("#bDirect").title=S.gate1?"":"Approve the master plan in step 2 first";
$("#choiceMsg").innerHTML=S.gate1?"":`Ordering and launching need your approval of the master plan. <button type="button" class="lnk" data-act="go" data-s="2">Go to step 2</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?"Approve the master plan in step 2 first.":E.goal==null?"Raise the fan price: it does not cover the cost per copy.":S.cap&&S.cap<E.goal?"The edition cap is below the goal.":"";}
function renderFund(){
const E=campEcon();$("#daysV").textContent=S.days;
if(document.activeElement!==$("#fanPrice"))$("#fanPrice").value=S.fanPrice;$("#fanR").value=Math.min(3000,Math.max(500,S.fanPrice));
let h="";
if(E.goal==null)h=`<div class="note bad">At ${money(E.p)} a copy, what is left after payment fees${S.vat?" and VAT":""} does not cover the cost per copy. Raise the fan price.</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>Campaign goal</small><b>${E.goal} fans</b></div><div class="kpi"><small>Your break-even</small><b>${E.be} fans</b></div><div class="kpi"><small>You keep at the goal</small><b>${money(g.per)}</b><em>a copy</em></div><div class="kpi"><small>That is</small><b>${money(g.keep)}</b><em>in total</em></div></div>
${S.cap&&S.cap<E.goal?`<div class="note bad">The edition is capped at ${S.cap}, below the goal of ${E.goal}: this campaign cannot succeed.</div>`:""}
<div class="panel"><h2>What you keep if more fans join</h2><div class="keep">${rows.map(r=>`<div class="krow${r.n===E.goal?" goal":""}"><span>${fmtInt(r.n)} fans</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>${money(r.per)} a copy</small></div></div>`).join("")}</div>
<p class="hint">The goal is the larger of the plant's ${E.min}-copy minimum and your break-even. After payment fees of about ${(R.fee.v*100).toFixed(1)} %${S.vat?" and VAT":""}, the per-title cost and the pressing. Shipping is paid by each fan at cost.</p></div>`;
}
$("#fundEcon").innerHTML=h;
$("#bLaunch").disabled=!canLaunch();
$("#launchMsg").textContent=launchHint()||"Simulated: no page goes live and nobody is charged.";
}
/* ---- 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 "Delivered. Every copy has a passport.";const r=P.rows[P.idx];return (r.gate?"Waiting for your approval: ":"Now: ")+r.t.replace(/^You approve /,"")+".";}
if(S.mode==="campaign"){const E=campEcon(),n=pledgesNow(E);return "Day "+Math.floor(S.simDay)+" of "+S.days+" · "+fmtInt(n)+" of "+(E.goal||0)+" fans";}
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=m==="direct"?"Your order":m==="campaign"?"Your campaign":"Campaign and production";
$("#s5l").textContent=m==="none"?"":m==="direct"?"Follow your record through the plant. The diamonds are your approvals: the plant waits for them.":"Your campaign page, live. Pick how it goes and press play: 30 days in about 8 seconds.";
if(m!==s5mode){s5mode=m;
if(m==="none")el.innerHTML=`<div class="panel empty"><h3>Nothing running yet</h3><p class="mute">Choose in step 3 how to pay for the record, or skip ahead with the demo record.</p><div class="rowc"><button type="button" class="btn" data-act="quickCamp">Run the demo campaign</button><button type="button" class="btn ghost" data-act="quickDone">Jump to a delivered record</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>Run the campaign <small>simulated</small></h2><div class="seg" data-k="scen"><button type="button" data-v="sell">Sells out</button><button type="button" data-v="make">Just makes it</button><button type="button" data-v="miss">Falls short</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="Campaign day"><span id="dayLbl" class="hint"></span></div><div class="kpis" id="money"></div>
<div class="mini">Latest pre-orders</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">Approve</button>`:`<button type="button" class="btn ghost sm" data-act="adv">Next &#x25B8;</button>`):"";
return `<li class="${st}${s.gate?" gate":""}"><span class="dot"><span>${i<P.idx?"&#x2713;":""}</span></span><div><b>${s.t}</b>${s.gate?`<span class="gtag">approval ${s.gate} of 3</span>`:""}<small>${i<P.idx?"done "+fmtD(s.date):(s.gate&&i===P.idx?"waiting for you · planned ":"planned ")+fmtD(s.date)}</small></div>${act}</li>`;}).join("");
const feed=P.events.slice().reverse().map(e=>`<li><small>${fmtD(e.date)}</small>${e.text}</li>`).join("");
const fin=P.idx>=P.rows.length;
return `<div class="pgrid" id="prodGrid"><div class="panel"><div class="ph"><h2>Production <small>${fmtInt(P.run)} copies</small></h2>${fin?`<span class="pill ok">delivered</span>`:`<button type="button" class="btn ghost sm" data-act="prodPlay">${PRODT?"&#x275A;&#x275A; Pause":"&#x25B6; Play to the next approval"}</button>`}</div><ol class="tl">${rows}</ol></div>
<div class="panel"><h2>${P.origin==="direct"?"Your order updates":"Updates your fans see"} <small>from the factory log</small></h2><ul class="feed">${feed}</ul><p class="hint">These come from the plant's own event log: the progress is the factory's record, not a status someone remembers to update.</p></div></div>${fin?passportHTML():""}`;
}
function newProd(origin,start,fans){
const L=leadInfo(true),st=STG.filter(s=>s.id!=="intake"&&s.id!=="g1"&&(!s.opt||S[s.opt]));
const 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),text:origin==="direct"?"Order confirmed and deposit received.":origin==="minimum"?"Going ahead at the "+R.minRun.v+"-copy minimum with "+fmtInt(fans)+" fans.":"Funded by "+fmtInt(fans)+" fans. The campaign is now a production order."}]};
}
function advance(){const P=S.prod;if(!P||P.idx>=P.rows.length)return;const s=P.rows[P.idx],u=UPD[s.id];P.events.push({date:s.date,text:typeof u==="function"?u(P):u});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>Direct order</h2><div class="kvl"><span>${fmtInt(S.qty)} copies, ${esc(descT())}</span><b>${money(Q.total)}</b><span>Deposit paid</span><b>${money(Q.total*R.deposit.v)}</b><span>Balance before dispatch</span><b>${money(Q.total*(1-R.deposit.v))}</b></div><p class="hint">Deposit terms are a placeholder. Labels with an account get their own rates and approve in the customer portal.</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="Day "+Math.floor(S.simDay)+" of "+S.days;
$("#playBtn").innerHTML=TIMER?"&#x275A;&#x275A; Pause":ended?"&#x21BA; Replay":"&#x25B6; Play";
const funded=ended&&n>=goal,soldOut=S.cap&&n>=S.cap;
const status=!ended?`If fewer than ${goal} fans pre-order by ${fmtD(close)}, everyone is refunded automatically.`
:funded?`<b style="color:var(--ok)">&#x2713; Funded by ${fmtInt(n)} fans${soldOut?", sold out":""}.</b> Pressing starts now.`
:S.outcomeAct==="min"?`<b>Going ahead at the ${E.min}-copy minimum.</b>`:S.outcomeAct==="refund"?`<b>Goal missed.</b> Every fan has been refunded.`:`<b>Goal missed.</b> Refunds go out unless the artist covers the gap.`;
const tracks=sideNames().map(sd=>S.tracks.filter(t=>t.side===sd).map(t=>`<li><span class="sd">${sd}</span><button type="button" class="pv${AUid===t.id?" on":""}" data-act="pv" data-id="${t.id}"${t.url?' title="Play a 30 second preview"':' disabled title="Previews are rendered by the plant from your master"'}>${AUid===t.id?"&#x25A0;":"&#x25B6;"}</button><span class="tn">${esc(t.title)}</span><small class="mute">${mmss(t.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>+ shipping</small></div>${S.cap?`<div class="hint">Limited, numbered edition of ${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><b>${fmtInt(n)}</b> of ${fmtInt(goal)} fans${n>goal?` · ${Math.round(n/goal*100)} %`:""}</span><span class="mute">${ended?"closed":Math.max(0,Math.ceil(S.days-S.simDay))+" days left"}</span></div>
<button type="button" class="btn wide" data-act="preorder"${ended?" disabled":""}>${ended?"Pre-orders closed":"Pre-order"}</button>
<p class="fp-s">${status}</p><div class="kvl" style="margin:4px 0"><span>Estimated to ship</span><b>${monthRange(ship[0],ship[1])}</b></div>
<div class="mini">Tracks</div><ol class="fp-tr">${tracks}</ol>
<div class="share"><button type="button" class="btn ghost sm" data-act="copy">Copy link</button><button type="button" class="btn ghost sm" disabled title="Shares to LINE at launch">LINE</button><button type="button" class="btn ghost sm" disabled title="A QR code for gigs and merch tables">QR code</button></div>
<p class="fp-f">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.</p>`;
const held=n*S.fanPrice;
$("#money").innerHTML=`<div class="kpi hl"><small>Fans</small><b>${fmtInt(n)} / ${fmtInt(goal)}</b></div><div class="kpi"><small>Held apart</small><b>${money(held)}</b></div><div class="kpi"><small>${ended?"Closed":"Days left"}</small><b>${ended?close.toLocaleDateString("en-GB",{day:"numeric",month:"short"}):Math.max(0,Math.ceil(S.days-S.simDay))}</b></div>`;
const tk=[];let ago=0;for(let i=n;i>Math.max(0,n-4);i--){const r=rnd(i*7919+13);tk.push(`<li><span>Fan #${i} · ${CITIES[Math.floor(r()*CITIES.length)]}</span><small>${ago?ago+" min ago":"just now"}</small></li>`);ago+=2+Math.floor(r()*6);}
$("#ticker").innerHTML=tk.join("")||`<li><small>No pre-orders yet. Press play.</small></li>`;
let oc="";
if(ended&&goal){
if(funded){const A=E.at(n);
oc=`<div class="panel ok-card block"><h2>Funded</h2><p class="mute" style="margin-top:0">${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.</p><div class="kvl"><span>Fan money after fees${S.vat?" and VAT":""}</span><b>${money(A.net)}</b><span>Per-title work</span><b>${money(-E.tSum)}</b><span>Pressing and packaging, ${fmtInt(A.run)} copies</span><b>${money(-A.run*A.c)}</b><span><b style="color:var(--ink)">You keep</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>Refunded</h2><p class="mute">${fmtInt(n)} refunds of ${money(S.fanPrice)} went back to each fan's original payment method. Nothing was pressed; your master and artwork stay in your vault for another try.</p><button type="button" class="btn ghost" data-act="retry">Run it again</button></div>`;
else if(S.outcomeAct==="min")oc=`<div class="panel ok-card block"><h2>Going ahead at the minimum</h2><p class="mute">${diff<0?"You paid the gap of <b>"+money(-diff)+"</b>":"The fans' money covered the minimum run; you keep <b>"+money(diff)+"</b>"}, and you receive the ${spare} copies no fan claimed.</p></div>`;
else oc=`<div class="panel warn-card block"><h2>Goal missed: ${fmtInt(n)} of ${fmtInt(goal)}</h2><p class="mute" style="margin:0">Two ways out. The artist chooses.</p><div class="opts"><div class="opt"><b>Refund everyone</b><p>Automatic, from the separate account. Nothing is pressed; your files stay in your vault.</p><button type="button" class="btn ghost sm" data-act="refund">Refund all ${fmtInt(n)} fans</button></div><div class="opt"><b>Go ahead at the ${E.min}-copy minimum</b><p>${diff<0?"You pay the gap of <b>"+money(-diff)+"</b>":"Costs you nothing: the fans' money covers it and you keep "+money(diff)}. You receive the ${spare} unclaimed copies.</p><button type="button" class="btn sm" data-act="goMin">Go ahead</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?"No. "+String(copy).padStart(3,"0")+" of "+fmtInt(P.run):"Copy "+copy+" of "+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=[["Master received","Checksums verified at both ends, stored in the client vault",P.start]];
if(by("g2"))it.push(["Mastered for vinyl","Master approved by the artist",by("g2").date]);
it.push(["Cut to lacquer","Lacquer "+lac+", serial scribed in the run-out groove",by("cut").date]);
it.push(["Plated in-house","Father "+fa+" → mother "+mo+" (archived for represses) → stamper "+st,by("plate").date]);
it.push(["Test pressing approved","Heard and approved by the artist",by("g3").date]);
it.push(["Pressed","Press D1-B (Newbilt Duplex) at "+ts.slice(11)+", compound lot "+lot+", press data logged for this cycle",run.date]);
it.push(["Inspected","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",run.date]);
it.push(["Packed","Sleeve"+(S.jacket!=="none"?" and jacket":"")+(S.shrink?", shrink wrap":"")+"; the jacket code is checked against this record",by("fin").date]);
it.push(["Shipped",P.origin==="direct"?"Dispatched with tracking":"Tracked parcel to the fan",by("ship").date]);
return `<div class="passport" id="passport"><div class="pp-head"><div><div class="eyebrow">Record passport</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>Record key: stamper · press · cycle time · side</span><code>${st}-A1 · D1-B · ${ts} · side A</code></div>
<ol class="pp-chain">${it.map(([t,d,dt])=>`<li><span class="ci ok">&#x2713;</span><div><b>${t}</b><small>${esc(d)}</small></div><span class="dt">${fmtD(dt)}</span></li>`).join("")}</ol>
<p class="hint">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.</p>
<div class="repress"><p><b>Sold out? Open a repress.</b> The mother is archived, so a repress skips cutting and plating: about 1.5 to 2.5 weeks of plant time, pressed when 100 more fans are in.</p><button type="button" class="btn ghost" data-act="repress">Open repress pre-orders</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:"Continue to music & art →",m:"Music & art →",f:()=>go(2)};
case 2:return S.gate1?{t:"See my price →",m:"My price →",f:()=>go(3)}:{t:"Approve and see my price →",m:"Approve →",f:approve,dis:!canApprove(),hint:approveHint()};
case 3:return {t:"Let my fans fund it →",m:"Fan funding →",f:()=>go(4),hint:"Or order directly, below."};
case 4:return {t:"Launch the campaign →",m:"Launch →",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||"Untitled";$("#stA").textContent=S.artist||"";
const sp=[(S.discs>1?"2 × ":"")+SIZE_MM[S.size].name,rpmT()];if(S.size==="12")sp.push(S.weight+" g");sp.push(colourName(),JNAME[S.jacket]);
Object.keys(XN).forEach(k=>{if(S[k])sp.push(XN[k]);});
$("#stSpecs").innerHTML=sp.map(x=>`<span>${esc(x)}</span>`).join("");
const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb;
if(camp){const E=campEcon();
pr=`<div class="sp"><div><small>Fan price</small><b class="pc">${money(S.fanPrice)}</b></div><div class="r"><small>Campaign goal</small><b>${E.goal?E.goal+" fans":"not reachable"}</b></div></div><div class="sl"><span>You keep at the goal</span><b>${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}</b></div>`;
mb=`<small>Fan price · goal</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?E.goal+" fans":""}</small>`;}
else{pr=`<div class="sp"><div><small>Per copy, all in</small><b class="pc">${money(Q.per)}</b></div><div class="r"><small>${fmtInt(S.qty)} copies</small><b>${money(Q.total)}</b></div></div><div class="sl"><span>Ordered today, ships</span><b>${monthRange(ship[0],ship[1])}</b></div>`;
mb=`<small>${fmtInt(S.qty)} copies · per copy</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>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();}
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,top=$("main").getBoundingClientRect().top+y-90;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);startSim();renderAll();}
function setQty(q){S.qty=Math.min(5000,Math.max(100,Math.round(q)));if(S.mode==="direct")resetCampaign();renderAll();}
function syncInputs(){
$("#artist").value=S.artist;$("#title").value=S.title;$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
$("#fanPrice").value=S.fanPrice;$("#fanR").value=Math.min(3000,Math.max(500,S.fanPrice));$("#days").value=S.days;$("#cap").value=S.cap;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
}
function resetDemo(){
stopSim();stopProd();if(AU){AU.pause();AU=null;AUid=null;}
const o=JSON.parse(S0);Object.keys(o).forEach(k=>{S[k]=o[k];});s5mode=null;syncInputs();renderTracks();renderAll();
}
/* ---- events ---- */
const PLAN_KEYS=["size","rpm","discs","loud"],RUN_KEYS=["weight","colour","inner","jacket","mastering"];
function setK(k,v){
if(S[k]===v)return;S[k]=v;
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)toast("Format changed: approve the master plan again in step 2.");}
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();},
direct:()=>{if(!S.gate1){toast("Approve the master plan first.");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],u=UPD[s.id];S.prod.events.push({date:s.date,text:typeof u==="function"?u(S.prod):u});S.prod.idx++;}go(5);setTimeout(()=>{const p=$("#passport");if(p)p.scrollIntoView({behavior:"smooth",block:"start"});},60);},
preorder:()=>toast("Demo: fans pay by PromptPay QR, card or wallet, held apart until the goal is met."),
repress:()=>toast("Simulated: the campaign page reopens as a repress pre-order."),
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(()=>toast("Copied (example link): "+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 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(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;renderSides();}
else if(id==="fanPrice"){const v=+t.value;if(v>=100&&v<=20000){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="fanR"){S.fanPrice=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="cap"){S.cap=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="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==="fanPrice"){S.fanPrice=Math.min(20000,Math.max(100,Math.round(+t.value||1200)));t.value=S.fanPrice;renderAll();}
else if(t.classList.contains("tdur")){const v=parseDur(t.value);if(v==null||v<=0){t.classList.add("err");return;}t.classList.remove("err");trkOf(t).sec=v;t.value=mmss(v);S.demoTracks=false;revokePlan();renderAll();}
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
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();}
}
async function handleArt(k,files){
const f=files.find(f=>/^image\//.test(f.type)||/\.(jpe?g|png|tiff?|webp)$/i.test(f.name));if(!f)return;
try{S.art[k]=await readImage(f);}catch(err){S.art[k]={name:f.name,err:"This browser cannot read the file; the real platform checks TIFF and PDF after upload."};}
if(S.mode)resetCampaign();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;
function renderTourBtn(){$("#tourBtn").innerHTML=TOUR?"&#x25A0;<span class=\"tl\"> Stop tour</span>":"&#x25B6;<span class=\"tl\"> Tour</span>";}
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,t)=>{$("#tourCap").hidden=false;$("#tourN").textContent=i+" / "+N;$("#tourT").textContent=t;};
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,"An artist designs a record. The mock-up is drawn to scale and follows every choice.");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,"Masters and artwork are checked before anyone pays: file format, side length against the format, print resolution.");await w(3800);
S.rights=true;syncInputs();renderAll();into("#checkPanel");await w(1200);
cap(3,"The artist approves the master plan, approval 1 of 3. Nothing goes in front of fans that the plant cannot make.");await w(2600);
approve();
cap(4,"An itemised price, every line visible. The price per copy falls fast with the run, because cutting and plating are in-house.");
await w(900);for(const q of [100,150,200,300,500,700,1000,700,500,300]){setQty(q);await w(280);}await w(2000);
go(4);cap(5,"No money up front: fans pre-order instead. The goal is the plant's minimum run or the artist's break-even, whichever is larger.");await w(4400);
S.scen="sell";launch();
cap(6,"A 30-day campaign, simulated. Fan money sits in a separate account until the goal is met; if it is missed, everyone is refunded.");
while(!S.outcome)await w(250);await w(1800);
into("#prodGrid");
cap(7,"Funded: the campaign becomes a production order. Fans follow the factory's own event log, and the plant waits for the artist's approvals.");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,"Delivered. Every copy has a passport: stamper, press, cycle time and side, with its inspection results.");await w(6000);
stopTour();toast("Tour finished. Everything on the page is clickable.");
}catch(e){if(TOUR===T)stopTour();}
}
/* ---- theme ---- */
(function(){
const b=$("#themeBtn"),de=document.documentElement;
function set(t,save){de.setAttribute("data-theme",t);if(save){try{localStorage.setItem("cnx-theme",t)}catch(e){}}b.textContent=t==="light"?"☾":"☀";b.title="Switch to "+(t==="light"?"dark":"light")+" mode";b.setAttribute("aria-label",b.title);}
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="${n}"></button>`).join("")+`<input type="color" aria-label="Custom colour">`;});
syncInputs();renderTracks();renderAll();
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,quote,campEcon,leadInfo,sideStats,checkList,go,ACT,setK,setQty,tour,stopTour,resetDemo};
})();
</script>
</body>
</html>