← back to Wallco Ai

public/collections/fliepaper-bugs.html

452 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>fliepaper × wallco — Gucci Resort Edition</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="Six luxury insect wallcoverings — scarab, luna moth, dragonfly, stag beetle, honeybee, firefly — in six Gucci-resort colorways. 36 SKUs from wallco.ai." />
<link rel="stylesheet" href="/marketplace/_layout.css" />
<style>
  :root {
    --fb-bg: #faf7f1;
    --fb-fg: #1a1814;
    --fb-muted: #6b665c;
    --fb-line: rgba(0,0,0,.08);
    --fb-card: #fff;
    --fb-accent: #0F4F38;
    --fb-cols: 3;
  }
  html[data-theme="dark"] {
    --fb-bg: #0e0d0b;
    --fb-fg: #f3eee2;
    --fb-muted: #a39c8b;
    --fb-line: rgba(255,255,255,.10);
    --fb-card: #191714;
    --fb-accent: #B8914A;
  }
  html,body { background: var(--fb-bg); color: var(--fb-fg); margin:0; padding:0;
    font-family: ui-sans-serif, system-ui, "Helvetica Neue", Arial, sans-serif;
    transition: background 200ms ease, color 200ms ease;
  }
  a { color: inherit; }

  /* ─────── HERO (Gucci-style) ─────── */
  .fb-hero {
    position: relative;
    height: min(96vh, 1080px);
    min-height: 540px;
    overflow: hidden;
    background: #14110d;
  }
  .fb-hero-img {
    position: absolute; inset:0;
    background-size: cover; background-position: center;
    transition: opacity 800ms ease;
    opacity: 0;
  }
  .fb-hero-img.is-active { opacity: 1; }
  .fb-hero-fallback {
    position: absolute; inset:0;
    background:
      radial-gradient(ellipse at 30% 20%, rgba(184,145,74,.30), transparent 55%),
      radial-gradient(ellipse at 70% 80%, rgba(15,79,56,.45), transparent 55%),
      linear-gradient(135deg, #0F4F38 0%, #1A1F3C 50%, #3D1F52 100%);
  }
  .fb-hero-overlay {
    position: absolute; inset:0;
    background: linear-gradient(180deg, rgba(0,0,0,.20) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,.55) 100%);
    pointer-events: none;
  }

  /* logo UL */
  .fb-logo {
    position: absolute; top: 20px; left: 28px; z-index: 5;
    color: #fff; font-weight: 700; letter-spacing: .12em; font-size: 14px;
    text-decoration: none; mix-blend-mode: difference;
  }
  /* hamburger UR ≡ MENU */
  .fb-menu {
    position: absolute; top: 18px; right: 24px; z-index: 5;
    display: inline-flex; align-items: center; gap: 8px;
    color: #fff; cursor: pointer; padding: 8px 4px;
    background: transparent; border: 0;
    font: 600 12px/1 ui-sans-serif, system-ui, sans-serif;
    letter-spacing: .22em; text-transform: uppercase;
    mix-blend-mode: difference;
  }
  .fb-menu .bars { display:inline-flex; flex-direction:column; gap:4px; }
  .fb-menu .bars span { width: 22px; height: 1px; background: currentColor; display:block; }

  /* dark/light toggle */
  .fb-theme-toggle {
    position: absolute; top: 20px; right: 110px; z-index: 5;
    background: transparent; border: 0; cursor: pointer;
    color: #fff; mix-blend-mode: difference;
    font-size: 18px; padding: 6px 8px;
  }

  /* wordmark BELOW the photo (i.e. centered, low in the hero) */
  .fb-wordmark {
    position: absolute; left:0; right:0; bottom: 22%;
    z-index: 4; text-align: center; color: #fff;
    text-shadow: 0 2px 28px rgba(0,0,0,.45);
    pointer-events: none;
  }
  .fb-wordmark .brand {
    font-family: "Times New Roman", Didot, Georgia, serif;
    font-size: clamp(48px, 9vw, 132px);
    font-weight: 400; letter-spacing: .02em; line-height: 1;
  }
  .fb-wordmark .tag {
    margin-top: 14px; font-style: italic; font-size: clamp(14px, 1.6vw, 20px);
    letter-spacing: .18em; text-transform: uppercase; opacity: .92;
  }
  /* white Enter pill — centered bottom of hero */
  .fb-enter {
    position: absolute; left: 50%; bottom: 6%; transform: translateX(-50%);
    z-index: 5;
    background: #fff; color: #111;
    padding: 14px 56px; border-radius: 4px;
    font: 600 13px/1 ui-sans-serif, system-ui, sans-serif;
    letter-spacing: .22em; text-transform: uppercase;
    text-decoration: none;
    transition: transform 200ms ease, box-shadow 200ms ease;
  }
  .fb-enter:hover { transform: translateX(-50%) translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,.20); }

  /* ─────── BODY ─────── */
  .fb-main { max-width: 1480px; margin: 0 auto; padding: 56px 28px 100px; }

  .fb-intro {
    text-align: center; max-width: 720px; margin: 0 auto 36px;
  }
  .fb-intro h2 {
    font-family: "Times New Roman", Didot, Georgia, serif; font-weight: 400;
    font-size: clamp(28px, 3.6vw, 44px); margin: 0 0 12px;
  }
  .fb-intro p { color: var(--fb-muted); line-height: 1.55; margin: 0; }

  /* color-dot row */
  .fb-palette {
    display: flex; gap: 18px; justify-content: center; align-items: center;
    margin: 28px auto 48px; flex-wrap: wrap;
  }
  .fb-palette .swatch {
    display: flex; flex-direction: column; align-items:center; gap: 8px;
    cursor: pointer; min-width: 84px;
  }
  .fb-palette .dot {
    width: 44px; height: 44px; border-radius: 50%;
    border: 1px solid var(--fb-line);
    box-shadow: inset 0 0 0 3px var(--fb-bg);
  }
  .fb-palette .swatch.is-active .dot { box-shadow: inset 0 0 0 3px var(--fb-bg), 0 0 0 2px var(--fb-fg); }
  .fb-palette .swatch .name { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--fb-muted); }

  /* controls (sort + density) */
  .fb-controls {
    display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
    margin: 0 0 28px; padding: 14px 16px;
    background: var(--fb-card);
    border: 1px solid var(--fb-line); border-radius: 12px;
  }
  .fb-controls label { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--fb-muted); }
  .fb-controls select {
    padding: 8px 14px; border-radius: 999px; border: 1px solid var(--fb-line);
    background: var(--fb-bg); color: var(--fb-fg); font-size: 14px;
  }
  .fb-controls .pill {
    margin-left: auto; font-size: 12px; color: var(--fb-muted);
    letter-spacing: .08em; text-transform: uppercase;
  }

  /* pattern grid */
  .fb-grid {
    display: grid; gap: 26px;
    grid-template-columns: repeat(var(--fb-cols), 1fr);
  }
  @media (max-width: 980px) { :root { --fb-cols: 2; } }
  @media (max-width: 600px) { :root { --fb-cols: 1; } }

  .fb-card {
    background: var(--fb-card); border: 1px solid var(--fb-line);
    border-radius: 14px; overflow: hidden;
    display: flex; flex-direction: column;
    transition: transform 180ms ease, box-shadow 180ms ease;
  }
  .fb-card:hover { transform: translateY(-3px); box-shadow: 0 12px 28px rgba(0,0,0,.10); }
  .fb-card .pat-img {
    aspect-ratio: 1 / 1.15;
    background-size: cover; background-position: center;
    background-color: #1a1814; position: relative;
  }
  .fb-card .pat-pending {
    position: absolute; top: 12px; left: 12px;
    background: rgba(0,0,0,.55); color: #fff;
    font: 600 10px/1 ui-sans-serif, system-ui;
    letter-spacing: .2em; text-transform: uppercase;
    padding: 6px 10px; border-radius: 999px;
  }
  .fb-card .body { padding: 16px 18px 18px; }
  .fb-card .name {
    font-family: "Times New Roman", Didot, Georgia, serif; font-size: 22px;
    margin: 0 0 4px;
  }
  .fb-card .bug-sub { color: var(--fb-muted); font-size: 13px; margin: 0 0 12px;
    letter-spacing: .04em; text-transform: capitalize; }
  .fb-card .chips { display: flex; gap: 6px; flex-wrap: wrap; }
  .fb-card .chip {
    width: 22px; height: 22px; border-radius: 50%;
    border: 1px solid var(--fb-line);
    cursor: pointer; position: relative;
  }
  .fb-card .chip.is-missing::after {
    content: ""; position: absolute; inset: 4px;
    border: 1px dashed rgba(0,0,0,.30); border-radius: 50%;
  }
  .fb-card .meta-row {
    display: flex; gap: 10px; justify-content: space-between;
    align-items: center; margin-top: 14px;
    font-size: 12px; color: var(--fb-muted);
  }

  .fb-footer {
    margin-top: 80px; padding: 28px 0; border-top: 1px solid var(--fb-line);
    text-align: center; color: var(--fb-muted); font-size: 12px; letter-spacing: .12em;
    text-transform: uppercase;
  }
  .fb-footer a { color: inherit; text-decoration: underline; text-underline-offset: 4px; }
</style>
</head>
<body data-theme="light">
  <!-- ───────────── HERO ───────────── -->
  <section class="fb-hero" id="fb-hero">
    <div class="fb-hero-fallback" id="fb-hero-fallback"></div>
    <a href="/" class="fb-logo">wallco.ai</a>
    <button class="fb-theme-toggle" id="fb-theme" title="Toggle light/dark">◐</button>
    <button class="fb-menu" type="button" onclick="document.dispatchEvent(new Event('wallco-open-menu'))" aria-label="Open menu">
      <span class="bars"><span></span><span></span><span></span></span>
      MENU
    </button>
    <div class="fb-hero-overlay"></div>
    <div class="fb-wordmark">
      <div class="brand">fliepaper × wallco</div>
      <div class="tag">Gucci Resort Edition · 6 patterns · 6 colorways · 36 SKUs</div>
    </div>
    <a class="fb-enter" href="#collection">Enter</a>
  </section>

  <!-- ───────────── BODY ───────────── -->
  <main class="fb-main" id="collection">
    <section class="fb-intro">
      <h2>Six bugs. Six Gucci-resort colorways.</h2>
      <p>Scarab, luna moth, dragonfly, stag beetle, honeybee, firefly — six luxury insect wallcoverings rendered in six Gucci-coordinated colorways: emerald, papyrus, midnight, antique horsebit, deep plum, oxblood. Designed in-house by Wallco House Studio. Every pattern generated through proprietary Wallco AI.</p>
    </section>

    <!-- Color dot row -->
    <div class="fb-palette" id="fb-palette"></div>

    <!-- Sort + density -->
    <div class="fb-controls">
      <label for="fb-sort">Sort</label>
      <select id="fb-sort">
        <option value="newest">Newest</option>
        <option value="color">Color</option>
        <option value="pattern">Pattern A→Z</option>
        <option value="sku">SKU A→Z</option>
      </select>

      <label for="fb-density">Density</label>
      <input type="range" id="fb-density" min="1" max="4" value="3" step="1" />

      <span class="pill" id="fb-meta">loading…</span>
    </div>

    <div class="fb-grid" id="fb-grid"></div>
  </main>

  <footer class="fb-footer">
    <a href="/">wallco.ai</a> · Fliepaper Bugs — Gucci Resort Edition · <span id="fb-status-count">0/36</span> SKUs generated
  </footer>

<script>
const STORAGE_KEY = 'wallco.fliepaper-bugs.prefs';
const FB = { spec: null, cells: [], colorwayFilter: null };

function loadPrefs() {
  try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; }
}
function savePrefs() {
  const p = {
    sort: document.getElementById('fb-sort').value,
    density: document.getElementById('fb-density').value,
    theme: document.body.dataset.theme,
  };
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); } catch {}
}

function applyDensity() {
  const v = parseInt(document.getElementById('fb-density').value, 10);
  // 1→1col, 2→2col, 3→3col, 4→4col
  document.documentElement.style.setProperty('--fb-cols', v);
}

function applyTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  document.body.dataset.theme = theme;
  document.getElementById('fb-theme').textContent = theme === 'dark' ? '☀' : '◐';
}

function placeholderPoster(hex, label) {
  // hsl-gradient style placeholder — never a 404, never a broken image.
  // Two-stop gradient seeded from the colorway hex so each pattern×colorway
  // pair gets a distinct placeholder.
  const seed = label.split('').reduce((s,c)=>s+c.charCodeAt(0),0);
  const hueShift = (seed % 60) - 30;
  return `linear-gradient(135deg,
    ${hex} 0%,
    color-mix(in oklab, ${hex} 65%, #000 35%) 100%)`;
}

function escHtml(s){ return String(s||'').replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }

async function fetchCollection() {
  try {
    const r = await fetch('/api/collections/fliepaper-bugs');
    if (!r.ok) throw new Error('http ' + r.status);
    const j = await r.json();
    FB.spec = j.spec;
    FB.cells = j.cells || [];
    renderPalette();
    renderGrid();
    updateMeta();
  } catch (e) {
    document.getElementById('fb-meta').textContent = 'load error: ' + e.message;
  }
}

function renderPalette() {
  const el = document.getElementById('fb-palette');
  el.innerHTML = '';
  for (const c of FB.spec.palette) {
    const w = document.createElement('div');
    w.className = 'swatch' + (FB.colorwayFilter === c.slug ? ' is-active' : '');
    w.innerHTML = `
      <div class="dot" style="background:${c.hex}"></div>
      <div class="name">${escHtml(c.name)}</div>
    `;
    w.addEventListener('click', () => {
      FB.colorwayFilter = (FB.colorwayFilter === c.slug) ? null : c.slug;
      renderPalette();
      renderGrid();
    });
    el.appendChild(w);
  }
}

function sortPatterns(patterns) {
  const mode = document.getElementById('fb-sort').value;
  const arr = patterns.slice();
  if (mode === 'pattern') arr.sort((a,b) => a.name.localeCompare(b.name));
  else if (mode === 'sku')   arr.sort((a,b) => a.slug.localeCompare(b.slug));
  else if (mode === 'color') {
    // group by dominant colorway present — placeholder until art lands, sort by first-cell hex
    arr.sort((a,b) => a.slug.localeCompare(b.slug));
  }
  // 'newest' = spec order (the order yolo-runner generates in)
  return arr;
}

function renderGrid() {
  const grid = document.getElementById('fb-grid');
  const patterns = sortPatterns(FB.spec.patterns);
  grid.innerHTML = '';

  for (const p of patterns) {
    const cellsForPat = FB.cells.filter(c => c.pattern_slug === p.slug);
    const visibleCells = FB.colorwayFilter
      ? cellsForPat.filter(c => c.colorway_slug === FB.colorwayFilter)
      : cellsForPat;

    if (FB.colorwayFilter && visibleCells.length === 0) continue;

    // Hero image for the card — prefer a generated cell that matches the
    // active colorway filter, else first GENERATED cell, else placeholder.
    const heroCell = (FB.colorwayFilter
      ? visibleCells.find(c => c.status === 'GENERATED')
      : null)
      || cellsForPat.find(c => c.status === 'GENERATED')
      || cellsForPat[0];

    const heroHex = (heroCell && heroCell.colorway_hex) || '#1A1F3C';
    const heroIsReal = heroCell && heroCell.status === 'GENERATED' && heroCell.thumbnail_url;

    const card = document.createElement('article');
    card.className = 'fb-card';
    card.innerHTML = `
      <div class="pat-img" style="${heroIsReal
        ? `background-image:url('${heroCell.thumbnail_url}')`
        : `background-image:${placeholderPoster(heroHex, p.slug)}`}">
        ${heroIsReal ? '' : `<span class="pat-pending">Pending</span>`}
      </div>
      <div class="body">
        <h3 class="name">${escHtml(p.name)}</h3>
        <div class="bug-sub">${escHtml(p.bug)} · ${escHtml(p.scale)}</div>
        <div class="chips" data-pat="${p.slug}"></div>
        <div class="meta-row">
          <span>${visibleCells.filter(c => c.status === 'GENERATED').length}/${visibleCells.length} colorways live</span>
          <span>${p.composition.split(';')[0]}</span>
        </div>
      </div>
    `;

    // chip row — one per colorway
    const chips = card.querySelector('.chips');
    for (const c of FB.spec.palette) {
      const cell = cellsForPat.find(x => x.colorway_slug === c.slug);
      const span = document.createElement('span');
      span.className = 'chip' + (cell?.status === 'GENERATED' ? '' : ' is-missing');
      span.style.background = c.hex;
      span.title = `${c.name} — ${cell?.status || 'PENDING'}`;
      span.addEventListener('click', () => {
        FB.colorwayFilter = (FB.colorwayFilter === c.slug) ? null : c.slug;
        renderPalette();
        renderGrid();
      });
      chips.appendChild(span);
    }
    grid.appendChild(card);
  }
}

function updateMeta() {
  const generated = FB.cells.filter(c => c.status === 'GENERATED').length;
  const total = FB.cells.length;
  document.getElementById('fb-meta').textContent =
    `${generated}/${total} SKUs generated · ${FB.spec.patterns.length} patterns · ${FB.spec.palette.length} colorways`;
  document.getElementById('fb-status-count').textContent = `${generated}/${total}`;
}

// ─── Init ───
(function init() {
  const prefs = loadPrefs();
  if (prefs.sort) document.getElementById('fb-sort').value = prefs.sort;
  if (prefs.density) document.getElementById('fb-density').value = prefs.density;
  applyDensity();
  applyTheme(prefs.theme === 'dark' ? 'dark' : 'light');

  document.getElementById('fb-sort').addEventListener('change', () => { savePrefs(); renderGrid(); });
  document.getElementById('fb-density').addEventListener('input', () => { applyDensity(); savePrefs(); });
  document.getElementById('fb-theme').addEventListener('click', () => {
    applyTheme(document.body.dataset.theme === 'dark' ? 'light' : 'dark');
    savePrefs();
  });

  fetchCollection();
  // Re-poll every 30s so newly generated cells swap in without reload.
  setInterval(fetchCollection, 30000);
})();
</script>
</body>
</html>