[object Object]

← back to Carmelwallpapers

snapshot: 2 file(s) changed, +2 new

1797e17ac243ce5e851ab26d8224c79c71addb14 · 2026-05-13 08:57:46 -0700 · Steve

Files touched

Diff

commit 1797e17ac243ce5e851ab26d8224c79c71addb14
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 13 08:57:46 2026 -0700

    snapshot: 2 file(s) changed, +2 new
---
 ecosystem.config.cjs     |  19 ++
 server.js.pre-sort-skill | 477 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 496 insertions(+)

diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
new file mode 100644
index 0000000..9aaab5c
--- /dev/null
+++ b/ecosystem.config.cjs
@@ -0,0 +1,19 @@
+module.exports = {
+  apps: [{
+    name: "carmelwallpapers",
+    script: "server.js",
+    cwd: __dirname,
+    env: {
+      NEXT_TELEMETRY_DISABLED: "1",
+      VITE_HOST: "0.0.0.0",
+      HOSTNAME: "0.0.0.0",
+      GUNICORN_BIND: "0.0.0.0:8000",
+      UVICORN_HOST: "0.0.0.0",
+      FLASK_RUN_HOST: "0.0.0.0",
+      BIND: "0.0.0.0",
+      HOST: "0.0.0.0",
+    },
+    autorestart: true,
+    max_memory_restart: "300M",
+  }],
+};
diff --git a/server.js.pre-sort-skill b/server.js.pre-sort-skill
new file mode 100644
index 0000000..05e067f
--- /dev/null
+++ b/server.js.pre-sort-skill
@@ -0,0 +1,477 @@
+'use strict';
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = process.env.PORT || 9832;
+const DATA_PATH = path.join(__dirname, 'data', 'products.json');
+
+// ── Junk filter ──────────────────────────────────────────────────────────────
+const JUNK_TITLE = /(lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine|stool|table|chair|sofa|curtain|bedding|towel|plate|bowl|mug)/i;
+function isJunk(p) {
+  if (!p.image_url) return true;
+  if (!p.product_type || !/wallcovering/i.test(p.product_type)) return true;
+  if (JUNK_TITLE.test(p.title || '')) return true;
+  return false;
+}
+
+// ── Sort helpers ─────────────────────────────────────────────────────────────
+const COLOR_RANK = {
+  black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,sand:9,stone:10,
+  copper:11,driftwood:12,taupe:13,oatmeal:14,linen:15,parchment:16,wheat:17,
+  red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+  yellow:30,gold:31,mustard:32,olive:33,sage:34,
+  green:40,lime:41,mint:42,seafoam:43,teal:50,aqua:51,turquoise:52,cyan:53,
+  blue:60,navy:61,indigo:62,periwinkle:63,
+  purple:70,violet:71,lavender:72,plum:73,
+};
+function colorRank(p) {
+  const tags = (p.tags || []).map(t => String(t).toLowerCase());
+  for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
+  return 999;
+}
+const STYLE_TAGS = ['grasscloth','linen','raffia','sisal','seagrass','jute','coastal','botanical','woven','natural','organic','bamboo','rattan','texture'];
+function styleKey(p) {
+  const tags = (p.tags || []).map(t => String(t).toLowerCase());
+  for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
+  return 'zzz';
+}
+function sortProducts(list, mode) {
+  if (mode === 'color') return [...list].sort((a,b) => colorRank(a)-colorRank(b) || (a.title||'').localeCompare(b.title||''));
+  if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || (a.title||'').localeCompare(b.title||''));
+  if (mode === 'title') return [...list].sort((a,b) => (a.title||'').localeCompare(b.title||''));
+  if (mode === 'vendor') return [...list].sort((a,b) => (a.vendor||'').localeCompare(b.vendor||'') || (a.title||'').localeCompare(b.title||''));
+  if (mode === 'price-asc')  return [...list].sort((a,b) => (Number(a.price)||0)-(Number(b.price)||0));
+  if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.price)||0)-(Number(a.price)||0));
+  return list; // newest = file order
+}
+
+// ── Load + filter data ───────────────────────────────────────────────────────
+let PRODUCTS = [];
+let DROPPED  = 0;
+let AESTHETICS = {};
+
+function loadData() {
+  try {
+    const raw = JSON.parse(fs.readFileSync(DATA_PATH, 'utf8'));
+    const clean = raw.filter(p => !isJunk(p));
+    DROPPED = raw.length - clean.length;
+    PRODUCTS = clean;
+    AESTHETICS = {};
+    for (const p of PRODUCTS) {
+      AESTHETICS[p.aesthetic] = (AESTHETICS[p.aesthetic] || 0) + 1;
+    }
+    console.log(`Loaded ${PRODUCTS.length} products (dropped ${DROPPED} junk)`);
+  } catch(e) {
+    console.error('Failed to load products.json:', e.message);
+    PRODUCTS = [];
+  }
+}
+loadData();
+
+const app = express();
+app.use(express.static(path.join(__dirname, 'public')));
+app.set('trust proxy', 1);
+
+// ── HTML helpers ─────────────────────────────────────────────────────────────
+const HTML_HEAD = (title, desc) => `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script>(function(){try{var t=localStorage.getItem('cw_theme')||((window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light');document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
+<!-- GA4 -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-ENN0QV40F8"></script>
+<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-ENN0QV40F8');</script>
+<title>${title}</title>
+<meta name="description" content="${desc}">
+<meta name="theme-color" content="#f7f3ec" media="(prefers-color-scheme: light)">
+<meta name="theme-color" content="#12100d" media="(prefers-color-scheme: dark)">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
+<link rel="stylesheet" href="/css/site.css">
+</head>`;
+
+const THEME_TOGGLE_SCRIPT = `<script>
+(function(){
+  var btn=document.getElementById('theme-toggle');if(!btn)return;
+  function sync(){var t=document.documentElement.getAttribute('data-theme')||'light';btn.setAttribute('aria-pressed',t==='dark'?'true':'false');}
+  sync();
+  btn.addEventListener('click',function(){
+    var cur=document.documentElement.getAttribute('data-theme')||'light';
+    var nxt=cur==='dark'?'light':'dark';
+    document.documentElement.setAttribute('data-theme',nxt);
+    try{localStorage.setItem('cw_theme',nxt);}catch(e){}
+    sync();
+  });
+})();
+</script>`;
+
+const SUN_MOON_SVG = `<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode" aria-pressed="false" title="Toggle dark / light">
+  <svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
+  <svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"/></svg>
+</button>`;
+
+const HEADER_HTML = (q, sort, density) => `
+<header class="site-header">
+  <a href="/" class="brand">CARMEL<small>Wallpapers for the California Coast</small></a>
+  <nav class="site-nav">
+    <a href="/?type=grasscloth">Grasscloth</a>
+    <a href="/?type=linen">Linen</a>
+    <a href="/?type=botanical">Botanical</a>
+    <a href="/?type=coastal">Coastal</a>
+    <a href="/?type=raffia">Raffia</a>
+    <a href="mailto:info@carmelwallpapers.com" class="nav-contact">Contact</a>
+    ${SUN_MOON_SVG}
+  </nav>
+</header>`;
+
+// ── API: products (infinite scroll) ──────────────────────────────────────────
+app.get('/api/products', (req, res) => {
+  const { q, type, sort = 'newest', page = 1, limit = 24 } = req.query;
+  let list = PRODUCTS;
+  if (q) {
+    const tokens = q.toLowerCase().split(/\s+/).filter(Boolean);
+    list = list.filter(p => {
+      const hay = ((p.title||'') + ' ' + (p.vendor||'') + ' ' + (p.tags||[]).join(' ')).toLowerCase();
+      return tokens.every(tok => hay.includes(tok));
+    });
+  }
+  if (type && type !== 'all') {
+    list = list.filter(p => p.aesthetic === type);
+  }
+  list = sortProducts(list, sort);
+  const total = list.length;
+  const pageNum = Math.max(1, parseInt(page)||1);
+  const lim = Math.min(60, parseInt(limit)||24);
+  const start = (pageNum-1)*lim;
+  res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total/lim), sort, items: list.slice(start, start+lim) });
+});
+
+// ── API: sliders ─────────────────────────────────────────────────────────────
+app.get('/api/sliders', (req, res) => {
+  const RAIL_ORDER = ['grasscloth','linen','botanical','coastal','raffia','natural'];
+  const out = [];
+  for (const a of RAIL_ORDER) {
+    const items = PRODUCTS.filter(p => p.aesthetic === a).slice(0, 12);
+    if (items.length >= 4) out.push({ aesthetic: a, items });
+  }
+  res.json({ rails: out });
+});
+
+// ── Health ────────────────────────────────────────────────────────────────────
+app.get('/health', (req, res) => {
+  res.json({ ok: true, products: PRODUCTS.length, dropped: DROPPED, aesthetics: AESTHETICS });
+});
+
+// ── Product detail ────────────────────────────────────────────────────────────
+app.get('/p/:handle', (req, res) => {
+  const p = PRODUCTS.find(x => x.handle === req.params.handle);
+  if (!p) return res.status(404).send('<h1>Not found</h1>');
+
+  const title = `${p.title} — Carmel Wallpapers | Free Memo Samples, Trade Service`;
+  const desc = `${p.title} by ${p.vendor}. Order a free memo sample. Coastal California's curated wallpaper source — expert trade service, hand-holding from Carmel to Pebble Beach.`;
+
+  const imgs = (p.images||[p.image_url]).filter(Boolean);
+  const imgHtml = imgs.map((src,i) =>
+    `<img src="${src}" alt="${p.title}${i?` view ${i+1}`:''}${i===0?' (main)':''}" loading="${i===0?'eager':'lazy'}" class="detail-img${i===0?' detail-img--main':''}">`
+  ).join('\n');
+
+  const tags = (p.tags||[]).filter(t => !/^\d+$/.test(t));
+
+  res.send(`${HTML_HEAD(title, desc)}
+<body>
+${HEADER_HTML()}
+<main class="detail-wrap">
+  <a href="/" class="back-link">&#8592; Back to collection</a>
+  <div class="detail-grid">
+    <div class="detail-images">${imgHtml}</div>
+    <div class="detail-info">
+      <p class="detail-vendor">${p.vendor}</p>
+      <h1 class="detail-title">${p.title}</h1>
+      <div class="detail-tags">${tags.slice(0,10).map(t=>`<span class="tag">${t}</span>`).join('')}</div>
+      <div class="detail-ctas">
+        <a href="${p.sample_url}" class="btn btn-sample" target="_blank" rel="noopener">Order Memo Sample (free)</a>
+        <a href="${p.product_url}" class="btn btn-full" target="_blank" rel="noopener">View Full Product</a>
+      </div>
+      <div class="detail-service">
+        <h3>Carmel Trade Service</h3>
+        <p>Designer Wallcoverings provides complimentary trade hand-holding for every Carmel and Pebble Beach project — from sampling through installation. <a href="mailto:info@carmelwallpapers.com">Reach us at info@carmelwallpapers.com</a>.</p>
+      </div>
+    </div>
+  </div>
+</main>
+<footer class="site-footer">
+  <div class="footer-inner">
+    <p>Carmel Wallpapers &mdash; a Designer Wallcoverings service</p>
+    <p><a href="mailto:info@carmelwallpapers.com">info@carmelwallpapers.com</a></p>
+    <p class="footer-fine">Serving Carmel-by-the-Sea, Pebble Beach &amp; Big Sur</p>
+  </div>
+</footer>
+<script type="application/ld+json">${JSON.stringify({
+  "@context":"https://schema.org","@type":"Product","name":p.title,"brand":{"@type":"Brand","name":p.vendor},
+  "image":imgs[0]||'',
+  "offers":{"@type":"Offer","priceCurrency":"USD","price":"0","description":"Free memo sample"},
+  "description":desc
+})}</script>
+${THEME_TOGGLE_SCRIPT}
+</body></html>`);
+});
+
+// ── Index (catalog) ──────────────────────────────────────────────────────────
+app.get('/', (req, res) => {
+  const { q='', type='', sort='newest', density='220' } = req.query;
+
+  const pageTitle = q
+    ? `"${q}" — Carmel Wallpapers`
+    : type
+      ? `${type.charAt(0).toUpperCase()+type.slice(1)} Wallpapers — Carmel, Pebble Beach & Big Sur`
+      : 'Carmel Wallpapers — Coastal California Luxury Wallcoverings';
+  const pageDesc = 'Curated grasscloth, linen, raffia and coastal-botanical wallcoverings for Carmel-by-the-Sea, Pebble Beach and Big Sur residences. Free memo samples. Expert trade service from Designer Wallcoverings.';
+
+  // Facet counts for filter chips
+  const aestheticCounts = {};
+  for (const p of PRODUCTS) {
+    aestheticCounts[p.aesthetic] = (aestheticCounts[p.aesthetic]||0)+1;
+  }
+
+  const aestheticChips = ['grasscloth','linen','botanical','coastal','raffia','natural']
+    .filter(a => aestheticCounts[a])
+    .map(a => {
+      const active = type === a ? ' chip--active' : '';
+      const href = type === a ? '/' : `/?type=${a}${q?'&q='+encodeURIComponent(q):''}`;
+      return `<a href="${href}" class="chip${active}">${a.charAt(0).toUpperCase()+a.slice(1)} <span class="chip-count">${aestheticCounts[a]}</span></a>`;
+    }).join('');
+
+  res.send(`${HTML_HEAD(pageTitle, pageDesc)}
+<body>
+${HEADER_HTML(q, sort, density)}
+
+<section class="hero" aria-label="Collection introduction">
+  <div class="hero-text">
+    <h1 class="hero-headline">CARMEL</h1>
+    <p class="hero-sub">Quiet luxury for the California coast.</p>
+    <p class="hero-body">Natural fibers. Coastal botanicals. The restraint of Carmel-by-the-Sea captured in texture and tone — curated from the world's finest wallcovering houses.</p>
+    <a href="#catalog" class="hero-cta">Explore the Collection</a>
+  </div>
+</section>
+
+<section class="sliders-section" id="sliders" aria-label="Browse by aesthetic">
+  <div class="sliders-wrap">
+    <div id="sliders-container"></div>
+  </div>
+</section>
+
+<section class="catalog-section" id="catalog">
+  <div class="catalog-controls">
+    <form class="search-form" method="GET" action="/" role="search">
+      ${type?`<input type="hidden" name="type" value="${type}">`:''}
+      <input type="search" name="q" value="${q.replace(/"/g,'&quot;')}" placeholder="Search ${PRODUCTS.length.toLocaleString()} wallcoverings…" class="search-input" aria-label="Search wallcoverings">
+      <button type="submit" class="search-btn" aria-label="Search">
+        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
+      </button>
+    </form>
+    <div class="sort-density">
+      <select id="sortSelect" class="sort-select" aria-label="Sort products">
+        <option value="newest" ${sort==='newest'?'selected':''}>Newest</option>
+        <option value="color" ${sort==='color'?'selected':''}>Color</option>
+        <option value="style" ${sort==='style'?'selected':''}>Style</option>
+        <option value="title" ${sort==='title'?'selected':''}>Title A→Z</option>
+        <option value="vendor" ${sort==='vendor'?'selected':''}>Vendor A→Z</option>
+        <option value="price-asc" ${sort==='price-asc'?'selected':''}>Price ↑</option>
+        <option value="price-desc" ${sort==='price-desc'?'selected':''}>Price ↓</option>
+      </select>
+      <label class="density-label" aria-label="Grid density">
+        <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="0" y="0" width="4" height="4"/><rect x="6" y="0" width="4" height="4"/><rect x="12" y="0" width="4" height="4"/><rect x="0" y="6" width="4" height="4"/><rect x="6" y="6" width="4" height="4"/><rect x="12" y="6" width="4" height="4"/></svg>
+        <input type="range" id="densitySlider" min="140" max="320" step="20" value="${density}" aria-label="Grid column width">
+      </label>
+    </div>
+  </div>
+  <div class="facet-chips" aria-label="Filter by aesthetic">
+    <a href="/${q?'?q='+encodeURIComponent(q):''}" class="chip${!type?' chip--active':''}">All <span class="chip-count">${PRODUCTS.length}</span></a>
+    ${aestheticChips}
+  </div>
+  <div id="grid" class="product-grid" role="list" style="--cols:${density}px"></div>
+  <div id="sentinel" class="sentinel" aria-hidden="true"></div>
+  <div id="grid-status" class="grid-status"></div>
+</section>
+
+<footer class="site-footer">
+  <div class="footer-inner">
+    <div class="footer-col">
+      <strong>Carmel Wallpapers</strong>
+      <p>A Designer Wallcoverings service for the Carmel Coast.</p>
+      <p><a href="mailto:info@carmelwallpapers.com">info@carmelwallpapers.com</a></p>
+    </div>
+    <div class="footer-col">
+      <strong>Our Service</strong>
+      <ul>
+        <li>Free memo samples on every pattern</li>
+        <li>Trade pricing available</li>
+        <li>Installation guidance</li>
+        <li>Serving Carmel, Pebble Beach &amp; Big Sur</li>
+      </ul>
+    </div>
+    <div class="footer-col">
+      <strong>Aesthetics</strong>
+      <ul>
+        ${['grasscloth','linen','botanical','coastal','raffia','natural'].map(a=>`<li><a href="/?type=${a}">${a.charAt(0).toUpperCase()+a.slice(1)}</a></li>`).join('')}
+      </ul>
+    </div>
+  </div>
+  <p class="footer-legal">© 2026 Carmel Wallpapers · Curated by Designer Wallcoverings · <a href="mailto:info@carmelwallpapers.com">info@carmelwallpapers.com</a></p>
+</footer>
+
+<script type="application/ld+json">${JSON.stringify({
+  "@context":"https://schema.org",
+  "@type":"LocalBusiness",
+  "name":"Carmel Wallpapers",
+  "description":pageDesc,
+  "url":"https://carmelwallpapers.com",
+  "email":"info@carmelwallpapers.com",
+  "areaServed":["Carmel-by-the-Sea","Pebble Beach","Big Sur","Monterey County"],
+  "sameAs":["https://designerwallcoverings.com"]
+})}</script>
+
+<script>
+// ── State ────────────────────────────────────────────────────────────────────
+const STATE = {
+  q: ${JSON.stringify(q)},
+  type: ${JSON.stringify(type)},
+  sort: ${JSON.stringify(sort)},
+  page: 1,
+  pages: 1,
+  total: 0,
+  loading: false,
+  exhausted: false
+};
+
+// ── Persist sort + density ───────────────────────────────────────────────────
+const sortSel = document.getElementById('sortSelect');
+const densSlider = document.getElementById('densitySlider');
+const grid = document.getElementById('grid');
+
+const savedSort = localStorage.getItem('cw_sort');
+const savedDensity = localStorage.getItem('cw_density');
+if(savedSort && !${JSON.stringify(!!req.query.sort)}) { sortSel.value = savedSort; STATE.sort = savedSort; }
+if(savedDensity) { densSlider.value = savedDensity; grid.style.setProperty('--cols', savedDensity+'px'); }
+
+sortSel.addEventListener('change', () => {
+  STATE.sort = sortSel.value;
+  localStorage.setItem('cw_sort', STATE.sort);
+  const url = new URL(location.href);
+  url.searchParams.set('sort', STATE.sort);
+  location.href = url.toString();
+});
+densSlider.addEventListener('input', () => {
+  const v = densSlider.value;
+  grid.style.setProperty('--cols', v+'px');
+  localStorage.setItem('cw_density', v);
+});
+
+// ── Card renderer ────────────────────────────────────────────────────────────
+function renderCard(p) {
+  const div = document.createElement('div');
+  div.className = 'product-card';
+  div.setAttribute('role','listitem');
+  div.innerHTML = \`
+    <a href="/p/\${p.handle}" class="card-link" aria-label="\${p.title.replace(/"/g,'&quot;')}">
+      <div class="card-img-wrap">
+        <img src="\${p.image_url}" alt="\${p.title.replace(/"/g,'&quot;')}" loading="lazy" class="card-img">
+      </div>
+      <div class="card-body">
+        <p class="card-vendor">\${p.vendor}</p>
+        <p class="card-title">\${p.title}</p>
+        <p class="card-aesthetic">\${p.aesthetic}</p>
+      </div>
+    </a>
+    <a href="\${p.sample_url}" class="card-sample" target="_blank" rel="noopener" aria-label="Order free memo sample of \${p.title.replace(/"/g,'&quot;')}">Order Memo Sample (free)</a>
+  \`;
+  return div;
+}
+
+// ── Load page ────────────────────────────────────────────────────────────────
+async function loadPage() {
+  if (STATE.loading || STATE.exhausted) return;
+  STATE.loading = true;
+  const status = document.getElementById('grid-status');
+  status.textContent = 'Loading…';
+  const params = new URLSearchParams({ sort: STATE.sort, page: STATE.page, limit: 24 });
+  if (STATE.q) params.set('q', STATE.q);
+  if (STATE.type) params.set('type', STATE.type);
+  try {
+    const res = await fetch('/api/products?' + params);
+    const data = await res.json();
+    STATE.pages = data.pages;
+    STATE.total = data.total;
+    if (STATE.page === 1) {
+      status.textContent = \`\${data.total.toLocaleString()} wallcoverings\`;
+    }
+    for (const p of data.items) grid.appendChild(renderCard(p));
+    if (STATE.page >= data.pages) {
+      STATE.exhausted = true;
+      status.textContent = \`— end of archive · \${data.total.toLocaleString()} patterns —\`;
+    } else {
+      STATE.page++;
+      status.textContent = \`\${data.total.toLocaleString()} wallcoverings · \${STATE.page-1} of \${data.pages} pages loaded\`;
+    }
+  } catch(e) {
+    status.textContent = 'Error loading products.';
+    console.error(e);
+  }
+  STATE.loading = false;
+}
+
+// ── Infinite scroll ──────────────────────────────────────────────────────────
+const sentinel = document.getElementById('sentinel');
+const observer = new IntersectionObserver((entries) => {
+  if (entries[0].isIntersecting) loadPage();
+}, { rootMargin: '600px 0px' });
+observer.observe(sentinel);
+
+// ── Grid sliders ─────────────────────────────────────────────────────────────
+async function loadSliders() {
+  try {
+    const res = await fetch('/api/sliders');
+    const data = await res.json();
+    const container = document.getElementById('sliders-container');
+    if (!data.rails || !data.rails.length) { container.closest('.sliders-section').style.display='none'; return; }
+    for (const rail of data.rails) {
+      const label = rail.aesthetic.charAt(0).toUpperCase() + rail.aesthetic.slice(1);
+      const trackId = 'track-' + rail.aesthetic;
+      const section = document.createElement('div');
+      section.className = 'rail';
+      section.innerHTML = \`
+        <div class="rail-header">
+          <h2 class="rail-title"><a href="/?type=\${rail.aesthetic}" class="rail-link">\${label}</a></h2>
+          <div class="rail-controls">
+            <button class="rail-btn rail-prev" aria-label="Scroll \${label} left">&#8592;</button>
+            <button class="rail-btn rail-next" aria-label="Scroll \${label} right">&#8594;</button>
+          </div>
+        </div>
+        <div class="rail-track" id="\${trackId}">
+          \${rail.items.map(p=>\`
+            <a href="/p/\${p.handle}" class="rail-card" aria-label="\${(p.title||'').replace(/"/g,'&quot;')}">
+              <div class="rail-img-wrap"><img src="\${p.image_url}" alt="\${(p.title||'').replace(/"/g,'&quot;')}" loading="lazy" class="rail-img"></div>
+              <p class="rail-card-title">\${p.title}</p>
+              <p class="rail-card-vendor">\${p.vendor}</p>
+            </a>
+          \`).join('')}
+        </div>
+      \`;
+      const track = section.querySelector('.rail-track');
+      section.querySelector('.rail-prev').addEventListener('click', () => track.scrollBy({ left: -track.clientWidth*.85, behavior:'smooth' }));
+      section.querySelector('.rail-next').addEventListener('click', () => track.scrollBy({ left: track.clientWidth*.85, behavior:'smooth' }));
+      container.appendChild(section);
+    }
+  } catch(e) { console.error('Sliders error:', e); }
+}
+
+loadSliders();
+</script>
+${THEME_TOGGLE_SCRIPT}
+</body></html>`);
+});
+
+app.listen(PORT, () => console.log(`carmelwallpapers listening on :${PORT}`));

← ade7d84 fleet-grid refine: all-pass (chip-ratio canonical reference  ·  back to Carmelwallpapers  ·  chore: untrack server.js.pre-sort-skill snapshot, broaden .g d43943c →