← back to Bestwallpaperplace

public/index.html

632 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- GA4 gtag (auto) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-SSNGG2BQC3"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-SSNGG2BQC3');
</script>
<!-- /GA4 gtag -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Best Wallpaper Place — An editor's cut of premium wallpaper, no sponsored placements</title>
<meta name="description" content="An editor's cut of 600 premium wallpapers from 32 design houses — Maya Romanoff, Phillipe Romano, Coordonné, Kravet, Cole &amp; Son and more. Curated, not aggregated; no sponsored placements; memo samples on every pattern.">
<meta name="theme-color" content="#faf8f3">
<link rel="canonical" href="https://bestwallpaperplace.com/">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ctext y='20' font-size='22'%3E%E2%98%85%3C/text%3E%3C/svg%3E">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">

<meta property="og:type" content="website">
<meta property="og:url" content="https://bestwallpaperplace.com/">
<meta property="og:image" content="https://bestwallpaperplace.com/hero-bg.jpg">
<meta property="og:title" content="Best Wallpaper Place — An editor's cut of premium wallpaper">
<meta property="og:description" content="600 patterns from 32 design houses. Curated, not aggregated. No sponsored placements. Memo samples on every pattern.">
<meta name="twitter:card" content="summary_large_image">

<script>
(function(){ try { var t = localStorage.getItem('bwp_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
</script>
<style>
:root {
  --bg: #faf8f3;
  --paper: #1a1a1a;
  --muted: #7a7a7a;
  --line: #e5e0d6;
  --accent: #b8860b;
  --bg-soft: #f0ebe0;
  --cols: 5;
}
html[data-theme="dark"] {
  --bg: #14110d;
  --paper: #faf8f3;
  --muted: #9a8f7a;
  --line: rgba(255,255,255,0.10);
  --accent: #d4a017;
  --bg-soft: #1f1a14;
}
* { margin:0; padding:0; box-sizing:border-box; }
html { scroll-behavior:smooth; }
body {
  font-family:'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  color:var(--paper);
  background:var(--bg);
  -webkit-font-smoothing:antialiased;
  min-height:100vh;
}
h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif; font-weight:500; letter-spacing:-0.01em; }

/* Header */
header {
  position:sticky; top:0; z-index:100;
  display:flex; justify-content:space-between; align-items:center;
  padding:18px 32px;
  background:color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter:blur(12px);
  border-bottom:1px solid var(--line);
}
.brand { display:flex; align-items:center; gap:12px; text-decoration:none; color:var(--paper); }
.brand .star { font-size:22px; color:var(--accent); line-height:1; }
.brand .name { font-family:'Cormorant Garamond', serif; font-size:20px; font-weight:600; letter-spacing:0.02em; }
.brand .name sub { font-size:9px; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted); display:block; margin-top:2px; font-family:'Inter',sans-serif; font-weight:600; }
.h-actions { display:flex; align-items:center; gap:14px; }
.theme-toggle {
  background:transparent; border:1px solid var(--line);
  width:36px; height:36px; border-radius:50%;
  cursor:pointer; color:var(--paper);
  display:inline-flex; align-items:center; justify-content:center;
  font-size:15px; transition:all .15s; padding:0;
}
.theme-toggle:hover { border-color:var(--accent); color:var(--accent); }

/* Hero */
.hero {
  padding:96px 32px 64px;
  text-align:center;
  border-bottom:1px solid var(--line);
}
.hero .eyebrow {
  font-size:11px; letter-spacing:0.4em; text-transform:uppercase;
  font-weight:700; color:var(--muted); margin-bottom:18px;
}
.hero h1 {
  font-size:clamp(48px, 7vw, 96px);
  line-height:1.05;
  margin:0 auto 18px;
  max-width:1100px;
}
.hero h1 .accent { color:var(--accent); font-style:italic; }
.hero .sub {
  font-size:clamp(15px, 1.6vw, 19px);
  color:var(--muted); max-width:620px; margin:0 auto 32px;
  line-height:1.55;
}
.hero .stats {
  display:flex; justify-content:center; gap:48px; flex-wrap:wrap;
  font-size:11px; letter-spacing:0.22em; text-transform:uppercase;
  font-weight:700; color:var(--muted);
}
.hero .stats span b { display:block; font-family:'Cormorant Garamond',serif; font-size:28px; font-weight:500; color:var(--paper); letter-spacing:0; text-transform:none; margin-top:4px; }

/* Editorial intro */
.editorial {
  padding:72px 32px;
  background:var(--bg-soft);
  border-bottom:1px solid var(--line);
}
.editorial-inner {
  max-width:780px; margin:0 auto;
}
.ed-eyebrow {
  font-size:11px; letter-spacing:0.4em; text-transform:uppercase;
  font-weight:700; color:var(--accent); margin-bottom:18px;
}
.editorial h2 {
  font-size:clamp(28px, 4vw, 44px);
  line-height:1.15; margin-bottom:24px;
  letter-spacing:-0.01em;
}
.editorial p {
  font-size:clamp(15px, 1.4vw, 17px);
  line-height:1.72; color:var(--paper);
  margin-bottom:18px;
  max-width:680px;
}
.editorial p:last-child { margin-bottom:0; }
@media (max-width:720px) { .editorial { padding:56px 20px; } }

/* Toolbar */
.section { padding:48px 32px 96px; max-width:1600px; margin:0 auto; }
.toolbar {
  display:flex; align-items:center; gap:16px;
  flex-wrap:wrap;
  margin-bottom:24px; padding-bottom:18px;
  border-bottom:1px solid var(--line);
}
.toolbar .search {
  flex:0 1 280px; display:flex; align-items:center; gap:10px;
  border-bottom:1px solid var(--line); padding:6px 0;
}
.toolbar .search input {
  flex:1; background:transparent; border:0; color:var(--paper);
  font-family:inherit; font-size:13px; outline:none; letter-spacing:0.02em;
}
.toolbar .search input::placeholder { color:var(--muted); }
.toolbar .ctl {
  display:flex; align-items:center; gap:10px;
  font-size:10px; letter-spacing:0.28em; text-transform:uppercase;
  font-weight:700; color:var(--muted);
}
.toolbar select {
  background:transparent; border:1px solid var(--line);
  padding:8px 12px; color:var(--paper);
  font-family:inherit; font-size:11px;
  letter-spacing:0.18em; text-transform:uppercase; font-weight:600;
  cursor:pointer; outline:none;
}
.toolbar select:hover { border-color:var(--accent); }
.density input[type=range] {
  -webkit-appearance:none; appearance:none;
  width:140px; height:1px; background:var(--line);
  outline:none;
}
.density input[type=range]::-webkit-slider-thumb {
  -webkit-appearance:none; appearance:none;
  width:14px; height:14px; background:var(--accent);
  cursor:pointer; border-radius:50%;
}
.density input[type=range]::-moz-range-thumb {
  width:14px; height:14px; background:var(--accent);
  cursor:pointer; border-radius:50%; border:0;
}
.stat-line {
  font-size:10px; letter-spacing:0.32em; text-transform:uppercase;
  font-weight:600; color:var(--muted); margin-bottom:24px;
}

/* Grid */
.grid {
  display:grid; grid-template-columns:repeat(var(--cols), 1fr);
  gap:2px;
}
.card {
  position:relative; aspect-ratio:1/1;
  cursor:pointer; overflow:hidden;
  background:var(--bg-soft);
  transition:transform 0.4s cubic-bezier(0.2,0.8,0.2,1);
}
.card:hover { transform:scale(1.04); z-index:5; }
.card img {
  width:100%; height:100%; object-fit:cover; display:block;
  transition:transform 0.4s ease;
}
.card .overlay {
  position:absolute; left:0; right:0; bottom:0;
  padding:18px 14px 12px;
  background:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.85) 100%);
  transform:translateY(50%); opacity:0.85;
  transition:transform 0.3s ease, opacity 0.3s ease;
}
.card:hover .overlay { transform:translateY(0); opacity:1; }
.card .pat {
  font-size:13px; font-weight:600; color:#fff;
  line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; overflow-wrap:anywhere }
.card .ven {
  font-size:9px; letter-spacing:0.18em; text-transform:uppercase;
  color:#fff; opacity:0.7; font-weight:500; margin-top:4px;
}
.card .actions { margin-top:10px; display:flex; gap:6px; }
.card .sample-btn {
  flex:1; padding:7px 10px;
  background:#fff; color:#000;
  font-family:inherit; font-size:10px; letter-spacing:0.22em;
  text-transform:uppercase; font-weight:700;
  border:0; cursor:pointer; text-align:center;
  text-decoration:none;
  transition:all 0.2s;
}
.card .sample-btn:hover { background:var(--accent); color:#fff; }

@media (max-width:1024px) { .grid { grid-template-columns:repeat(min(var(--cols), 4), 1fr); } }
@media (max-width:720px) {
  .grid { grid-template-columns:repeat(2, 1fr); }
  /* keep sort + search on mobile — only the column-density slider is meaningless at a fixed 2-col layout */
  .density { display:none; }
  header { padding:14px 20px; }
  .section { padding-left:20px; padding-right:20px; }
  .hero { padding:64px 20px 48px; }
  .card .sample-btn { padding:12px 10px; }   /* ≥44px touch target */
}
/* Touch devices have no hover — reveal the card overlay so the title +
   Request Sample button are reachable without a hover state. */
@media (hover: none) {
  .card .overlay { transform:translateY(0); opacity:1; }
}

.loading {
  text-align:center; color:var(--muted);
  padding:32px;
  font-size:10px; letter-spacing:0.32em; text-transform:uppercase; font-weight:700;
}

/* Footer */
footer { padding:64px 32px 32px; border-top:1px solid var(--line); margin-top:32px; }
.footer-grid {
  max-width:1400px; margin:0 auto;
  display:grid; grid-template-columns:2fr 1fr 1fr;
  gap:48px; margin-bottom:48px;
}
.footer-brand { font-family:'Cormorant Garamond',serif; font-size:24px; font-weight:500; margin-bottom:12px; }
.footer-text { font-size:13px; line-height:1.6; color:var(--muted); max-width:380px; }
.footer-col h4 {
  font-size:10px; letter-spacing:0.32em; text-transform:uppercase;
  font-weight:700; color:var(--paper); margin-bottom:18px;
  font-family:'Inter',sans-serif;
}
.footer-col a, .footer-col p {
  display:block; font-size:13px; color:var(--muted);
  text-decoration:none; margin-bottom:8px; transition:color 0.2s;
}
.footer-col a:hover { color:var(--accent); }
.footer-bottom {
  max-width:1400px; margin:0 auto;
  padding-top:24px; border-top:1px solid var(--line);
  display:flex; justify-content:space-between; flex-wrap:wrap; gap:16px;
  font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted);
}
@media (max-width:720px) { .footer-grid { grid-template-columns:1fr; gap:32px; } }

/* Focus */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
</style>
<!-- Meta Pixel — INERT until set-fb-pixel.sh is run (does NOT load fbevents.js or hit Meta until flipped) -->
<script>
(function(){
  var PIXEL_ID = '{{FACEBOOK_PIXEL_ID}}';
  // Inert until flipped — the brace check prevents BOTH the loader IIFE
  // AND the init call from running, so fbevents.js is never fetched.
  if (PIXEL_ID.indexOf('{{') !== -1) return;
  !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
  n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
  n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
  t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
  document,'script','https://connect.facebook.net/en_US/fbevents.js');
  fbq('init', PIXEL_ID);
  fbq('track', 'PageView');
})();
</script>
<!-- noscript fallback is added by set-fb-pixel.sh on flip (omitted here to avoid unflipped img phone-home) -->
<!-- End Meta Pixel -->
</head>
<body>
<!-- Theme toggle (theme1 / theme2) — query-param-gated preview surface
     (Steve 2026-05-29). ?theme=2 in URL flips to theme2 and persists in
     localStorage. Toggle UI hidden until theme2 mode active. Same key
     ('wallco-page-theme') as wallco.ai for cross-site continuity. -->
<script>
(function(){
  var qs = new URLSearchParams(location.search);
  var fromUrl = qs.get('theme');
  var saved = null; try { saved = localStorage.getItem('wallco-page-theme'); } catch(e){}
  var theme = null;
  if (fromUrl === '2' || fromUrl === 'theme2') theme = 'theme2';
  else if (fromUrl === '1' || fromUrl === 'theme1') theme = 'theme1';
  else if (saved === 'theme2' || saved === 'theme1') theme = saved;
  if (theme) {
    document.documentElement.setAttribute('data-page-theme', theme);
    if (fromUrl) { try { localStorage.setItem('wallco-page-theme', theme); } catch(e){} }
  }
})();
</script>
<style id="page-theme-css-fleet">
  [data-page-theme="theme2"] body{
    background:#fff !important; color:#0a0a0a !important;
  }
  [data-page-theme="theme2"] h1,
  [data-page-theme="theme2"] h2,
  [data-page-theme="theme2"] h3{ letter-spacing:-0.01em; font-weight:500; }
  [data-page-theme="theme2"] .card,
  [data-page-theme="theme2"] [class*="card"]{
    background:#fafafa !important; border:1px solid #e5e5e5 !important; box-shadow:none !important;
  }
  #page-theme-toggle{ display:none; }
  [data-page-theme] #page-theme-toggle{ display:inline-flex; }
  #page-theme-toggle{
    position:fixed; top:96px; right:14px; z-index:90;
    gap:0; align-items:center;
    background:rgba(255,255,255,.92); border:1px solid #d8d2c5;
    border-radius:999px; padding:3px; backdrop-filter:blur(6px);
    box-shadow:0 2px 10px rgba(0,0,0,.08);
    font:600 11px ui-sans-serif,system-ui; letter-spacing:.06em; text-transform:uppercase;
  }
  #page-theme-toggle button{
    border:0; background:transparent; color:#5a5048; cursor:pointer;
    padding:6px 14px; border-radius:999px; font:inherit; transition:all .15s;
  }
  #page-theme-toggle button[aria-pressed="true"]{ background:#1a1714; color:#faf7f2; }
  #page-theme-toggle button:hover:not([aria-pressed="true"]){ color:#1a1714; }
</style>
<div id="page-theme-toggle" role="group" aria-label="Page theme">
  <button type="button" data-theme="theme1" aria-pressed="false">Theme 1</button>
  <button type="button" data-theme="theme2" aria-pressed="false">Theme 2</button>
</div>
<script>
(function(){
  var cur = document.documentElement.getAttribute('data-page-theme') || 'theme1';
  var btns = document.querySelectorAll('#page-theme-toggle button');
  function sync(t){ btns.forEach(function(b){ b.setAttribute('aria-pressed', b.dataset.theme === t ? 'true' : 'false'); }); }
  sync(cur);
  btns.forEach(function(b){
    b.addEventListener('click', function(){
      var t = b.dataset.theme;
      document.documentElement.setAttribute('data-page-theme', t);
      try { localStorage.setItem('wallco-page-theme', t); } catch(e){}
      sync(t);
    });
  });
})();
</script>

<header>
  <a href="/" class="brand">
    <span class="star">★</span>
    <span class="name">Best Wallpaper Place<sub>Hand-picked favorites</sub></span>
  </a>
  <div class="h-actions">
    <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark mode" title="Toggle theme">☾</button>
  </div>
</header>

<section class="hero">
  <div class="eyebrow">Curated · Independent · No sponsored placements</div>
  <h1>Six hundred wallpapers.<br><span class="accent">Every one earns its wall.</span></h1>
  <p class="sub">An editor's cut from 32 design houses — Maya Romanoff, Cole &amp; Son, Coordonné, Phillipe Romano, Kravet and the rest. We keep patterns on this list only if we'd hang them ourselves. Sample any of them; memo's on us.</p>
  <div class="stats">
    <span><b id="statCount">—</b>Patterns</span>
    <span><b id="statVendors">—</b>Houses</span>
    <span><b>2026</b>Updated</span>
  </div>
</section>

<section class="editorial" id="about" aria-labelledby="ed-h">
  <div class="editorial-inner">
    <div class="ed-eyebrow">About this cut</div>
    <h2 id="ed-h">Curated, not aggregated.</h2>
    <p>Most wallpaper directories are vendor-fed catalogs — every pattern from every house, ranked by whoever pays for placement. This one is the opposite. There are no sponsored slots here, no algorithmic boosting, no padding to hit a number. The list is short on purpose.</p>
    <p>Patterns earn their spot one at a time. We hang each candidate against three other contenders, judge it on motif, scale, ground color and how the print behaves under afternoon light, and keep only the ones still interesting after the second look. If you find something here, it's because somebody on the curation side would put it on their own wall — and a memo sample is one click away, free, no account needed.</p>
  </div>
</section>

<!-- SWEEP-A1: Browse-by-style rails — populated client-side from /api/products?tag=<name> -->
<section class="section" id="browse-rails" style="padding-bottom:0">
  <div class="section-header" style="margin-bottom:24px">
    <div>
      <div class="section-eyebrow" style="font-size:10px;letter-spacing:.4em;text-transform:uppercase;font-weight:700;color:var(--muted);margin-bottom:8px">Browse</div>
      <h2 style="font-size:clamp(28px,4vw,42px);font-weight:300;letter-spacing:-0.01em;margin:0">By style.</h2>
    </div>
  </div>
  <div id="brwRails"></div>
</section>
<style>
#browse-rails .brw-block { margin-bottom:48px }
#browse-rails .brw-head { display:flex;justify-content:space-between;align-items:baseline;margin-bottom:18px }
#browse-rails .brw-title { font-size:clamp(20px,2.2vw,28px);font-weight:500;letter-spacing:-0.01em;margin:0;color:var(--ink,#111) }
#browse-rails .brw-allink { font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted,#888);text-decoration:none;border-bottom:1px solid var(--line,rgba(0,0,0,.2));padding-bottom:2px }
#browse-rails .brw-rail { display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:8px;scrollbar-width:none }
#browse-rails .brw-rail::-webkit-scrollbar { display:none }
#browse-rails .brw-card { flex:0 0 240px;scroll-snap-align:start;cursor:pointer;transition:transform 0.2s;text-decoration:none;color:inherit }
#browse-rails .brw-card:hover { transform:translateY(-3px) }
#browse-rails .brw-card img { width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#eee }
#browse-rails .brw-card .brw-cap { font-size:13px;font-weight:500;padding:8px 0 2px;line-height:1.3 }
@media (max-width:720px) { #browse-rails .brw-card { flex:0 0 160px } }
</style>
<script>
// SWEEP-A1: tag-bucket rails — Browse by style
(async function hydrateBrowseRails() {
  try {
    const tags = ["textured","contemporary","traditional","beige","brown"];
    const host = document.getElementById('brwRails');
    if (!host) return;
    function cardOne(p) {
      const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
      const title = ((p.title || '').split('|')[0] || '').trim().replace(/"/g, '&quot;');
      return '<a class="brw-card" href="/sample/' + encodeURIComponent(p.handle_display || p.handle || p.sku) + '">' +
        '<img src="' + url + '" alt="' + title + '" loading="lazy">' +
        '<div class="brw-cap">' + title + '</div>' +
      '</a>';
    }
    for (const tag of tags) {
      const r = await fetch('/api/products?limit=12&tag=' + encodeURIComponent(tag));
      if (!r.ok) continue;
      const d = await r.json();
      const items = (d.items || []).filter(p => p && p.image_url);
      if (items.length < 4) continue;
      const block = document.createElement('div');
      block.className = 'brw-block';
      const nice = tag.replace(/\b\w/g, c => c.toUpperCase());
      block.innerHTML =
        '<div class="brw-head">' +
          '<h3 class="brw-title">' + nice + '</h3>' +
          '<a class="brw-allink" href="/?tag=' + encodeURIComponent(tag) + '">View all ' + (d.total || items.length) + ' &rarr;</a>' +
        '</div>' +
        '<div class="brw-rail">' + items.map(cardOne).join('') + '</div>';
      host.appendChild(block);
    }
  } catch (e) {}
})();
</script>

<section class="section">
  <div class="toolbar">
    <div class="search">
      <svg width="16" height="16" stroke="currentColor" stroke-width="1.5" fill="none" viewBox="0 0 24 24" style="color:var(--muted);"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
      <input id="searchBox" type="text" placeholder="Search patterns, colors, vendors…">
    </div>
    <div class="ctl">
      <span>Sort</span>
      <select id="sortSelect">
        <option value="newest">Newest</option>
        <option value="light-dark">Light → Dark</option>
        <option value="dark-light">Dark → Light</option>
        <option value="wheel">Color Wheel</option>
        <option value="color">Color</option>
        <option value="style">Style</option>
        <option value="sku">SKU A→Z</option>
        <option value="title">Title A→Z</option>
        <option value="price-asc">Price ↑</option>
        <option value="price-desc">Price ↓</option>
      </select>
    </div>
    <div class="ctl density">
      <span>Density</span>
      <input id="densitySlider" type="range" min="2" max="8" value="5">
    </div>
  </div>
  <div class="stat-line" id="statLine">Loading…</div>
  <div class="grid" id="grid"></div>
  <div class="loading" id="loading" style="display:none;">Loading more…</div>
</section>

<footer>
  <div class="footer-grid">
    <div>
      <div class="footer-brand">Best Wallpaper Place</div>
      <p class="footer-text">A curated showcase of premium wallpaper from independent and design-house vendors. Memo samples available on every pattern. Sourced and curated by Designer Wallcoverings.</p>
    </div>
    <div class="footer-col">
      <h4>Contact</h4>
      <a href="mailto:info@bestwallpaperplace.com">info@bestwallpaperplace.com</a>
      <a href="tel:+18883734564">888-373-4564</a>
    </div>
    <div class="footer-col">
      <h4>Visit</h4>
      <p>Designer Wallcoverings</p>
      <p>15442 Ventura Bl #102</p>
      <p>Sherman Oaks, CA 91403</p>
    </div>
  </div>
  <div class="footer-bottom">
    <span>© 2026 Best Wallpaper Place</span>
    <span>A Designer Wallcoverings property</span>
  </div>
</footer>

<script>
(function(){
  const grid = document.getElementById('grid');
  const statLine = document.getElementById('statLine');
  const loading = document.getElementById('loading');
  const sortSelect = document.getElementById('sortSelect');
  const searchBox = document.getElementById('searchBox');
  const density = document.getElementById('densitySlider');
  const themeBtn = document.getElementById('themeToggle');

  // theme
  function applyTheme(t) {
    document.documentElement.dataset.theme = t;
    themeBtn.textContent = t === 'dark' ? '☀' : '☾';
    try { localStorage.setItem('bwp_theme', t); } catch(e){}
  }
  applyTheme(localStorage.getItem('bwp_theme') || 'light');
  themeBtn.addEventListener('click', () => {
    applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
  });

  // density
  function applyCols(n) {
    document.documentElement.style.setProperty('--cols', n);
    try { localStorage.setItem('bwp_cols', n); } catch(e){}
  }
  const savedCols = parseInt(localStorage.getItem('bwp_cols') || '5', 10);
  density.value = savedCols;
  applyCols(savedCols);
  density.addEventListener('input', e => applyCols(e.target.value));

  // sort + search persistence
  const savedSort = localStorage.getItem('bwp_sort') || 'newest';
  sortSelect.value = savedSort;
  sortSelect.addEventListener('change', () => {
    try { localStorage.setItem('bwp_sort', sortSelect.value); } catch(e){}
    reset();
  });
  let searchTimer;
  searchBox.addEventListener('input', () => {
    clearTimeout(searchTimer);
    searchTimer = setTimeout(reset, 250);
  });

  // facets
  fetch('/api/facets').then(r => r.json()).then(d => {
    document.getElementById('statCount').textContent = d.total.toLocaleString();
    document.getElementById('statVendors').textContent = d.vendor_count ?? Object.keys(d.vendors || {}).length;
  }).catch(()=>{});

  // grid render + infinite scroll
  let page = 1, pages = 1, total = 0, busy = false;
  function reset() {
    page = 1; pages = 1; total = 0;
    grid.innerHTML = '';
    statLine.textContent = 'Loading…';
    fetchPage();
  }
  function card(p) {
    const handle = p.handle || p.sku || '';
    const title = (p.title || '').replace(/ \| .*$/, '');
    const img = p.image_url || '';
    const sampleUrl = '/sample/' + encodeURIComponent(handle);
    return `<article class="card">
      <a href="${sampleUrl}" target="_blank" rel="noopener noreferrer" aria-label="View ${title}">
        <img loading="lazy" src="${img}" alt="${title.replace(/"/g,'&quot;')}">
      </a>
      <div class="overlay">
        <div class="pat">${title.replace(/</g,'&lt;')}</div>
        <div class="actions">
          <a class="sample-btn" href="${sampleUrl}" target="_blank" rel="noopener noreferrer">Request Sample</a>
        </div>
      </div>
    </article>`;
  }
  function fetchPage() {
    if (busy || page > pages) return;
    busy = true; loading.style.display = 'block';
    const q = encodeURIComponent(searchBox.value.trim());
    const s = encodeURIComponent(sortSelect.value);
    fetch(`/api/products?page=${page}&limit=30&q=${q}&sort=${s}`)
      .then(r => r.json())
      .then(d => {
        pages = d.pages; total = d.total;
        statLine.textContent = `${total.toLocaleString()} patterns · sort: ${d.sort}`;
        grid.insertAdjacentHTML('beforeend', d.items.map(card).join(''));
        page++;
      })
      .finally(() => { busy = false; loading.style.display = 'none'; });
  }
  // infinite scroll
  window.addEventListener('scroll', () => {
    if (busy) return;
    if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 600) fetchPage();
  });
  fetchPage();
})();
</script>

<!-- corner-nav disabled (UL-hamburger rebuild) -->

<!-- Big Red customer chat — lower-left, woman-avatar, vendor-redacted retail persona. -->
<script data-bigred-widget src="https://chat.designerwallcoverings.com/widget.js"
        data-host="https://chat.designerwallcoverings.com"
        data-mode="retail"
        defer onerror="this.remove()"></script>

<script>window.DwHeaderConfig={siteName:"Best Wallpaper Place",accent:"#9B7B4F",navLinks:[{name:'Home',url:'/'},{name:'Collections',url:'#shop'},{name:'About',url:'/about'},{name:'Care',url:'/care.html'},{name:'History',url:'/history.html'}],adminLinks:[{name:'Shopify admin',url:'https://admin.shopify.com',external:true},{name:'Edit hero',url:'#'},{name:'Analytics',url:'#'}]};</script>
<script src="/dw-header.js" defer></script>
<script src="/dw-theme-nav.js" defer></script>
</body>
</html>