← back to Stringwallpaper

public/index.html

1185 lines

<!doctype html>
<html lang="en">
<head>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">

<meta charset="utf-8">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-FJVH10XR5E"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-FJVH10XR5E');
</script>

<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>stringwallpaper — Soft as string. Made slowly.</title>
<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to string. We do one thing — and we do it carefully.">
<meta name="theme-color" content="#F2EDE2">
<link rel="canonical" href="https://stringwallpaper.com/">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "stringwallpaper",
  "url": "https://stringwallpaper.com/",
  "email": "info@stringwallpaper.com"
}
</script>
<script src="/zd-loader.js" defer></script>
<style>
:root {
  --bg: #F2EDE2;
  --paper: #2C1F0F;
  --muted: #8B7866;
  --line: #C9BFA7;
  --accent: #9B7B4F;
  --bg-soft: #C9BFA7;
  --cols: 6;
}
html[data-theme="dark"] {
  --bg: #2C1F0F;
  --paper: #F2EDE2;
  --muted: #C9B89E;
  --line: rgba(255,254,250,0.12);
  --accent: #9B7B4F;
  --bg-soft: #2a1f15;
}
* { margin:0; padding:0; box-sizing:border-box }
html { scroll-behavior:smooth }
body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
/* DW-FASHION-TEMPLATE-OVERRIDE */
.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important;  }

/* ===== Auto-hide header ===== */
header { position:fixed; top:0; left:0; right:0; display:flex; justify-content:space-between; align-items:center; padding:22px 32px; z-index:100; background:rgba(0,0,0,0.55); backdrop-filter:blur(10px); border-bottom:1px solid var(--line); transition:transform .3s }
.h-link { display:flex; align-items:center; gap:10px; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; font-weight:700; color:var(--paper); text-decoration:none; cursor:pointer; background:transparent; border:0 }
.h-link:hover { opacity:0.7 }
.h-icon { width:18px; height:18px; stroke:currentColor; fill:none; stroke-width:1.5 }

/* ===== Cinema hero ===== */
.cinema { position:relative; height:100vh; width:100%; overflow:hidden }
.cinema-bg { position:absolute; inset:0; background-size:cover; background-position:center; background-image:url('/hero-bg.jpg') }
.cinema-bg::after { content:""; position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0) 60%, rgba(0,0,0,0.75) 100%) }
.corner-mark { position:absolute; top:24px; left:32px; font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; mix-blend-mode:difference; color:#fff; z-index:5 }
.corner-mark::before { content:"◆ "; margin-right:6px }
.center-mark { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:clamp(34px,5vw,72px); font-weight:300; letter-spacing:0.42em; text-transform:uppercase; text-align:center; color:#fff; z-index:5; mix-blend-mode:difference }
.center-mark .tm { font-size:14px; vertical-align:super; opacity:0.7; margin-left:0.2em }
.center-mark .sub { display:block; font-family:'Playfair Display', Georgia, serif; font-style:italic; font-weight:400; font-size:0.4em; letter-spacing:0.04em; text-transform:none; margin-top:14px; opacity:0.85 }
.meta-line { position:absolute; bottom:32px; left:32px; font-size:10px; letter-spacing:0.28em; text-transform:uppercase; font-weight:700; color:#fff; z-index:5; opacity:0.85 }
.meta-line .num { font-size:12px; display:block; margin-top:4px; letter-spacing:0.05em }
.enter { position:absolute; bottom:32px; right:32px; display:flex; align-items:center; gap:14px; font-size:11px; letter-spacing:0.36em; text-transform:uppercase; font-weight:700; color:#fff; text-decoration:none; padding-bottom:6px; border-bottom:1px solid #fff; transition:gap 0.25s; z-index:5 }
.enter:hover { gap:22px }
.enter svg { width:24px; height:12px }

/* ===== Section ===== */
.section { padding:96px 32px }
.section-header { display:flex; justify-content:space-between; align-items:flex-end; flex-wrap:wrap; gap:24px; margin-bottom:32px; padding-bottom:24px; border-bottom:1px solid var(--line) }
.section-title { font-size:clamp(36px,5vw,64px); font-weight:300; letter-spacing:-0.02em; line-height:1 }
.section-title .accent { color: var(--accent); font-family:'Playfair Display', Georgia, serif; font-style:italic; font-weight:400 }
.section-eyebrow { font-size:10px; letter-spacing:0.4em; text-transform:uppercase; font-weight:700; color:var(--muted); margin-bottom:16px }
.section-meta { font-size:11px; letter-spacing:0.22em; text-transform:uppercase; font-weight:600; color:var(--muted); text-align:right; line-height:1.6 }

/* ===== Filter bar ===== */
.filters { display:flex; gap:8px; align-items:center; margin-bottom:18px; flex-wrap:wrap }
.chip { padding:8px 16px; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; font-weight:700; color:var(--paper); background:transparent; border:1px solid var(--line); cursor:pointer; transition:all 0.2s; font-family:inherit }
.chip:hover { border-color:var(--paper) }
.chip.active { background:var(--paper); color:var(--bg); border-color:var(--paper) }
.search { flex:0 1 280px; margin-left:auto; display:flex; align-items:center; gap:10px; border-bottom:1px solid var(--line); padding:6px 0 }
.search input { flex:1; background:transparent; border:0; color:var(--paper); font-family:inherit; font-size:13px; outline:none; letter-spacing:0.04em }
.search input::placeholder { color:var(--muted) }
.search svg { width:16px; height:16px; stroke:var(--muted); fill:none; stroke-width:1.5 }

/* ===== Grid-density slider ===== */
.density { display:flex; align-items:center; gap:14px; padding:6px 0; margin-bottom:24px }
.density label { font-size:10px; letter-spacing:0.32em; text-transform:uppercase; font-weight:700; color:var(--muted) }
.density input[type=range] { flex:1; max-width:240px; -webkit-appearance:none; appearance:none; 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 }
.density .ct { font-size:11px; color:var(--muted); letter-spacing:0.18em; text-transform:uppercase; font-weight:600; min-width:80px }

/* ===== Stats line ===== */
.stat-line { font-size:10px; letter-spacing:0.32em; text-transform:uppercase; font-weight:600; color:var(--muted); margin-bottom:24px }

/* ===== Product grid (novasuede pattern: flush grid + slide-up overlay) ===== */
.grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:0 }
.card { position:relative; aspect-ratio:1/1; cursor:pointer; overflow:hidden; border:1px solid var(--bg); transition:transform 0.4s cubic-bezier(0.2,0.8,0.2,1); background:var(--bg-soft) }
.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:20px 16px 14px; background:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.85) 100%); transform:translateY(48%); opacity:0.85; transition:transform 0.3s ease, opacity 0.3s ease }
.card:hover .overlay, .card.revealed .overlay { transform:translateY(0); opacity:1 }
.card .pat { font-size:13px; font-weight:600; letter-spacing:0.02em; 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; 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 + view toggle + search on mobile — only the column-density slider is meaningless at a fixed 2-col layout */
  .density { flex-wrap:wrap; gap:10px }
  .dens-only, #densitySlider, #densityLabel { display:none }
  .density .search { flex:1 1 100%; order:5 }
  .card .sample-btn { padding:12px 10px }   /* ≥44px touch target */
}

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

/* ===== Grid / List view toggle + sortable list columns ===== */
.viewtoggle { display:inline-flex; border:1px solid var(--line); margin-right:14px }
.viewtoggle button { background:transparent; border:0; padding:5px 9px; cursor:pointer; color:var(--muted); display:flex; align-items:center }
.viewtoggle button.active { background:var(--accent); color:#fff }
.viewtoggle svg { width:13px; height:13px }
.list-head { display:none }
[data-view=list] .list-head { display:grid; grid-template-columns:56px 2.6fr 1.2fr 1fr .8fr; gap:14px; align-items:center; padding:9px 12px; border-bottom:2px solid var(--accent) }
.list-head span, .list-head button { font-size:9px; letter-spacing:0.2em; text-transform:uppercase; font-weight:700; color:var(--muted) }
.list-head button { background:transparent; border:0; cursor:pointer; padding:0; display:inline-flex; gap:3px; align-items:center; font-family:inherit }
.list-head button:hover { color:var(--paper) }
.list-head button.sorted { color:var(--accent) }
.list-head .arr { font-size:8px }
.list-head button[data-sort-col=price] { justify-self:end }
[data-view=list] .grid { display:block }
[data-view=list] .card { display:grid; grid-template-columns:56px 2.6fr 1.2fr 1fr .8fr; gap:14px; align-items:center; aspect-ratio:auto; padding:8px 12px; border:0; border-bottom:1px solid var(--line); background:transparent; overflow:visible; text-decoration:none }
[data-view=list] .card:hover { transform:none; background:var(--bg-soft) }
[data-view=list] .card img { width:56px; height:56px; object-fit:cover }
[data-view=list] .r-pat { font-size:13px; font-weight:600; color:var(--paper); white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
[data-view=list] .r-col { font-size:11px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
[data-view=list] .r-sku { font-variant-numeric:tabular-nums; opacity:.8 }
[data-view=list] .r-aes { text-transform:capitalize }
[data-view=list] .r-price { text-align:right; color:var(--paper); font-weight:600 }
@media (max-width:720px) {
  [data-view=list] .list-head, [data-view=list] .card { grid-template-columns:44px 2fr .9fr }
  .list-head .col-sku, .list-head .col-aes, [data-view=list] .r-sku, [data-view=list] .r-aes { display:none }
}

/* ===== 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-size:24px; font-weight:300; letter-spacing:0.32em; text-transform:uppercase; 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 }
.footer-col a { display:block; font-size:13px; color:var(--muted); text-decoration:none; margin-bottom:8px; transition:color 0.2s; cursor:pointer; background:transparent; border:0; font-family:inherit; padding:0; text-align:left }
.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) {
  .corner-mark, .meta-line, .enter { font-size:10px }
  .center-mark { font-size:30px; letter-spacing:0.32em }
  .section, footer { padding-left:20px; padding-right:20px }
  .footer-grid { grid-template-columns:1fr; gap:32px }
  .search { margin-left:0; flex:1 1 100% }
}

.theme-toggle { background:transparent; border:1px solid var(--line); width:32px; height:32px; cursor:pointer; color:var(--paper); display:inline-flex; align-items:center; justify-content:center; font-size:14px; line-height:1; transition:all .15s; padding:0 }
.theme-toggle:hover { border-color:var(--accent); color:var(--accent) }

/* ===== WCAG 2.4.7 — keyboard focus indicators ===== */
/* Strictly :focus-visible so mouse clicks do NOT show the ring */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* === RAILS PATCH CSS (DW fleet sweep A4 2026-05-26) === */
#rails{display:block;margin:0 0 40px}
.rail-section{margin-bottom:48px}
.rail-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--line,rgba(0,0,0,.12))}
.rail-title{font-family:inherit;font-size:18px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent,#9A6E3A);font-weight:600;margin:0}
.rail-all{font-family:inherit;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted,#5b5042);font-weight:500;transition:color .15s;cursor:pointer}
.rail-all:hover{color:var(--accent,#9A6E3A);text-decoration:none}
.grid.rail-grid{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;padding-bottom:8px;scrollbar-width:none}
.grid.rail-grid::-webkit-scrollbar{display:none}
.grid.rail-grid > .card{flex:0 0 240px;scroll-snap-align:start}
@media (max-width:720px){.grid.rail-grid > .card{flex:0 0 160px}}
/* === END RAILS PATCH CSS === */

</style>
<script>
(function(){ try { var t = localStorage.getItem('str_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
</script>
<style id="ns-modal-fix">.ns-modal{display:none}.ns-modal.show{display:flex;align-items:center;justify-content:center;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:1000}.ns-modal-card{background:#faf6ef;color:#1a1714;padding:32px;border-radius:12px;max-width:480px;width:90%;position:relative;box-shadow:0 20px 60px rgba(0,0,0,0.3)}.ns-modal .close-x{position:absolute;top:12px;right:16px;background:transparent;border:0;font-size:24px;cursor:pointer;color:#1a1714}.ns-modal h3{margin:0 0 8px;font-size:22px;font-weight:400}.ns-modal .sub{color:rgba(0,0,0,0.5);margin:0 0 20px;font-size:14px}.ns-actions{display:grid;gap:10px}.ns-action{display:flex;gap:14px;align-items:center;color:inherit;text-decoration:none;padding:14px 16px;border:1px solid rgba(0,0,0,0.12);border-radius:8px}.ns-action:hover{background:rgba(0,0,0,0.04)}.ns-action .ico{font-size:20px}.ns-action .txt{display:flex;flex-direction:column}.ns-action .txt b{font-weight:600}.ns-action .txt span{font-size:12px;color:rgba(0,0,0,0.5)}</style><style id="hero4-anti-header-crop">
/* Never-show-images-with-headers rule (2026-05-14): every hero image
   is cropped to center 12% from the top to defeat vendor watermarks /
   scraped-site chrome / banner overlays. */
.cinema-bg, .cell { background-position: center 12% !important; }
.card img, .product img { object-position: center 12% !important; }
</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>
<h1 style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0">String Wallpaper</h1>
<!-- 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>

<style id="ns-header-modal-styles">
        .ns-header { position: sticky; top: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; padding: 18px 36px; background: rgba(10,10,10,0.92) !important; backdrop-filter: blur(8px); border-bottom: 1px solid rgba(245,241,232,0.06); }
        .ns-brand { display: inline-flex; align-items: center; gap: 14px; text-decoration: none; }
        .ns-dot { width: 28px; height: 28px; border-radius: 50%; background: var(--ns-dot-bg, radial-gradient(circle at 35% 35%, #b66a4d 0%, #8a4a30 60%, #5a2d1c 100%)); box-shadow: 0 0 0 1px rgba(245,241,232,0.10), 0 2px 12px rgba(180,90,60,0.30); flex-shrink: 0; }
        .ns-name { font-family: -apple-system, "SF Pro Display", "Helvetica Neue", system-ui, sans-serif; font-size: 18px !important; font-weight: 300 !important; letter-spacing: 0.02em !important; color: #f5f1e8 !important; text-transform: none !important; margin: 0 !important; }
        .ns-name sup { font-size: 0.55em; font-weight: 300; margin-left: 1px; }
        .ns-nav a, .ns-nav button { font-family: -apple-system, "SF Pro Text", system-ui, sans-serif; font-size: 15px !important; font-weight: 400 !important; color: #f5f1e8 !important; text-decoration: none !important; background: none !important; border: 0 !important; cursor: pointer; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
        .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
        /* All widths: hide nav by default, hamburger always visible on right, nav opens as right-anchored dropdown panel */
        .ns-nav {
            display: none !important;
            position: absolute; top: 100%; right: 18px;
            flex-direction: column; align-items: stretch; gap: 0;
            background: rgba(10,10,10,0.96); backdrop-filter: blur(10px);
            border: 1px solid rgba(245,241,232,0.10); border-top: 0;
            min-width: 220px; padding: 10px 0; margin: 0;
            box-shadow: 0 16px 40px rgba(0,0,0,0.55);
            z-index: 150;
        }
        .ns-nav.open { display: flex !important; }
        .ns-nav a, .ns-nav button {
            padding: 12px 22px !important;
            border-bottom: 1px solid rgba(245,241,232,0.06);
            width: 100%; text-align: left;
        }
        .ns-nav a:last-child, .ns-nav button:last-child { border-bottom: 0; }
        .ns-nav a:hover, .ns-nav button:hover { background: rgba(245,241,232,0.06) !important; color: #ffffff !important; }
        @media (max-width: 480px) { .ns-nav { right: 12px; min-width: 200px; } }
        .ns-header .mobile-toggle { display: flex !important; align-items: center; justify-content: center; width: 36px; height: 36px; background: transparent; border: 0; cursor: pointer; padding: 0; gap: 4px; flex-direction: column; }
        .ns-header .mobile-toggle span { display: block; width: 22px; height: 1.5px; background: #f5f1e8; transition: transform 200ms, opacity 200ms; }
        .ns-header .mobile-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(5px) rotate(45deg); }
        .ns-header .mobile-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
        .ns-header .mobile-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }
    </style>
<style id="ns-gold-kill">
    /* novasuede: kill gold-solid fills, off-white instead. Edge color stays gold. */
    .new-badge, .new-arrivals-pill,
    .color-filter-btn.active, .drapery-btn,
    .sample-btn-chip, .cb-cta {
      background: #f5f1e8 !important;
      color: #0a0a0a !important;
      border-color: #f5f1e8 !important;
    }
    .new-badge:hover, .new-arrivals-pill:hover,
    .color-filter-btn.active:hover, .drapery-btn:hover,
    .sample-btn-chip:hover, .cb-cta:hover {
      background: #fff !important;
      color: #0a0a0a !important;
    }
  </style>
<header class="ns-header">
        <a href="/" class="ns-brand" aria-label="Home">
            <span class="ns-dot"></span>
            <span class="ns-name">String Wallpaper</span>
        </a>
        <nav class="ns-nav" id="nsPrimaryNav" aria-label="Primary">
            <a href="/">Home</a>
            <a href="/about">About</a>
            <button type="button" onclick="nsContactOpen()">Contact</button>
        </nav>
        <button type="button" class="mobile-toggle" aria-label="Toggle menu" aria-controls="nsPrimaryNav" aria-expanded="false" onclick="nsToggleNav(this)">
            <span></span><span></span><span></span>
        </button>
    </header>


<script>
  function nsContactOpen() { var m = document.getElementById('nsContactModal'); if (m) m.classList.add('show'); }
  function nsContactClose() { var m = document.getElementById('nsContactModal'); if (m) m.classList.remove('show'); }
  function nsToggleNav(btn) {
    var nav = document.getElementById('nsPrimaryNav');
    if (!nav) return;
    var open = nav.classList.toggle('open');
    btn.setAttribute('aria-expanded', open ? 'true' : 'false');
  }
  document.addEventListener('keydown', function(e){ if (e.key === 'Escape') nsContactClose(); });
</script>











<section class="cinema">
  <div class="cinema-bg" id="heroBg"></div>
  <div class="corner-mark">Threadwork · Linear</div>
  <div class="center-mark">Loma Studio<span class="tm">.</span><span class="sub">Threads, woven</span></div>
  <div class="meta-line">String · Linear · Woven<span class="num" id="heroNum"></span></div>
  <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
</section>

<section class="section" id="shop">
  <div class="section-header">
    <div>
      <div class="section-eyebrow">The Collection</div>
      <h2 class="section-title"><span class="accent" id="totalCount">—</span> Patterns.</h2>
    </div>
    <div class="section-meta">String · Linear · Woven</div>
  </div>

  <div class="filters" id="facets">
    <button class="chip active" data-facet="all">All</button>
  </div>

  <div class="density">
    <div class="viewtoggle" id="viewToggle">
      <button data-view="grid" class="active" aria-label="Grid view" title="Grid view"><svg viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg></button>
      <button data-view="list" aria-label="List view" title="List view"><svg viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="4" width="18" height="3"/><rect x="3" y="10" width="18" height="3"/><rect x="3" y="16" width="18" height="3"/></svg></button>
    </div>
    <label>Sort</label>
    <select id="sortSelect" aria-label="Sort products" style="background:transparent;color:inherit;border:1px solid var(--line,rgba(255,255,255,.2));font:inherit;font-size:11px;letter-spacing:.10em;text-transform:uppercase;padding:4px 8px;cursor:pointer;outline:none;margin-right:14px">
      <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>
    <label class="dens-only">Grid</label>
    <input type="range" id="densitySlider" min="4" max="12" step="1" value="6" aria-label="Grid columns">
    <span class="ct" id="densityLabel">6 cols</span>
    <div class="search">
      <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-5.5-5.5"/></svg>
      <input type="text" id="searchInput" placeholder="Search by pattern, color…" autocomplete="off">
    </div>
  </div>

  <div class="stat-line" id="statLine">Loading…</div>
  <div class="list-head" id="listHead">
    <span></span>
    <button data-sort-col="title">Pattern <span class="arr"></span></button>
    <button data-sort-col="sku" class="col-sku">SKU <span class="arr"></span></button>
    <button data-sort-col="aesthetic" class="col-aes">Aesthetic <span class="arr"></span></button>
    <button data-sort-col="price">Price <span class="arr"></span></button>
  </div>
  <div id="rails" style="display:none"></div>
  <div class="grid" id="grid"></div>
  <div class="loading" id="loading">Loading more…</div>
  <div class="sentinel" id="sentinel"></div>
</section>

<footer>
  <div class="footer-grid">
    <div>
      <div class="footer-brand">stringwallpaper</div>
      <p class="footer-text">Loma Studio is a curated atelier dedicated entirely to string. We do one thing — and we do it carefully.</p>
      <p class="footer-text" style="margin-top:14px;font-size:11px;letter-spacing:0.18em;text-transform:uppercase;font-weight:600;color:var(--paper);opacity:0.7"><a href="/about" style="color:inherit;text-decoration:none;border-bottom:1px solid var(--accent)">About →</a></p>
    </div>
    <div class="footer-col">
      <h4>Aesthetic</h4>
      <div id="footerFacets"></div>
    </div>
    <div class="footer-col">
      <h4>Trade</h4>
      <button onclick="dwmOpen('Inquiry')">Project Inquiry</button>
      <button onclick="dwmOpen('Sample')">Request Sample</button>
      <button onclick="dwmOpen('Contact')">Contact</button>
    </div>
  </div>
  <div class="footer-bottom">
    <span>stringwallpaper.com · Loma Studio</span>
    <span><a href="mailto:info@stringwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@stringwallpaper.com</a></span>
    <span id="footerStat"></span>
  </div>
</footer>

<script>
// mini-constellation — 43 niche-stars, current site highlighted in gold
(function(){
  const SLUG = "stringwallpaper";
  const NICHES = [
    ['silkwallpaper','Silk','mat'],['silkwallcoverings','Silk W/C','mat'],['linenwallpaper','Linen','mat'],
    ['jutewallpaper','Jute','mat'],['raffiawallcoverings','Raffia W/C','mat'],['raffiawalls','Raffia Walls','mat'],
    ['corkwallcovering','Cork','mat'],['fabricwallpaper','Fabric','mat'],['textilewallpaper','Textile','mat'],
    ['metallicwallpaper','Metallic','mat'],['silverleafwallpaper','Silver Leaf','mat'],['vinylwallpaper','Vinyl','mat'],
    ['micawallpaper','Mica','mat'],['madagascarwallpaper','Madagascar','mat'],['mylarwallpaper','Mylar','mat'],
    ['suedewallpaper','Suede','mat'],
    ['1800swallpaper','1800s','dec'],['1890swallpaper','1890s','dec'],['1900swallpaper','1900s','dec'],
    ['1920swallpaper','1920s','dec'],['1930swallpaper','1930s','dec'],['1940swallpaper','1940s','dec'],
    ['1950swallpaper','1950s','dec'],['1960swallpaper','1960s','dec'],['1970swallpaper','1970s','dec'],
    ['1980swallpaper','1980s','dec'],['retrowalls','Retro','dec'],['wallpapersback','W. Back','dec'],
    ['agedwallpaper','Aged','cra'],['handcraftedwallpaper','Handcrafted','cra'],['museumwallpaper','Museum','cra'],
    ['restorationwallpaper','Restoration','cra'],['pastelwallpaper','Pastel','cra'],['glitterwalls','Glitter','cra'],
    ['greenwallcoverings','Green','cra'],['naturalwallcoverings','Natural','cra'],['saloonwallpaper','Saloon','cra'],
    ['contractwallpaper','Contract','use'],['hotelwallcoverings','Hotel','use'],['hospitalitywallpaper','Hospitality','use'],
    ['healthcarewallpaper','Healthcare','use'],['restaurantwallpaper','Restaurant','use'],['architecturalwallcoverings','Architectural','use']
  ];
  const COLOR = { mat:'#c9b687', dec:'#b86a4a', cra:'#6b8e6f', use:'#4a6b8e' };
  const W = 1080, H = 200, BANDS = ['mat','dec','cra','use'];
  const groups = { mat:[], dec:[], cra:[], use:[] };
  for (const n of NICHES) groups[n[2]].push(n);
  const svg = document.getElementById('miniConst');
  if (!svg) return;
  const bandW = W / BANDS.length;
  let nodes = '';
  BANDS.forEach((band, bi) => {
    const list = groups[band];
    const cx = bi * bandW + bandW / 2;
    list.forEach((n, i) => {
      const t = list.length === 1 ? 0.5 : i / (list.length - 1);
      // deterministic jitter per slug
      let h = 0; for (let k = 0; k < n[0].length; k++) h = (h * 31 + n[0].charCodeAt(k)) | 0;
      const jx = ((h & 0xff) / 255 - 0.5) * (bandW * 0.5);
      const jy = (((h >> 8) & 0xff) / 255 - 0.5) * 6;  // tightened from 14 → 6 to prevent same-band collisions (16 mat nodes / 144px = 9px spacing)
      const x = cx + jx, y = 28 + t * (H - 56) + jy;
      const isCurrent = n[0] === SLUG;
      const r = isCurrent ? 8 : 4;
      const op = isCurrent ? 1 : 0.55;
      nodes += `<g class="mc-node" data-slug="${n[0]}" data-label="${n[1]}" style="cursor:pointer">
        <circle cx="${x}" cy="${y}" r="${r}" fill="url(#dna-${band})" fill-opacity="${op}" stroke="${COLOR[band]}" stroke-opacity="${isCurrent?1:0.7}" stroke-width="${isCurrent?2:1}">
          <title>${n[1]} — ${n[0]}.com</title>
        </circle>
        ${isCurrent ? `<circle cx="${x}" cy="${y}" r="${r+5}" fill="none" stroke="${COLOR[band]}" stroke-opacity="0.4" stroke-width="1"><animate attributeName="r" values="${r+5};${r+12};${r+5}" dur="2.4s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.4;0;0.4" dur="2.4s" repeatCount="indefinite"/></circle>` : ''}
      </g>`;
    });
  });
  // append nodes after defs
  svg.insertAdjacentHTML('beforeend', nodes);
  svg.querySelectorAll('.mc-node').forEach(n => {
    n.addEventListener('mouseenter', () => n.querySelector('circle').setAttribute('fill-opacity', '1'));
    n.addEventListener('mouseleave', e => {
      const isCurrent = n.dataset.slug === SLUG;
      n.querySelector('circle').setAttribute('fill-opacity', isCurrent ? '1' : '0.55');
    });
    n.addEventListener('click', () => window.open('https://' + n.dataset.slug + '.com', '_blank'));
  });
})();
</script>

<script>
const state = { q:'', facet:'all', sort:'newest', view:'grid', page:1, pages:1, total:0, loading:false, exhausted:false };
// Touch device = no hover. Drives tap-to-reveal card behavior.
const TOUCH = window.matchMedia('(hover: none)').matches;
const HOSTKEY = location.hostname.replace(/\./g, '_');
// DW STANDARD: hydrate saved sort + view BEFORE the first grid load.
(function(){
  try {
    const sortSel = document.getElementById('sortSelect');
    const saved = localStorage.getItem(HOSTKEY + '_sort');
    if (saved) { state.sort = saved; if (sortSel) sortSel.value = saved; }
    else if (sortSel) state.sort = sortSel.value;
    const savedView = localStorage.getItem(HOSTKEY + '_view');
    if (savedView === 'list' || savedView === 'grid') state.view = savedView;
    document.documentElement.dataset.view = state.view;
  } catch(e){}
})();
const LABELS = {"string":"String"};

function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
function cleanSku(s){
  var V=/(?:^|-)(versace|kravet|fentucci|sandberg|harlequin|blithfield|westport|thibaut|koroseal|schumacher|scalamandre|fromental|dedar|carnegie|marburg|fabricut|coordonne|nina-campbell|lee-jofa(?:-modern)?|clarke(?:-and)?-clarke|clarke|brunschwig(?:-and)?(?:-fils)?|designers-guild|graham-(?:and-)?brown|andrew-martin|candice-olson|ronald-redding|jeffrey-stevens|sister-parish|arte-international|wolf-gordon|phillip-jeffries|cole-(?:and-)?son|maya-romanoff|g-p-j-baker|les-ensembliers|breegan(?:-jane)?)(?:-\\d+)?(?=-|$)/gi;
  return String(s||'').replace(V,'').replace(/--+/g,'-').replace(/^-+|-+$/g,'');
}
// Image-URL allowlist: same-origin proxied /img/ (vendor-neutral image proxy, 2026-06-03) +
// DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json).
// /img/ is server-controlled same-origin (not protocol-relative, not a javascript: URI) so it's safe.
function safeImg(u) { u = u || ''; return (/^\/img\//.test(u) || /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(u)) ? u : '/hero-bg.jpg'; }
function cardHTML(p) {
  const eager = state.page === 1;
  return '<img loading="' + (eager ? 'eager' : 'lazy') + '"' + (eager ? ' fetchpriority="high"' : '') + ' src="' + escAttr(safeImg(p.image_url)) + '" alt="' + escAttr(p.title) + '">'
    + '<div class="overlay">'
    + '<div class="pat">' + escAttr(p.pattern_name || p.title) + '</div>'
    + '<div class="ven">Designer Wallcoverings' + (cleanSku(p.sku || p.handle) ? ' · ' + escAttr(cleanSku(p.sku || p.handle)) : '') + '</div>'
    + '<div class="actions">'
    + '<button class="sample-btn" onclick="event.stopPropagation();dwmOpen(\'Sample\',' + JSON.stringify({sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}).replace(/"/g,'&quot;') + ')">Sample</button>'
    + '</div></div>';
}

// List-view row — same product, table-style columns. Vendor is NEVER shown
// on the customer-facing storefront (always "Designer Wallcoverings").
function rowHTML(p) {
  const mp = Number(p.max_price);
  const price = (p.max_price != null && !isNaN(mp) && mp > 0) ? '$' + mp.toFixed(2) : '—';
  const aes = LABELS[p.aesthetic] || p.aesthetic || '—';
  return '<img loading="lazy" src="' + escAttr(safeImg(p.image_url)) + '" alt="' + escAttr(p.title) + '">'
    + '<div class="r-pat">' + escAttr(p.pattern_name || p.title) + '</div>'
    + '<div class="r-col r-sku">' + escAttr(cleanSku(p.sku || p.handle) || '—') + '</div>'
    + '<div class="r-col r-aes">' + escAttr(String(aes).replace(/-/g, ' ')) + '</div>'
    + '<div class="r-col r-price">' + price + '</div>';
}

// Click-to-open-full-details — every card opens a rich detail modal showing
// the full image, pattern name, SKU, retail price (when known), aesthetic
// chips, and CTAs for sample + Big Red chat. The modal NEVER reveals vendor.
function openDetails(p) {
  const safeP = {
    sku: p.sku || p.handle || '',
    title: p.title || '',
    pattern: p.pattern_name || p.title || '',
    image_url: safeImg(p.image_url),
    price: p.price != null ? Number(p.price) : null,
    aesthetic: Array.isArray(p.aesthetics) ? p.aesthetics : (p.aesthetic ? [p.aesthetic] : []),
    width: p.width || p.roll_width || '',
    repeat: p.repeat || p.pattern_repeat || '',
    description: (p.description || '').slice(0, 600),
  };
  const m = document.getElementById('dwmDetails');
  if (!m) return; // safety
  m.querySelector('[data-d-img]').src = safeP.image_url;
  m.querySelector('[data-d-img]').alt = safeP.title;
  m.querySelector('[data-d-pat]').textContent = safeP.pattern;
  m.querySelector('[data-d-brand]').textContent = 'Designer Wallcoverings' + (cleanSku(safeP.sku) ? ' · ' + cleanSku(safeP.sku) : '');
  const priceEl = m.querySelector('[data-d-price]');
  priceEl.textContent = safeP.price != null && !isNaN(safeP.price) ? ('$' + safeP.price.toFixed(2) + ' / roll') : 'Pricing on request';
  const chipsEl = m.querySelector('[data-d-chips]');
  chipsEl.innerHTML = '';
  for (const a of safeP.aesthetic) {
    if (!a) continue;
    const c = document.createElement('span');
    c.className = 'd-chip';
    c.textContent = ((typeof LABELS !== 'undefined' && LABELS[a]) || a).replace(/-/g, ' ');
    chipsEl.appendChild(c);
  }
  const specsEl = m.querySelector('[data-d-specs]');
  specsEl.innerHTML = '';
  if (safeP.width) specsEl.insertAdjacentHTML('beforeend', '<dt>Width</dt><dd>' + escAttr(safeP.width) + '</dd>');
  if (safeP.repeat) specsEl.insertAdjacentHTML('beforeend', '<dt>Repeat</dt><dd>' + escAttr(safeP.repeat) + '</dd>');
  if (safeP.description) specsEl.insertAdjacentHTML('beforeend', '<dt>Notes</dt><dd>' + escAttr(safeP.description) + '</dd>');
  m.querySelector('[data-d-sample]').onclick = () => {
    dwmClose('Details');
    dwmOpen('Sample', { sku: safeP.sku, title: safeP.title, image_url: safeP.image_url });
  };
  dwmOpen('Details');
  try { if (typeof gtag === 'function') gtag('event', 'product_details_open', { sku: safeP.sku, pattern: safeP.pattern }); } catch(_){}
}


async function loadFacets() {
  let f;
  try {
    const r = await fetch('/api/facets');
    if (!r.ok) throw new Error('HTTP ' + r.status);
    f = await r.json();
  } catch (e) { console.error('loadFacets failed:', e); return; }
  const el = document.getElementById('facets');
  for (const [k, v] of Object.entries(f.aesthetics).sort((a,b) => b[1] - a[1])) {
    const b = document.createElement('button');
    b.className = 'chip'; b.dataset.facet = k;
    b.innerHTML = (LABELS[k] || k) + ' <span style="opacity:.55;font-weight:500;margin-left:4px">' + v + '</span>';
    el.appendChild(b);
  }
  el.addEventListener('click', e => {
    if (e.target.tagName !== 'BUTTON') return;
    document.querySelectorAll('#facets button').forEach(b => b.classList.remove('active'));
    e.target.classList.add('active');
    state.facet = e.target.dataset.facet;
    resetGrid();
  });
  document.getElementById('totalCount').textContent = f.total;
  document.getElementById('footerStat').textContent = f.total + ' patterns · live archive';
  // Footer aesthetic links
  const footerEl = document.getElementById('footerFacets');
  for (const [k] of Object.entries(f.aesthetics)) {
    const b = document.createElement('button');
    b.textContent = LABELS[k] || k;
    b.onclick = () => { state.facet = k; resetGrid(); document.querySelectorAll('#facets button').forEach(x => x.classList.toggle('active', x.dataset.facet === k)); document.getElementById('shop').scrollIntoView({behavior:'smooth'}); };
    footerEl.appendChild(b);
  }
}

function resetGrid() {
  state.page = 1; state.exhausted = false;
  document.getElementById('grid').innerHTML = '';
  document.getElementById('loading').textContent = 'Loading…';
  loadGridPage();
  if (typeof renderRailSections === 'function') renderRailSections();
}

async function loadGridPage() {
  if (state.loading || state.exhausted) return;
  state.loading = true;
  const params = new URLSearchParams({ page:state.page, limit:24 });
  if (state.q) params.set('q', state.q);
  if (state.facet !== 'all') params.set('aesthetic', state.facet);
  if (state.sort && state.sort !== 'newest') params.set('sort', state.sort);
  let data;
  try {
    const r = await fetch('/api/products?' + params);
    if (!r.ok) throw new Error('HTTP ' + r.status);
    data = await r.json();
  } catch (e) {
    console.error('loadGridPage failed:', e);
    document.getElementById('loading').textContent = '— offline — refresh to retry —';
    state.loading = false;
    return;
  }
  state.total = data.total; state.pages = data.pages;

  if (state.page === 1) {
    document.getElementById('statLine').textContent = data.total + ' patterns · ' + (state.facet === 'all' ? 'all aesthetics' : (LABELS[state.facet] || state.facet)) + (state.q ? ' · matching "' + state.q + '"' : '');
  }
  const grid = document.getElementById('grid');
  for (const p of data.items) {
    const a = document.createElement('a');
    a.className = 'card'; a.href = '#';
    a.onclick = (e) => {
      e.preventDefault();
      // Touch devices have no hover — 1st tap reveals the card's info overlay,
      // 2nd tap on the revealed card opens the detail modal. Desktop (hover)
      // shows the overlay on hover, so a click opens immediately.
      if (TOUCH && !a.classList.contains('revealed')) {
        document.querySelectorAll('.card.revealed').forEach(c => c.classList.remove('revealed'));
        a.classList.add('revealed');
        return;
      }
      openDetails(p);
    };
    a.innerHTML = state.view === 'list' ? rowHTML(p) : cardHTML(p);
    grid.appendChild(a);
  }
  if (state.page >= state.pages || data.items.length === 0) {
    state.exhausted = true;
    document.getElementById('loading').textContent = state.total > 0 ? '— end of archive · ' + state.total + ' patterns —' : '';
  } else state.page++;
  state.loading = false;
}

const io = new IntersectionObserver(es => { for (const e of es) if (e.isIntersecting) loadGridPage(); }, { rootMargin:'600px 0px' });
io.observe(document.getElementById('sentinel'));

document.getElementById('searchInput').addEventListener('input', e => {
  state.q = e.target.value.trim();
  clearTimeout(window._t);
  window._t = setTimeout(resetGrid, 220);
});

// Density slider
const slider = document.getElementById('densitySlider');
const dlabel = document.getElementById('densityLabel');
function setDensity(n) {
  document.documentElement.style.setProperty('--cols', n);
  dlabel.textContent = n + ' cols';
  try { localStorage.setItem('str_theme_density', n); } catch(e){}
}
slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
const savedDensity = parseInt(localStorage.getItem('str_theme_density') || '6');
if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }

// ===== Grid / List view toggle =====
const viewToggle = document.getElementById('viewToggle');
function setView(v) {
  state.view = (v === 'list') ? 'list' : 'grid';
  document.documentElement.dataset.view = state.view;
  for (const b of viewToggle.querySelectorAll('button')) b.classList.toggle('active', b.dataset.view === state.view);
  try { localStorage.setItem(HOSTKEY + '_view', state.view); } catch(e){}
  resetGrid();
}
viewToggle.addEventListener('click', e => {
  const btn = e.target.closest('button[data-view]');
  if (btn) setView(btn.dataset.view);
});
for (const b of viewToggle.querySelectorAll('button')) b.classList.toggle('active', b.dataset.view === state.view);

// ===== Sortable list-view columns ===== (column -> [ascSort, descSort])
const COL_SORTS = {
  title:     ['title', 'title-desc'],
  sku:       ['sku', 'sku-desc'],
  aesthetic: ['aesthetic', 'aesthetic-desc'],
  price:     ['price-asc', 'price-desc'],
};
function syncColHeaders() {
  for (const b of document.querySelectorAll('#listHead button[data-sort-col]')) {
    const pair = COL_SORTS[b.dataset.sortCol] || [];
    const arr = b.querySelector('.arr');
    if (state.sort === pair[0]) { b.classList.add('sorted'); arr.textContent = '▲'; }
    else if (state.sort === pair[1]) { b.classList.add('sorted'); arr.textContent = '▼'; }
    else { b.classList.remove('sorted'); arr.textContent = ''; }
  }
}
document.getElementById('listHead').addEventListener('click', e => {
  const btn = e.target.closest('button[data-sort-col]');
  if (!btn) return;
  const pair = COL_SORTS[btn.dataset.sortCol];
  if (!pair) return;
  state.sort = (state.sort === pair[0]) ? pair[1] : pair[0];
  const sortSel = document.getElementById('sortSelect');
  if (sortSel && [...sortSel.options].some(o => o.value === state.sort)) sortSel.value = state.sort;
  try { localStorage.setItem(HOSTKEY + '_sort', state.sort); } catch(e){}
  syncColHeaders();
  resetGrid();
});
syncColHeaders();

// Theme toggle
const tb = (document.getElementById('theme-toggle')||{textContent:'',classList:{add(){},remove(){},toggle(){}},addEventListener(){},style:{}});
function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('str_theme', t); } catch(e){} tb.textContent = t === 'dark' ? '☾' : '☀'; }
setTheme(document.documentElement.dataset.theme || 'light');
tb.addEventListener('click', () => setTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'));

// === RAILS PATCH (DW fleet sweep A4 2026-05-26) ===
// Renders rail sections on the unfiltered homepage above the flat grid.
// Fed by /api/sliders. Hides flat grid + sentinel while rails are visible.
async function renderRailSections(){
  const railsHost = document.getElementById('rails');
  const grid = document.getElementById('grid');
  const sentinel = document.getElementById('sentinel');
  const loading = document.getElementById('loading');
  const listHead = document.getElementById('listHead');
  if (!railsHost) return;
  // Only render rails on the unfiltered "all" home view.
  if (state.q || state.facet !== 'all') {
    railsHost.innerHTML = '';
    railsHost.style.display = 'none';
    if (grid) grid.style.display = '';
    if (sentinel) sentinel.style.display = '';
    if (loading) loading.style.display = '';
    return;
  }
  let payload;
  try {
    const r = await fetch('/api/sliders');
    if (!r.ok) throw new Error('HTTP ' + r.status);
    payload = await r.json();
  } catch (e) {
    console.warn('renderRailSections failed:', e);
    return;
  }
  const rails = Array.isArray(payload && payload.rails) ? payload.rails : [];
  if (rails.length < 2) return; // no meaningful segmentation
  const frag = document.createElement('div');
  for (const rail of rails) {
    const aes = rail.aesthetic;
    const items = (rail.items || []).slice(0, 8);
    if (items.length < 4) continue;
    const sec = document.createElement('section');
    sec.className = 'rail-section';
    const head = document.createElement('div');
    head.className = 'rail-head';
    const title = document.createElement('h2');
    title.className = 'rail-title';
    title.textContent = ((typeof LABELS !== 'undefined' && LABELS[aes]) || aes).replace(/-/g,' ').replace(/\b\w/g, c => c.toUpperCase());
    const all = document.createElement('a');
    all.className = 'rail-all';
    all.href = '#shop';
    all.textContent = 'View all →';
    all.dataset.facet = aes;
    all.onclick = (e) => {
      e.preventDefault();
      state.facet = aes;
      document.querySelectorAll('#facets button').forEach(x => x.classList.toggle('active', x.dataset.facet === aes));
      resetGrid();
      const shop = document.getElementById('shop');
      if (shop) shop.scrollIntoView({behavior:'smooth'});
    };
    head.appendChild(title); head.appendChild(all);
    const grid8 = document.createElement('div');
    grid8.className = 'grid rail-grid';
    for (const p of items) {
      const card = document.createElement('a');
      card.className = 'card';
      card.href = '#';
      card.onclick = (e) => { e.preventDefault(); openDetails(p); };
      card.innerHTML = cardHTML(p);
      grid8.appendChild(card);
    }
    sec.appendChild(head);
    sec.appendChild(grid8);
    frag.appendChild(sec);
  }
  if (!frag.childNodes.length) return;
  railsHost.innerHTML = '';
  railsHost.appendChild(frag);
  railsHost.style.display = '';
  // Hide flat grid + scroll bait while rails are showing — clicking a rail
  // "View all →" or a chip will un-hide them via resetGrid path.
  if (grid) grid.style.display = 'none';
  if (sentinel) sentinel.style.display = 'none';
  if (loading) loading.style.display = 'none';
  if (listHead) listHead.style.display = 'none';
  const stat = document.getElementById('statLine');
  if (stat) stat.textContent = (state.total || '') + ' patterns · browse by aesthetic';
}
// === END RAILS PATCH ===

loadFacets();
loadGridPage();
// renderRailSections is defined in a later <script> block; defer to DOMContentLoaded.
document.addEventListener('DOMContentLoaded', function(){ try { renderRailSections(); } catch(e){ console.warn('rails defer', e); } });
</script>
<!-- ============================================================
     DW UNIVERSAL CONTACT MODULE — fashion-house UX
     Inject before   <script src="/hero-4grid.js" defer></script>
<script src="/dw-theme-nav.js" defer></script>
</body> in every DW-family sister site.
     Self-contained: CSS + 4 modals + JS submission handlers.
     Uses host site's CSS vars (--bg, --fg, --gold, --rule).
     ============================================================ -->
<style>
  /* === MODAL BASE === */
  .dwm{position:fixed;inset:0;background:rgba(0,0,0,0.78);display:none;align-items:center;justify-content:center;z-index:9999;padding:24px;overflow-y:auto}
  .dwm.open{display:flex}
  .dwm-box{background:var(--bg);border:1px solid var(--rule);max-width:520px;width:100%;padding:36px 32px;position:relative;max-height:90vh;overflow-y:auto;animation:dwm-in 0.25s ease}
  @keyframes dwm-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
  .dwm-box h3{font-family:'Playfair Display',Georgia,serif;font-style:italic;font-weight:400;font-size:32px;line-height:1.05;margin-bottom:6px;color:var(--fg);letter-spacing:-0.01em}
  .dwm-box .sub{color:var(--muted);font-size:12px;letter-spacing:0.16em;text-transform:uppercase;margin-bottom:24px;font-weight:500}
  .dwm-close{position:absolute;top:18px;right:18px;background:transparent;border:0;color:var(--muted);font-size:24px;cursor:pointer;width:32px;height:32px;line-height:1;padding:0}
  .dwm-close:hover{color:var(--gold)}
  .dwm-preview{display:flex;gap:14px;align-items:center;padding:12px;background:var(--bg-soft);margin-bottom:18px}
  .dwm-preview img{width:64px;height:64px;object-fit:cover}
  .dwm-preview .pn{font-family:'Playfair Display',Georgia,serif;font-style:italic;font-size:15px;line-height:1.2;color:var(--fg)}
  .dwm-preview .pc{font-size:10px;letter-spacing:0.14em;text-transform:uppercase;color:var(--muted);margin-top:4px}
  .dwm-field{margin-bottom:14px}
  .dwm-field label{display:block;font-size:10px;letter-spacing:0.18em;text-transform:uppercase;color:var(--muted);margin-bottom:6px;font-weight:500}
  .dwm-field input,.dwm-field textarea,.dwm-field select{width:100%;border:0;border-bottom:1px solid var(--rule);background:transparent;padding:8px 4px;font-size:14px;font-family:inherit;color:var(--fg);outline:none;transition:border-color 0.2s}
  .dwm-field input:focus,.dwm-field textarea:focus{border-bottom-color:var(--gold)}
  .dwm-field textarea{min-height:60px;resize:vertical}
  .dwm-submit{background:transparent;color:var(--muted, #c9b89e);border:1px solid var(--muted, #c9b89e);padding:14px 28px;font-family:inherit;font-size:11px;letter-spacing:0.20em;text-transform:uppercase;font-weight:700;cursor:pointer;width:100%;margin-top:8px;transition:background 0.15s, color 0.15s}
  .dwm-submit:hover{background:var(--muted, #c9b89e);color:var(--bg, #000)}
  
  .dwm-submit:disabled{opacity:0.5;cursor:default}
  .dwm-status{margin-top:14px;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;text-align:center;display:none}
  .dwm-status.ok{color:var(--sage);display:block}
  .dwm-status.err{color:#e08070;display:block}
  /* contact-options */
  .dwm-options{display:flex;flex-direction:column;gap:10px}
  .dwm-option{display:flex;align-items:center;gap:14px;padding:18px 20px;background:transparent;border:1px solid var(--rule);color:var(--fg);font-family:inherit;font-size:11px;letter-spacing:0.20em;text-transform:uppercase;font-weight:600;cursor:pointer;text-decoration:none;transition:all 0.2s;text-align:left;width:100%}
  .dwm-option:hover{border-color:var(--gold);color:var(--gold)}
  .dwm-option svg{width:22px;height:22px;stroke:currentColor;fill:none;stroke-width:1.5;flex-shrink:0}
  .dwm-option .lbl{flex:1;display:block}
  .dwm-option .val{display:block;font-size:10px;letter-spacing:0.14em;text-transform:none;font-weight:500;opacity:0.65;margin-top:4px}
</style>

<!-- Product details modal — opens on any chip click. Vendor-redacted. -->
<style id="dwm-details-style">
  #dwmDetails .dwm-box { max-width: 880px; padding: 0; overflow: hidden; }
  #dwmDetails .d-wrap { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 0; }
  #dwmDetails .d-img-wrap { aspect-ratio: 1/1; background: #1a1714; overflow: hidden; }
  #dwmDetails .d-img-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
  #dwmDetails .d-meta { padding: 32px 32px 28px; display: flex; flex-direction: column; gap: 14px; max-height: 600px; overflow-y: auto; }
  #dwmDetails .d-brand { font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase; color: rgba(0,0,0,0.55); font-weight: 600; }
  #dwmDetails .d-pat { font-size: 24px; line-height: 1.15; font-weight: 400; color: #1a1714; margin: 0; }
  #dwmDetails .d-price { font-size: 18px; font-weight: 500; color: #1a1714; }
  #dwmDetails .d-chips { display: flex; flex-wrap: wrap; gap: 6px; }
  #dwmDetails .d-chip { font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; padding: 5px 10px; border: 1px solid rgba(0,0,0,0.18); border-radius: 999px; color: rgba(0,0,0,0.7); }
  #dwmDetails .d-specs { display: grid; grid-template-columns: 90px 1fr; gap: 6px 14px; font-size: 13px; color: rgba(0,0,0,0.75); margin: 4px 0 8px; }
  #dwmDetails .d-specs dt { font-weight: 600; color: rgba(0,0,0,0.55); }
  #dwmDetails .d-specs dd { margin: 0; }
  #dwmDetails .d-ctas { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 6px; }
  #dwmDetails .d-cta { padding: 13px 14px; font-family: inherit; font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; font-weight: 700; border: 1px solid #1a1714; cursor: pointer; transition: all .15s; text-align: center; }
  #dwmDetails .d-cta.primary { background: #1a1714; color: #faf6ef; }
  #dwmDetails .d-cta.primary:hover { background: var(--accent, #c4574d); border-color: var(--accent, #c4574d); }
  #dwmDetails .d-cta.ghost { background: transparent; color: #1a1714; }
  #dwmDetails .d-cta.ghost:hover { background: #1a1714; color: #faf6ef; }
  @media (max-width: 720px) {
    #dwmDetails .dwm-box { max-width: 96vw; }
    #dwmDetails .d-wrap { grid-template-columns: 1fr; }
    #dwmDetails .d-img-wrap { aspect-ratio: 4/3; }
    #dwmDetails .d-meta { max-height: none; padding: 22px 20px 24px; }
  }
</style>
<div class="dwm" id="dwmDetails" onclick="if(event.target===this)dwmClose('Details')">
  <div class="dwm-box">
    <button class="dwm-close" onclick="dwmClose('Details')" aria-label="Close" style="position:absolute;top:12px;right:14px;z-index:2;background:rgba(255,255,255,.85);border-radius:50%;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:0;cursor:pointer;font-size:20px;line-height:1">×</button>
    <div class="d-wrap">
      <div class="d-img-wrap"><img data-d-img alt="" loading="lazy"></div>
      <div class="d-meta">
        <div class="d-brand" data-d-brand>Designer Wallcoverings</div>
        <h3 class="d-pat" data-d-pat></h3>
        <div class="d-price" data-d-price></div>
        <div class="d-chips" data-d-chips></div>
        <dl class="d-specs" data-d-specs></dl>
        <div class="d-ctas">
          <button class="d-cta primary" type="button" data-d-sample>Order Memo Sample</button>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Contact options modal -->
<div class="dwm" id="dwmContact" onclick="if(event.target===this)dwmClose('Contact')">
  <div class="dwm-box" style="max-width:440px">
    <button class="dwm-close" onclick="dwmClose('Contact')" aria-label="Close">×</button>
    <h3>Contact</h3>
    <p class="sub" id="dwmContactSub">Three ways to reach us</p>
    <div class="dwm-options">
      <button class="dwm-option" type="button" onclick="dwmClose('Contact');dwmOpen('Inquiry')">
        <svg viewBox="0 0 24 24"><path d="M4 4h16v12H7l-3 3z"/></svg>
        <span class="lbl">Send an Inquiry<span class="val">Project name · scope · all the details</span></span>
      </button>
      <button class="dwm-option" type="button" onclick="dwmClose('Contact');dwmOpen('Sample')">
        <svg viewBox="0 0 24 24"><rect x="4" y="6" width="16" height="14"/><path d="M4 10h16M9 6V3h6v3"/></svg>
        <span class="lbl">Request a Sample<span class="val">Memo sample · ships free · 3–5 business days</span></span>
      </button>
      <a class="dwm-option" id="dwmContactEmailLink" href="mailto:info@stringwallpaper.com">
        <svg viewBox="0 0 24 24"><path d="M3 6h18v12H3z"/><path d="M3 6l9 7 9-7"/></svg>
        <span class="lbl">Email Us<span class="val" id="dwmContactEmailLabel">info@stringwallpaper.com</span></span>
      </a>
    </div>
  </div>
</div>

<!-- Inquiry modal -->
<div class="dwm" id="dwmInquiry" onclick="if(event.target===this)dwmClose('Inquiry')">
  <div class="dwm-box">
    <button class="dwm-close" onclick="dwmClose('Inquiry')" aria-label="Close">×</button>
    <h3>Project Inquiry</h3>
    <p class="sub">We'll respond within one business day</p>
    <form id="dwmInquiryForm" onsubmit="return dwmSubmit(event,'inquiry')">
      <div class="dwm-field"><label>Name</label><input aria-label="Name" type="text" name="name" required></div>
      <div class="dwm-field"><label>Email</label><input aria-label="Email" type="email" name="email" required></div>
      <div class="dwm-field"><label>Phone</label><input aria-label="Phone" type="tel" name="phone"></div>
      <div class="dwm-field"><label>Company / Trade</label><input aria-label="Company / Trade" type="text" name="company"></div>
      <div class="dwm-field"><label>Project Name</label><input aria-label="Project Name" type="text" name="projectName" required></div>
      <div class="dwm-field"><label>Project Scope</label><textarea aria-label="Project Scope" name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
      <div class="dwm-field"><label>Additional Information</label><textarea aria-label="Additional Information" name="message" placeholder="Patterns of interest, finish preferences, budget"></textarea></div>
      <button type="submit" class="dwm-submit">Send Inquiry</button>
      <div class="dwm-status" id="dwmInquiryStatus"></div>
    </form>
  </div>
</div>

<!-- Sample-request modal -->
<div class="dwm" id="dwmSample" onclick="if(event.target===this)dwmClose('Sample')">
  <div class="dwm-box">
    <button class="dwm-close" onclick="dwmClose('Sample')" aria-label="Close">×</button>
    <h3>Request a Sample</h3>
    <p class="sub">Memo sample · ships free · 3–5 business days</p>
    <div class="dwm-preview" id="dwmSamplePreview" style="display:none">
      <img id="dwmSampleImg" src="" alt="">
      <div><div class="pn" id="dwmSampleName"></div><div class="pc" id="dwmSampleSku"></div></div>
    </div>
    <form id="dwmSampleForm" onsubmit="return dwmSubmit(event,'sample')">
      <input type="hidden" name="sku" id="dwmSampleSkuInput">
      <input type="hidden" name="title" id="dwmSampleTitleInput">
      <input type="hidden" name="image_url" id="dwmSampleImageInput">
      <div class="dwm-field"><label>Name</label><input aria-label="Name" type="text" name="name" required></div>
      <div class="dwm-field"><label>Email</label><input aria-label="Email" type="email" name="email" required></div>
      <div class="dwm-field"><label>Company / Trade</label><input aria-label="Company / Trade" type="text" name="company"></div>
      <div class="dwm-field"><label>Shipping Address</label><input aria-label="Shipping Address" type="text" name="address" placeholder="Street" required></div>
      <div class="dwm-field" style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px">
        <input type="text" name="city" placeholder="City" required>
        <input type="text" name="state" placeholder="State" required>
        <input type="text" name="zip" placeholder="ZIP" required>
      </div>
      <div class="dwm-field"><label>Notes (optional)</label><textarea aria-label="Notes (optional)" name="message" placeholder="Project, sq.ft., timing"></textarea></div>
      <button type="submit" class="dwm-submit">Send Sample Request</button>
      <div class="dwm-status" id="dwmSampleStatus"></div>
    </form>
  </div>
</div>

<script>
  // Universal modal control
  function dwmOpen(name, opts){
    document.getElementById('dwm' + name).classList.add('open');
    document.body.style.overflow = 'hidden';
    if (name === 'Sample' && opts) {
      document.getElementById('dwmSamplePreview').style.display = 'flex';
      document.getElementById('dwmSampleImg').src = opts.image_url || '';
      document.getElementById('dwmSampleName').textContent = opts.title || '';
      document.getElementById('dwmSampleSku').textContent = opts.sku || '';
      document.getElementById('dwmSampleSkuInput').value = opts.sku || '';
      document.getElementById('dwmSampleTitleInput').value = opts.title || '';
      document.getElementById('dwmSampleImageInput').value = opts.image_url || '';
    } else if (name === 'Sample') {
      document.getElementById('dwmSamplePreview').style.display = 'none';
      document.getElementById('dwmSampleSkuInput').value = '';
      document.getElementById('dwmSampleTitleInput').value = '';
      document.getElementById('dwmSampleImageInput').value = '';
    }
  }
  function dwmClose(name){ document.getElementById('dwm'+name).classList.remove('open'); document.body.style.overflow=''; }

  // Universal form submit — POSTs to /api/send-{kind}
  async function dwmSubmit(e, kind) {
    e.preventDefault();
    const form = e.target;
    const data = Object.fromEntries(new FormData(form).entries());
    const btn = form.querySelector('.dwm-submit');
    const status = form.querySelector('.dwm-status');
    btn.disabled = true; const orig = btn.textContent; btn.textContent = 'Sending…';
    status.className = 'dwm-status';
    try {
      const r = await fetch('/api/send-' + kind, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
      if (!r.ok) throw new Error('HTTP '+r.status);
      const j = await r.json();
      status.textContent = kind === 'sample' ? 'Sample request sent — ships within 3–5 business days' : 'Inquiry sent — we respond within 1 business day';
      status.className = 'dwm-status ok';
      btn.textContent = 'Sent ✓';
      setTimeout(() => { dwmClose(kind === 'sample' ? 'Sample' : 'Inquiry'); btn.disabled = false; btn.textContent = orig; status.className = 'dwm-status'; form.reset(); }, 2800);
    } catch (err) {
      status.textContent = 'Send failed — please email info@designerwallcoverings.com';
      status.className = 'dwm-status err';
      btn.disabled = false; btn.textContent = orig;
    }
    return false;
  }

  // Wire up "Memo samples ship free" badge in hero to open sample modal directly
  document.querySelectorAll('.hero .badge, [data-open-sample]').forEach(el => {
    el.style.cursor = 'pointer';
    el.addEventListener('click', () => dwmOpen('Sample'));
  });

  // Wire nav "Trade" link to inquiry modal
  document.querySelectorAll('nav a[href*="designerwallcoverings.com"]').forEach(a => {
    a.removeAttribute('target');
    a.removeAttribute('href');
    a.style.cursor = 'pointer';
    a.addEventListener('click', e => { e.preventDefault(); dwmOpen('Contact'); });
  });

  // Override product card clicks — open Sample modal pre-filled instead of redirecting to DW
  document.addEventListener('click', e => {
    const card = e.target.closest('.card, .rail-card');
    if (!card) return;
    if (card.tagName === 'A' && (card.getAttribute('href') || '').startsWith('/sample/')) {
      e.preventDefault();
      const img = card.querySelector('img');
      const pat = card.querySelector('.pat');
      const sku = card.getAttribute('href').replace('/sample/', '');
      dwmOpen('Sample', {
        sku,
        title: pat ? pat.textContent.trim() : '',
        image_url: img ? img.src : ''
      });
    }
  }, true);
// DW STANDARD: hero pulls from products.json[0].image_url so dw-collections-viewer
// drag-drop / hero-pin updates the live front page on next deploy.
// Always upgrades to the largest Shopify variant (strips _NxN size suffix) so
// the full-page hero stays sharp.
(async function hydrateHero() {
  try {
    const r = await fetch('/api/products?limit=1');
    if (!r.ok) return;
    const d = await r.json();
    let url = d.items && d.items[0] && d.items[0].image_url;
    if (!url) return;
    if (/^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(url)) {
      url = url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
      const bg = document.getElementById('heroBg');
      if (bg) bg.style.backgroundImage = "url('" + url + "')";
    }
  } catch(e){}
})();


// DW STANDARD sort-select wire-up (added by fleet patcher)
(function(){
  const sortSel = document.getElementById('sortSelect');
  if (!sortSel) return;
  const KEY = location.hostname.replace(/\./g, '_') + '_sort';
  const saved = localStorage.getItem(KEY);
  if (saved) { try { sortSel.value = saved; } catch(e){} }
  if (typeof state !== 'undefined') state.sort = sortSel.value;
  sortSel.addEventListener('change', e => {
    const v = e.target.value;
    try { localStorage.setItem(KEY, v); } catch(e){}
    if (typeof state !== 'undefined') state.sort = v;
    if (typeof syncColHeaders === 'function') syncColHeaders();
    if (typeof resetGrid === 'function') resetGrid();
    else location.reload();
  });
})();
</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:"String Wallpaper",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>
</body>
</html>