← back to CelebritySignatures

public/murals.html

544 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Celebrity Signature Murals — made to order</title>
<style>
  :root{
    --ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e3dc; --bg:#f7f5f0; --card:#fff;
    --accent:#1d7a36; --gold:#9a6b00; --wall:#efe9df; --floor:#d9cfbf;
  }
  *{box-sizing:border-box}
  body{margin:0;font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg)}
  h1,h2,h3{font-family:Georgia,"Times New Roman",serif;font-weight:600;letter-spacing:.01em}
  a{color:var(--accent)}
  header{padding:24px 30px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:0;z-index:30;display:flex;align-items:baseline;gap:16px;flex-wrap:wrap}
  header h1{margin:0;font-size:24px}
  header .sub{color:var(--muted);font-size:13px}
  header nav{margin-left:auto;display:flex;gap:18px;font-size:14px}
  header nav a{color:var(--muted);text-decoration:none}
  header nav a:hover{color:var(--ink)}
  main{max-width:1180px;margin:0 auto;padding:30px 30px 80px}
  .intro{max-width:760px;margin-bottom:30px}
  .intro h2{font-size:30px;margin:0 0 10px}
  .intro p{color:#3c3c3c;margin:0 0 8px}
  .spec{display:flex;gap:26px;flex-wrap:wrap;margin-top:16px;padding:14px 18px;background:#fff;border:1px solid var(--line);border-radius:12px}
  .spec div{font-size:13px}
  .spec b{display:block;font-size:18px;font-family:Georgia,serif}
  /* gallery */
  .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:22px;margin-top:10px}
  .m-card{background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden;display:flex;flex-direction:column}
  .m-card .pic{background:#fff;border-bottom:1px solid var(--line);padding:14px;display:flex;align-items:center;justify-content:center;aspect-ratio:2/1;overflow:hidden}
  .m-card .pic img{max-width:100%;max-height:100%;object-fit:contain}
  .m-card .body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:8px;flex:1}
  .m-card h3{margin:0;font-size:19px}
  .m-card .blurb{color:var(--muted);font-size:13.5px;flex:1}
  .m-card .facts{display:flex;gap:8px;flex-wrap:wrap;font-size:11.5px;color:#555}
  .pill{padding:3px 10px;border-radius:999px;background:#f0ece3;border:1px solid var(--line)}
  .m-card .foot{display:flex;align-items:center;gap:10px;margin-top:4px}
  .price{font-family:Georgia,serif;font-size:22px}
  .btn{appearance:none;border:1px solid var(--ink);background:var(--ink);color:#fff;font:inherit;font-size:14px;padding:9px 16px;border-radius:9px;cursor:pointer;text-decoration:none;display:inline-block}
  .btn.ghost{background:#fff;color:var(--ink)}
  .btn:hover{opacity:.9}
  .btn.full{width:100%;text-align:center}
  /* studio */
  .studio{margin-top:46px;border-top:1px solid var(--line);padding-top:34px;scroll-margin-top:90px}
  .studio h2{font-size:26px;margin:0 0 4px}
  .studio .lead{color:var(--muted);margin:0 0 18px;max-width:680px}
  .stage-wrap{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px}
  .ctrls{display:flex;gap:22px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
  .ctrls .f{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--muted)}
  .ctrls select,.ctrls input[type=range]{font:inherit}
  .ctrls select{padding:7px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);font-size:14px}
  .ctrls output{font-family:Georgia,serif;color:var(--ink);font-size:15px}
  .scene{position:relative;width:100%;background:linear-gradient(#f4eee4,#efe9df);border:1px solid var(--line);border-radius:10px;overflow:hidden;user-select:none;touch-action:none}
  .floor{position:absolute;left:0;right:0;bottom:0;background:var(--floor);border-top:2px solid #c9bda9}
  .baseboard{position:absolute;left:0;right:0;background:#fff;border-top:1px solid #cfc4b2;border-bottom:1px solid #cfc4b2}
  .human{position:absolute;bottom:0;width:14px;background:#cdbfa9;border-radius:7px 7px 0 0;opacity:.85}
  .human::after{content:"6 ft";position:absolute;top:-18px;left:50%;transform:translateX(-50%);font-size:10px;color:#8a7c63;white-space:nowrap}
  .mural{position:absolute;cursor:grab;border:1px solid rgba(0,0,0,.18);box-shadow:0 6px 22px rgba(60,50,30,.22);background-size:cover;background-position:center}
  .mural.drag{cursor:grabbing;box-shadow:0 10px 30px rgba(60,50,30,.32)}
  .mural .grip{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;letter-spacing:.08em;text-shadow:0 1px 3px rgba(0,0,0,.5);opacity:0;transition:.15s}
  .mural:hover .grip{opacity:.9}
  .dims{position:absolute;font-size:11px;color:#8a7c63;pointer-events:none}
  .readout{display:flex;gap:24px;flex-wrap:wrap;margin-top:14px;font-size:13px}
  .readout b{font-family:Georgia,serif;font-weight:600;font-size:15px}
  .hint{font-size:12px;color:var(--muted);margin-top:8px}
  /* order */
  .order{margin-top:40px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:22px 24px;max-width:680px}
  .order h2{font-size:22px;margin:0 0 6px}
  .order p{color:var(--muted);margin:0 0 16px;font-size:14px}
  .order form{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .order label{display:flex;flex-direction:column;gap:5px;font-size:12px;color:var(--muted)}
  .order label.wide{grid-column:1/3}
  .order input,.order textarea{font:inherit;padding:9px 11px;border:1px solid var(--line);border-radius:8px;background:#fff}
  .order textarea{min-height:64px;resize:vertical}
  .order .summary{grid-column:1/3;background:var(--bg);border:1px dashed var(--line);border-radius:10px;padding:12px 14px;font-size:13px}
  .order .summary b{font-family:Georgia,serif}
  .msg{grid-column:1/3;font-size:14px}
  .msg.ok{color:var(--accent)} .msg.err{color:#b3261e}
  footer{max-width:1180px;margin:0 auto;padding:0 30px 50px;color:var(--muted);font-size:12.5px;line-height:1.7}
  @media(max-width:620px){.order form{grid-template-columns:1fr}.order label.wide,.order .summary,.msg{grid-column:1}}
  /* — name finder (consensus component) — */
  .finder{margin-top:18px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:15px 17px}
  .finder .lab{font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px}
  .finder .row{display:flex;gap:10px;flex-wrap:wrap}
  .finder input{flex:1;min-width:230px;font:inherit;padding:10px 12px;border:1px solid var(--line);border-radius:8px}
  .finder .res{margin-top:11px;display:flex;flex-direction:column;gap:7px;font-size:14px}
  .finder .hit{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
  .finder .hit img{height:26px;max-width:96px;object-fit:contain}
  .finder .hit b{font-family:Georgia,serif;font-weight:600}
  .finder .miss{color:var(--muted)}
  /* — named-toggle pill on card image — */
  .name-toggle{position:absolute;top:10px;right:10px;font-size:11.5px;padding:4px 11px;border-radius:999px;background:rgba(255,255,255,.93);border:1px solid var(--line);cursor:pointer;color:var(--ink);z-index:2}
  .name-toggle:hover{background:#fff}
  /* — roster drawer — */
  .roster-trig{background:none;border:none;color:var(--accent);font:inherit;font-size:12.5px;cursor:pointer;padding:0;text-decoration:underline;align-self:flex-start}
  .roster{margin-top:6px;border-top:1px dashed var(--line);padding-top:10px;display:none}
  .roster.open{display:block}
  .roster .r-search{width:100%;font:inherit;font-size:13px;padding:7px 9px;border:1px solid var(--line);border-radius:7px;margin-bottom:8px}
  .roster .r-list{display:grid;grid-template-columns:1fr 1fr;gap:5px;max-height:260px;overflow:auto}
  .roster .r-item{display:flex;align-items:center;gap:8px;font-size:12.5px;padding:4px 6px;border-radius:6px;cursor:pointer;text-align:left;background:none;border:1px solid transparent;width:100%;font-family:inherit;color:var(--ink)}
  .roster .r-item:hover{background:#f4f0e8;border-color:var(--line)}
  .roster .r-item img{height:20px;width:50px;object-fit:contain;flex:none}
  .roster .r-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* — provenance modal — */
  .modal{position:fixed;inset:0;background:rgba(20,18,14,.55);display:none;align-items:center;justify-content:center;z-index:60;padding:20px}
  .modal.open{display:flex}
  .modal .box{background:#fff;border-radius:14px;max-width:440px;width:100%;padding:22px 24px;position:relative}
  .modal .x{position:absolute;top:10px;right:14px;border:none;background:none;font-size:22px;cursor:pointer;color:var(--muted);line-height:1}
  .modal .sig{background:#fff;border:1px solid var(--line);border-radius:10px;padding:16px;display:flex;align-items:center;justify-content:center;min-height:90px;margin-bottom:14px}
  .modal .sig img{max-width:100%;max-height:120px;object-fit:contain}
  .modal h3{margin:0 0 4px;font-size:21px}
  .modal .why{color:var(--muted);font-size:13px;margin:0 0 14px}
  .modal .prov{display:grid;grid-template-columns:auto 1fr;gap:7px 14px;font-size:13px;margin:0}
  .modal .prov dt{color:var(--muted)}
  .modal .prov dd{margin:0}
  .modal .badge{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;background:#eaf5ec;color:#1d7a36;border:1px solid #cfe8d5}
  .modal .badge.medium{background:#fdf4e3;color:#9a6b00;border-color:#f0e2c0}
  .modal .badge.high{background:#fdecea;color:#b3261e;border-color:#f3cdc8}
  /* — scene presets in studio — */
  .scene-presets{display:flex;gap:6px;flex-wrap:wrap}
  .scene-presets button{font:inherit;font-size:12.5px;padding:6px 11px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer;color:var(--muted)}
  .scene-presets button.on{border-color:var(--ink);color:var(--ink);background:#f4f0e8}
  .scene .swindow{position:absolute;top:8%;background:linear-gradient(160deg,#dCE7f0,#eef3f7);border:3px solid #fff;box-shadow:0 0 0 1px #c9bda9, 0 8px 20px rgba(0,0,0,.12)}
  .scene .sart{position:absolute;top:14%;background:#fff;border:4px solid #fff;box-shadow:0 0 0 1px #c9bda9,0 6px 16px rgba(0,0,0,.14)}
</style>
</head>
<body>
<header>
  <h1>Celebrity Signature Murals</h1>
  <span class="sub">made to order · Designer Wallcoverings</span>
  <nav>
    <a href="/">Signature archive</a>
    <a href="#studio">Wall studio</a>
    <a href="#order">Order</a>
  </nav>
</header>

<main>
  <section class="intro">
    <h2>A wall of history, in their own hand.</h2>
    <p>Each mural is a curated wall of authentic signatures — every name traced to a
       public-domain or openly licensed source, no signature invented. Printed to order
       as a single seamless piece for a full feature wall.</p>
    <div class="spec" id="spec"><!-- filled by JS --></div>
    <div class="finder">
      <p class="lab">Is your icon on a wall?</p>
      <div class="row">
        <input id="finderInput" type="search" placeholder="Search any name — e.g. Lincoln, Chaplin, Babe Ruth…" autocomplete="off">
      </div>
      <div class="res" id="finderRes"></div>
    </div>
  </section>

  <div class="gallery" id="gallery"></div>

  <!-- WALL PLACEMENT STUDIO -->
  <section class="studio" id="studio">
    <h2>Place it on your wall</h2>
    <p class="lead">Pick a mural, set your wall width, then <b>drag the mural</b> to exactly
       where you want it. The 6&nbsp;ft figure is there for scale.</p>
    <div class="stage-wrap">
      <div class="ctrls">
        <div class="f">Mural
          <select id="selMural"></select>
        </div>
        <div class="f">Wall width — <output id="wwOut"></output>
          <input id="wallW" type="range" min="24" max="60" step="1" value="32">
        </div>
        <div class="f">Wall height — <output id="whOut"></output>
          <input id="wallH" type="range" min="12" max="20" step="1" value="14">
        </div>
        <div class="f">Room scene
          <div class="scene-presets" id="scenePresets"></div>
        </div>
        <div class="f">&nbsp;
          <button class="btn ghost" id="btnCenter" type="button">Center on wall</button>
        </div>
      </div>
      <div class="scene" id="scene"></div>
      <div class="readout" id="readout"></div>
      <div class="hint">Mural is fixed at 24&nbsp;ft wide × 12&nbsp;ft tall (150&nbsp;DPI print master). Drag to set horizontal &amp; vertical placement; it can't leave the wall.</div>
    </div>
  </section>

  <!-- ORDER -->
  <section class="order" id="order">
    <h2>Order this mural</h2>
    <p>Tell us your wall and we'll prepare your 150&nbsp;DPI print master and a quote.</p>
    <form id="orderForm">
      <label>Your name<input name="name" required></label>
      <label>Email<input name="email" type="email" required></label>
      <label>Wall width (ft)<input name="wall_w" type="number" min="1" step="0.5" required></label>
      <label>Wall height (ft)<input name="wall_h" type="number" min="1" step="0.5" required></label>
      <label class="wide">Notes (room, install date, anything)<textarea name="notes"></textarea></label>
      <div class="summary" id="orderSummary"></div>
      <div style="grid-column:1/3"><button class="btn" type="submit">Request this mural</button></div>
      <div class="msg" id="orderMsg"></div>
    </form>
  </section>
</main>

<!-- provenance modal (consensus component) -->
<div class="modal" id="provModal">
  <div class="box">
    <button class="x" id="provX" type="button" aria-label="Close">×</button>
    <div class="sig" id="provSig"></div>
    <h3 id="provName"></h3>
    <p class="why" id="provWhy"></p>
    <dl class="prov" id="provFields"></dl>
  </div>
</div>

<footer>
  Every signature on these murals is sourced from Wikimedia Commons under a public-domain or
  open license and is rendered at uniform size — no signature is emphasized over another.
  Living-celebrity signatures are excluded (right of publicity). Print masters are produced to
  order at 150&nbsp;DPI; final clarity scales with viewing distance for large-format installs.
</footer>

<script>
const FT = " ft";
let CAT = null;          // catalog
let cur = null;          // current mural in studio
let pos = {x:0, y:0};    // mural top-left in FEET on the wall

// signature archive (powers the consensus components: finder, roster, provenance)
const CATEGORY_OF = {
  'oldest-signatures':'Oldest Signatures',
  'declaration-of-independence':'Declaration of Independence', 'politics':'Politics',
  'sports':'Sports', 'hollywood':'Hollywood', 'movies-classic':'Movies (Classic)', 'tv':'TV'
};
let SIGS = [];           // usable=yes rows only (what's actually on the murals)
let byMural = {};        // mural code -> [rows] (rank-sorted)
let sigByKey = {};       // "category|rank" -> row

// room-scene presets for the wall studio (CSS-only, $0)
const SCENES = {
  plain:   {label:'Plain',   wall:'linear-gradient(#f4eee4,#efe9df)', floor:'#d9cfbf', extra:''},
  study:   {label:'Study',   wall:'linear-gradient(#e9ddc9,#ddcdb2)', floor:'#7c5a3a', extra:'art'},
  lounge:  {label:'Lounge',  wall:'linear-gradient(#e7e8e3,#d8dad2)', floor:'#b29a7c', extra:'window'},
  gallery: {label:'Gallery', wall:'linear-gradient(#fafafa,#eeeeec)', floor:'#cfcabf', extra:''},
};
let curScene = 'plain';

const money = n => "$" + Math.round(n).toLocaleString();
const $ = s => document.querySelector(s);

async function boot(){
  CAT = await (await fetch('/api/murals-catalog')).json();
  const {widthFt,heightFt,dpi,pxW,pxH,sqft} = CAT.size;
  const price = CAT.pricePerSqFt * sqft;

  $('#spec').innerHTML = `
    <div><b>${heightFt} × ${widthFt} ft</b>height × width</div>
    <div><b>${dpi} DPI</b>${pxH.toLocaleString()} × ${pxW.toLocaleString()} px master</div>
    <div><b>${sqft} sq ft</b>seamless feature wall</div>
    <div><b>${money(price)}</b>from, ${CAT.material.toLowerCase()}</div>`;

  // gallery
  $('#gallery').innerHTML = CAT.murals.map(m => `
    <div class="m-card" data-code="${m.code}">
      <div class="pic">
        <button class="name-toggle" data-toggle="${m.code}" type="button">Show names</button>
        <img loading="lazy" data-base="/output/collage-${m.slug}.png" data-named="/output/collage-${m.slug}-named.png"
             src="/output/collage-${m.slug}.png" alt="${m.title} signature mural">
      </div>
      <div class="body">
        <h3>${m.title}</h3>
        <div class="blurb">${m.blurb}</div>
        <div class="facts">
          <span class="pill">${m.sigs} signatures</span>
          <span class="pill">${heightFt}×${widthFt} ft</span>
          <span class="pill">${dpi} DPI TIF</span>
        </div>
        <button class="roster-trig" data-roster="${m.code}" type="button">See who's on this wall ▾</button>
        <div class="roster" data-rosterbody="${m.code}"></div>
        <div class="foot">
          <span class="price">${money(price)}</span>
          <button class="btn ghost" data-place="${m.code}" type="button">Place on wall →</button>
        </div>
      </div>
    </div>`).join('');

  // mural selector
  $('#selMural').innerHTML = CAT.murals.map(m => `<option value="${m.code}">${m.title}</option>`).join('');

  // load the signature archive (usable rows only) → finder, roster, provenance
  try{
    const all = await (await fetch('/api/signatures')).json();
    SIGS = all.filter(r => r.usable_in_commercial_collage === 'yes');
    for(const r of SIGS) sigByKey[r.category + '|' + r.rank] = r;
    for(const m of CAT.murals){
      if(CATEGORY_OF[m.slug]){
        byMural[m.code] = SIGS.filter(r => r.category === CATEGORY_OF[m.slug]).sort((a,b)=>a.rank-b.rank);
      } else if(Array.isArray(m.roster)){
        // themed murals carry their resolved roster in the catalog entry
        byMural[m.code] = m.roster.map((nm,i)=>({full_name:nm, category:m.slug, rank:i+1, signature_image_url:''}));
      } else {
        byMural[m.code] = [];
      }
    }
  }catch(e){ SIGS = []; }

  // gallery actions: place-on-wall · named toggle · roster drawer · roster item → provenance
  $('#gallery').addEventListener('click', e => {
    const place = e.target.closest?.('[data-place]');
    if(place){ const code=place.getAttribute('data-place'); $('#selMural').value=code; selectMural(code); $('#studio').scrollIntoView({behavior:'smooth'}); return; }
    const tog = e.target.closest?.('[data-toggle]');
    if(tog){ const img=tog.parentElement.querySelector('img'); const showing=tog.textContent.startsWith('Show');
      img.src = showing ? img.dataset.named : img.dataset.base; tog.textContent = showing ? 'Hide names' : 'Show names'; return; }
    const rt = e.target.closest?.('[data-roster]');
    if(rt){ toggleRoster(rt.getAttribute('data-roster'), rt); return; }
    const item = e.target.closest?.('[data-sig]');
    if(item){ openProv(sigByKey[item.getAttribute('data-sig')]); return; }
  });
  $('#gallery').addEventListener('input', e => {
    const rs = e.target.closest?.('.r-search'); if(!rs) return;
    const q = rs.value.trim().toLowerCase();
    rs.closest('.roster').querySelectorAll('.r-item').forEach(it =>
      it.style.display = it.textContent.toLowerCase().includes(q) ? '' : 'none');
  });

  buildScenePresets();
  wireFinder();
  $('#provX').addEventListener('click', ()=>$('#provModal').classList.remove('open'));
  $('#provModal').addEventListener('click', e => { if(e.target.id==='provModal') $('#provModal').classList.remove('open'); });
  document.addEventListener('keydown', e => { if(e.key==='Escape') $('#provModal').classList.remove('open'); });

  $('#selMural').addEventListener('change', e => selectMural(e.target.value));
  $('#wallW').addEventListener('input', drawScene);
  $('#wallH').addEventListener('input', drawScene);
  $('#btnCenter').addEventListener('click', centerMural);
  window.addEventListener('resize', drawScene);

  selectMural(CAT.murals[0].code);
  wireOrder();
}

// — roster drawer (consensus #1) —
function toggleRoster(code, trig){
  const body = document.querySelector(`[data-rosterbody="${code}"]`);
  const open = body.classList.toggle('open');
  trig.textContent = (open ? "Hide who's on this wall ▴" : "See who's on this wall ▾");
  if(open && !body.dataset.filled){
    const rows = byMural[code] || [];
    body.innerHTML =
      `<input class="r-search" type="search" placeholder="Filter ${rows.length} names…" autocomplete="off">
       <div class="r-list">` +
      rows.map(r => `<button class="r-item" data-sig="${r.category}|${r.rank}" type="button">
          <img loading="lazy" src="${r.signature_image_url}" alt=""><span>${r.full_name}</span></button>`).join('') +
      `</div>`;
    body.dataset.filled = '1';
  }
}

// — provenance modal (consensus #2) —
function openProv(row){
  if(!row) return;
  $('#provSig').innerHTML = `<img src="${row.signature_image_url}" alt="${row.full_name} signature">`;
  $('#provName').textContent = row.full_name;
  $('#provWhy').textContent = row.reason_for_ranking || '';
  const deceased = row.deceased ? ('Deceased' + (row.death_date ? ' ' + String(row.death_date).slice(0,4) : '')) : 'Living';
  const risk = (row.risk_level || 'low').toLowerCase();
  const link = row.wikidata || (row.ranking_source_urls||'').split('|')[0].trim() || row.signature_image_url;
  $('#provFields').innerHTML = `
    <dt>License</dt><dd>${row.image_license || '—'}</dd>
    <dt>Source</dt><dd>${row.signature_source_type || 'Wikimedia Commons'}</dd>
    <dt>Status</dt><dd>${deceased}</dd>
    <dt>Use</dt><dd><span class="badge ${risk}">${risk} risk · collage-cleared</span></dd>
    <dt>Provenance</dt><dd><a href="${link}" target="_blank" rel="noopener noreferrer">Wikidata / source ↗</a></dd>`;
  $('#provModal').classList.add('open');
}

// — name finder (consensus #3) —
function wireFinder(){
  const inp = $('#finderInput'), res = $('#finderRes');
  const codeOfCat = {}; CAT.murals.forEach(m => codeOfCat[CATEGORY_OF[m.slug]] = m);
  inp.addEventListener('input', () => {
    const q = inp.value.trim().toLowerCase();
    if(q.length < 2){ res.innerHTML = ''; return; }
    const hits = SIGS.filter(r => r.full_name.toLowerCase().includes(q)).slice(0, 8);
    if(!hits.length){ res.innerHTML = `<div class="miss">Not in the openly-licensed archive yet — <a href="#order">request a custom mural</a> and we'll source them.</div>`; return; }
    res.innerHTML = hits.map(r => { const m = codeOfCat[r.category];
      return `<div class="hit"><img src="${r.signature_image_url}" alt=""> <b>${r.full_name}</b> — on ${m ? `<a href="#" data-go="${m.code}">${m.title}</a>` : r.category}</div>`;
    }).join('');
  });
  res.addEventListener('click', e => {
    const go = e.target.closest?.('[data-go]'); if(!go) return;
    e.preventDefault(); const code = go.getAttribute('data-go');
    $('#selMural').value = code; selectMural(code); $('#studio').scrollIntoView({behavior:'smooth'});
  });
}

// — room-scene presets (consensus #4) —
function buildScenePresets(){
  $('#scenePresets').innerHTML = Object.entries(SCENES).map(([k,v]) =>
    `<button type="button" data-scene="${k}" class="${k===curScene?'on':''}">${v.label}</button>`).join('');
  $('#scenePresets').addEventListener('click', e => {
    const b = e.target.closest('[data-scene]'); if(!b) return;
    curScene = b.getAttribute('data-scene');
    $('#scenePresets').querySelectorAll('button').forEach(x => x.classList.toggle('on', x===b));
    drawScene();
  });
}

function muralByCode(c){ return CAT.murals.find(m => m.code === c); }

function selectMural(code){
  cur = muralByCode(code);
  centerMural();           // also draws
}

function wallDims(){ return { W:+$('#wallW').value, H:+$('#wallH').value }; }

function centerMural(){
  const {W,H} = wallDims();
  const mw = CAT.size.widthFt, mh = CAT.size.heightFt;
  pos.x = Math.max(0,(W-mw)/2);
  pos.y = Math.max(0,(H-mh)/2);
  drawScene();
}

function drawScene(){
  if(!cur) return;
  const scene = $('#scene');
  const {W,H} = wallDims();
  const mw = CAT.size.widthFt, mh = CAT.size.heightFt;
  // scale: fit wall width to scene width; cap scene height sensibly
  const sw = scene.clientWidth || 900;
  let pxPerFt = sw / W;
  const sceneH = H * pxPerFt;
  scene.style.height = sceneH + "px";

  // clamp position to wall
  pos.x = Math.min(Math.max(0,pos.x), Math.max(0,W-mw));
  pos.y = Math.min(Math.max(0,pos.y), Math.max(0,H-mh));

  $('#wwOut').textContent = W + FT;
  $('#whOut').textContent = H + FT;

  const floorH = Math.min(sceneH*0.10, pxPerFt*1.0);
  const SC = SCENES[curScene] || SCENES.plain;
  scene.style.background = SC.wall;
  let extra = '';
  if(SC.extra === 'window') extra = `<div class="swindow" style="right:7%;width:${pxPerFt*3.5}px;height:${pxPerFt*4.5}px"></div>`;
  else if(SC.extra === 'art') extra = `<div class="sart" style="left:6%;width:${pxPerFt*2}px;height:${pxPerFt*2.6}px"></div>`;
  scene.innerHTML = `
    <div class="floor" style="height:${floorH}px;background:${SC.floor}"></div>
    ${extra}
    <div class="baseboard" style="bottom:${floorH}px;height:${Math.max(6,pxPerFt*0.4)}px"></div>
    <div class="human" style="left:${pxPerFt*1.2}px;height:${6*pxPerFt - floorH}px;bottom:${floorH}px"></div>
    <div class="mural" id="muralEl"
         style="width:${mw*pxPerFt}px;height:${mh*pxPerFt}px;
                left:${pos.x*pxPerFt}px;top:${(H-mh-pos.y)*pxPerFt}px;
                background-image:url('/output/collage-${cur.slug}.png')">
      <div class="grip">⟵ drag ⟶</div>
    </div>`;
  enableDrag(pxPerFt);

  const floorGap = pos.y;                       // ft from mural bottom to floor
  const leftGap = pos.x;                        // ft from left wall to mural
  const rightGap = Math.max(0, W - mw - pos.x);
  $('#readout').innerHTML = `
    <div>From left wall: <b>${leftGap.toFixed(1)}${FT}</b></div>
    <div>From right wall: <b>${rightGap.toFixed(1)}${FT}</b></div>
    <div>Bottom off floor: <b>${floorGap.toFixed(1)}${FT}</b></div>
    <div>Mural: <b>${mh}×${mw}${FT}</b></div>`;
  syncOrderSummary();
}

function enableDrag(pxPerFt){
  const el = $('#muralEl');
  let start=null;
  el.addEventListener('pointerdown', e => {
    start = {mx:e.clientX, my:e.clientY, px:pos.x, py:pos.y};
    el.setPointerCapture(e.pointerId); el.classList.add('drag');
  });
  el.addEventListener('pointermove', e => {
    if(!start) return;
    const dxFt = (e.clientX-start.mx)/pxPerFt;
    const dyFt = (e.clientY-start.my)/pxPerFt;
    pos.x = start.px + dxFt;
    pos.y = start.py - dyFt;       // up = increase floor gap
    const {W,H}=wallDims(), mw=CAT.size.widthFt, mh=CAT.size.heightFt;
    pos.x = Math.min(Math.max(0,pos.x), Math.max(0,W-mw));
    pos.y = Math.min(Math.max(0,pos.y), Math.max(0,H-mh));
    el.style.left = pos.x*pxPerFt + "px";
    el.style.top  = (H-mh-pos.y)*pxPerFt + "px";
    liveReadout();
  });
  const end = e => { if(start){ start=null; el.classList.remove('drag'); syncOrderSummary(); } };
  el.addEventListener('pointerup', end);
  el.addEventListener('pointercancel', end);
}

function liveReadout(){
  const {W}=wallDims(), mw=CAT.size.widthFt;
  $('#readout').children[0].querySelector('b').textContent = pos.x.toFixed(1)+FT;
  $('#readout').children[1].querySelector('b').textContent = Math.max(0,W-mw-pos.x).toFixed(1)+FT;
  $('#readout').children[2].querySelector('b').textContent = pos.y.toFixed(1)+FT;
}

function syncOrderSummary(){
  if(!cur) return;
  const {W,H}=wallDims();
  const price = CAT.pricePerSqFt * CAT.size.sqft;
  $('#orderSummary').innerHTML =
    `<b>${cur.title}</b> · ${CAT.size.heightFt}×${CAT.size.widthFt}${FT} · ${money(price)}<br>
     Placement on a ${W}×${H}${FT} wall: ${pos.x.toFixed(1)}${FT} from left, ${pos.y.toFixed(1)}${FT} off floor.`;
  // mirror into order form if blank
  const f = $('#orderForm');
  if(!f.wall_w.value) f.wall_w.value = W;
  if(!f.wall_h.value) f.wall_h.value = H;
}

function wireOrder(){
  $('#orderForm').addEventListener('submit', async e => {
    e.preventDefault();
    const msg = $('#orderMsg'); msg.className='msg'; msg.textContent='Sending…';
    const f = e.target;
    const payload = {
      mural: cur?.slug, mural_title: cur?.title,
      name: f.name.value, email: f.email.value,
      wall_w: +f.wall_w.value, wall_h: +f.wall_h.value,
      placement: { from_left_ft:+pos.x.toFixed(2), off_floor_ft:+pos.y.toFixed(2) },
      size: CAT.size, price: CAT.pricePerSqFt*CAT.size.sqft,
      notes: f.notes.value
    };
    try{
      const r = await fetch('/api/mural-order',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
      const j = await r.json();
      if(j.ok){ msg.className='msg ok'; msg.textContent='Thank you — request #'+j.id+' received. We’ll email you a proof and quote.'; f.reset(); syncOrderSummary(); }
      else { msg.className='msg err'; msg.textContent = j.error || 'Something went wrong.'; }
    }catch(err){ msg.className='msg err'; msg.textContent='Network error — please email steve@designerwallcoverings.com.'; }
  });
}

boot();
</script>
</body>
</html>