You are viewing a single comment's thread from:

RE: Object Type - html

@localguide added htmlContent (English):

<!DOCTYPE html>
<html lang=en>
<head>
  <meta charset=UTF-8 />
  <meta name=viewport content=width=device-width, initial-scale=1 />
  <title>Image Effects Gallery – Demo</title>
  <style>
    :root
      --bg:#0b0d10;--panel:#11151a;--ink:#e9eef5;--muted:#9fb0c3;--brand:#6ee7ff;--brand2:#a78bfa;
      --radius:18px;--shadow:0 10px 30px rgba(0,0,0,.35)
    
    *box-sizing:border-box
    html,bodymin-height:100%;background:#0b0d10
    body
      margin:0;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial;
      color:var(--ink);background:linear-gradient(180deg,#0b0d10 0%, #0e1217 60%, #0b0d10 100%);
    
    acolor:var(--brand)
    header
      position:sticky;top:0;z-index:50;backdrop-filter:saturate(140%) blur(8px);
      background:rgba(11,13,16,.55);border-bottom:1px solid rgba(255,255,255,.06)
    
    .wrapmax-width:1200px;margin:auto;padding:22px
    .title
      display:flex;align-items:center;gap:16px;flex-wrap:wrap
    
    .title h1margin:0;font-weight:800;letter-spacing:.2px;font-size:clamp(20px,3vw,34px)
    navmargin-left:auto;display:flex;gap:14px;flex-wrap:wrap
    nav afont-size:14px;text-decoration:none;color:var(--muted);padding:8px 12px;border-radius:999px;background:#0f141a;border:1px solid rgba(255,255,255,.06)
    nav a:hovercolor:var(--ink);border-color:rgba(255,255,255,.18)

    .heroposition:relative;overflow:hidden
    .hero .wrapdisplay:grid;grid-template-columns:1.2fr .8fr;gap:28px;align-items:center
    .chipdisplay:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:var(--muted);font-size:12px
    .hero h2margin:10px 0 6px;font-size:clamp(28px,6vw,54px);line-height:1.02
    .hero pcolor:var(--muted);max-width:60ch
    .hero .stackposition:relative;height:320px
    .hero .stack imgposition:absolute;inset:auto;max-width:60%;border-radius:14px;box-shadow:var(--shadow);transform-origin:center;transition:transform .6s ease,opacity .6s ease
    .hero .stack img:nth-child(1)left:0;top:20px;transform:rotate(-6deg)
    .hero .stack img:nth-child(2)left:120px;top:40px;transform:rotate(5deg)
    .hero .stack img:nth-child(3)left:240px;top:0;transform:rotate(-2deg)
    .hero .stack:hover imgtransform:translateY(-6px) rotate(0deg)

    sectionscroll-margin-top:90px
    .sectionpadding:42px 0;border-top:1px solid rgba(255,255,255,.06)
    .section h3margin:0 0 8px;font-size:clamp(20px,3vw,28px)
    .section p.leadcolor:var(--muted);margin:0 0 22px

    /* 1) Hover Zoom + Caption */
    .griddisplay:grid;grid-template-columns:repeat(12,1fr);gap:16px
    .cardgrid-column:span 4;background:var(--panel);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);overflow:hidden;position:relative
    .card imgwidth:100%;height:260px;object-fit:cover;display:block;transition:transform .5s ease, filter .5s ease
    .card .capposition:absolute;inset:auto 0 0; padding:12px 14px;background:linear-gradient(180deg,transparent, rgba(0,0,0,.75)); color:#fff; font-size:14px;opacity:.0; transform:translateY(8px); transition:all .4s ease
    .card:hover imgtransform:scale(1.06)
    .card:hover .capopacity:1;transform:none

    /* 2) CSS Filters Showcase */
    .filtersdisplay:grid;grid-template-columns:repeat(7,1fr);gap:12px
    .fBoxborder-radius:14px;overflow:hidden;border:1px solid rgba(255,255,255,.06);position:relative;background:var(--panel)
    .fBox imgwidth:100%;height:180px;object-fit:cover;display:block;filter:var(--f, none);transition:filter .35s ease, transform .35s ease
    .fBox:hover imgtransform:scale(1.03);filter:none
    .fBox smallposition:absolute;left:10px;bottom:10px;background:rgba(0,0,0,.55);padding:4px 8px;border-radius:999px;color:#e6eef9

    /* 3) Masonry (CSS columns) */
    .masonrycolumn-count:4;column-gap:16px
    .mItembreak-inside:avoid;border-radius:16px;overflow:hidden;margin:0 0 16px;border:1px solid rgba(255,255,255,.06);background:var(--panel)
    .mItem imgwidth:100%;height:auto;display:block

    /* 4) Parallax Banner */
    .parallaxheight:42vh;min-height:320px;border-radius:20px;overflow:hidden;border:1px solid rgba(255,255,255,.08);background:#000;position:relative
    .parallax .pbgposition:absolute;inset:-20% 0 -20% 0;background:url('https://waivio.nyc3.digitaloceanspaces.com/adcfd48796b4ed01be18c5cc6e8bf9b8e9a466fbd1ec7dcadf238c58318fbd01') center / cover no-repeat;will-change:transform;filter:contrast(1.05) saturate(1.1)
    .parallax h4position:absolute;inset:auto 0 16px; text-align:center;margin:0;font-size:clamp(20px,3.2vw,30px);background:linear-gradient(180deg,transparent,rgba(0,0,0,.7)); padding:28px 10px

    /* 5) Carousel */
    .carouselposition:relative;border-radius:18px;overflow:hidden;border:1px solid rgba(255,255,255,.06);background:var(--panel)
    .carousel-trackdisplay:flex;transition:transform .6s cubic-bezier(.22,.61,.36,1)
    .carousel imgwidth:100%;height:420px;object-fit:cover;display:block
    .cBtnposition:absolute;top:50%;transform:translateY(-50%);background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.2); color:#fff;border-radius:12px;padding:10px 14px;cursor:pointer
    .cPrevleft:10px.cNextright:10px
    .dotsdisplay:flex;justify-content:center;gap:8px;padding:10px
    .dotwidth:8px;height:8px;border-radius:999px;background:#3a4654;border:1px solid rgba(255,255,255,.15);transition:transform .2s
    .dot.activetransform:scale(1.3);background:linear-gradient(135deg,var(--brand),var(--brand2))

    /* 6) Tilt / Mouse Parallax */
    .tiltdisplay:grid;grid-template-columns:repeat(3,1fr);gap:16px
    .tilt .cellperspective:600px
    .tilt .innertransform-style:preserve-3d;transition:transform .08s ease;will-change:transform;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,.06);background:var(--panel)
    .tilt imgwidth:100%;height:260px;object-fit:cover;display:block

    /* 7) Lazy-load with blur-up */
    .lazy-griddisplay:grid;grid-template-columns:repeat(6,1fr);gap:12px
    .lazyposition:relative;border-radius:14px;overflow:hidden;border:1px solid rgba(255,255,255,.06);background:var(--panel)
    .lazy imgwidth:100%;height:160px;object-fit:cover;display:block;filter:blur(18px);transform:scale(1.08);transition:filter .5s ease, transform .5s ease
    .lazy.loaded imgfilter:blur(0);transform:none

    /* 8) Lightbox */
    .lightboxposition:fixed;inset:0;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;z-index:100
    .lightbox.opendisplay:flex
    .lightbox .panelbackground:#0b0d10;border:1px solid rgba(255,255,255,.18);border-radius:16px;box-shadow:var(--shadow);max-width:min(92vw,1200px);width:calc(100% - 24px)
    .lightbox .toolbardisplay:flex;align-items:center;gap:8px;padding:10px;border-bottom:1px solid rgba(255,255,255,.12);background:#0f141a
    .lightbox .toolbar .spacerflex:1
    .lightbox .toolbar buttonbackground:rgba(255,255,255,.06);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:10px;padding:6px 10px;cursor:pointer
    .lightbox .viewportposition:relative;height:70vh;min-height:320px;overflow:hidden;background:#000;border-bottom-left-radius:16px;border-bottom-right-radius:16px
    .lightbox .viewport imgposition:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(1);transform-origin:center center; user-select:none; -webkit-user-drag:none;
    .lightbox .closeposition:absolute;top:14px;right:14px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:10px;padding:8px 12px;cursor:pointer

    footercolor:var(--muted);text-align:center;padding:40px 0

    /* Explain panels */
    .explainmargin:12px 0 22px;padding:16px 18px;border:1px solid rgba(255,255,255,.08);background:var(--panel);border-radius:14px;box-shadow:var(--shadow)
    .explain h4margin:0 0 8px;font-size:15px;color:#e6eef9;font-weight:700;letter-spacing:.2px
    .explain pmargin:0 0 10px;color:var(--muted)
    .explain premargin:8px 0 0;background:#0a0d11;border:1px solid rgba(255,255,255,.08);padding:10px 12px;border-radius:10px;white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.45;color:#e7eef7

    /* Responsive */
    @media (max-width:1000px)
      .hero .wrapgrid-template-columns:1fr
      .cardgrid-column:span 6
      .filtersgrid-template-columns:repeat(4,1fr)
      .masonrycolumn-count:3
      .tiltgrid-template-columns:repeat(2,1fr)
      .lazy-gridgrid-template-columns:repeat(4,1fr)
    
    @media (max-width:720px)
      navdisplay:none
      .cardgrid-column:span 12
      .filtersgrid-template-columns:repeat(2,1fr)
      .masonrycolumn-count:2
      .tiltgrid-template-columns:1fr
      .lazy-gridgrid-template-columns:repeat(2,1fr)
      .carousel imgheight:300px
    
  </style>
</head>
<body>
  <header>
    <div class=wrap title>
      <h1>Image Effects Gallery</h1>
      <nav aria-label=Sections>
        <a href=#hover>Hover Zoom</a>
        <a href=#filters>CSS Filters</a>
        <a href=#masonry>Masonry</a>
        <a href=#parallax>Parallax</a>
        <a href=#carousel>Carousel</a>
        <a href=#tilt>Tilt</a>
        <a href=#lazy>Lazy-load</a>
        <a href=#lightbox-section>Lightbox</a>
      </nav>
    </div>
  </header>

  <section class=hero>
    <div class=wrap>
      <div>
        <span class=chip>HTML • CSS • JS</span>
        <h2>Showcasing visual effects for image galleries</h2>
        <p>Below are practical patterns you can reuse in your projects. Each section briefly explains the idea and then demonstrates it using the provided images. Click any image to view it in a full‑screen lightbox.</p>
      </div>
      <div class=stack aria-hidden=true>
        <img src=https://waivio.nyc3.digitaloceanspaces.com/9768387be2797fe6f5f5f283940c1eeff36788b27220ece58cc5588f6fabb3ca alt=Stack sample 1>
        <img src=https://waivio.nyc3.digitaloceanspaces.com/436be9350bccc5dfd437005dba32ad9bc6f5e015ab9f7a5a45d2d91d574da573 alt=Stack sample 2>
        <img src=https://waivio.nyc3.digitaloceanspaces.com/adcfd48796b4ed01be18c5cc6e8bf9b8e9a466fbd1ec7dcadf238c58318fbd01 alt=Stack sample 3>
      </div>
    </div>
  </section>

  <section id=hover class=section>
    <div class=wrap>
      <h3>1) Hover Zoom + Caption Reveal</h3>
      <p class=lead>Scale the image slightly on hover and fade in a caption via a gradient overlay. This adds affordance without heavy JS.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>Pure CSS transforms are GPU‑accelerated. We animate <code>transform: scale()</code> and reveal a caption at the bottom with a translucent gradient.</p>
        <h4>Prompt to recreate</h4>
        <pre>Create a card-style image gallery with a subtle hover zoom (CSS transform: scale ~1.06) and a bottom caption that fades in over a gradient. Use my provided images; make each card clickable to open a lightbox.</pre>
      </div>
      <div class=grid id=hoverGrid></div>
    </div>
  </section>

  <section id=filters class=section>
    <div class=wrap>
      <h3>2) CSS Filters (grayscale, sepia, blur, etc.)</h3>
      <p class=lead>Apply stylistic filters to thumbnails and remove them on hover to draw attention. Zero JS needed.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>Each tile receives a different <code>filter</code> value (grayscale, sepia, hue-rotate…). On hover we transition back to the original image.</p>
        <h4>Prompt to recreate</h4>
        <pre>Showcase CSS filter effects (grayscale, sepia, contrast, saturate, hue-rotate, brightness, slight blur). Display images in a grid with filters active by default and cleared on hover.</pre>
      </div>
      <div class=filters id=filterGrid></div>
    </div>
  </section>

  <section id=masonry class=section>
    <div class=wrap>
      <h3>3) Masonry Layout with CSS Columns</h3>
      <p class=lead>A Pinterest‑style waterfall using multi‑column layout. Great for mixed heights and minimal JS.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>The container uses <code>column-count</code>; each item sets <code>break-inside: avoid</code> so images don’t split between columns.</p>
        <h4>Prompt to recreate</h4>
        <pre>Create a responsive masonry gallery using CSS columns (no JS layout). Use 3–4 columns on desktop and fewer on mobile; items must avoid breaking across columns.</pre>
      </div>
      <div class=masonry id=masonryWrap></div>
    </div>
  </section>

  <section id=parallax class=section>
    <div class=wrap>
      <h3>4) Parallax Banner</h3>
      <p class=lead>Depth effect by gently translating a background layer as the page scrolls—compatible across deployments.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>A child <code>.pbg</code> layer is translated with JS based on scroll position (center‑relative) and clamped to stay within the frame.</p>
        <h4>Prompt to recreate</h4>
        <pre>Build a parallax banner that moves an inner background layer on scroll (translateY), clamped to the container to avoid empty edges. Use one of my images as the background.</pre>
      </div>
      <div class=parallax><div class=pbg></div><h4>Parallax banner using one of the gallery images</h4></div>
    </div>
  </section>

  <section id=carousel class=section>
    <div class=wrap>
      <h3>5) Minimal Carousel (Vanilla JS)</h3>
      <p class=lead>A small slider with previous/next controls and dots. Touch and resize aware.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>Slides sit in a flex track translated with <code>transform: translateX()</code>. Dots mirror the active index; touch events handle swipes.</p>
        <h4>Prompt to recreate</h4>
        <pre>Create a vanilla‑JS carousel: full‑width slides inside a flex track, prev/next buttons, pagination dots, and swipe gestures. Clicking a slide opens a lightbox.</pre>
      </div>
      <div class=carousel aria-roledescription=carousel>
        <div class=carousel-track id=track></div>
        <button class=cBtn cPrev aria-label=Previous id=prev></button>
        <button class=cBtn cNext aria-label=Next id=next></button>
      </div>
      <div class=dots id=dots aria-label=Slide pagination></div>
    </div>
  </section>

  <section id=tilt class=section>
    <div class=wrap>
      <h3>6) Subtle Tilt (Mouse Parallax)</h3>
      <p class=lead>A small 3D tilt that follows the cursor with perspective and a slight pop.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>Mouse position within a tile maps to <code>rotateX/rotateY</code> on an inner wrapper plus a tiny <code>translateZ/scale</code> for depth.</p>
        <h4>Prompt to recreate</h4>
        <pre>Create a grid of images where each card tilts toward the cursor (perspective on parent, rotateX/rotateY on child) with a subtle depth pop. Click opens a lightbox.</pre>
      </div>
      <div class=tilt id=tiltWrap></div>
    </div>
  </section>

  <section id=lazy class=section>
    <div class=wrap>
      <h3>7) Lazy‑load with Blur‑up</h3>
      <p class=lead>Defer image loading until they approach the viewport, starting from a blurred placeholder that sharpens on load.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p><code>IntersectionObserver</code> swaps <code>data-src</code> into <code>src</code> when visible, then removes blur on <code>load</code> for a pleasant reveal.</p>
        <h4>Prompt to recreate</h4>
        <pre>Implement a lazy‑loaded thumbnail grid using IntersectionObserver. Start each image blurred and slightly scaled; on load, transition to sharp at 1.0 scale.</pre>
      </div>
      <div class=lazy-grid id=lazyGrid></div>
    </div>
  </section>

  <section id=lightbox-section class=section>
    <div class=wrap>
      <h3>8) Click‑to‑Zoom Lightbox</h3>
      <p class=lead>Open images in a modal viewer with zoom controls, fit/actual size, wheel‑zoom, drag‑to‑pan, and keyboard navigation.</p>
      <div class=explain>
        <h4>How it works</h4>
        <p>A centered modal hosts an image in a viewport. We adjust <code>scale</code> and translate offsets for zooming/panning, with toolbar buttons and wheel handling.</p>
        <h4>Prompt to recreate</h4>
        <pre>Create a modal image viewer: toolbar with close/+ / − / 100% / Fit, wheel zoom centered under the cursor, drag to pan, and arrow‑key navigation between images.</pre>
      </div>
    </div>
  </section>

  <div class=lightbox id=lightboxModal aria-modal=true role=dialog aria-label=Image viewer>
    <div class=panel>
      <div class=toolbar>
        <button class=close id=closeLb aria-label=Close>✕ Close</button>
        <div class=spacer></div>
        <button id=zoomOut aria-label=Zoom out></button>
        <button id=zoomIn aria-label=Zoom in></button>
        <button id=zoomReset aria-label=Actual size>100%</button>
        <button id=zoomFit aria-label=Fit to window>Fit</button>
      </div>
      <div class=viewport id=lbViewport>
        <img id=lbImg alt=Expanded view />
      </div>
    </div>
  </div>

  <footer>
    Built with HTML, CSS, and vanilla JS — no dependencies.
  </footer>

<script>
  // ===== Data =====
  const IMAGES = [
    src:'https://waivio.nyc3.digitaloceanspaces.com/9768387be2797fe6f5f5f283940c1eeff36788b27220ece58cc5588f6fabb3ca', alt:'Gallery 1',
    src:'https://waivio.nyc3.digitaloceanspaces.com/436be9350bccc5dfd437005dba32ad9bc6f5e015ab9f7a5a45d2d91d574da573', alt:'Gallery 2',
    src:'https://waivio.nyc3.digitaloceanspaces.com/adcfd48796b4ed01be18c5cc6e8bf9b8e9a466fbd1ec7dcadf238c58318fbd01', alt:'Gallery 3',
    src:'https://waivio.nyc3.digitaloceanspaces.com/dd72b5140384eb808fd68d05823a2309c41bef1ee2cbf39d4c308cc4f1f7757f', alt:'Gallery 4',
    src:'https://waivio.nyc3.digitaloceanspaces.com/bb56fc3211726906b689019358ba5bbc03699d57782c010cce94c4c945c87b72', alt:'Gallery 5',
    src:'https://waivio.nyc3.digitaloceanspaces.com/a2e0d499c3b88432544764602742688c825b99cf109a1f1104677f05245a4f35', alt:'Gallery 6',
    src:'https://waivio.nyc3.digitaloceanspaces.com/bface45647c141be27d422c96f2e3764a041c617290ffa539401be320c42569a', alt:'Gallery 7',
  ];

  // Utility: create element
  const el = (tag, props=, children=[])=>
    const n = document.createElement(tag);
    Object.assign(n, props);
    children.forEach(c=> n.append(c));
    return n;
  ;

  // ===== 1) Hover Zoom + Caption =====
  const hoverGrid = document.getElementById('hoverGrid');
  IMAGES.slice(0,6).forEach((img, i)=>
    const card = el('figure', className:'card');
    const image = el('img', src: img.src, alt: img.alt);
    const cap = el('figcaption', className:'cap', innerHTML:`Sample caption #$i+1`);
    card.append(image, cap);
    card.addEventListener('click', ()=> openLightbox(img.src));
    hoverGrid.append(card);
  );

  // ===== 2) CSS Filters Showcase =====
  const filterGrid = document.getElementById('filterGrid');
  const filters = [
    ['grayscale(100%)','grayscale'],
    ['sepia(80%)','sepia'],
    ['contrast(1.3)','contrast+'],
    ['saturate(1.5)','saturate+'],
    ['hue-rotate(20deg)','hue-rotate'],
    ['brightness(1.2)','bright+'],
    ['blur(1px) contrast(1.1)','blur+contrast']
  ];
  IMAGES.forEach((img, i)=>
    const f = filters[i % filters.length];
    const box = el('div', className:'fBox', style:`--f:$f[0]`);
    const image = el('img', src: img.src, alt: img.alt);
    const label = el('small', innerText: f[1]);
    box.append(image, label);
    box.addEventListener('click', ()=> openLightbox(img.src));
    filterGrid.append(box);
  );

  // ===== 3) Masonry Layout =====
  const masonryWrap = document.getElementById('masonryWrap');
  for(let i=0;i<8;i++)
    const m = el('div', className:'mItem');
    const img = IMAGES[i % IMAGES.length];
    const image = el('img', src: img.src, alt: img.alt + ' (masonry)');
    m.append(image);
    m.addEventListener('click', ()=> openLightbox(img.src));
    masonryWrap.append(m);
  

  // Parallax JS fallback (works across deployments)
  const parallax = document.querySelector('.parallax');
  const pbg = document.querySelector('.parallax .pbg');
  function updateParallax()
    if(!parallax || !pbg) return;
    const rect = parallax.getBoundingClientRect();
    const scrollY = window.scrollY || window.pageYOffset || 0;
    const vh = window.innerHeight || document.documentElement.clientHeight;
    const elemTop = rect.top + scrollY;
    const elemCenter = elemTop + rect.height/2;
    const viewCenter = scrollY + vh/2;
    const delta = viewCenter - elemCenter; // centered distance in px
    let offset = delta * 0.15; // parallax strength
    const clamp = rect.height * 0.5; // keep within bounds to avoid empty area
    if(offset > clamp) offset = clamp;
    if(offset < -clamp) offset = -clamp;
    pbg.style.transform = `translateY($offsetpx)`;
  
  ['scroll','resize'].forEach(ev=> window.addEventListener(ev, updateParallax, passive:true));
  document.addEventListener('DOMContentLoaded', updateParallax);
  updateParallax();

  // ===== 5) Carousel =====
  const track = document.getElementById('track');
  const dotsWrap = document.getElementById('dots');
  let idx = 0;
  function buildCarousel()
    track.innerHTML=''; dotsWrap.innerHTML='';
    IMAGES.forEach((img,i)=>
      const frame = el('div', style:'min-width:100%');
      frame.append(el('img',src:img.src,alt:img.alt));
      frame.addEventListener('click', ()=> openLightbox(img.src));
      track.append(frame);
      const dot = el('div', className:'dot' + (i===idx?' active':''));
      dot.addEventListener('click', ()=> go(i));
      dotsWrap.append(dot);
    );
    resizeCarousel();
  
  function go(i) idx = (i+IMAGES.length)%IMAGES.length; update(); 
  function next() go(idx+1); 
  function prev() go(idx-1); 
  function update()
    const w = document.querySelector('.carousel').clientWidth;
    track.style.transform = `translateX($-idx*wpx)`;
    [...dotsWrap.children].forEach((d,i)=> d.classList.toggle('active', i===idx));
  
  function resizeCarousel() update(); 
  document.getElementById('next').addEventListener('click', next);
  document.getElementById('prev').addEventListener('click', prev);
  window.addEventListener('resize', resizeCarousel);
  let startX=null; track.addEventListener('touchstart',e=> startX=e.touches[0].clientX);
  track.addEventListener('touchend',e=> if(startX==null) return; const dx=e.changedTouches[0].clientX-startX; if(Math.abs(dx)>40) dx<0?next():prev();  startX=null; );
  buildCarousel();

  // ===== 6) Tilt / Mouse Parallax =====
  const tiltWrap = document.getElementById('tiltWrap');
  IMAGES.slice(0,6).forEach(img=>
    const cell = el('div', className:'cell');
    const inner = el('div', className:'inner');
    const image = el('img', src: img.src, alt: img.alt);
    inner.append(image); cell.append(inner); tiltWrap.append(cell);
    cell.addEventListener('mousemove', (e)=>
      const r = cell.getBoundingClientRect();
      const x = (e.clientX - r.left) / r.width; // 0..1
      const y = (e.clientY - r.top) / r.height; // 0..1
      const rx = (y - .5) * -12; // tilt up/down
      const ry = (x - .5) * 12;  // tilt left/right
      inner.style.transform = `rotateX($rxdeg) rotateY($rydeg) translateZ(24px) scale(1.03)`;
    );
    cell.addEventListener('mouseleave', ()=> inner.style.transform='');
    cell.addEventListener('click', ()=> openLightbox(img.src));
  );

  // ===== 7) Lazy-load with blur-up =====
  const lazyGrid = document.getElementById('lazyGrid');
  for(let i=0;i<12;i++)
    const box = el('div',className:'lazy');
    const img = IMAGES[i % IMAGES.length];
    const image = el('img',alt:img.alt, loading:'lazy'); image.setAttribute('data-src', img.src);
    box.append(image); lazyGrid.append(box);
    box.addEventListener('click', ()=> openLightbox(img.src));
  
  const io = 'IntersectionObserver' in window ? new IntersectionObserver((entries,obs)=>
    entries.forEach(en=>
      if(en.isIntersecting)
        const img = en.target.querySelector('img');
        if(img && !img.src) img.src = img.dataset.src; img.addEventListener('load', ()=> en.target.classList.add('loaded'), once:true); 
        obs.unobserve(en.target);
      
    )
  ,rootMargin:'120px') : null;
  document.querySelectorAll('.lazy').forEach(b=> io? io.observe(b) : (b.querySelector('img').src = b.querySelector('img').dataset.src));

  // ===== 8) Lightbox =====
  const lb = document.getElementById('lightboxModal');
  const lbImg = document.getElementById('lbImg');
  const lbViewport = document.getElementById('lbViewport');
  const closeLb = document.getElementById('closeLb');
  const btnIn = document.getElementById('zoomIn');
  const btnOut = document.getElementById('zoomOut');
  const btnReset = document.getElementById('zoomReset');
  const btnFit = document.getElementById('zoomFit');

  let currentSrc = null;
  let scale = 1, tx = 0, ty = 0;
  let dragging = false, sx = 0, sy = 0, stx = 0, sty = 0;

  function apply() lbImg.style.transform = `translate(calc(-50% + $txpx), calc(-50% + $typx)) scale($scale)`; 

  function fit()
    const vw = lbViewport.clientWidth; const vh = lbViewport.clientHeight;
    const iw = lbImg.naturalWidth || 1; const ih = lbImg.naturalHeight || 1;
    scale = Math.min(vw/iw, vh/ih);
    if(!isFinite(scale) || scale<=0) scale = 1;
    tx = 0; ty = 0; apply();
  

  function openLightbox(src)
    lbImg.onload = ()=> fit(); ;
    lbImg.src = src; currentSrc = src; lb.classList.add('open');
  
  function closeLightbox() lb.classList.remove('open'); lbImg.removeAttribute('src'); 

  closeLb.addEventListener('click', closeLightbox);
  lb.addEventListener('click', e=>  if(e.target===lb) closeLightbox(); );
  document.addEventListener('keydown', e=>
    if(!lb.classList.contains('open')) return;
    if(e.key==='Escape') closeLightbox();
    if(e.key==='ArrowRight') nextInLightbox(1);
    if(e.key==='ArrowLeft') nextInLightbox(-1);
  );
  function nextInLightbox(step)
    const i = IMAGES.findIndex(i=> i.src===currentSrc);
    const ni = (i + step + IMAGES.length) % IMAGES.length;
    currentSrc = IMAGES[ni].src; lbImg.src = currentSrc;
  

  // Zoom controls
  btnIn.addEventListener('click', ()=>  scale = Math.min(scale*1.25, 8); apply(); );
  btnOut.addEventListener('click', ()=>  scale = Math.max(scale/1.25, 0.1); apply(); );
  btnReset.addEventListener('click', ()=>  scale = 1; tx = 0; ty = 0; apply(); );
  btnFit.addEventListener('click', fit);

  // Wheel zoom (centered)
  lbViewport.addEventListener('wheel', (e)=> e.preventDefault();
    const delta = Math.sign(e.deltaY);
    const old = scale; const factor = delta>0? 1/1.15 : 1.15; scale = Math.min(Math.max(scale*factor, 0.1), 8);
    // Keep focal point roughly under cursor
    const rect = lbViewport.getBoundingClientRect();
    const cx = e.clientX - rect.left - rect.width/2 - tx;
    const cy = e.clientY - rect.top - rect.height/2 - ty;
    tx -= cx*(scale/old - 1); ty -= cy*(scale/old - 1);
    apply();
  , passive:false);

  // Drag to pan
  lbViewport.addEventListener('mousedown', (e)=> dragging = true; sx = e.clientX; sy = e.clientY; stx = tx; sty = ty; e.preventDefault(); );
  window.addEventListener('mousemove', (e)=> if(!dragging) return; tx = stx + (e.clientX - sx); ty = sty + (e.clientY - sy); apply(); );
  window.addEventListener('mouseup', ()=> dragging=false);
</script>
</body>
</html>