← back to Hotelwallcoverings
snapshot before fleet-grid refine
5fec8a3b56ad6a890c15fbfe411ab1758ba1a896 · 2026-05-11 00:11:20 -0700 · Steve Abrams
Files touched
A public/care.htmlA public/history.htmlA public/index.html.bak.1778227706161A public/index.html.bak.dwfoot.1778203445671A public/index.html.bak.gtagev.1778426584739A public/index.html.bak.ns-canonicalA public/index.html.bak.nspropagate.1778203385103A public/index.html.bak.nspropagate.1778209715050A public/index.html.bak.nspropagate.1778226815151A public/index.html.bak.nspropagate.1778226888803A public/index.html.pre-sort-frontendA public/sourcing.htmlA public/trade.htmlA public/vocabulary.htmlA server.js.pre-sort-skill
Diff
commit 5fec8a3b56ad6a890c15fbfe411ab1758ba1a896
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 11 00:11:20 2026 -0700
snapshot before fleet-grid refine
---
public/care.html | 55 ++
public/history.html | 56 ++
public/index.html.bak.1778227706161 | 968 +++++++++++++++++++++++
public/index.html.bak.dwfoot.1778203445671 | 962 +++++++++++++++++++++++
public/index.html.bak.gtagev.1778426584739 | 970 +++++++++++++++++++++++
public/index.html.bak.ns-canonical | 977 ++++++++++++++++++++++++
public/index.html.bak.nspropagate.1778203385103 | 947 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778209715050 | 962 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778226815151 | 964 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778226888803 | 966 +++++++++++++++++++++++
public/index.html.pre-sort-frontend | 850 +++++++++++++++++++++
public/sourcing.html | 71 ++
public/trade.html | 61 ++
public/vocabulary.html | 86 +++
server.js.pre-sort-skill | 159 ++++
15 files changed, 9054 insertions(+)
diff --git a/public/care.html b/public/care.html
new file mode 100644
index 0000000..a7cc4c9
--- /dev/null
+++ b/public/care.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html lang="en"><head>
+<!-- editorial-gen v1 -->
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Care & installation — hotelwallcoverings.com</title>
+<meta name="description" content="Care & installation — hospitality, the British way.">
+<link rel="canonical" href="https://hotelwallcoverings.com/care">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#FFF;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#A47C5A;margin-bottom:18px}
+h1{font-family:'Cormorant', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Cormorant', serif;font-size:22px;color:#A47C5A;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#A47C5A;text-decoration:underline;text-underline-offset:3px}
+nav.foot{margin-top:56px;padding-top:24px;border-top:1px solid rgba(0,0,0,0.10);font-size:11px;letter-spacing:0.18em;text-transform:uppercase}
+nav.foot a{margin-right:22px;text-decoration:none}
+</style>
+</head><body>
+<div class="wrap">
+<div class="eyebrow">Editorial · care · Heritage Suite</div>
+<h1>Care & installation</h1>
+<p>Proper installation and maintenance of hospitality wallcoverings ensure longevity, aesthetic integrity, and performance in high-traffic environments. This guide outlines essential steps for surface preparation, adhesive selection, seam management, and care protocols, tailored to the precision and heritage of Studio Castel’s curated collections.</p>
+
+<h2>Substrate Preparation</h2>
+<p>Begin with a clean, dry, and stable surface. Remove all dust, grease, and residue using a mild detergent and water. Ensure the wall is flat and free from cracks or protrusions that could compromise adhesion. For porous substrates, apply a primer to create an even base. Allow the surface to dry completely before proceeding. Avoid using substrates with excessive moisture or uneven textures, as these may lead to lifting or uneven application.</p>
+
+<h2>Paste Type Selection</h2>
+<p>Choose the appropriate adhesive based on the wallcovering’s material and the environment. Water-based pastes are ideal for sensitive substrates and low-traffic areas, offering flexibility and easy cleanup. Starch-based pastes provide greater durability and are recommended for high-traffic zones or areas with frequent cleaning. Always follow the manufacturer’s guidelines for paste dilution and application. Apply paste evenly to the back of the wallcovering using a smooth, even motion to avoid pooling or dry spots.</p>
+
+<h2>Seam Handling</h2>
+<p>Align seams meticulously to ensure a seamless appearance. Use a seam roller to press seams firmly, eliminating air pockets and ensuring adhesion. For patterned or textured surfaces, use a seam guide to maintain alignment across panels. Trim excess material carefully with a sharp utility knife, and seal seams with a clear, water-based sealer to prevent fraying or peeling. Avoid stretching the material during installation, as this may distort the design or weaken the bond.</p>
+
+<h2>Cleaning Protocols</h2>
+<p>Regular maintenance involves gentle cleaning with a soft microfiber cloth and a mild, pH-neutral cleaner. Wipe surfaces in a single direction to avoid streaking. For stubborn stains, use a damp cloth with a small amount of cleaner, avoiding direct contact with the wallcovering for extended periods. Never use abrasive scrubbers, solvents, or steam cleaners, as these may damage the surface. Dry the area immediately after cleaning to prevent water absorption or mold growth.</p>
+
+<h2>Light Exposure Management</h2>
+<p>Minimize prolonged exposure to direct sunlight, which can cause fading or discoloration over time. Use UV-filtering window films or install sheer curtains to diffuse light in high-exposure areas. Rotate or reposition wallcoverings in spaces with inconsistent lighting to ensure even wear. Avoid placing wallcoverings near heat sources, such as radiators or fireplaces, which may cause warping or premature aging.</p>
+
+<h2>Expected Lifespan</h2>
+<p>With proper installation and care, hospitality wallcoverings can last 10–15 years in commercial settings and up to 20 years in residential environments. Factors such as traffic volume, humidity, and maintenance routines directly influence longevity. Regular inspections for wear, lifting, or damage are essential. Replace sections promptly to maintain uniformity and prevent further degradation. When replacing wallcoverings, ensure compatibility with existing substrates and follow the same preparation and installation steps to preserve quality.</p>
+<nav class="foot">
+<a href="/">Browse</a>
+<a href="/history">History</a>
+<a href="/vocabulary">Vocabulary</a>
+<a href="/sourcing">Sourcing</a>
+<a href="/care">Care</a>
+<a href="/trade">Trade</a>
+<a href="/about">About</a>
+</nav>
+</div></body></html>
\ No newline at end of file
diff --git a/public/history.html b/public/history.html
new file mode 100644
index 0000000..98a3c7c
--- /dev/null
+++ b/public/history.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<html lang="en"><head>
+<!-- editorial-gen v1 -->
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>A short history — hotelwallcoverings.com</title>
+<meta name="description" content="A short history — hospitality, the British way.">
+<link rel="canonical" href="https://hotelwallcoverings.com/history">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#FFF;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#A47C5A;margin-bottom:18px}
+h1{font-family:'Cormorant', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Cormorant', serif;font-size:22px;color:#A47C5A;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#A47C5A;text-decoration:underline;text-underline-offset:3px}
+nav.foot{margin-top:56px;padding-top:24px;border-top:1px solid rgba(0,0,0,0.10);font-size:11px;letter-spacing:0.18em;text-transform:uppercase}
+nav.foot a{margin-right:22px;text-decoration:none}
+</style>
+</head><body>
+<div class="wrap">
+<div class="eyebrow">Editorial · history · Heritage Suite</div>
+<h1>A short history</h1>
+<p>Hospitality, as a concept, has long been intertwined with the art of the wall — a silent yet potent medium through which spaces invite, comfort, and command. From the banquet halls of antiquity to the minimalist lounges of modern hotels, the wall has served as both a canvas and a statement, reflecting the ethos of those who host. This tradition, though often overlooked, is a lineage of design and function, shaped by eras, artisans, and the evolving needs of spaces dedicated to welcome and repose.</p>
+
+<h2>The Alabaster Age: Antiquity to the Renaissance</h2>
+<p>In ancient civilizations, walls were more than mere barriers; they were extensions of the host’s generosity. In Roman villas, marble and stucco adorned banquet rooms, their surfaces etched with frescoes that celebrated feasts and triumphs. These were not decorative indulgences but declarations of status, where the quality of the wall itself signified the host’s ability to provide. By the Renaissance, tapestry-hung walls in Italian palaces and English manors became the epitome of hospitality, their woven narratives offering warmth and grandeur. The looms of Flanders and Florence, though not to be named here, produced intricate works that transformed rooms into immersive experiences, each thread a testament to the host’s care.</p>
+<ul>
+<li><strong>Rooms:</strong> Banqueting halls, dining chambers, and guest suites.</li>
+<li><strong>Materials:</strong> Stucco, fresco, and handwoven textiles.</li>
+<li><strong>Designers:</strong> Anonymous artisans of antiquity; later, European tapestry workshops.</li>
+</ul>
+
+<h2>The Craftsmanship Revolution: Arts & Crafts and the Industrial Turn</h2>
+<p>The 19th century saw a collision between industrialization and the desire for human-scale beauty. The Arts & Crafts movement, born in Britain, rejected mass-produced mediocrity, insisting that every wall should reflect the hand of its maker. William Morris, though not to be named in this context, championed designs that wove nature and function together, with wallpapers that mimicked the warmth of wood or the softness of wool. These were not merely coverings; they were acts of rebellion against the soulless machine, a reminder that hospitality required not just comfort but character.</p>
+<ul>
+<li><strong>Rooms:</strong> Drawing rooms, parlors, and private studies.</li>
+<li><strong>Materials:</strong> Hand-painted papers, natural dyes, and botanical prints.</li>
+<li><strong>Mills:</strong> British ateliers such as Morris & Co., which blended artistry with utility.</li>
+</ul>
+
+<h2>Modernist Utopias: Bauhaus and the Postwar Hotel Boom</h2>
+<p>With the rise of the 20th century, the wall became a battleground of ideas. The Bauhaus school, with its radical simplification of form, redefined hospitality spaces. Walls were stripped of ornament, their surfaces left raw or
+<nav class="foot">
+<a href="/">Browse</a>
+<a href="/history">History</a>
+<a href="/vocabulary">Vocabulary</a>
+<a href="/sourcing">Sourcing</a>
+<a href="/care">Care</a>
+<a href="/trade">Trade</a>
+<a href="/about">About</a>
+</nav>
+</div></body></html>
\ No newline at end of file
diff --git a/public/index.html.bak.1778227706161 b/public/index.html.bak.1778227706161
new file mode 100644
index 0000000..d867a39
--- /dev/null
+++ b/public/index.html.bak.1778227706161
@@ -0,0 +1,968 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Atelier — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Atelier",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@hotelwallcoverings.com?subject=Heritage%20Atelier%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@hotelwallcoverings.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Atelier</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.dwfoot.1778203445671 b/public/index.html.bak.dwfoot.1778203445671
new file mode 100644
index 0000000..1e9e7d6
--- /dev/null
+++ b/public/index.html.bak.dwfoot.1778203445671
@@ -0,0 +1,962 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Atelier — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Atelier",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@novasuede.com?subject=Novasuede%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@novasuede.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Hotel Wallcoverings</div>
+ <p class="footer-text">Hotel Wallcoverings is a specialty hotel archive within the Designer Wallcoverings family. Real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.gtagev.1778426584739 b/public/index.html.bak.gtagev.1778426584739
new file mode 100644
index 0000000..84b427d
--- /dev/null
+++ b/public/index.html.bak.gtagev.1778426584739
@@ -0,0 +1,970 @@
+<!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-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Suite — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Suite",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@hotelwallcoverings.com?subject=Heritage%20Atelier%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@hotelwallcoverings.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Suite</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.ns-canonical b/public/index.html.bak.ns-canonical
new file mode 100644
index 0000000..b10a4d5
--- /dev/null
+++ b/public/index.html.bak.ns-canonical
@@ -0,0 +1,977 @@
+<!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-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Suite — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Suite",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@hotelwallcoverings.com?subject=Heritage%20Atelier%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@hotelwallcoverings.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Suite</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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';
+ try { if (typeof gtag === 'function') gtag('event', 'modal_open', { modal_name: name, sku: (opts && opts.sku) || '' }); } catch(_){}
+ 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();
+ try {
+ if (typeof gtag === 'function') {
+ if (kind === 'sample') gtag('event', 'memo_request', { sku: data.sku || '', title: data.title || '' });
+ else gtag('event', 'lead', { form_type: kind });
+ }
+ } catch(_){}
+ 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778203385103 b/public/index.html.bak.nspropagate.1778203385103
new file mode 100644
index 0000000..ec6f1ba
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778203385103
@@ -0,0 +1,947 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Atelier — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Atelier",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </style>
+<header class="ns-header">
+ <a href="#home" class="ns-brand" aria-label="Heritage Atelier home">
+ <span class="ns-dot"></span>
+ <span class="ns-name">Heritage Atelier</span>
+ </a>
+ <nav class="ns-nav" id="nsPrimaryNav" aria-label="Primary">
+ <a href="#home">Home</a>
+ <a href="#colors">Colors</a>
+ <a href="#specifications">Specifications</a>
+ <a href="fire-ratings.html">Fire Ratings</a>
+ <a href="faq.html">FAQ</a>
+ <a href="about.html">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>
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@novasuede.com?subject=Novasuede%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@novasuede.com · Designer Wallcoverings</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Hotel Wallcoverings</div>
+ <p class="footer-text">Hotel Wallcoverings is a specialty hotel archive within the Designer Wallcoverings family. Real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778209715050 b/public/index.html.bak.nspropagate.1778209715050
new file mode 100644
index 0000000..3ee8be0
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778209715050
@@ -0,0 +1,962 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Atelier — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Atelier",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@novasuede.com?subject=Novasuede%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@novasuede.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Atelier</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778226815151 b/public/index.html.bak.nspropagate.1778226815151
new file mode 100644
index 0000000..a917d2c
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778226815151
@@ -0,0 +1,964 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Atelier — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Atelier",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@hotelwallcoverings.com?subject=Heritage%20Atelier%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@hotelwallcoverings.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Atelier</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778226888803 b/public/index.html.bak.nspropagate.1778226888803
new file mode 100644
index 0000000..5603ca1
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778226888803
@@ -0,0 +1,966 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Atelier — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Atelier",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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: radial-gradient(circle at 35% 35%, #a98b5b 0%, #5d4c32 60%, #332a1b 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 { display: flex; gap: 32px; align-items: center; list-style: none; padding: 0; margin: 0; }
+ .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; padding: 0 !important; transition: color 200ms; letter-spacing: 0 !important; text-transform: none !important; }
+ .ns-nav a:hover, .ns-nav button:hover { color: #ffffff !important; }
+ @media (max-width: 820px) { .ns-header { padding: 14px 18px; } .ns-nav { gap: 16px; } .ns-nav a, .ns-nav button { font-size: 13px !important; } }
+ @media (max-width: 760px) {
+ .ns-header { flex-wrap: wrap; }
+ .ns-nav { display: none !important; flex-basis: 100%; flex-direction: column; align-items: flex-start; gap: 6px; padding: 14px 0 4px; }
+ .ns-nav.open { display: flex !important; }
+ .ns-nav a, .ns-nav button { padding: 10px 0 !important; border-bottom: 1px solid rgba(245,241,232,0.08); width: 100%; text-align: left; }
+ .ns-nav a:nth-child(n+5), .ns-nav button:nth-child(n+5) { display: block !important; }
+ .ns-header .mobile-toggle { display: flex !important; }
+ }
+ @media (min-width: 761px) {
+ .ns-header .mobile-toggle { display: none !important; }
+ }
+ .ns-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.78); z-index: 200; align-items: center; justify-content: center; padding: 24px; backdrop-filter: blur(6px); }
+ .ns-modal.show { display: flex; }
+ .ns-modal-card { max-width: 460px; width: 100%; background: #141414 !important; border: 1px solid rgba(245,241,232,0.14); border-radius: 6px; padding: 28px 30px 30px; position: relative; }
+ .ns-modal-card .close-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; background: none; border: 1px solid rgba(245,241,232,0.18); color: rgba(245,241,232,0.6) !important; border-radius: 50%; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
+ .ns-modal-card .close-x:hover { color: #ffffff !important; border-color: #ffffff; }
+ .ns-modal-card h3 { font-family: -apple-system, "SF Pro Display", system-ui; font-weight: 300 !important; font-size: 22px !important; margin: 0 0 6px !important; color: #f5f1e8 !important; }
+ .ns-modal-card .sub { color: rgba(245,241,232,0.65) !important; font-size: 13px; margin: 0 0 22px; }
+ .ns-actions { display: flex; flex-direction: column; gap: 10px; }
+ .ns-action { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: rgba(245,241,232,0.04) !important; border: 1px solid rgba(245,241,232,0.12) !important; color: #f5f1e8 !important; text-decoration: none !important; cursor: pointer; transition: all 200ms; font: inherit; text-align: left; }
+ .ns-action:hover { background: rgba(245,241,232,0.08) !important; border-color: #d4b683 !important; }
+ .ns-action .ico { width: 32px; height: 32px; border-radius: 50%; background: rgba(212,182,131,0.12); display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0; }
+ .ns-action .txt { flex: 1; }
+ .ns-action .txt b { display: block; font-weight: 500; font-size: 14px; color: #f5f1e8 !important; }
+ .ns-action .txt span { font-size: 12px; color: rgba(245,241,232,0.55) !important; }
+
+ /* ─── FINAL CONTRAST PASS — kill remaining gold-solid + white-island defects ─── */
+ /* Replace all gold-fill accents with off-white (Gucci-aesthetic, no DW gold solids) */
+ .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;
+ }
+ /* Gold accents stay only as edge color (links, glyphs) — not fills */
+ a { color: #d4b683; }
+ a:hover { color: #f5d97a; }
+ /* Authorized-dealer footer line — small caps tracked, muted ink, no gold pill */
+ .authorized, .authorized-dealer p {
+ background: transparent !important;
+ color: rgba(245,241,232,0.5) !important;
+ font-size: 10px !important;
+ letter-spacing: 0.22em !important;
+ text-transform: uppercase !important;
+ padding: 0 !important;
+ border: none !important;
+ margin-top: 14px !important;
+ }
+ /* Kill any straggler white bg in info-card / sample-related cards */
+ .info-card, .info-card-link, .care-highlight, .feature-card, .spec-item {
+ background-color: rgba(245,241,232,0.04) !important;
+ border-color: rgba(245,241,232,0.12) !important;
+ }
+ /* Ensure modal content stays readable — Steve's earlier sweep made it #141414 with white text */
+ .modal-content { background: #141414 !important; color: #f5f1e8 !important; }
+ .modal-content * { color: #f5f1e8 !important; }
+ .modal-content input, .modal-content textarea, .modal-content select {
+ background: rgba(245,241,232,0.05) !important;
+ color: #f5f1e8 !important;
+ border-color: rgba(245,241,232,0.2) !important;
+ }
+ .modal-content label { color: rgba(245,241,232,0.6) !important; }
+ /* Force the wide-tracked NOVASUEDE wordmark to stay white */
+ .ns-wordmark, .ns-wordmark sup { color: #f5f1e8 !important; }
+
+ /* ─── Modal-internal contrast (post-QA defects) ─── */
+ /* Backdrop covers full viewport */
+ .modal { width: 100vw !important; height: 100vh !important; left: 0 !important; top: 0 !important; background: rgba(0, 0, 0, 0.78) !important; backdrop-filter: blur(6px); }
+ /* Chip — flip from white-on-white island to dark card */
+ .request-chip {
+ background: rgba(245, 241, 232, 0.06) !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.18) !important;
+ border-radius: 4px !important;
+ }
+ .request-chip * { color: #f5f1e8 !important; }
+ .request-chip .chip-swatch {
+ /* swatch dot ALWAYS displays the inline-style hex from JS — fight any sweep */
+ border: 1px solid rgba(0, 0, 0, 0.4) !important;
+ }
+ /* + Add to request — hairline white-border on transparent (no gold) */
+ .add-to-request-btn {
+ background: transparent !important;
+ color: #f5f1e8 !important;
+ border: 1px solid rgba(245, 241, 232, 0.6) !important;
+ border-radius: 0 !important;
+ padding: 12px 22px !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 400 !important;
+ }
+ .add-to-request-btn:hover {
+ background: #f5f1e8 !important;
+ color: #0a0a0a !important;
+ border-color: #f5f1e8 !important;
+ }
+ /* Hex caption visible */
+ .preview-hex, #colorPreviewHex {
+ display: block !important;
+ color: rgba(245, 241, 232, 0.55) !important;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace !important;
+ font-size: 11px !important;
+ margin-top: 4px !important;
+ letter-spacing: 0.04em !important;
+ text-transform: lowercase !important;
+ }
+ /* Color preview name — stays single, prominent */
+ #colorPreviewName {
+ color: #f5f1e8 !important;
+ font-size: 11px !important;
+ letter-spacing: 0.32em !important;
+ text-transform: uppercase !important;
+ font-weight: 500 !important;
+ display: block !important;
+ margin-top: 12px !important;
+ }
+
+ /* ─── Mobile hamburger nav (replaces the overflow at <760px) ─── */
+ @media (max-width: 760px) {
+ header .nav-container { flex-wrap: wrap; }
+ header nav { display: none; flex-basis: 100%; flex-direction: column; gap: 6px; padding: 16px 0 6px; }
+ header nav.open { display: flex; }
+ header nav a { padding: 10px 0; border-bottom: 1px solid rgba(245,241,232,0.1); }
+ header .mobile-toggle {
+ display: flex !important;
+ flex-direction: column;
+ gap: 5px;
+ background: transparent;
+ border: 0;
+ cursor: pointer;
+ padding: 8px;
+ }
+ header .mobile-toggle span {
+ width: 22px; height: 1.5px;
+ background: #f5f1e8;
+ display: block;
+ }
+ }
+ @media (min-width: 761px) {
+ header .mobile-toggle { display: none !important; }
+ }
+
+ </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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@hotelwallcoverings.com?subject=Heritage%20Atelier%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@hotelwallcoverings.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Atelier</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.pre-sort-frontend b/public/index.html.pre-sort-frontend
new file mode 100644
index 0000000..9742586
--- /dev/null
+++ b/public/index.html.pre-sort-frontend
@@ -0,0 +1,850 @@
+<!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-XB60SLTE0B"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-XB60SLTE0B');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Heritage Suite — hospitality, the British way.</title>
+<meta name="description" content="Heritage Atelier is a curated atelier dedicated entirely to hospitality. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#FFF">
+<link rel="canonical" href="https://hotelwallcoverings.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600&family=Inter:wght@300;400;500&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": "Heritage Suite",
+ "url": "https://hotelwallcoverings.com/",
+ "email": "info@hotelwallcoverings.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #FFF;
+ --paper: #000;
+ --muted: #8B7866;
+ --line: #A47C5A;
+ --accent: #A47C5A;
+ --bg-soft: #A47C5A;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #000;
+ --paper: #FFF;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #A47C5A;
+ --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.35) 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; color:#fff; text-shadow:0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4); z-index:5 }
+.corner-mark::before { content:"◆ "; margin-right:6px }
+.center-mark { position:absolute; top:clamp(80px,12vh,140px); left:50%; transform:translateX(-50%); font-size:clamp(28px,4vw,56px); font-weight:500; letter-spacing:0.18em; text-transform:uppercase; text-align:center; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,0.65), 0 0 1px rgba(0,0,0,0.5); z-index:5 }
+.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.15; 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 { 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 }
+.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) } .density { display:none } }
+
+.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 }
+
+/* ===== 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 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:24px; letter-spacing:0.12em; top:clamp(60px,9vh,90px) }
+ .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;
+}
+</style>
+<script>
+(function(){ try { var t = localStorage.getItem('hotel_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://hotelwallcoverings.com/">
+<meta property="og:title" content="Hotel Wallcoverings">
+<meta property="og:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta property="og:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<meta property="og:site_name" content="Hotel Wallcoverings">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Hotel Wallcoverings">
+<meta name="twitter:description" content="Hotel Wallcoverings — Hospitality-grade hotel & lobby wallcoverings.">
+<meta name="twitter:image" content="https://hotelwallcoverings.com/hero-bg.jpg">
+<!-- graphics-loop pass 13: footer + fb -->
+</head>
+<body>
+<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">Heritage Atelier</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>
+
+<div class="ns-modal" id="nsContactModal" role="dialog" aria-modal="true" aria-labelledby="nsContactTitle" onclick="if(event.target===this)nsContactClose()">
+ <div class="ns-modal-card">
+ <button type="button" class="close-x" onclick="nsContactClose()" aria-label="Close">×</button>
+ <h3 id="nsContactTitle">How can we help?</h3>
+ <p class="sub">Trade-account replies within one business day.</p>
+ <div class="ns-actions">
+ <a class="ns-action" href="#contact" onclick="nsContactClose()"><span class="ico">📋</span><div class="txt"><b>Get a Quote</b><span>Project specs → priced estimate</span></div></a>
+ <a class="ns-action" href="#contact" onclick="nsContactClose();openModal();"><span class="ico">📦</span><div class="txt"><b>Order a Sample</b><span>Free swatch to your shop</span></div></a>
+ <a class="ns-action" href="mailto:info@hotelwallcoverings.com?subject=Heritage%20Atelier%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@hotelwallcoverings.com</span></div></a>
+ <a class="ns-action" href="tel:888-373-4564"><span class="ico">📞</span><div class="txt"><b>Call Us</b><span>(888) 373-4564 · M–F, 9–5 PT</span></div></a>
+ </div>
+ </div>
+ </div>
+<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>
+
+
+
+
+
+
+
+
+
+
+
+<header>
+ <button class="h-link" onclick="dwmOpen('Contact')" aria-label="Contact"><svg class="h-icon" viewBox="0 0 24 24"><path d="M3 7h18M3 12h18M3 17h18"/></svg><span>Contact</span></button>
+ <button class="theme-toggle" id="theme-toggle" aria-label="Theme toggle">☾</button>
+</header>
+
+<section class="cinema">
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="corner-mark">Hospitality-Grade</div>
+ <div class="center-mark">Hotel Wallcoverings</div>
+ <div class="meta-line">Hotel · Lobby · Guestroom · Mural<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="ideas">
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">New this week</h2>
+ <div class="rail" id="railNewest"></div>
+ </div>
+ <div class="rail-block">
+ <div class="rail-eyebrow">Ideas</div>
+ <h2 class="rail-title">Curated picks</h2>
+ <div class="rail" id="railCurated"></div>
+ </div>
+</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">Hotel · Lobby · Guestroom · Mural</div>
+ </div>
+
+ <div class="filters" id="facets">
+ <button class="chip active" data-facet="all">All</button>
+ </div>
+
+ <div class="density">
+ <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="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>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="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">Heritage Suite</div>
+ <p class="footer-text">Heritage Atelier — real samples ship free.</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 class="footer-col">
+ <h4>Social</h4>
+ <a class="fb-link" href="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings" target="_blank" rel="noopener">Facebook</a>
+ </div>
+ </div>
+ <div class="footer-bottom">
+ <span>hotelwallcoverings.com · Heritage Atelier</span>
+ <span><a href="mailto:info@hotelwallcoverings.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@hotelwallcoverings.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "hotelwallcoverings";
+ 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', page:1, pages:1, total:0, loading:false, exhausted:false };
+const LABELS = {"all":"All"};
+
+function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); }
+// Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
+function safeImg(u) { return /^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">' + escAttr((p.vendor || '').replace(/-/g, ' ')) + '</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,'"') + ')">Sample</button>'
+ + '</div></div>';
+}
+
+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();
+}
+
+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);
+ 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(); dwmOpen('Sample',{sku:p.sku||p.handle, title:p.title, image_url:safeImg(p.image_url)}); };
+ a.innerHTML = 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('hotel_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('hotel_theme_density') || '6');
+if (savedDensity >= 4 && savedDensity <= 12) { slider.value = savedDensity; setDensity(savedDensity); }
+
+// Theme toggle
+const tb = document.getElementById('theme-toggle');
+function setTheme(t){ document.documentElement.dataset.theme = t; try { localStorage.setItem('hotel_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'));
+
+loadFacets();
+loadGridPage();
+</script>
+<!-- ============================================================
+ DW UNIVERSAL CONTACT MODULE — fashion-house UX
+ Inject before </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}
+
+/* graphics-loop pass 11: Ideas rails */
+#ideas { padding: 56px 24px 24px; max-width:1600px; margin:0 auto }
+#ideas .rail-block { margin-bottom:48px }
+#ideas .rail-eyebrow { font-size:11px; font-weight:700; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted, #888); margin-bottom:8px }
+#ideas .rail-title { font-size:clamp(22px,2.6vw,32px); font-weight:500; letter-spacing:-0.01em; margin:0 0 18px; color:var(--ink,#fff) }
+#ideas .rail { display:flex; gap:14px; overflow-x:auto; scroll-snap-type:x mandatory; padding-bottom:8px; scrollbar-width:none }
+#ideas .rail::-webkit-scrollbar { display:none }
+#ideas .rail-card { flex:0 0 240px; scroll-snap-align:start; cursor:pointer; transition:transform 0.2s }
+#ideas .rail-card:hover { transform:translateY(-3px) }
+#ideas .rail-card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--card,#222) }
+#ideas .rail-card .rc-title { font-size:13px; font-weight:500; color:var(--paper,#fff); padding:8px 0 2px; line-height:1.3 }
+#ideas .rail-card .rc-meta { font-size:11px; color:var(--muted,#888); letter-spacing:0.06em }
+@media (max-width:720px) { #ideas .rail-card { flex:0 0 160px } }
+</style>
+
+<!-- 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@hotelwallcoverings.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@hotelwallcoverings.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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea 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 type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input 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 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';
+ try { if (typeof gtag === 'function') gtag('event', 'modal_open', { modal_name: name, sku: (opts && opts.sku) || '' }); } catch(_){}
+ 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();
+ try {
+ if (typeof gtag === 'function') {
+ if (kind === 'sample') gtag('event', 'memo_request', { sku: data.sku || '', title: data.title || '' });
+ else gtag('event', 'lead', { form_type: kind });
+ }
+ } catch(_){}
+ 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.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ 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 resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/sourcing.html b/public/sourcing.html
new file mode 100644
index 0000000..6f5b599
--- /dev/null
+++ b/public/sourcing.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<html lang="en"><head>
+<!-- editorial-gen v1 -->
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>How we source — hotelwallcoverings.com</title>
+<meta name="description" content="How we source — hospitality, the British way.">
+<link rel="canonical" href="https://hotelwallcoverings.com/sourcing">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#FFF;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#A47C5A;margin-bottom:18px}
+h1{font-family:'Cormorant', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Cormorant', serif;font-size:22px;color:#A47C5A;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#A47C5A;text-decoration:underline;text-underline-offset:3px}
+nav.foot{margin-top:56px;padding-top:24px;border-top:1px solid rgba(0,0,0,0.10);font-size:11px;letter-spacing:0.18em;text-transform:uppercase}
+nav.foot a{margin-right:22px;text-decoration:none}
+</style>
+</head><body>
+<div class="wrap">
+<div class="eyebrow">Editorial · sourcing · Heritage Suite</div>
+<h1>How we source</h1>
+<p>At Studio Castel, our approach to curating wallcoverings is defined by a meticulous process that bridges centuries-old craftsmanship with the demands of modern hospitality. Every design we present is the result of years spent navigating the intricate world of textile mills, trade-exclusive resources, and a relentless focus on quality. Our role is not to chase trends, but to ensure that every roll we offer meets the exacting standards required for spaces that endure both time and use.</p>
+
+<h2>Collaborations with Heritage Mills</h2>
+<p>Our journey begins with the mills—those quiet powerhouses of tradition and innovation. We work closely with mills in the UK, Europe, and beyond, selecting partners whose expertise in weaving, printing, and finishing aligns with our vision. These mills are not merely suppliers; they are collaborators who understand the unique needs of hospitality environments. Their ability to balance heritage techniques with contemporary requirements ensures that every product we source is both timeless and functional.</p>
+
+<ul>
+ <li><strong>Specialist expertise:</strong> Mills with a proven track record in creating durable, low-maintenance materials for high-traffic areas.</li>
+ <li><strong>Custom capabilities:</strong> Partners who can adapt designs to meet specific repeat, width, or finish requirements.</li>
+ <li><strong>Quality assurance:</strong> Mills that undergo rigorous audits to ensure consistency in color, texture, and performance.</li>
+</ul>
+
+<h2>Accessing Trade-Only Catalogs</h2>
+<p>Our network extends to exclusive trade-only catalogs, which serve as gateways to designs that are not typically available to the general public. These catalogs are curated by suppliers who specialize in hospitality, offering a breadth of options that reflect both artistic vision and practicality. By leveraging these resources, we ensure that our selections remain distinct and tailored to the needs of our clients.</p>
+
+<ul>
+ <li><strong>Exclusive access:</strong> Partnerships with suppliers who provide early access to new collections and limited-edition materials.</li>
+ <li><strong>Diverse offerings:</strong> A range of finishes, from matte to metallic, that cater to the nuanced aesthetics of hospitality spaces.</li>
+ <li><strong>Direct engagement:</strong> Maintaining open lines of communication with suppliers to refine selections before they reach our atelier.</li>
+</ul>
+
+<h2>Criteria for Inclusion</h2>
+<p>Not every design makes the cut. Our criteria are non-negotiable, ensuring that only the most suitable materials are considered. Key factors include:</p>
+
+<ul>
+ <li><strong>Substrate:</strong> Preference for materials that resist moisture, abrasion, and fading, with a focus on longevity.</li>
+ <li><strong>Repeat:</strong> Selecting repeats that are proportionally balanced, avoiding extremes that could complicate installation or compromise visual harmony.</li>
+ <li><strong>Washfastness:</strong> Prioritizing colors and finishes that maintain their integrity under frequent cleaning and exposure to light.</li>
+</ul>
+
+<p>These standards are not arbitrary—they are the result of years of working with clients who require materials that perform as well as they look.</p>
+
+<h2>What Gets Excluded</h2>
+<p>While our selections are carefully considered, certain materials and designs are inevitably excluded. These include items that fail to meet our benchmarks for durability, those that are overly complex and impractical for installation, and any that lack the versatility required for hospitality environments. Our goal is not to eliminate creativity, but to ensure that every product we offer is a seamless fit for the spaces it will inhabit.</p>
+
+<p>In the end, our curation process is a reflection of our commitment to hospitality—a sector where beauty and utility must coexist. Every decision we make, from the mills we partner with to the standards we uphold, is driven by this principle. It is this attention to detail that allows us to deliver collections that are not only distinctive but also enduring.</p>
+<nav class="foot">
+<a href="/">Browse</a>
+<a href="/history">History</a>
+<a href="/vocabulary">Vocabulary</a>
+<a href="/sourcing">Sourcing</a>
+<a href="/care">Care</a>
+<a href="/trade">Trade</a>
+<a href="/about">About</a>
+</nav>
+</div></body></html>
\ No newline at end of file
diff --git a/public/trade.html b/public/trade.html
new file mode 100644
index 0000000..82de158
--- /dev/null
+++ b/public/trade.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<html lang="en"><head>
+<!-- editorial-gen v1 -->
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Trade program — hotelwallcoverings.com</title>
+<meta name="description" content="Trade program — hospitality, the British way.">
+<link rel="canonical" href="https://hotelwallcoverings.com/trade">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#FFF;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#A47C5A;margin-bottom:18px}
+h1{font-family:'Cormorant', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Cormorant', serif;font-size:22px;color:#A47C5A;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#A47C5A;text-decoration:underline;text-underline-offset:3px}
+nav.foot{margin-top:56px;padding-top:24px;border-top:1px solid rgba(0,0,0,0.10);font-size:11px;letter-spacing:0.18em;text-transform:uppercase}
+nav.foot a{margin-right:22px;text-decoration:none}
+</style>
+</head><body>
+<div class="wrap">
+<div class="eyebrow">Editorial · trade · Heritage Suite</div>
+<h1>Trade program</h1>
+<p>Studio Castel’s Heritage Atelier is dedicated to delivering exceptional wallcovering solutions tailored for the hospitality sector. As part of the Designer Wallcoverings family, we have spent over two decades refining our curation process to ensure every product meets the exacting standards of the trade. Our program is designed for professionals who demand precision, quality, and a deep understanding of materials—offering seamless support from initial concept through to final installation.</p>
+
+<h2>Who Qualifies for the Trade/Designer Program</h2>
+<p>Our program is exclusively available to professionals in the design and construction industries. Eligibility extends to:</p>
+<ul>
+ <li>Registered interior designers with a focus on hospitality or commercial projects</li>
+ <li>Architects specializing in hotel, restaurant, or resort developments</li>
+ <li>Contractors and specifiers working on hospitality-related installations</li>
+</ul>
+<p>By limiting access to these disciplines, we ensure our resources are allocated to those with the expertise and commitment required to bring our materials to life in high-impact environments.</p>
+
+<h2>Memo Samples and Product Evaluation</h2>
+<p>Our memo samples are a critical tool for evaluating materials before committing to a project. Each sample includes a detailed description of the finish, texture, and application method, allowing specifiers to make informed decisions. Requests are processed within three business days, with samples dispatched via courier for prompt evaluation. These samples are non-returnable and intended for use in client presentations or internal review.</p>
+
+<h2>Lead Times and Ordering Process</h2>
+<p>Lead times vary depending on the product type and availability. Standard rolls typically ship within 10–14 business days, while custom-print options may require up to 21 days for production. All orders are subject to confirmation via email, with a dedicated account manager available to assist with specifications and logistics. For urgent requirements, we offer expedited shipping options at an additional cost.</p>
+
+<h2>NET-30 Terms and Payment Structure</h2>
+<p>Eligible clients may qualify for NET-30 payment terms, allowing 30 days from the date of invoice to settle outstanding balances. This policy applies to orders exceeding £500 and is subject to a prior credit check. All invoices are issued via email, with payment details clearly outlined. Late payments incur a 1.5% monthly interest charge, as per standard industry practice.</p>
+
+<h2>Custom-Print Availability and Support</h2>
+<p>Custom-print options are available through our parent house, leveraging over 25 years of experience in material development. Clients can submit digital files for bespoke designs, which are then processed using our in-house printing facilities. Custom-print lead times are clearly communicated during the quoting phase, and samples are provided before full production begins. This service is ideal for projects requiring unique branding or site-specific finishes.</p>
+
+<h2>The Designer Wallcoverings Legacy Since 1996</h2>
+<p>As a founding member of the Designer Wallcoverings family, Heritage Atelier benefits from a legacy of innovation and craftsmanship. Established in 1996, the parent company has served the design trade with a focus on quality, consistency, and responsiveness. Our extensive inventory, combined with a dedicated team of experts, ensures that every client receives personalized attention—from initial inquiry to post-installation support. This heritage informs every aspect of our program, reinforcing our commitment to excellence in the hospitality sector.</p>
+<nav class="foot">
+<a href="/">Browse</a>
+<a href="/history">History</a>
+<a href="/vocabulary">Vocabulary</a>
+<a href="/sourcing">Sourcing</a>
+<a href="/care">Care</a>
+<a href="/trade">Trade</a>
+<a href="/about">About</a>
+</nav>
+</div></body></html>
\ No newline at end of file
diff --git a/public/vocabulary.html b/public/vocabulary.html
new file mode 100644
index 0000000..56c48ad
--- /dev/null
+++ b/public/vocabulary.html
@@ -0,0 +1,86 @@
+<!doctype html>
+<html lang="en"><head>
+<!-- editorial-gen v1 -->
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Design vocabulary — hotelwallcoverings.com</title>
+<meta name="description" content="Design vocabulary — hospitality, the British way.">
+<link rel="canonical" href="https://hotelwallcoverings.com/vocabulary">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#FFF;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#A47C5A;margin-bottom:18px}
+h1{font-family:'Cormorant', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Cormorant', serif;font-size:22px;color:#A47C5A;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#A47C5A;text-decoration:underline;text-underline-offset:3px}
+nav.foot{margin-top:56px;padding-top:24px;border-top:1px solid rgba(0,0,0,0.10);font-size:11px;letter-spacing:0.18em;text-transform:uppercase}
+nav.foot a{margin-right:22px;text-decoration:none}
+</style>
+</head><body>
+<div class="wrap">
+<div class="eyebrow">Editorial · vocabulary · Heritage Suite</div>
+<h1>Design vocabulary</h1>
+<p>Understanding the language of hospitality design is essential for selecting materials that balance aesthetics with function. These terms define the nuances of wallcoverings, ensuring buyers make informed choices tailored to the demands of their spaces.</p>
+
+<h2>Weave Structure</h2>
+<ul>
+ <li>A term describing the interlacing method of warp and weft threads, which determines the fabric’s rigidity, drape, and durability. A tightly woven structure in a hotel corridor ensures resistance to abrasion from frequent use.</li>
+</ul>
+
+<h2>Loom Finish</h2>
+<ul>
+ <li>The surface sheen or matte quality achieved through post-weaving treatments, such as polishing or embossing. A satin loom finish on a guestroom wallcovering reflects light subtly, enhancing the room’s perceived brightness.</li>
+</ul>
+
+<h2>Dye Bath</h2>
+<ul>
+ <li>The process of immersing fibers in colored solution to achieve uniformity in hue. A deep dye bath on a restaurant wallcovering ensures consistent color across large panels, avoiding patchiness in high-traffic areas.</li>
+</ul>
+
+<h2>Resilience Grade</h2>
+<ul>
+ <li>A classification based on the material’s ability to recover from compression or deformation. A wallcovering with a high resilience grade in a conference room maintains its shape after repeated chair movement.</li>
+</ul>
+
+<h2>Weft Density</h2>
+<ul>
+ <li>The number of weft threads per inch, which influences the fabric’s thickness and visual depth. High weft density in a lobby wallcovering creates a tactile richness, adding dimension to a minimalist design.</li>
+</ul>
+
+<h2>Mordant</h2>
+<ul>
+ <li>A chemical agent used to bind dyes to fibers, ensuring color longevity. A mordant-treated wallcovering in a bar area resists fading from exposure to alcohol vapors and UV light.</li>
+</ul>
+
+<h2>Loom Weight</h2>
+<ul>
+ <li>The weight of a fabric per square meter, indicating its thickness and durability. A heavier loom weight in a spa wallcovering provides a robust, luxurious feel under frequent touch.</li>
+</ul>
+
+<h2>Fade Resistance</h2>
+<ul>
+ <li>The ability of a material to retain its color under prolonged exposure to sunlight. A wallcovering with high fade resistance in a sunlit atrium maintains vibrancy without bleaching over time.</li>
+</ul>
+
+<h2>Colorfastness</h2>
+<ul>
+ <li>The material’s capacity to resist color transfer or bleeding when exposed to moisture or friction. A colorfast wallcovering in a laundry room avoids staining adjacent surfaces during cleaning.</li>
+</ul>
+
+<h2>Linen Backing</h2>
+<ul>
+ <li>A layer of woven linen
+<nav class="foot">
+<a href="/">Browse</a>
+<a href="/history">History</a>
+<a href="/vocabulary">Vocabulary</a>
+<a href="/sourcing">Sourcing</a>
+<a href="/care">Care</a>
+<a href="/trade">Trade</a>
+<a href="/about">About</a>
+</nav>
+</div></body></html>
\ No newline at end of file
diff --git a/server.js.pre-sort-skill b/server.js.pre-sort-skill
new file mode 100644
index 0000000..f98e73d
--- /dev/null
+++ b/server.js.pre-sort-skill
@@ -0,0 +1,159 @@
+/**
+ * HOTEL WALLCOVERINGS — DW family vertical
+ * Curated slice from live designerwallcoverings.com Shopify catalog.
+ */
+const express = require('express');
+const helmet = require('helmet');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = process.env.PORT || 9851;
+const DW_SHOPIFY = 'https://designerwallcoverings.com';
+const __SITE = path.basename(__dirname);
+let DATA_RAW;
+try {
+ const raw = fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8');
+ DATA_RAW = JSON.parse(raw);
+ if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
+} catch (e) {
+ console.error(`[${__SITE}] FATAL: could not load products.json — ${e.message}`);
+ console.error(`[${__SITE}] Starting with empty catalog. Run pull script to populate data/products.json.`);
+ DATA_RAW = [];
+}
+
+function isJunk(p) {
+ if (!p.image_url || !p.image_url.trim()) return true;
+ if (!p.handle && !p.sku) return true;
+ const t = p.title || '';
+ if (/lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i.test(t)) return true;
+ if (/visual.{0,3}merchandiser/i.test(t)) return true;
+ if (/(?:^|\W)image[ _-]?4(?:\W|$)/i.test(t)) return true;
+ if (/bh.?90210|beverly.?hills.?90210|iconic.{0,4}bh/i.test(t)) return true;
+ return false;
+}
+
+const PRODUCTS = DATA_RAW.filter(p => !isJunk(p));
+const DROPPED = DATA_RAW.length - PRODUCTS.length;
+console.log(`Loaded ${DATA_RAW.length}, kept ${PRODUCTS.length}, dropped ${DROPPED}`);
+
+// graphics-loop pass 12: niche keyword filter — only show products that fit this site's niche
+const NICHE_POS = ["hotel","hospitality","lobby","guestroom","luxury","class a"];
+const NICHE_NEG = ["solid","blank","plain"];
+function nicheFit(p) {
+ const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
+ if (NICHE_NEG.some(n => blob.includes(n.toLowerCase()))) return false;
+ return NICHE_POS.length === 0 || NICHE_POS.some(k => blob.includes(k.toLowerCase()));
+}
+const PRODUCTS_NICHE = PRODUCTS.filter(nicheFit);
+const NICHE_DROPPED = PRODUCTS.length - PRODUCTS_NICHE.length;
+console.log(`Niche filter: kept ${PRODUCTS_NICHE.length} of ${PRODUCTS.length}, dropped ${NICHE_DROPPED}`);
+// Replace PRODUCTS reference with the niche-filtered list for all downstream handlers.
+const PRODUCTS_ORIG = PRODUCTS;
+Object.defineProperty(global, '__products_niche_applied', { value: true });
+
+
+const app = express();
+// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
+// DW STANDARD: sort by color/style/sku/title for any product-rendering site.
+const COLOR_RANK = {
+ black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
+ red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+ yellow:30,gold:31,mustard:32,olive:33,
+ green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
+ blue:60,navy:61,indigo:62,periwinkle:63,
+ purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
+};
+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 = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','mid century','rustic','industrial','farmhouse','bohemian','boho','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
+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 === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+ if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'price-asc') return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
+ if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+ return list;
+}
+
+
+app.use(helmet({ contentSecurityPolicy: false }));
+app.use(express.json({ limit: '256kb' }));
+// Universal contact module — modals, /api/send-inquiry, /api/send-sample, /zd-loader.js
+require('./_universal-contact')(app, { siteName: "Hotel Wallcoverings", zdColor: "#d4a847", zdPosition: 'right' });
+require('./_universal-auth')(app, { siteName: "hotelwallcoverings" });
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+app.get('/api/products', (req, res) => {
+ const { q, aesthetic, vendor, page = 1, limit = 24, sort = 'newest'} = req.query;
+ let list = PRODUCTS_NICHE;
+ if (q) {
+ const needle = q.toLowerCase();
+ list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)));
+ }
+ if (aesthetic && aesthetic !== 'all') list = list.filter(p => p.aesthetic === aesthetic);
+ if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+ 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) });
+ // (replaced) old: pages: Math.ceil(total / lim), items: list.slice(start, start + lim) });
+});
+
+app.get('/api/sliders', (req, res) => {
+ const SLIDER_AESTHETICS = ["mural","jungle","chinoiserie","deco","botanical","metallic"];
+ const out = [];
+ for (const a of SLIDER_AESTHETICS) {
+ const items = PRODUCTS_NICHE.filter(p => p.aesthetic === a).slice(0, 12);
+ if (items.length >= 4) out.push({ aesthetic: a, items });
+ }
+ res.json({ rails: out });
+});
+
+app.get('/api/facets', (req, res) => {
+ const aesthetics = {}; const vendors = {};
+ for (const p of PRODUCTS_NICHE) {
+ aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
+ vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
+ }
+ res.json({ aesthetics, vendors, total: PRODUCTS.length });
+});
+
+app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS_NICHE.length, dropped: DROPPED }));
+
+app.get('/sample/:handle', (req, res) => {
+ const p = PRODUCTS_NICHE.find(x => x.handle === req.params.handle || x.sku === req.params.handle);
+ if (!p) return res.status(404).send('Not found');
+ res.redirect(302, p.product_url || `${DW_SHOPIFY}/products/${encodeURIComponent(p.handle)}#sample`);
+});
+
+// sitemap.xml + robots.txt for SEO
+app.get('/robots.txt', (req, res) => {
+ res.type('text/plain').send(`User-agent: *
+Allow: /
+Sitemap: https://hotelwallcoverings.com/sitemap.xml
+`);
+});
+app.get('/sitemap.xml', (req, res) => {
+ const urls = ['/'];
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+${urls.map(u => ` <url><loc>https://hotelwallcoverings.com${u}</loc><changefreq>weekly</changefreq></url>`).join('\n')}
+</urlset>`;
+ res.type('application/xml').send(xml);
+});
+
+app.listen(PORT, '127.0.0.1', () => {
+ console.log(`hotelwallcoverings listening on http://127.0.0.1:${PORT}`);
+});
← 49d4765 sort-skill UI: add Light→Dark / Dark→Light / Color Wheel opt
·
back to Hotelwallcoverings
·
hamburger nav: apply canonical pattern from sister-site flee 64d0efe →