← back to Museumwallpaper
snapshot before fleet-grid refine tick 7
c5271121b12464481e66ea66219b3c51626ef6ec · 2026-05-11 04:06:33 -0700 · Steve
Files touched
A public/care.htmlA public/history.htmlA public/index.html.bak.dwfoot.1778203445685A public/index.html.bak.gtagev.1778426585325A public/index.html.bak.ns-canonicalA public/index.html.bak.nspropagate.1778203385126A public/index.html.bak.nspropagate.1778209715065A public/index.html.bak.nspropagate.1778226815164A public/index.html.bak.nspropagate.1778226888814A public/index.html.pre-sort-frontendA public/sourcing.htmlA public/trade.htmlA public/vocabulary.htmlA server.js.pre-sort-skill
Diff
commit c5271121b12464481e66ea66219b3c51626ef6ec
Author: Steve <steve@designerwallcoverings.com>
Date: Mon May 11 04:06:33 2026 -0700
snapshot before fleet-grid refine tick 7
---
public/care.html | 55 ++
public/history.html | 49 ++
public/index.html.bak.dwfoot.1778203445685 | 962 +++++++++++++++++++++++
public/index.html.bak.gtagev.1778426585325 | 972 +++++++++++++++++++++++
public/index.html.bak.ns-canonical | 979 ++++++++++++++++++++++++
public/index.html.bak.nspropagate.1778203385126 | 947 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778209715065 | 962 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778226815164 | 966 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778226888814 | 968 +++++++++++++++++++++++
public/index.html.pre-sort-frontend | 852 +++++++++++++++++++++
public/sourcing.html | 57 ++
public/trade.html | 55 ++
public/vocabulary.html | 61 ++
server.js.pre-sort-skill | 156 ++++
14 files changed, 8041 insertions(+)
diff --git a/public/care.html b/public/care.html
new file mode 100644
index 0000000..7261089
--- /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 — museumwallpaper.com</title>
+<meta name="description" content="Care & installation — Whispered archive editions.">
+<link rel="canonical" href="https://museumwallpaper.com/care">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#F8F6F1;color:#1A1A1A;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:#1A1A1A;margin-bottom:18px}
+h1{font-family:ui-serif, Georgia, serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1A1A1A;margin-bottom:32px}
+h2{font-family:ui-serif, Georgia, serif;font-size:22px;color:#1A1A1A;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:#1A1A1A}
+a{color:#1A1A1A;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 · Studio Olsen</div>
+<h1>Care & installation</h1>
+<p>Archive editions wallcoverings demand a deliberate approach to installation and maintenance. Their historical provenance and material integrity require precision, patience, and an understanding of their unique needs. The following guide outlines the essential steps for proper application and preservation, ensuring longevity and fidelity to the original design.</p>
+
+<h2>Substrate Preparation</h2>
+<p>Begin with a clean, dry, and stable surface. Any existing paint or wallpaper must be removed, and the wall must be free of dust, grease, and moisture. For porous or uneven substrates, apply a thin, even coat of primer to create a uniform base. Ensure the surface is level and firm, as archive materials are sensitive to movement or deformation over time. Avoid using adhesives or sealants that contain acids or solvents, as these can compromise the integrity of the paper or textile.</p>
+
+<h2>Paste Application</h2>
+<p>Use a high-quality, pH-neutral adhesive formulated for delicate materials. Apply the paste in thin, even layers, avoiding over-saturation. For paper-based archive editions, a water-based paste with low viscosity is recommended to prevent warping. Allow the adhesive to reach room temperature before use, and mix only the amount needed for immediate application. Do not reuse leftover paste, as it may degrade or harbor contaminants.</p>
+
+<h2>Seam Handling</h2>
+<p>Align seams with care, ensuring they are straight and free of tension. Use a seam roller to press the edges firmly into place, smoothing out any wrinkles or bubbles. For complex patterns or large panels, work in sections, overlapping slightly before trimming excess. Avoid stretching or pulling the material, as this can distort the weave or print. If seams are unavoidable, position them in areas of low visibility, such as behind furniture or in corners.</p>
+
+<h2>Cleaning and Maintenance</h2>
+<p>Regularly dust the surface with a soft, dry microfiber cloth to prevent the buildup of particulate matter. For spot cleaning, use a mild solution of distilled water and a neutral pH detergent, applied sparingly with a clean cloth. Wipe in the direction of the weave or grain, and avoid saturating the material. Never use abrasive cleaners, steam, or harsh chemicals, as these can cause discoloration or degradation. Allow the surface to air dry completely before resuming use.</p>
+
+<h2>Light Exposure</h2>
+<p>Archive editions are sensitive to prolonged exposure to ultraviolet light, which can cause fading or brittleness. Position them away from direct sunlight, and consider using UV-protective window films or sheer curtains to filter light. In high-traffic or brightly lit spaces, rotate the wallcoverings periodically to ensure even exposure. Avoid placing them near heat sources, such as radiators or fireplaces, which can accelerate deterioration.</p>
+
+<h2>Expected Lifespan</h2>
+<p>With proper care, archive editions wallcoverings can last 10–15 years, depending on the material, environmental conditions, and frequency of use. Paper-based finishes may require more frequent attention in humid climates, while textile or synthetic variants may show greater resilience. Regular inspections for signs of wear, discoloration, or loosening seams are essential. If damage occurs, consult a professional for repairs rather than attempting to alter the material yourself.</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..d1f2b09
--- /dev/null
+++ b/public/history.html
@@ -0,0 +1,49 @@
+<!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 — museumwallpaper.com</title>
+<meta name="description" content="A short history — Whispered archive editions.">
+<link rel="canonical" href="https://museumwallpaper.com/history">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#F8F6F1;color:#1A1A1A;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:#1A1A1A;margin-bottom:18px}
+h1{font-family:ui-serif, Georgia, serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1A1A1A;margin-bottom:32px}
+h2{font-family:ui-serif, Georgia, serif;font-size:22px;color:#1A1A1A;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:#1A1A1A}
+a{color:#1A1A1A;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 · Studio Olsen</div>
+<h1>A short history</h1>
+<p>Archive wallcoverings exist as fragments of time, their histories etched into the fibers and finishes they bear. Unlike the ephemeral nature of many decorative arts, these materials persist through meticulous curation, their origins rooted in specific moments, hands, and rooms. To trace their arc is to follow a thread through the 20th century and beyond, where the intersection of craftsmanship, ideology, and materiality defined the walls of homes and institutions alike.</p>
+
+<h2>The Arts & Crafts Revival: Handwoven Intent</h2>
+<p>The late 19th century saw a deliberate rejection of industrial uniformity in favor of human-scale production. William Morris’s workshops in London became a crucible for this ethos, with designs like “Strawberry Thief” emerging from a philosophy that tied artistry to utility. These wallcoverings, often woven in small batches, were installed in drawing rooms and study halls, their botanical motifs a quiet assertion of nature’s presence in domestic spaces. Mills like Morris & Co. and the Glasgow School of Art’s workshops produced materials that prioritized the visible hand of the maker, a principle that would echo in later movements.</p>
+
+<h2>Bauhaus: Geometry as a Language</h2>
+<p>Decades later, the Bauhaus school in Dessau reimagined wallcoverings as a medium for modernist abstraction. Collaborations between architects like Walter Gropius and artists such as Anni Albers led to the development of geometric patterns derived from weaving techniques. These designs, often created using looms adapted for industrial production, adorned the interiors of the Bauhaus building itself—a space where the wall was both a structural element and a canvas for innovation. The emphasis on repetition and balance in these materials reflected a broader shift toward functional aesthetics, a legacy preserved in archive collections today.</p>
+
+<h2>Postwar America: The Quiet Persistence of Tradition</h2>
+<p>As the United States emerged from the Second World War, the rise of mass production threatened the survival of handcrafted techniques. Yet certain mills, such as the now-defunct Zuber in France and Schumacher in New York, maintained archives of pre-1960s designs, many of which were inspired by earlier European traditions. These materials, often found in mid-century interiors, featured intricate floral patterns and metallic accents that contrasted with the era’s minimalist trends. Their presence in living rooms and dining areas of the 1950s and 1960s marked a subtle resistance to the homogenization of taste, a thread later picked up by curators and collectors.</p>
+
+<h2>Contemporary Revival: The Curated Past</h2>
+<p>Today, the practice of archiving these materials has become a discipline in itself. Studios like Studio Olsen meticulously trace the lineage of each roll, from the original mill to the rooms where it once hung. This work involves not only preserving the physical artifact but also reconstructing the context in which it was created. For example, a 1930s textile from a Swiss mill might have adorned the walls of a private library, its design influenced by the
+<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.dwfoot.1778203445685 b/public/index.html.bak.dwfoot.1778203445685
new file mode 100644
index 0000000..2ad72d2
--- /dev/null
+++ b/public/index.html.bak.dwfoot.1778203445685
@@ -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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Museum Wallpaper</div>
+ <p class="footer-text">Museum Wallpaper is a specialty museum 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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.1778426585325 b/public/index.html.bak.gtagev.1778426585325
new file mode 100644
index 0000000..f50710f
--- /dev/null
+++ b/public/index.html.bak.gtagev.1778426585325
@@ -0,0 +1,972 @@
+<!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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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@museumwallpaper.com?subject=Studio%20Olsen%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@museumwallpaper.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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Studio Olsen</div>
+ <p class="footer-text">Studio Olsen — 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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>
+
+<script src="/dw-fleet-nav.js" defer></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..d788de7
--- /dev/null
+++ b/public/index.html.bak.ns-canonical
@@ -0,0 +1,979 @@
+<!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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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@museumwallpaper.com?subject=Studio%20Olsen%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@museumwallpaper.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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Studio Olsen</div>
+ <p class="footer-text">Studio Olsen — 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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>
+
+<script src="/dw-fleet-nav.js" defer></script>
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778203385126 b/public/index.html.bak.nspropagate.1778203385126
new file mode 100644
index 0000000..f860144
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778203385126
@@ -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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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="Studio Olsen home">
+ <span class="ns-dot"></span>
+ <span class="ns-name">Studio Olsen</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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Museum Wallpaper</div>
+ <p class="footer-text">Museum Wallpaper is a specialty museum 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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.1778209715065 b/public/index.html.bak.nspropagate.1778209715065
new file mode 100644
index 0000000..3ff993a
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778209715065
@@ -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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Studio Olsen</div>
+ <p class="footer-text">Studio Olsen — 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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.1778226815164 b/public/index.html.bak.nspropagate.1778226815164
new file mode 100644
index 0000000..14fe5d4
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778226815164
@@ -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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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@museumwallpaper.com?subject=Studio%20Olsen%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@museumwallpaper.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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Studio Olsen</div>
+ <p class="footer-text">Studio Olsen — 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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>
+
+<script src="/dw-fleet-nav.js" defer></script>
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778226888814 b/public/index.html.bak.nspropagate.1778226888814
new file mode 100644
index 0000000..d92bd1c
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778226888814
@@ -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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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@museumwallpaper.com?subject=Studio%20Olsen%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@museumwallpaper.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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Studio Olsen</div>
+ <p class="footer-text">Studio Olsen — 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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>
+
+<script src="/dw-fleet-nav.js" defer></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..cd08a58
--- /dev/null
+++ b/public/index.html.pre-sort-frontend
@@ -0,0 +1,852 @@
+<!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-E6Y8PDCT60"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-E6Y8PDCT60');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Studio Olsen — Whispered archive editions.</title>
+<meta name="description" content="Studio Olsen is a curated atelier dedicated entirely to archive editions. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F8F6F1">
+<link rel="canonical" href="https://museumwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;1,400&family=Inter:wght@300;400&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": "Studio Olsen",
+ "url": "https://museumwallpaper.com/",
+ "email": "info@museumwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+:root {
+ --bg: #F8F6F1;
+ --paper: #1A1A1A;
+ --muted: #8B7866;
+ --line: #E2DDD2;
+ --accent: #1A1A1A;
+ --bg-soft: #E2DDD2;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #1a1410;
+ --paper: #F8F6F1;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #1A1A1A;
+ --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:400 !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('museum_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://museumwallpaper.com/">
+<meta property="og:title" content="Museum Wallpaper">
+<meta property="og:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta property="og:image" content="https://museumwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Museum Wallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Museum Wallpaper">
+<meta name="twitter:description" content="Museum Wallpaper — Archival & historical-reproduction museum wallcoverings.">
+<meta name="twitter:image" content="https://museumwallpaper.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">Studio Olsen</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@museumwallpaper.com?subject=Studio%20Olsen%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@museumwallpaper.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">Archival Grade</div>
+ <div class="center-mark">Museum Wallpaper</div>
+ <div class="meta-line">Reproduction · Heritage · Historical<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">Reproduction · Heritage · Historical</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">Studio Olsen</div>
+ <p class="footer-text">Studio Olsen — 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>museumwallpaper.com · Studio Olsen</span>
+ <span><a href="mailto:info@museumwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@museumwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "museumwallpaper";
+ 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('museum_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('museum_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('museum_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@museumwallpaper.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@museumwallpaper.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>
+
+<script src="/dw-fleet-nav.js" defer></script>
+</body>
+</html>
diff --git a/public/sourcing.html b/public/sourcing.html
new file mode 100644
index 0000000..eef1373
--- /dev/null
+++ b/public/sourcing.html
@@ -0,0 +1,57 @@
+<!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 — museumwallpaper.com</title>
+<meta name="description" content="How we source — Whispered archive editions.">
+<link rel="canonical" href="https://museumwallpaper.com/sourcing">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#F8F6F1;color:#1A1A1A;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:#1A1A1A;margin-bottom:18px}
+h1{font-family:ui-serif, Georgia, serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1A1A1A;margin-bottom:32px}
+h2{font-family:ui-serif, Georgia, serif;font-size:22px;color:#1A1A1A;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:#1A1A1A}
+a{color:#1A1A1A;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 · Studio Olsen</div>
+<h1>How we source</h1>
+<p>Curating archive editions is an exercise in restraint, a quiet dialogue between the past and the present. We do not collect for the sake of accumulation, nor do we chase novelty. Instead, we listen — to the hush of mill looms, the whisper of paper in storage, the unspoken histories of materials that have outlived their original purpose. Every selection is made with the weight of time in mind, a balance between preservation and utility. The process is deliberate, layered, and often invisible to those who do not look closely.</p>
+
+<h2>From Mills to Memory</h2>
+<p>Our journey begins with the mills — the few that remain, the ones that have endured the erosion of mass production. These are not factories, but sanctuaries of craftsmanship, where looms hum in rhythm with the seasons. We visit them not as buyers, but as custodians of a shared language. Mill owners, many of whom have inherited their trade, guard their archives with a reverence that mirrors our own. They show us rolls that have slept in vaults for decades, some never intended to leave. We select only those that feel like echoes of their original intent, materials that have not merely survived but retained their character.</p>
+
+<h2>The Unseen Catalogs</h2>
+<p>Trade-only catalogs are our compass. These are not public-facing collections, but private repositories curated for industry insiders. They contain the detritus of bygone trends — patterns that once graced the walls of hotels, offices, and homes, now quietly retired. We pore over these catalogs with the precision of archaeologists, scanning for materials that meet our thresholds. Some entries are forgotten; others are deliberately obscured. We seek out those that resonate with a quiet authority, unburdened by the clamor of fashion.</p>
+
+<h2>Criteria: The Unspoken Rules</h2>
+<p>Our criteria are not arbitrary. They are born of experience, of knowing what holds up and what fades. Substrate is our first test — the material’s integrity, its ability to withstand the demands of installation and time. We favor those that are neither too fragile nor too rigid, those that bend without breaking. Repeat is next: the scale of the design must interact with light and space in a way that feels intentional, not forced. Washfastness is our final gatekeeper. Colors must endure the slow decay of sun and moisture, their vibrancy preserved as if time itself had paused.</p>
+
+<ul>
+ <li><strong>Substrate:</strong> We exclude any material that cannot support the weight of history without cracking or warping.</li>
+ <li><strong>Repeat:</strong> Patterns that feel overwhelming or fragmented are discarded, regardless of their original beauty.</li>
+ <li><strong>Washfastness:</strong> Anything that cannot resist the slow erosion of time is left behind, even if its initial allure is undeniable.</li>
+</ul>
+
+<h2>What Gets Cut</h2>
+<p>Not everything that appears in these catalogs is worthy of our shelves. Some rolls are rejected for reasons that cannot be quantified — a color that feels too loud, a texture that resists the calm we seek. Others fail on technical grounds: a repeat that is too small, a substrate that cannot hold its shape. We do not make exceptions for sentimentality. If a material does not meet our standards, it is cut — not discarded, but returned to the vaults where it belongs. This is our way of honoring the past without compromising the future.</p>
+
+<p>Curating archive editions is not about resurrection. It is about curation — the act of letting some things remain and others fade. We do not speak loudly, but we listen carefully. And in that silence, we find what endures.</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..91247a8
--- /dev/null
+++ b/public/trade.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>Trade program — museumwallpaper.com</title>
+<meta name="description" content="Trade program — Whispered archive editions.">
+<link rel="canonical" href="https://museumwallpaper.com/trade">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#F8F6F1;color:#1A1A1A;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:#1A1A1A;margin-bottom:18px}
+h1{font-family:ui-serif, Georgia, serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1A1A1A;margin-bottom:32px}
+h2{font-family:ui-serif, Georgia, serif;font-size:22px;color:#1A1A1A;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:#1A1A1A}
+a{color:#1A1A1A;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 · Studio Olsen</div>
+<h1>Trade program</h1>
+<p>At Studio Olsen, the focus is not on volume but on the quiet resonance of each piece we curate. This is a space where history breathes through the material, where every selection is made with a deliberate hand. The trade and designer program is an invitation to those who seek not just aesthetics, but a dialogue with time itself—crafted for professionals who understand the weight of intention in design.</p>
+
+<h2>Who Qualifies</h2>
+<p>The program is reserved for a specific circle: designers, architects, and contractors who work with a lens that values depth over haste. These are creators who prioritize the integrity of the material, who see walls as canvases for narratives that outlast trends. Access is by invitation, extending to those whose portfolios reflect a commitment to the tactile and the timeless.</p>
+
+<h2>Memo Samples</h2>
+<p>Each request for a memo sample begins with a whisper—a note sent through the atelier’s channels. Upon approval, a curated set of swatches arrives, each accompanied by a small card that captures the essence of the piece: its origin, its condition, and the story it carries. These are not mere samples; they are fragments of a larger conversation, meant to be held, studied, and returned with care.</p>
+
+<h2>Lead Times</h2>
+<p>Lead times are measured in weeks, not days. Each roll is sourced, prepared, and dispatched with the patience of a craftsman. While the exact duration varies depending on availability, the process is transparent, with updates provided at every stage. This is not a system of urgency, but of precision—ensuring that what arrives is as it was intended to be.</p>
+
+<h2>NET-30 Terms</h2>
+<p>For those who qualify, NET-30 terms are extended as a gesture of trust. Payments are expected within 30 days of delivery, a balance struck between the immediacy of commerce and the slow burn of archival work. This structure is designed for those who understand that value is not always immediate, but accumulates over time.</p>
+
+<h2>Custom-Print Availability</h2>
+<p>Custom-print options are available, though they are not a given. Requests are evaluated on a case-by-case basis, with consideration given to the material’s original intent and the feasibility of adaptation. This is not a space for replication, but for reinterpretation—where the past is honored even as it is reshaped.</p>
+
+<h2>The Designer Wallcoverings Relationship</h2>
+<p>Studio Olsen exists within the broader ecosystem of Designer Wallcoverings, a family of curated collections that has been cultivating depth since 1996. This relationship ensures that every roll, every swatch, and every inquiry is met with the same level of care that has defined the parent house for decades. The atelier benefits from the legacy of real samples, the richness of a stock that has been thoughtfully expanded over time, and the presence of a human voice at the other
+<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..41d5435
--- /dev/null
+++ b/public/vocabulary.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>Design vocabulary — museumwallpaper.com</title>
+<meta name="description" content="Design vocabulary — Whispered archive editions.">
+<link rel="canonical" href="https://museumwallpaper.com/vocabulary">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-sans-serif;background:#F8F6F1;color:#1A1A1A;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:#1A1A1A;margin-bottom:18px}
+h1{font-family:ui-serif, Georgia, serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1A1A1A;margin-bottom:32px}
+h2{font-family:ui-serif, Georgia, serif;font-size:22px;color:#1A1A1A;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:#1A1A1A}
+a{color:#1A1A1A;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 · Studio Olsen</div>
+<h1>Design vocabulary</h1>
+<p>Understanding the language of archive editions is essential for discerning buyers. These terms, rooted in the history of textile and decorative arts, offer insight into the materials, techniques, and intentions behind each piece. They are not mere descriptors but markers of a dialogue between past and present, craft and context.</p>
+
+<h2>Archival Integrity</h2>
+<ul>
+<li>Refers to the preservation of original materials and processes, ensuring the piece remains true to its historical and technical origins. A product may show this through untreated fibers and adhesives that resist modern chemical treatments.</li>
+</ul>
+
+<h2>Handloom</h2>
+<ul>
+<li>A weaving method using hand-operated looms, which allows for subtle irregularities and a tactile presence. A product might display this through uneven warp threads or a slightly uneven surface.</li>
+</ul>
+
+<h2>Mordant</h2>
+<ul>
+<li>A chemical agent used in dyeing to fix color to fabric, enhancing longevity. A product could show this through a color that resists fading even under prolonged light exposure.</li>
+</ul>
+
+<h2>Tonal Variation</h2>
+<ul>
+<li>Natural differences in hue within a single design, often due to hand-dyeing or aging. A product might exhibit this through a gradient in a monochrome scheme, with shifts in value across the roll.</li>
+</ul>
+
+<h2>Reticulated</h2>
+<ul>
+<li>A design element featuring a network of intersecting lines or shapes, often resembling a lattice. A product might use this in a geometric motif, with lines that form a cohesive, inter
+<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..012b8f4
--- /dev/null
+++ b/server.js.pre-sort-skill
@@ -0,0 +1,156 @@
+/**
+ * MUSEUM WALLPAPER — DW family vertical
+ * Curated slice from live designerwallcoverings.com Shopify catalog.
+ */
+try { require('dotenv').config(); } catch (e) {}
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = process.env.PORT || 9881;
+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 = ["victorian","nouveau","edwardian","chinoiserie","damask","toile","reproduction","historical"];
+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();
+// 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(express.json({ limit: "256kb" }));
+const cfg = require('../_shared/site-config').load(__dirname);
+require("./_universal-contact")(app, cfg.contact);
+require("./_universal-auth")(app, cfg.auth);
+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 = ["victorian","chinoiserie","damask","baroque","mural","botanical"];
+ 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://museumwallpaper.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://museumwallpaper.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(`museumwallpaper listening on http://127.0.0.1:${PORT}`);
+});
← a920f9e sort-skill UI: add Light→Dark / Dark→Light / Color Wheel opt
·
back to Museumwallpaper
·
fleet-grid refine: chip_uniform_aspect (museumwallpaper) 367cd55 →