<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8" />

  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <title>Packshot Lineup Builder</title>

  <link rel="stylesheet" href="main.2a3cba51.css" />

  <style>

    :root{

      --acc: #183EF6;


      --surface: #ffffff;

      --surface-subtle: #f7f9fb;

      --border: #e6e8ee;

      --text: #0f1222;

      --muted: #627089;


      /* Canvas tokens (on-screen; export scales proportionally) */

      --imgH: 360px;

      --spacing: 120px;

      --scaleStep: 0.08;

      --tilt: 2.5deg;

      --arc: 20px;

      --shadow: 24px;


      --radius: .75rem;

      --radius-lg: 1rem;

    }


    * { box-sizing: border-box; }

    html, body { height: 100%; }

    body{

      margin:0;

      font-family: "Work Sans", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans";

      color: var(--text);

      background: #fbfcfe;

    }


    header{

      position: sticky; top: 0; z-index: 10;

      backdrop-filter: saturate(140%) blur(8px);

      background: color-mix(in oklab, #fff 72%, transparent);

      border-bottom: 1px solid var(--border);

      display: flex; align-items: center; gap: 12px; flex-wrap: wrap;

      padding: 14px 16px;

    }

    .brand{ display:flex; align-items:center; gap:12px; min-height:24px; }

    .brand img{ height:24px; width:auto; display:block; }

    .brand .divider{ opacity:.25; }

    .brand .title{ font-weight:700; letter-spacing:.2px; }


    .controls{ margin-left: auto; display: flex; gap: 10px; align-items:center; }


    .button{

      appearance: none; border: 1px solid var(--border); background: var(--surface);

      color: var(--text); padding: 10px 14px; border-radius: var(--radius); cursor: pointer; font-weight: 700;

      transition: .18s ease; line-height: 1;

    }

    .button:hover{ border-color: color-mix(in oklab, var(--acc) 35%, var(--border)); box-shadow: 0 1px 0 rgba(0,0,0,.03); }

    .button.ghost{ background: transparent; }

    .button.primary{ background: var(--acc); color: #fff; border-color: var(--acc); }

    .button.primary:hover{ filter: saturate(1.05) brightness(0.98); }


    .btn-group { position: relative; display: inline-flex; }

    .split { display: inline-flex; }

    .split .button { border-radius: var(--radius) 0 0 var(--radius); border-right: none; }

    .split .caret { border-radius: 0 var(--radius) var(--radius) 0; padding: 10px 12px; width: 40px; display:grid; place-items:center; }

    .menu{

      position:absolute; top:calc(100% + 6px); right:0; min-width: 280px;

      background:#fff; border:1px solid var(--border); border-radius: .75rem; box-shadow:0 12px 28px rgba(17,24,39,.12);

      display:none; overflow:hidden; z-index:20;

    }

    .menu.open{ display:block; }

    .menu button{

      width:100%; text-align:left; padding:10px 12px; border:0; background:#fff; cursor:pointer; font-weight:600;

    }

    .menu button:hover{ background: var(--surface-subtle); }


    main{ display: grid; grid-template-columns: 360px 1fr; min-height: calc(100vh - 56px); }


    aside{

      border-right: 1px solid var(--border);

      padding: 14px; background: var(--surface);

      position: sticky; top: 56px; height: calc(100vh - 56px); overflow: auto;

    }


    .stack{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; }

    .row{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; }

    .hr{ height:1px; background: var(--border); margin: 14px 0; }

    .hint{ font-size: 12px; color: var(--muted); }


    .dropzone{

      display:grid; gap:10px; text-align:center; padding:16px;

      border: 1px dashed color-mix(in oklab, var(--border) 70%, #cfd6e6);

      border-radius: var(--radius); background: var(--surface-subtle);

      color: var(--muted); transition: .18s;

    }

    .dropzone .dz-inner{ display:grid; place-items:center; gap:8px; }

    .dropzone strong{ color:#1f2a44; }

    .dropzone:hover{ border-color: var(--acc); background: color-mix(in oklab, var(--surface-subtle) 80%, var(--acc)); }

    .dropzone.dragover{ border-color: var(--acc); box-shadow: 0 0 0 3px color-mix(in oklab, var(--acc) 20%, transparent) inset; color:#163142; }

    .file input{ display:none; }


    #thumbs{ display:grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap:10px; margin-top:10px; }

    .thumb{

      position:relative; background:#fff; border:1px solid var(--border); border-radius: var(--radius);

      aspect-ratio:1/1; display:grid; place-items:center; overflow:hidden;

    }

    .thumb img{ width:100%; height:100%; object-fit:contain; pointer-events:none; }

    .thumb button.remove{

      position:absolute; top:4px; right:4px; background:#ffffff; border:1px solid var(--border); color:#4a5568;

      border-radius: .5rem; padding:2px 6px; font-size:11px; cursor:pointer;

    }

    .thumb.dragging{ opacity:.5; outline:2px dashed var(--acc); }

    .thumb.drag-over{ outline:2px dashed var(--acc); }


    #stageWrap{ background: #f3f6fb; border-left: 1px solid var(--border); }

    #stage{ position:relative; height:min(78vh, 900px); width:100%; overflow:hidden; }

    #stageInner{ position:absolute; inset:0; display:grid; place-items:center; }


    .canvas{

      position:relative; width: min(1100px, 92%); height: min(78vh, 820px);

      border-radius: var(--radius-lg); background: var(--bgCanvas, #fff); border:1px solid var(--border);

      box-shadow: 0 10px 28px rgba(17, 24, 39, .06);

      overflow:hidden; display:grid; place-items:center;

    }


    .slider{ display:grid; grid-template-columns: 140px 1fr auto; gap:8px; align-items:center; }

    .slider label{ color: var(--muted); font-size: 13px; }

    .slider input[type=range]{ width:100%; }

    .slider output{ font-variant-numeric: tabular-nums; color:#1f2a44; font-size: 13px; }


    .color{ display:flex; gap:10px; align-items:center; }


    details.advanced{

      border: 1px solid var(--border);

      background: var(--surface-subtle);

      border-radius: var(--radius);

      padding: 10px 12px;

    }

    details.advanced summary{

      cursor: pointer;

      list-style: none;

      font-weight: 700;

      color: #1f2a44;

      display:flex; align-items:center; justify-content:space-between;

    }

    details.advanced summary::-webkit-details-marker { display: none; }

    .chev{ transition: transform .2s ease; }

    details[open] .chev{ transform: rotate(90deg); }


    .adv-row{ display:flex; flex-direction:column; gap:6px; padding:10px 0; border-top: 1px dashed var(--border); }

    .adv-row:first-of-type{ border-top:none; }


    .adv-header{ display:flex; gap:8px; align-items:center; color:#27324a; font-weight:600; overflow:hidden; }

    .adv-index{ display:inline-grid; place-items:center; width:24px; height:24px; border-radius:6px; background:#fff; border:1px solid var(--border); font-size:12px; flex:0 0 auto; }


    .name{ display:flex; gap:0; align-items:center; min-width:0; overflow:hidden; }

    .name-base{ min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }

    .name-ext{ flex:0 0 auto; white-space:nowrap; }


    .adv-hint{ color: var(--muted); font-size:12px; }


    .adv-controls{ display:grid; grid-template-columns: 1fr auto auto; gap:8px; align-items:center; }

    .adv-reset{ border:1px solid var(--border); background:#fff; border-radius:.5rem; padding:6px 8px; cursor:pointer; white-space:nowrap; }


    .item{

      position:absolute; left:50%; top:50%;

      transform: translate(-50%,-50%);

      will-change: transform; transition: transform .28s ease, filter .22s ease, opacity .22s ease;

      filter: drop-shadow(0 14px var(--shadow) rgba(0,0,0,.25));

      background: transparent; border: 0; padding: 0; appearance: none; -webkit-appearance: none; cursor: pointer;

    }

    .item img{ height:var(--imgH); width:auto; display:block; object-fit:contain; pointer-events:none; }

    .item:focus-visible{ outline:2px solid var(--acc); outline-offset:4px; border-radius:12px; }


    .legend{

      position:absolute; left:12px; bottom:12px; font-size:12px; color: var(--muted); background: #ffffffd9;

      border: 1px solid var(--border); padding: 6px 8px; border-radius: .5rem;

    }


    .empty{ display:grid; place-items:center; gap:8px; color:#6b7a90; text-align:center; width:100%; height:100%; }


    @media (max-width: 980px){

      main{ grid-template-columns: 1fr; }

      aside{ position:static; height:auto; }

      #stage{ height: 70vh; }

      .slider{ grid-template-columns: 1fr; }

      .adv-controls{ grid-template-columns: 1fr; }

    }

  </style>

</head>

<body>

  <header>

    <div class="brand">

      <img src="https://bcloud.bright.blue/static/media/Logos.94306c697a9c364c7d63c17e5172e5a3.svg" alt="Bright Blue logo" />

      <span class="divider">|</span>

      <span class="title">Packshot Lineup Builder</span>

    </div>

    <div class="controls">

      <button class="button ghost" id="clearBtn" title="Remove all images">Clear</button>


      <!-- Split dropdown download -->

      <div class="btn-group" id="downloadGroup">

        <div class="split">

          <button class="button primary" id="dlMain" title="Download">Download</button>

          <button class="button primary caret" id="dlCaret" aria-haspopup="menu" aria-expanded="false">▾</button>

        </div>

        <div class="menu" id="dlMenu" role="menu" aria-label="Download options">

          <button type="button" data-preset="hd" role="menuitem">HD Uncompressed PNG (width 2000)</button>

          <button type="button" data-preset="sd" role="menuitem">SD Uncompressed PNG (width 1000)</button>

          <button type="button" data-preset="sd-compressed" role="menuitem">SD Compressed PNG (≤150KB, keeps transparency)</button>

        </div>

      </div>

    </div>

  </header>


  <main>

    <aside>

      <!-- Upload box with Browse inside -->

      <div class="dropzone" id="drop" tabindex="0" role="button" aria-label="Add images: browse or drop">

        <div class="dz-inner">

          <strong>Drop images here</strong>

          <div class="hint">…or</div>

          <div class="file">

            <input id="file" type="file" accept="image/*" multiple>

            <label for="file" class="button">Browse</label>

          </div>

          <div class="hint">PNG with transparency looks best. Minimum 800px height recommended.</div>

        </div>

      </div>


      <div class="stack" style="margin-top:10px;">

        <button class="button ghost" id="sampleBtn" title="Adds placeholders you can replace later">Add placeholders</button>

      </div>


      <div id="thumbs"></div>


      <div class="hr"></div>


      <div class="stack" style="gap:16px; align-items:stretch;">

        <div class="slider">

          <label for="height">Image height</label>

          <input id="height" type="range" min="120" max="720" step="2" value="360">

          <output id="heightOut">360 px</output>

        </div>

        <div class="slider">

          <label for="spacing">Spacing (overlap ↔)</label>

          <input id="spacing" type="range" min="0" max="300" step="2" value="120">

          <output id="spacingOut">120 px</output>

        </div>

        <div class="slider">

          <label for="scale">Scale step</label>

          <input id="scale" type="range" min="0" max="0.25" step="0.005" value="0.08">

          <output id="scaleOut">0.08</output>

        </div>

        <div class="slider">

          <label for="tilt">Tilt per step</label>

          <input id="tilt" type="range" min="0" max="12" step="0.1" value="2.5">

          <output id="tiltOut">2.5°</output>

        </div>

        <div class="slider">

          <label for="arc">Arc (lift)</label>

          <input id="arc" type="range" min="0" max="120" step="1" value="20">

          <output id="arcOut">20 px</output>

        </div>


        <div class="slider">

          <label for="edgeTuck">Edge tuck (outermost)</label>

          <input id="edgeTuck" type="range" min="0" max="0.95" step="0.01" value="0">

          <output id="edgeTuckOut">0.00</output>

        </div>


        <div class="slider" id="centreWrap" style="display:none;">

          <label for="centre">Centre index</label>

          <input id="centre" type="range" min="0" max="0" step="1" value="0">

          <output id="centreOut">0</output>

        </div>


        <div class="row">

          <label class="color" title="Set background behind the lineup">

            <span>Canvas background</span>

            <input id="bgColor" type="color" value="#ffffff">

          </label>

          <label class="color" title="Shadow strength">

            <span>Shadow</span>

            <input id="shadow" type="range" min="0" max="48" step="1" value="24">

          </label>

          <label class="color" title="Make the canvas transparent for export">

            <input id="transparent" type="checkbox"> Transparent canvas

          </label>

        </div>

      </div>


      <div class="hr"></div>


      <details class="advanced" id="advanced">

        <summary>

          Advanced adjustments

          <span class="chev">▶</span>

        </summary>

        <div class="hint" style="margin:8px 0 10px;">

          Fine-tune horizontal position of individual images. Positive = right, negative = left.

        </div>

        <div class="row" style="justify-content: flex-end; margin-bottom:6px;">

          <button class="adv-reset" id="resetAllOffsets" title="Reset all fine-tune offsets">Reset all</button>

        </div>

        <div id="advancedList" aria-live="polite"></div>

      </details>


      <div class="hr"></div>


      <div class="hint">Tips: Click an image to set it as centre • Use ←/→ to nudge centre • Hold Shift while clicking to remove an item • Drag thumbnails to reorder.</div>

    </aside>


    <section id="stageWrap">

      <div id="stage">

        <div id="stageInner">

          <div class="canvas" id="canvas" aria-live="polite" aria-label="Lineup canvas">

            <div class="empty" id="empty">

              <div>Upload images to see the lineup.</div>

              <div class="hint">PNG with transparency looks best. Minimum 800px height recommended.</div>

            </div>

            <div class="legend">Click an item to centre • ← / → keys supported</div>

          </div>

        </div>

      </div>

    </section>

  </main>


  <script>

    let uidCounter = 1;

    const MAX_COMPRESSED_BYTES = 150 * 1024;


    const state = {

      files: [],            // { id, url, name, offset, sig }

      centre: 0,

      objURLs: new Set(),

      seenSigs: new Set(),

    };


    const els = {

      file: document.getElementById('file'),

      drop: document.getElementById('drop'),

      thumbs: document.getElementById('thumbs'),

      canvas: document.getElementById('canvas'),

      empty: document.getElementById('empty'),

      clearBtn: document.getElementById('clearBtn'),


      dlMain: document.getElementById('dlMain'),

      dlCaret: document.getElementById('dlCaret'),

      dlMenu: document.getElementById('dlMenu'),


      sampleBtn: document.getElementById('sampleBtn'),

      height: document.getElementById('height'), heightOut: document.getElementById('heightOut'),

      spacing: document.getElementById('spacing'), spacingOut: document.getElementById('spacingOut'),

      scale: document.getElementById('scale'), scaleOut: document.getElementById('scaleOut'),

      tilt: document.getElementById('tilt'), tiltOut: document.getElementById('tiltOut'),

      arc: document.getElementById('arc'), arcOut: document.getElementById('arcOut'),

      edgeTuck: document.getElementById('edgeTuck'), edgeTuckOut: document.getElementById('edgeTuckOut'),

      centre: document.getElementById('centre'), centreOut: document.getElementById('centreOut'), centreWrap: document.getElementById('centreWrap'),

      bgColor: document.getElementById('bgColor'),

      shadow: document.getElementById('shadow'),

      transparent: document.getElementById('transparent'),

      advancedList: document.getElementById('advancedList'),

      resetAllOffsets: document.getElementById('resetAllOffsets'),

    };


    const clamp = (v, min, max) => Math.max(min, Math.min(max, v));

    const splitName = (name='') => { const m = name.match(/^(.*?)(\.[^.]*)?$/); return { base:(m?.[1]??''), ext:(m?.[2]??'') }; };


    /* ----- Keep on-screen image height within canvas ----- */

    function syncImgHeightMax(){

      const CH = Math.max(1, Math.floor(els.canvas.clientHeight));

      const maxH = Math.max(120, CH); // WYSIWYG: image height cannot exceed canvas height

      els.height.max = String(maxH);

      if (parseInt(els.height.value, 10) > maxH) els.height.value = String(maxH);

      document.documentElement.style.setProperty('--imgH', els.height.value + 'px');

      els.heightOut.textContent = els.height.value + ' px';

    }


    function updateCSSVars(){

      syncImgHeightMax();

      document.documentElement.style.setProperty('--spacing', els.spacing.value + 'px');

      document.documentElement.style.setProperty('--scaleStep', els.scale.value);

      document.documentElement.style.setProperty('--tilt', els.tilt.value + 'deg');

      document.documentElement.style.setProperty('--arc', els.arc.value + 'px');

      document.documentElement.style.setProperty('--shadow', els.shadow.value + 'px');


      const bg = els.transparent.checked ? 'transparent' : els.bgColor.value;

      els.canvas.style.setProperty('--bgCanvas', bg);


      els.spacingOut.textContent = els.spacing.value + ' px';

      els.scaleOut.textContent = Number(els.scale.value).toFixed(3).replace(/0+$/,'').replace(/\.$/,'');

      els.tiltOut.textContent = els.tilt.value + '°';

      els.arcOut.textContent = els.arc.value + ' px';

      els.edgeTuckOut.textContent = Number(els.edgeTuck.value).toFixed(2);

    }


    function buildAdvancedPanel(){

      const list = document.getElementById('advancedList');

      list.innerHTML = '';

      if(!state.files.length) return;


      state.files.forEach((f, i) => {

        if(typeof f.offset !== 'number') f.offset = 0;

        const row = document.createElement('div');

        row.className = 'adv-row';

        row.dataset.uid = f.id;


        const title = document.createElement('div');

        title.className = 'adv-header';

        const idx = document.createElement('span'); idx.className='adv-index'; idx.textContent = String(i+1);

        const nameWrap = document.createElement('div'); nameWrap.className='name';

        const {base,ext} = splitName(f.name || ('image-'+(i+1)));

        const baseSpan = document.createElement('span'); baseSpan.className='name-base'; baseSpan.textContent = base;

        const extSpan = document.createElement('span'); extSpan.className='name-ext'; extSpan.textContent = ext;

        nameWrap.append(baseSpan,extSpan);

        title.append(idx,nameWrap); title.title = f.name || ('image-'+(i+1));


        const hint = document.createElement('div'); hint.className='adv-hint'; hint.textContent='Offset (px)';


        const controls = document.createElement('div'); controls.className='adv-controls';

        const slider = document.createElement('input'); slider.type='range'; slider.min='-200'; slider.max='200'; slider.step='1'; slider.value=String(f.offset||0);

        const out = document.createElement('output'); out.textContent=`${f.offset||0} px`;

        const resetBtn = document.createElement('button'); resetBtn.className='adv-reset'; resetBtn.textContent='Reset';


        slider.addEventListener('input', () => { f.offset = parseInt(slider.value,10)||0; out.textContent=`${f.offset} px`; layout(); });

        resetBtn.addEventListener('click', () => { f.offset=0; slider.value='0'; out.textContent='0 px'; layout(); });


        controls.append(slider,out,resetBtn);

        row.append(title,hint,controls);

        list.appendChild(row);

      });

    }


    function layout(){

      const items = Array.from(els.canvas.querySelectorAll('.item'));

      if(!items.length){ els.empty.style.display = 'grid'; return; }

      els.empty.style.display = 'none';


      const spacing = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--spacing'));

      const scaleStep = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--scaleStep'));

      const tiltPer = parseFloat(els.tilt.value);

      const arc = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--arc'));

      const edgeTuck = parseFloat(els.edgeTuck.value || '0');


      const twoMode = items.length === 2;


      items.forEach((node, i) => {

        const d = i - state.centre;

        const ad = Math.abs(d);


        let x, s, r, y;

        if (twoMode) {

          x = (i === 0 ? -1 : 1) * (spacing / 2);

          s = 1; r = (i === 0 ? -1 : 1) * tiltPer; y = 0;

        } else {

          x = d * spacing;

          if ((i === 0 || i === items.length - 1) && edgeTuck > 0) x *= (1 - edgeTuck);

          s = clamp(1 - ad * scaleStep, 0.35, 1.0);

          r = d * tiltPer;

          y = -ad * arc;

        }


        const f = state.files[i]; x += (f && typeof f.offset==='number') ? f.offset : 0;


        const z = twoMode ? (i === 1 ? 1001 : 1000) : (1000 - ad);

        node.style.zIndex = String(z);

        node.style.transform = `translate(-50%,-50%) translateX(${x}px) translateY(${y}px) rotate(${r}deg) scale(${s})`;

        node.style.filter = ad === 0 ? `drop-shadow(0 16px var(--shadow) rgba(0,0,0,.28))` : `drop-shadow(0 14px var(--shadow) rgba(0,0,0,.22))`;

        node.style.opacity = ad > 5 && !twoMode ? 0 : 1;

      });


      els.centre.max = Math.max(0, state.files.length - 1);

      els.centre.value = state.centre;

      els.centreOut.textContent = state.centre;

      els.centreWrap.style.display = state.files.length ? 'grid' : 'none';

    }


    function render(){

      els.canvas.querySelectorAll('.item').forEach(n => n.remove());

      state.files.forEach((f, i) => {

        const wrap = document.createElement('button');

        wrap.className = 'item'; wrap.type='button'; wrap.dataset.index=i; wrap.title=`Click to centre (index ${i})`;

        const img = document.createElement('img'); img.alt = f.name || `image-${i+1}`; img.src = f.url;

        wrap.appendChild(img);

        wrap.addEventListener('click', (ev) => { if(ev.shiftKey){ removeAt(i); return; } state.centre = i; layout(); });

        els.canvas.appendChild(wrap);

      });

      updateThumbs();

      buildAdvancedPanel();

      syncImgHeightMax();

      layout();

    }


    function updateThumbs(){

      els.thumbs.innerHTML = '';

      state.files.forEach((f, i) => {

        const t = document.createElement('div');

        t.className = 'thumb'; t.draggable = true; t.dataset.index = i;

        const img = document.createElement('img'); img.src = f.url; img.alt = f.name || `thumb-${i+1}`;

        const b = document.createElement('button'); b.className='remove'; b.textContent='×'; b.title='Remove';

        b.addEventListener('click', (ev) => { ev.stopPropagation(); removeAt(i); });


        t.addEventListener('dragstart', (e) => { t.classList.add('dragging'); e.dataTransfer.effectAllowed='move'; e.dataTransfer.setData('text/plain', String(i)); });

        t.addEventListener('dragend', () => t.classList.remove('dragging'));

        t.addEventListener('dragover', (e) => { e.preventDefault(); t.classList.add('drag-over'); });

        t.addEventListener('dragleave', () => t.classList.remove('drag-over'));

        t.addEventListener('drop', (e) => {

          e.preventDefault(); t.classList.remove('drag-over');

          const from = parseInt(e.dataTransfer.getData('text/plain'), 10);

          const to = parseInt(t.dataset.index, 10);

          if (isNaN(from) || isNaN(to) || from === to) return;

          moveFile(from, to);

        });


        t.append(img, b);

        els.thumbs.appendChild(t);

      });

    }


    function moveFile(from, to){

      const arr = state.files;

      const item = arr.splice(from, 1)[0];

      arr.splice(to, 0, item);

      state.centre = clamp(state.centre, 0, Math.max(0, arr.length-1));

      render();

    }


    function removeAt(i){

      const f = state.files[i];

      if(!f) return;

      if(f.url && state.objURLs.has(f.url)){ URL.revokeObjectURL(f.url); state.objURLs.delete(f.url); }

      if (f.sig) state.seenSigs.delete(f.sig);

      state.files.splice(i,1);

      state.centre = clamp(state.centre, 0, Math.max(0, state.files.length-1));

      render();

    }


    function clearAll(){

      state.files.forEach(f => { if(state.objURLs.has(f.url)){ URL.revokeObjectURL(f.url); } if (f.sig) state.seenSigs.delete(f.sig); });

      state.objURLs.clear(); state.files = []; state.centre = 0; render();

    }


    function addFiles(fileList){

      const incoming = Array.from(fileList).filter(f => f.type.startsWith('image/'));

      if(!incoming.length) return;

      const fresh = [];

      incoming.forEach(f => {

        const sig = `${f.name}|${f.size}|${f.lastModified || 0}`;

        if (state.seenSigs.has(sig)) return;

        const url = URL.createObjectURL(f);

        state.objURLs.add(url); state.seenSigs.add(sig);

        fresh.push({ id: uidCounter++, url, name: f.name, offset: 0, sig });

      });

      if(!fresh.length) return;

      state.files.push(...fresh);

      state.centre = Math.floor((state.files.length - 1) / 2);

      els.file.value = '';

      render();

    }


    /* ---- Controls wiring ---- */

    [els.height, els.spacing, els.scale, els.tilt, els.arc, els.bgColor, els.shadow, els.transparent, els.edgeTuck]

      .forEach(inp => inp.addEventListener('input', () => { updateCSSVars(); layout(); }));


    window.addEventListener('resize', () => { syncImgHeightMax(); layout(); });


    els.centre.addEventListener('input', () => { state.centre = parseInt(els.centre.value,10); layout(); });


    els.file.addEventListener('change', (e) => addFiles(e.target.files));

    function openPicker(){ els.file.click(); }

    els.drop.addEventListener('click', (e) => { if(!(e.target instanceof HTMLLabelElement)) openPicker(); });

    els.drop.addEventListener('keydown', (e) => { if(e.key==='Enter' || e.key===' '){ e.preventDefault(); openPicker(); } });

    ;['dragenter','dragover'].forEach(type => { els.drop.addEventListener(type, (e) => { e.preventDefault(); e.stopPropagation(); els.drop.classList.add('dragover'); }); });

    ;['dragleave','drop'].forEach(type => { els.drop.addEventListener(type, (e) => { e.preventDefault(); e.stopPropagation(); els.drop.classList.remove('dragover'); }); });

    els.drop.addEventListener('drop', (e) => { addFiles(e.dataTransfer.files); });


    els.clearBtn.addEventListener('click', clearAll);


    window.addEventListener('keydown', (e) => {

      if(!state.files.length) return;

      if(e.key === 'ArrowLeft'){ state.centre = clamp(state.centre - 1, 0, state.files.length-1); layout(); }

      if(e.key === 'ArrowRight'){ state.centre = clamp(state.centre + 1, 0, state.files.length-1); layout(); }

    });


    document.getElementById('resetAllOffsets').addEventListener('click', () => {

      state.files.forEach(f => f.offset = 0);

      buildAdvancedPanel();

      layout();

    });


    /* ---- Demo placeholders ---- */

    els.sampleBtn.addEventListener('click', () => {

      const make = (w, h, hue) => {

        const svg = encodeURIComponent(`<?xml version='1.0' encoding='UTF-8'?>\n<svg xmlns='http://www.w3.org/2000/svg' width='${w}' height='${h}' viewBox='0 0 ${w} ${h}'>\n  <defs>\n    <linearGradient id='g' x1='0' y1='0' x2='0' y2='1'>\n      <stop offset='0' stop-color='hsl(${hue},70%,62%)' stop-opacity='.95'/>\n      <stop offset='1' stop-color='hsl(${hue},70%,48%)' stop-opacity='.95'/>\n    </linearGradient>\n    <filter id='shadow' x='-50%' y='-50%' width='200%' height='200%'>\n      <feDropShadow dx='0' dy='8' stdDeviation='8' flood-opacity='.15'/>\n    </filter>\n  </defs>\n  <rect width='100%' height='100%' fill='none'/>\n  <g filter='url(#shadow)'>\n    <rect x='${w*0.3}' y='${h*0.08}' rx='${w*0.05}' ry='${w*0.05}' width='${w*0.4}' height='${h*0.84}' fill='url(#g)'/>\n    <rect x='${w*0.4}' y='${h*0.02}' rx='${w*0.02}' ry='${w*0.02}' width='${w*0.2}' height='${h*0.08}' fill='hsl(${hue},60%,35%)'/>\n  </g>\n</svg>`);

        return `data:image/svg+xml;charset=UTF-8,${svg}`;

      };

      const hues = [205, 15, 265, 35, 125, 300, 185];

      const fresh = hues.map((hue, i) => ({ id: uidCounter++, url: make(800, 1200, hue), name: `placeholder-${i+1}.svg`, offset: 0 }));

      state.files.push(...fresh);

      state.centre = Math.floor((state.files.length - 1) / 2);

      render();

    });


    /* ---- Download split menu ---- */

    els.dlMain.addEventListener('click', () => runDownloadPreset('hd'));

    els.dlCaret.addEventListener('click', (e) => {

      e.stopPropagation();

      const open = els.dlMenu.classList.toggle('open');

      els.dlCaret.setAttribute('aria-expanded', String(open));

    });

    els.dlMenu.addEventListener('click', (e) => {

      if(e.target instanceof HTMLButtonElement && e.target.dataset.preset){

        els.dlMenu.classList.remove('open');

        els.dlCaret.setAttribute('aria-expanded','false');

        runDownloadPreset(e.target.dataset.preset);

      }

    });

    document.addEventListener('click', (e) => {

      if(!document.getElementById('downloadGroup').contains(e.target)){

        els.dlMenu.classList.remove('open');

        els.dlCaret.setAttribute('aria-expanded','false');

      }

    });


    /* ---------- EXPORT: WYSIWYG SCALING ---------- */

    const getCanvasSize = () => {

      const rect = els.canvas.getBoundingClientRect();

      return { W: Math.max(1, Math.floor(rect.width)), H: Math.max(1, Math.floor(rect.height)) };

    };


    function drawToCanvas(targetW, targetH){

      // Scale factor from on-screen canvas to off-screen export

      const { W: screenW, H: screenH } = getCanvasSize();

      const k = targetW / screenW; // same as targetH/screenH because we preserve aspect


      const c = document.createElement('canvas');

      c.width = targetW; c.height = targetH;

      const ctx = c.getContext('2d');


      const transparent = !!els.transparent.checked;

      if(!transparent){

        ctx.fillStyle = getComputedStyle(els.canvas).getPropertyValue('--bgCanvas') || '#fff';

        ctx.fillRect(0,0,targetW,targetH);

      }


      // Read on-screen values and scale them by k

      const spacing = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--spacing')) * k;

      const scaleStep = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--scaleStep'));

      const tiltPer = parseFloat(els.tilt.value);

      const arc = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--arc')) * k;

      const imgH = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--imgH')) * k;

      const edgeTuck = parseFloat(els.edgeTuck.value || '0');


      const cx = targetW/2, cy = targetH/2;

      const twoMode = state.files.length === 2;


      const order = state.files.map((_,i)=>i).sort((a,b)=>Math.abs(b - state.centre) - Math.abs(a - state.centre));


      order.forEach(i => {

        const imgEl = els.canvas.querySelector(`.item[data-index="${i}"] img`);

        if(!imgEl) return;


        let x, s, r, y;

        if (twoMode) {

          x = (i === 0 ? -1 : 1) * (spacing / 2);

          s = 1;

          r = (i === 0 ? -1 : 1) * (tiltPer * Math.PI/180);

          y = 0;

        } else {

          const d = i - state.centre;

          const ad = Math.abs(d);

          x = d * spacing;

          if ((i === 0 || i === (state.files.length - 1)) && edgeTuck > 0) x = x * (1 - edgeTuck);

          s = Math.max(0.35, 1 - ad * scaleStep);

          r = d * tiltPer * Math.PI/180;

          y = -ad * arc;

        }


        // per-image offset (px on screen) -> scale to export

        const f = state.files[i];

        const perImageOffset = ((f && typeof f.offset === 'number') ? f.offset : 0) * k;

        x += perImageOffset;


        const natW = imgEl.naturalWidth || 1000;

        const natH = imgEl.naturalHeight || 1000;

        const drawH = imgH;                  // already scaled to k

        const drawW = drawH * (natW / natH);


        ctx.save();

        ctx.translate(cx + x, cy + y);

        ctx.rotate(r);

        ctx.scale(s, s);

        ctx.drawImage(imgEl, -drawW/2, -drawH/2, drawW, drawH);

        ctx.restore();

      });


      return c;

    }


    async function runDownloadPreset(preset){

      if(!state.files.length){ alert('Add at least one image first.'); return; }


      const { W: screenW, H: screenH } = getCanvasSize();

      const aspect = screenH / screenW;


      let outW = 2000;

      if(preset === 'hd') outW = 2000;

      if(preset === 'sd') outW = 1000;

      if(preset === 'sd-compressed') outW = 1000;


      const outH = Math.round(outW * aspect);


      if(preset === 'hd' || preset === 'sd'){

        const drawCanvas = drawToCanvas(outW, outH);

        const blob = await new Promise(res => drawCanvas.toBlob(res, 'image/png'));

        if(!blob){ alert('Export failed.'); return; }

        triggerDownload(blob, `packshot-lineup-${preset}-${outW}w.png`);

        return;

      }


      if(preset === 'sd-compressed'){

        // Always PNG with alpha. Try 1000px; if >150KB, shrink keeping WYSIWYG.

        let testW = outW, blob = null, lastBlob = null;

        while (testW >= 320) {

          const testH = Math.round(testW * aspect);

          const testCanvas = drawToCanvas(testW, testH);

          blob = await new Promise(res => testCanvas.toBlob(res, 'image/png'));

          if (!blob) break;

          lastBlob = blob;

          if (blob.size <= MAX_COMPRESSED_BYTES) break;

          testW = Math.floor(testW * 0.9);

        }

        if (!lastBlob) { alert('Export failed.'); return; }

        triggerDownload(lastBlob, `packshot-lineup-sd-compressed-${testW}w.png`);

        return;

      }

    }


    function triggerDownload(blob, filename){

      const a = document.createElement('a');

      const url = URL.createObjectURL(blob);

      a.href = url; a.download = filename;

      document.body.appendChild(a); a.click(); a.remove();

      setTimeout(()=>URL.revokeObjectURL(url), 1000);

    }


    /* ---- Init ---- */

    syncImgHeightMax();

    updateCSSVars();


    // Upload wiring

    els.file.addEventListener('change', (e) => addFiles(e.target.files));

    function openPicker(){ els.file.click(); }

    els.drop.addEventListener('click', (e) => { if(!(e.target instanceof HTMLLabelElement)) openPicker(); });

    els.drop.addEventListener('keydown', (e) => { if(e.key==='Enter' || e.key===' '){ e.preventDefault(); openPicker(); } });

    ;['dragenter','dragover'].forEach(type => { els.drop.addEventListener(type, (e) => { e.preventDefault(); e.stopPropagation(); els.drop.classList.add('dragover'); }); });

    ;['dragleave','drop'].forEach(type => { els.drop.addEventListener(type, (e) => { e.preventDefault(); e.stopPropagation(); els.drop.classList.remove('dragover'); }); });

    els.drop.addEventListener('drop', (e) => { addFiles(e.dataTransfer.files); });


    render();

  </script>

</body>

</html>