← back to CelebritySignatures

public/murals.html

714 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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<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:"Playfair Display",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:"Playfair Display",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:"Playfair Display",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}
  .wall-row{display:flex;align-items:center;gap:10px}
  .wall-row input[type=number]{width:64px;font:inherit;padding:6px 8px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}
  .wall-row input[type=range]{flex:1;min-width:90px}
  .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}}
  @media(max-width:700px){
    header{padding:12px 14px;gap:8px 12px}
    header h1{font-size:19px}
    header nav{margin-left:0;width:100%;gap:14px;overflow-x:auto;-webkit-overflow-scrolling:touch}
    header nav a{white-space:nowrap;font-size:12.5px}
    main{padding:18px 14px 60px}
    .intro h2{font-size:23px}
    .ctrls{gap:12px}
    .ctrls .f{font-size:11px}
    .readout{gap:12px;font-size:12px}
  }
  /* — 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}
  /* — custom-size live signature grid inside the mural — */
  .mural .sig-grid{position:absolute;inset:0;display:grid;gap:2px;padding:3px;background:#fff}
  .mural .sg-cell{display:flex;align-items:center;justify-content:center;background:#fff;overflow:hidden}
  .mural .sg-cell img{max-width:94%;max-height:88%;object-fit:contain;pointer-events:none}
  /* Colored-ink signatures on clean white via the shared preview component. */
  .mural .sg-cell .signature-preview{width:100%;height:100%;pointer-events:none}
  .roster .r-item .signature-preview{height:20px;width:50px;flex:none}
  .finder .hit .signature-preview{height:26px;width:96px;flex:none}
  #provSig .signature-preview{height:120px;width:100%}
  /* — 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>
<!-- Google Analytics 4 (G-2HEVP6TD0J) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2HEVP6TD0J"></script>
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-2HEVP6TD0J');</script>
<script src="/assets/ga-events.js" defer></script>
<link rel="canonical" href="https://celebsignatures.com/murals">
<meta name="description" content="A wall of history in their own hand. Custom-size signature murals from $10/sq ft, printed to order.">
<link rel="apple-touch-icon" href="/assets/favicon.png">
<link rel="icon" type="image/png" href="/assets/favicon.png">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Celebrity Signatures">
<meta property="og:title" content="Celebrity Signature Murals — made to order">
<meta property="og:description" content="A wall of history in their own hand. Custom-size signature murals from $10/sq ft, printed to order.">
<meta property="og:url" content="https://celebsignatures.com/murals">
<meta property="og:image" content="https://celebsignatures.com/assets/og-share.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="stylesheet" href="/assets/signature-preview.css">
<script type="module" src="/assets/signature-preview.js"></script>
<meta name="twitter:title" content="Celebrity Signature Murals — made to order">
<meta name="twitter:description" content="A wall of history in their own hand. Custom-size signature murals from $10/sq ft, printed to order.">
<meta name="twitter:image" content="https://celebsignatures.com/assets/og-share.jpg">
</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" id="finder" style="scroll-margin-top:80px">
      <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">Mural width (ft)
          <input id="murW" type="number" min="4" max="40" step="0.5" value="24" style="width:76px;font:inherit;padding:6px 8px;border:1px solid var(--line);border-radius:8px">
        </div>
        <div class="f">Mural height (ft)
          <input id="murH" type="number" min="3" max="16" step="0.5" value="12" style="width:76px;font:inherit;padding:6px 8px;border:1px solid var(--line);border-radius:8px">
        </div>
        <div class="f">Wall width — <output id="wwOut"></output>
          <div class="wall-row">
            <input id="wallWnum" type="number" min="8" max="80" step="0.5" value="32" aria-label="Wall width in feet">
            <input id="wallW" type="range" min="8" max="80" step="1" value="32">
          </div>
        </div>
        <div class="f">Wall height — <output id="whOut"></output>
          <div class="wall-row">
            <input id="wallHnum" type="number" min="8" max="30" step="0.5" value="14" aria-label="Wall height in feet">
            <input id="wallH" type="range" min="8" max="30" step="1" value="14">
          </div>
        </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">Enter <b>your</b> mural size — the signatures re-arrange live to fill it (each signature prints ~2&nbsp;ft wide at 150&nbsp;DPI; a bigger wall shows more names). Drag to set 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;display:flex;gap:10px;flex-wrap:wrap">
        <button class="btn" type="button" id="payBtn">Pay &amp; order →</button>
        <button class="btn ghost" type="submit">Request a quote instead</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));
  wireWall('wallWnum', 'wallW', 8, 80);
  wireWall('wallHnum', 'wallH', 8, 30);
  $('#murW').addEventListener('input', centerMural);
  $('#murH').addEventListener('input', centerMural);
  $('#btnCenter').addEventListener('click', centerMural);
  window.addEventListener('resize', drawScene);

  selectMural(CAT.murals[0].code);
  wireOrder();
  // Honor the gallery's selected signature, including older ?name= links.
  // Use the existing finder/roster rather than inventing a custom mural.
  const selected = new URLSearchParams(location.search);
  if (selected.has('qid') || selected.has('name')) {
    const sig = selected.has('qid')
      ? SIGS.find(s=>(s.wikidata||'').split('/').pop()===selected.get('qid'))
      : SIGS.find(s=>s.full_name.toLowerCase()===selected.get('name').toLowerCase());
    const finder = $('#finderInput');
    finder.value = sig ? sig.full_name : (selected.get('name') || selected.get('qid') || '');
    finder.dispatchEvent(new Event('input'));
    if (sig) {
      const mural = CAT.murals.find(m=>(byMural[m.code]||[]).some(s=>s.full_name===sig.full_name));
      if (mural) { $('#selMural').value=mural.code; selectMural(mural.code); }
    }
  }
}

// — 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">
          <span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'&quot;')}" data-signature-tint-key="${(r.qid||r.full_name||'').replace(/"/g,'&quot;')}"></span><span>${r.full_name}</span></button>`).join('') +
      `</div>`;
    body.dataset.filled = '1';
  }
}

// — provenance modal (consensus #2) —
function openProv(row){
  if(!row) return;
  $('#provSig').innerHTML = `<span class="signature-preview" data-signature-src="${row.signature_image_url}" data-signature-label="Signature of ${(row.full_name||'').replace(/"/g,'&quot;')}" data-signature-tint-key="${(row.qid||row.full_name||'').replace(/"/g,'&quot;')}"></span>`;
  $('#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"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'&quot;')}" data-signature-tint-key="${(r.qid||r.full_name||'').replace(/"/g,'&quot;')}"></span> <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
}

// The number inputs are the source of truth for wall size (they allow precise,
// half-foot entry); the range sliders mirror them. wireWall() keeps both in sync.
function wallDims(){ return { W:+$('#wallWnum').value || 32, H:+$('#wallHnum').value || 14 }; }
function wireWall(numId, rangeId, mn, mx){
  const num = $('#'+numId), rng = $('#'+rangeId);
  rng.addEventListener('input', () => { num.value = rng.value; drawScene(); });
  num.addEventListener('input', () => {
    const v = +num.value;
    if(!isNaN(v)) rng.value = Math.min(mx, Math.max(mn, Math.round(v)));
    drawScene();
  });
  num.addEventListener('change', () => {            // clamp the field itself on commit
    let v = +num.value; if(isNaN(v)) v = +rng.value;
    v = Math.min(mx, Math.max(mn, v));
    num.value = v; rng.value = Math.round(v); drawScene();
  });
}

// ── custom mural size: the user enters width/height and the signature chips
// re-arrange to fill it at true print scale (~1.9 × 0.85 ft per signature).
// A bigger mural pulls the NEXT set of famous-first signatures into the grid.
function murDims(){
  return { w: Math.min(40, Math.max(4, +$('#murW').value || 24)),
           h: Math.min(16, Math.max(3, +$('#murH').value || 12)) };
}
const CELL_FT_W = 1.9, CELL_FT_H = 0.85;
// Commons URL → reliably-loadable thumbnail. Hand-built /thumb/…/200px-X.svg.png
// paths 400 on Wikimedia for SVG originals (the signature images are mostly SVG),
// which is what made selected murals render as a blank white box. Route SVGs
// through Special:FilePath?width=N (302s to a rasterized PNG — the same mechanism
// the mobile app's sigImg() uses). Raster originals + non-Wikimedia (e.g. Met)
// URLs load fine as-is and pass through untouched.
function thumbUrl(u, width = 200){
  u = String(u || '');
  const m = u.match(/^https?:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/[0-9a-f]\/[0-9a-f]{2}\/([^/?#]+\.svg)$/i);
  if (m) return `https://commons.wikimedia.org/wiki/Special:FilePath/${m[1]}?width=${width}`;
  return u;
}
function rosterFor(code){
  const rows = (byMural[code]||[]).filter(r => r.signature_image_url);
  if (rows.length) return rows;
  // themed murals carry names only — resolve images from the archive by name
  return (byMural[code]||[]).map(r => SIGS.find(s => s.full_name === r.full_name)).filter(Boolean);
}
function muralInnerHTML(pxPerFt){
  const {w,h} = murDims();
  const roster = rosterFor(cur.code);
  if (!roster.length) return null;                       // fall back to the PNG
  let cols = Math.max(1, Math.floor(w / CELL_FT_W));
  let rows = Math.max(1, Math.floor(h / CELL_FT_H));
  let n = Math.min(roster.length, cols * rows);
  if (n < cols * rows) {                                 // small roster → balance the grid
    cols = Math.max(1, Math.min(cols, Math.ceil(Math.sqrt(n * (w/h) / (CELL_FT_W/CELL_FT_H)))));
    rows = Math.ceil(n / cols);
  }
  const shown = roster.slice(0, n);
  return { n, html: `<div class="sig-grid" style="grid-template-columns:repeat(${cols},1fr);grid-auto-rows:1fr">${
    shown.map(r=>`<div class="sg-cell" title="${(r.full_name||'').replace(/"/g,'&quot;')}"><span class="signature-preview" data-signature-src="${r.signature_image_url}" data-signature-label="Signature of ${(r.full_name||'').replace(/"/g,'&quot;')}" data-signature-tint-key="${(r.qid||r.full_name||'').replace(/"/g,'&quot;')}"></span></div>`).join('')
  }</div>` };
}

function centerMural(){
  const {W,H} = wallDims();
  const {w:mw, h:mh} = murDims();
  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');
  let {W,H} = wallDims();
  const {w:mw, h:mh} = murDims();
  W = Math.max(W, mw); H = Math.max(H, mh);   // wall always fits the mural
  // 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>`;
  // live signature grid at the CUSTOM size (chips re-arrange as dims change)
  const inner = muralInnerHTML(pxPerFt);
  shownCount = inner ? inner.n : (cur.sigs || 0);
  if (inner) {
    const mel = $('#muralEl');
    mel.style.backgroundImage = 'none';
    mel.style.background = '#fff';
    mel.insertAdjacentHTML('afterbegin', inner.html);
  }
  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);
  const sqft = Math.round(mw * mh);
  $('#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> · ${sqft} sq ft</div>
    <div>Signatures shown: <b>${shownCount}</b></div>
    <div>Price: <b>${money(CAT.pricePerSqFt * sqft)}</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(), {w:mw,h:mh}=murDims();   // clamp to the user's CURRENT mural size, not the fixed catalog size
    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);
}

let shownCount = 0;
function liveReadout(){
  const {W}=wallDims(), mw=murDims().w;
  $('#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 {w:mw,h:mh}=murDims();
  const price = CAT.pricePerSqFt * mw * mh;
  $('#orderSummary').innerHTML =
    `<b>${cur.title}</b> · ${mh}×${mw}${FT} custom · ${shownCount} signatures · ${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) },
      mural_size: { widthFt: murDims().w, heightFt: murDims().h, signatures: shownCount },
      size: CAT.size, price: CAT.pricePerSqFt * murDims().w * murDims().h,
      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.';
        // Preserve name+email across reset so a follow-up "Pay & order" isn't blocked (TK-10193 BUG 1); only clear the free-text notes.
        f.notes.value=''; 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.'; }
  });
  // Pay & order → Stripe Checkout (custom mural size drives the price)
  $('#payBtn').addEventListener('click', async () => {
    const f = $('#orderForm'), msg = $('#orderMsg');
    if(!f.name.value || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(f.email.value)){ msg.className='msg err'; msg.textContent='Enter your name and a valid email first.'; return; }
    msg.className='msg'; msg.textContent='Opening secure checkout…';
    const {w,h}=murDims();
    try{
      const r = await fetch('/api/mural-checkout',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
        mural:cur?.slug, mural_title:cur?.title, name:f.name.value, email:f.email.value,
        widthFt:w, heightFt:h, placement:{ from_left_ft:+pos.x.toFixed(2), off_floor_ft:+pos.y.toFixed(2) } })});
      const j = await r.json();
      if(j.ok && j.url){ location.href = j.url; }
      else if(r.status===503){ msg.className='msg err'; msg.textContent='Card checkout is being set up — use “Request a quote” for now.'; }
      else { msg.className='msg err'; msg.textContent = j.error || 'Could not start checkout.'; }
    }catch(err){ msg.className='msg err'; msg.textContent='Network error starting checkout.'; }
  });
}

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