← back to Linenwallpaper
snapshot before fleet-grid refine
5d525efe37047eb34b73c2e830e664cdf8a5062d · 2026-05-11 03:02:57 -0700 · Steve Abrams
Files touched
A public/care.htmlA public/history.htmlA public/index.html.bak.dwfoot.1778203445675A public/index.html.bak.gtagev.1778426584894A public/index.html.bak.ns-canonicalA public/index.html.bak.nspropagate.1778203385110A public/index.html.bak.nspropagate.1778209715055A public/index.html.bak.nspropagate.1778226815155A public/index.html.bak.nspropagate.1778226888806A public/index.html.bak.r1234.26162A public/index.html.pre-sort-frontendA public/sourcing.htmlA public/trade.htmlA public/vocabulary.htmlA server.js.pre-sort-skill
Diff
commit 5d525efe37047eb34b73c2e830e664cdf8a5062d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon May 11 03:02:57 2026 -0700
snapshot before fleet-grid refine
---
public/care.html | 55 ++
public/history.html | 49 ++
public/index.html.bak.dwfoot.1778203445675 | 960 +++++++++++++++++++++
public/index.html.bak.gtagev.1778426584894 | 1022 ++++++++++++++++++++++
public/index.html.bak.ns-canonical | 1029 +++++++++++++++++++++++
public/index.html.bak.nspropagate.1778203385110 | 945 +++++++++++++++++++++
public/index.html.bak.nspropagate.1778209715055 | 960 +++++++++++++++++++++
public/index.html.bak.nspropagate.1778226815155 | 962 +++++++++++++++++++++
public/index.html.bak.nspropagate.1778226888806 | 964 +++++++++++++++++++++
public/index.html.bak.r1234.26162 | 968 +++++++++++++++++++++
public/index.html.pre-sort-frontend | 902 ++++++++++++++++++++
public/sourcing.html | 59 ++
public/trade.html | 63 ++
public/vocabulary.html | 46 +
server.js.pre-sort-skill | 159 ++++
15 files changed, 9143 insertions(+)
diff --git a/public/care.html b/public/care.html
new file mode 100644
index 0000000..77ecd1d
--- /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 — linenwallpaper.com</title>
+<meta name="description" content="Care & installation — Soft as linen. Made slowly.">
+<link rel="canonical" href="https://linenwallpaper.com/care">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-serif;background:#F2EDE2;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#9B7B4F;margin-bottom:18px}
+h1{font-family:'Saol Display', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Saol Display', serif;font-size:22px;color:#9B7B4F;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#9B7B4F;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 · Loma Studio</div>
+<h1>Care & installation</h1>
+<p>Linen wallcoverings, when treated with care and installed with intention, become enduring elements of a space. Their softness, resilience, and warmth are not merely aesthetic qualities but reflections of the material’s inherent character. This guide outlines the practical steps for installation and long-term care, ensuring your linen wallcoverings remain as rich and inviting as the day they were applied.</p>
+
+<h2>Substrate Preparation</h2>
+<p>Begin with a clean, dry, and smooth surface. Any existing wallcoverings, paint, or residue must be removed to ensure proper adhesion. For drywall, ensure the surface is fully cured and free of dust or grease. If the wall is uneven or porous, apply a thin layer of primer to create an even base. Avoid using surfaces that are excessively slick or oily, as these can compromise the bond between the linen and the wall.</p>
+
+<h2>Paste Selection</h2>
+<p>Use a high-quality, starch-based paste designed for natural fibers. Avoid pastes containing synthetic polymers or excessive water, as these can weaken the linen over time. Mix the paste according to the manufacturer’s instructions, ensuring it is smooth and free of lumps. Apply the paste evenly to the back of the linen, working in sections to prevent drying before application. For large areas, use a paste float to distribute the adhesive uniformly.</p>
+
+<h2>Seam Handling</h2>
+<p>Seams should be handled with care to maintain the integrity of the linen. Overlap seams by approximately 2 inches, pressing gently with a seam roller to ensure a flat, secure bond. Avoid stretching the material during application, as this can lead to puckering or unevenness. For seams that must be joined vertically, use a seam tape or a thin layer of paste to reinforce the connection. Allow seams to dry completely before trimming excess material.</p>
+
+<h2>Cleaning and Maintenance</h2>
+<p>Regular cleaning is straightforward: use a soft, damp cloth with mild soap and water to wipe the surface. Avoid abrasive sponges, harsh chemicals, or excessive moisture, which can damage the fibers. For stubborn stains, gently blot the affected area with a cloth soaked in a solution of water and a small amount of dish soap. Rinse the cloth frequently and pat dry. Never use steam cleaners or direct water onto the wallcovering, as this may cause warping or discoloration.</p>
+
+<h2>Light Exposure</h2>
+<p>Linen is sensitive to prolonged exposure to direct sunlight. Over time, ultraviolet rays can cause fading and degradation of the fibers. To minimize this, position wallcoverings away from windows or use window treatments such as sheer curtains or UV-protective films. If the wallcovering is in a high-traffic area with natural light, consider rotating decorative elements to distribute light exposure evenly across the surface.</p>
+
+<h2>Expected Lifespan</h2>
+<p>With proper installation and care, linen wallcoverings can last for decades. Their durability is rooted in the material’s natural strength and the precision of their preparation. Regular maintenance, such as gentle cleaning and protection from excessive moisture or light, will extend their life. Avoid using the wallcovering as a surface for hanging objects or applying pressure, as this can cause indentations or damage. When the time comes to replace the wallcovering, remove it carefully by lifting the edges and peeling it away from the wall, taking care not to tear the fibers.</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..cce20cd
--- /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 — linenwallpaper.com</title>
+<meta name="description" content="A short history — Soft as linen. Made slowly.">
+<link rel="canonical" href="https://linenwallpaper.com/history">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-serif;background:#F2EDE2;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#9B7B4F;margin-bottom:18px}
+h1{font-family:'Saol Display', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Saol Display', serif;font-size:22px;color:#9B7B4F;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#9B7B4F;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 · Loma Studio</div>
+<h1>A short history</h1>
+<p>Linen has long occupied a quiet, unassuming corner of design history—its presence felt more than seen, its legacy woven into the fabric of rooms that demand patience and care. Unlike the flamboyance of gilded mirrors or the theatricality of painted murals, linen’s story unfolds in the hush of a library wall, the softness of a sunlit alcove, or the understated dignity of a room where time seems to slow. This is the essence of linen as a wallcovering: not a statement, but a companion to the spaces it inhabits. Its arc through history is one of resilience, refinement, and a quiet reverence for materiality that transcends trends.</p>
+
+<h2>The Arts & Crafts Era: A Return to the Handmade</h2>
+<p>In the late 19th century, as industrialization threatened to erase the soul from craftsmanship, linen became a symbol of resistance. The Arts & Crafts movement, with its devotion to handwork and natural materials, found in linen a material that resisted mechanization. William Morris, though more famously associated with printed textiles, occasionally wove linen into his designs, using its raw, unadorned surface to emphasize the beauty of the weave itself. In Britain, small mills in Lancashire and Yorkshire produced linen wallcoverings that echoed the rough textures of the countryside, their irregularities celebrated as marks of authenticity. These were not decorative coverings but functional elements—walls that breathed, absorbed light, and aged with the inhabitants of a room. Rooms from this period often featured linen in modest, utilitarian spaces: kitchens, workshops, and the walls of rural homes where the material’s durability was essential.</p>
+
+<h2>Bauhaus and the Modernist Turn</h2>
+<p>The early 20th century saw linen take on a new role in the hands of modernists. The Bauhaus school, with its radical focus on function and minimalism, found in linen a material that aligned with its ideals. Unlike the gilded excesses of Art Nouveau, linen’s simplicity made it a natural fit for the clean lines and open spaces of modernist interiors. At the Dessau Bauhaus building, linen was used in classrooms and common areas, its neutral tones and matte finish complementing the stark geometry of the architecture. Designers like Marcel Breuer and Hannes Meyer incorporated linen into their work, not as a luxury but as a material that embodied the movement’s ethos of “form follows function.” In this era, linen became a tool for abstraction, its surface used to highlight the interplay of light and shadow in a room. The material’s presence was subtle, yet it carried the weight of a new design philosophy.</p>
+
+<h2>Postwar America: The Quiet Elegance of Mid-Century Living</h2>
+<p>Following World War II, linen found a new home in the domestic spaces of postwar America. As the country embraced modernity, linen became a staple in mid-century living rooms, where its soft sheen and natural warmth provided a counterpoint to the hard edges of steel and glass. Designers like Eero Saarinen and Charles and Ray Eames incorporated linen into their furniture and interiors, using it to add a human touch to the machine-age aesthetics of the time. In the 1950s and ’60s, linen wallcoverings appeared in the homes of architects and designers who sought to create spaces that felt both contemporary and comforting. These were not grand gestures; they were intimate choices—linen in the curve of a sofa wall, in the hush of a bedroom, or the quiet glow of a hallway. The material’s ability to absorb and reflect light made it a favorite for creating mood, a quality that resonated with the era’s focus on domesticity and emotional well-being.</p>
+
+<h2>The Contemporary Revival: A Material Reclaimed</h2>
+<p>Today, linen’s historical arc finds a new chapter in the hands of curators and designers who value its quiet elegance. At Loma Studio, the material is not just a relic of the past but a living tradition, reimagined for the present. The atelier’s approach is deliberate, rooted in the belief that linen’s beauty lies in its imperfections, its history, and its ability to
+<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.1778203445675 b/public/index.html.bak.dwfoot.1778203445675
new file mode 100644
index 0000000..2db3939
--- /dev/null
+++ b/public/index.html.bak.dwfoot.1778203445675
@@ -0,0 +1,960 @@
+<!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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Loma Studio — Soft as linen. Made slowly.</title>
+<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to linen. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Loma Studio",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* Loma Studio · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="Linen Wallpaper">
+<meta property="og:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Linen 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="Linen Wallpaper">
+<meta name="twitter:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta name="twitter:image" content="https://linenwallpaper.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">Loma Studio</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">Natural Fiber</div>
+ <div class="center-mark">Linen Wallpaper</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+
+
+<section class="section" id="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">Linen · Natural · Woven</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">Linen Wallpaper</div>
+ <p class="footer-text">Linen Wallpaper is a specialty linen 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · Loma Studio</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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.1778426584894 b/public/index.html.bak.gtagev.1778426584894
new file mode 100644
index 0000000..a4e6cc9
--- /dev/null
+++ b/public/index.html.bak.gtagev.1778426584894
@@ -0,0 +1,1022 @@
+<!doctype html>
+<html lang="en" data-hero-mode="huge">
+<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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>linenwallpaper — curated wallcoverings, real samples ship free.</title>
+<meta name="description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "linenwallpaper",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* linenwallpaper · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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 (R2: huge full-bleed photo + top-bar logo + below-photo wordmark) ===== */
+.cinema { position:relative; min-height:clamp(560px, 90vh, 1080px); 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.20) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,0) 65%, rgba(0,0,0,0.55) 100%) }
+/* Hero-mode hooks (R4): both modes share the same min-height — CLS-free */
+[data-hero-mode="four-square"] .cinema-bg { display:none }
+[data-hero-mode="four-square"] .cinema-grid { display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; position:absolute; inset:0 }
+[data-hero-mode="four-square"] .cinema-grid .cell { background-size:cover; background-position:center; border:1px solid rgba(0,0,0,0.4) }
+.cinema-grid { display:none }
+.cinema-logo { position:absolute; top:22px; left:32px; font-family: ui-serif, Georgia, serif; font-size:18px; font-weight:500; letter-spacing:0.02em; color:#fff; text-decoration:none; z-index:6; text-shadow:0 1px 4px rgba(0,0,0,0.5) }
+/* Below-photo wordmark (R2): brand display lives below the hero, not painted on it */
+.hero-wordmark-block { position:relative; padding:38px 32px 24px; text-align:center; background:var(--bg) }
+.hero-wordmark { font-family: ui-serif, Georgia, 'Cormorant Garamond', serif; font-weight:400; font-size:clamp(48px, 9vw, 128px); letter-spacing:-0.01em; line-height:1; color:var(--paper); margin:0; text-transform:none }
+.hero-wordmark-sub { display:block; font-family:'Playfair Display', Georgia, serif; font-style:italic; font-weight:400; font-size:clamp(13px, 1.4vw, 17px); letter-spacing:0.04em; text-transform:none; margin-top:14px; color:var(--muted) }
+@media (max-width:760px) { .hero-wordmark { font-size:clamp(32px, 12vw, 64px) } }
+.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 deprecated 2026-05-10 per R2 — hidden */
+.center-mark { display:none !important; 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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="linenwallpaper — curated wallcoverings, real samples ship free.">
+<meta property="og:description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="linenwallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="linenwallpaper — curated wallcoverings, real samples ship free.">
+<meta name="twitter:description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta name="twitter:image" content="https://linenwallpaper.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">linenwallpaper</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@linenwallpaper.com?subject=Loma%20Studio%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@linenwallpaper.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" id="hero">
+ <a class="cinema-logo" href="/" aria-label="linenwallpaper home">linenwallpaper</a>
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="cinema-grid" id="heroGrid" aria-hidden="true"></div>
+ <div class="corner-mark">Natural Fiber</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+<div class="hero-wordmark-block">
+ <h1 class="hero-wordmark">linenwallpaper</h1>
+ <span class="hero-wordmark-sub" id="heroWordmarkSub"></span>
+</div>
+
+
+<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">Linen · Natural · Woven</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">linenwallpaper</div>
+ <p class="footer-text">linenwallpaper — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · linenwallpaper</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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>
+/* R4 — Alternating hero modes (huge / four-square). Server-side default is huge (HTML attr).
+ Client-only flip starts on visit 2. Both modes share the same .cinema min-height (CLS-free).
+ Crawlers without JS always see huge. */
+(function(){
+ try {
+ var k='dw-hero-mode';
+ var prev=parseInt(localStorage.getItem(k)||'0',10);
+ var next=(prev+1)%2;
+ localStorage.setItem(k,String(next));
+ if (prev>0) {
+ var mode = next===0 ? 'huge' : 'four-square';
+ document.documentElement.dataset.heroMode = mode;
+ if (mode === 'four-square') {
+ fetch('/api/products?limit=4&page=1').then(r=>r.ok?r.json():null).then(j=>{
+ if (!j || !j.items || j.items.length < 4) {
+ document.documentElement.dataset.heroMode = 'huge';
+ console.warn('dw-hero-stale: insufficient items for four-square, falling back to huge');
+ return;
+ }
+ var grid = document.getElementById('heroGrid');
+ if (!grid) return;
+ j.items.slice(0,4).forEach(function(it){
+ var d = document.createElement('div');
+ d.className = 'cell';
+ var img = (it.image_url || it.image || '/hero-bg.jpg');
+ d.style.backgroundImage = "url('" + img + "')";
+ grid.appendChild(d);
+ });
+ }).catch(function(){
+ document.documentElement.dataset.heroMode = 'huge';
+ });
+ }
+ }
+ } catch(e) { /* no-op: keep server default */ }
+})();
+</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..e71902c
--- /dev/null
+++ b/public/index.html.bak.ns-canonical
@@ -0,0 +1,1029 @@
+<!doctype html>
+<html lang="en" data-hero-mode="huge">
+<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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>linenwallpaper — curated wallcoverings, real samples ship free.</title>
+<meta name="description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "linenwallpaper",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* linenwallpaper · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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 (R2: huge full-bleed photo + top-bar logo + below-photo wordmark) ===== */
+.cinema { position:relative; min-height:clamp(560px, 90vh, 1080px); 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.20) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,0) 65%, rgba(0,0,0,0.55) 100%) }
+/* Hero-mode hooks (R4): both modes share the same min-height — CLS-free */
+[data-hero-mode="four-square"] .cinema-bg { display:none }
+[data-hero-mode="four-square"] .cinema-grid { display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; position:absolute; inset:0 }
+[data-hero-mode="four-square"] .cinema-grid .cell { background-size:cover; background-position:center; border:1px solid rgba(0,0,0,0.4) }
+.cinema-grid { display:none }
+.cinema-logo { position:absolute; top:22px; left:32px; font-family: ui-serif, Georgia, serif; font-size:18px; font-weight:500; letter-spacing:0.02em; color:#fff; text-decoration:none; z-index:6; text-shadow:0 1px 4px rgba(0,0,0,0.5) }
+/* Below-photo wordmark (R2): brand display lives below the hero, not painted on it */
+.hero-wordmark-block { position:relative; padding:38px 32px 24px; text-align:center; background:var(--bg) }
+.hero-wordmark { font-family: ui-serif, Georgia, 'Cormorant Garamond', serif; font-weight:400; font-size:clamp(48px, 9vw, 128px); letter-spacing:-0.01em; line-height:1; color:var(--paper); margin:0; text-transform:none }
+.hero-wordmark-sub { display:block; font-family:'Playfair Display', Georgia, serif; font-style:italic; font-weight:400; font-size:clamp(13px, 1.4vw, 17px); letter-spacing:0.04em; text-transform:none; margin-top:14px; color:var(--muted) }
+@media (max-width:760px) { .hero-wordmark { font-size:clamp(32px, 12vw, 64px) } }
+.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 deprecated 2026-05-10 per R2 — hidden */
+.center-mark { display:none !important; 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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="linenwallpaper — curated wallcoverings, real samples ship free.">
+<meta property="og:description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="linenwallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="linenwallpaper — curated wallcoverings, real samples ship free.">
+<meta name="twitter:description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta name="twitter:image" content="https://linenwallpaper.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">linenwallpaper</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@linenwallpaper.com?subject=linenwallpaper%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@linenwallpaper.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" id="hero">
+ <a class="cinema-logo" href="/" aria-label="linenwallpaper home">linenwallpaper</a>
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="cinema-grid" id="heroGrid" aria-hidden="true"></div>
+ <div class="corner-mark">Natural Fiber</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+<div class="hero-wordmark-block">
+ <h1 class="hero-wordmark">linenwallpaper</h1>
+ <span class="hero-wordmark-sub" id="heroWordmarkSub"></span>
+</div>
+
+
+<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">Linen · Natural · Woven</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">linenwallpaper</div>
+ <p class="footer-text">linenwallpaper — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · linenwallpaper</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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>
+/* R4 — Alternating hero modes (huge / four-square). Server-side default is huge (HTML attr).
+ Client-only flip starts on visit 2. Both modes share the same .cinema min-height (CLS-free).
+ Crawlers without JS always see huge. */
+(function(){
+ try {
+ var k='dw-hero-mode';
+ var prev=parseInt(localStorage.getItem(k)||'0',10);
+ var next=(prev+1)%2;
+ localStorage.setItem(k,String(next));
+ if (prev>0) {
+ var mode = next===0 ? 'huge' : 'four-square';
+ document.documentElement.dataset.heroMode = mode;
+ if (mode === 'four-square') {
+ fetch('/api/products?limit=4&page=1').then(r=>r.ok?r.json():null).then(j=>{
+ if (!j || !j.items || j.items.length < 4) {
+ document.documentElement.dataset.heroMode = 'huge';
+ console.warn('dw-hero-stale: insufficient items for four-square, falling back to huge');
+ return;
+ }
+ var grid = document.getElementById('heroGrid');
+ if (!grid) return;
+ j.items.slice(0,4).forEach(function(it){
+ var d = document.createElement('div');
+ d.className = 'cell';
+ var img = (it.image_url || it.image || '/hero-bg.jpg');
+ d.style.backgroundImage = "url('" + img + "')";
+ grid.appendChild(d);
+ });
+ }).catch(function(){
+ document.documentElement.dataset.heroMode = 'huge';
+ });
+ }
+ }
+ } catch(e) { /* no-op: keep server default */ }
+})();
+</script>
+</body>
+</html>
diff --git a/public/index.html.bak.nspropagate.1778203385110 b/public/index.html.bak.nspropagate.1778203385110
new file mode 100644
index 0000000..662240d
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778203385110
@@ -0,0 +1,945 @@
+<!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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Loma Studio — Soft as linen. Made slowly.</title>
+<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to linen. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Loma Studio",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* Loma Studio · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="Linen Wallpaper">
+<meta property="og:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Linen 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="Linen Wallpaper">
+<meta name="twitter:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta name="twitter:image" content="https://linenwallpaper.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="Loma Studio home">
+ <span class="ns-dot"></span>
+ <span class="ns-name">Loma Studio</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">Natural Fiber</div>
+ <div class="center-mark">Linen Wallpaper</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+
+
+<section class="section" id="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">Linen · Natural · Woven</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">Linen Wallpaper</div>
+ <p class="footer-text">Linen Wallpaper is a specialty linen 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · Loma Studio</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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.1778209715055 b/public/index.html.bak.nspropagate.1778209715055
new file mode 100644
index 0000000..4a260aa
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778209715055
@@ -0,0 +1,960 @@
+<!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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Loma Studio — Soft as linen. Made slowly.</title>
+<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to linen. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Loma Studio",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* Loma Studio · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="Linen Wallpaper">
+<meta property="og:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Linen 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="Linen Wallpaper">
+<meta name="twitter:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta name="twitter:image" content="https://linenwallpaper.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">Loma Studio</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">Natural Fiber</div>
+ <div class="center-mark">Linen Wallpaper</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+
+
+<section class="section" id="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">Linen · Natural · Woven</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">Loma Studio</div>
+ <p class="footer-text">Loma Studio — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · Loma Studio</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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.1778226815155 b/public/index.html.bak.nspropagate.1778226815155
new file mode 100644
index 0000000..abde618
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778226815155
@@ -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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Loma Studio — Soft as linen. Made slowly.</title>
+<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to linen. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Loma Studio",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* Loma Studio · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="Linen Wallpaper">
+<meta property="og:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Linen 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="Linen Wallpaper">
+<meta name="twitter:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta name="twitter:image" content="https://linenwallpaper.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">Loma Studio</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@linenwallpaper.com?subject=Loma%20Studio%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@linenwallpaper.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">Natural Fiber</div>
+ <div class="center-mark">Linen Wallpaper</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+
+
+<section class="section" id="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">Linen · Natural · Woven</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">Loma Studio</div>
+ <p class="footer-text">Loma Studio — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · Loma Studio</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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.1778226888806 b/public/index.html.bak.nspropagate.1778226888806
new file mode 100644
index 0000000..61ed150
--- /dev/null
+++ b/public/index.html.bak.nspropagate.1778226888806
@@ -0,0 +1,964 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<!-- Google tag (gtag.js) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Loma Studio — Soft as linen. Made slowly.</title>
+<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to linen. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Loma Studio",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* Loma Studio · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="Linen Wallpaper">
+<meta property="og:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Linen 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="Linen Wallpaper">
+<meta name="twitter:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta name="twitter:image" content="https://linenwallpaper.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">Loma Studio</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@linenwallpaper.com?subject=Loma%20Studio%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@linenwallpaper.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">Natural Fiber</div>
+ <div class="center-mark">Linen Wallpaper</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+
+
+<section class="section" id="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">Linen · Natural · Woven</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">Loma Studio</div>
+ <p class="footer-text">Loma Studio — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · Loma Studio</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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.r1234.26162 b/public/index.html.bak.r1234.26162
new file mode 100644
index 0000000..a768a21
--- /dev/null
+++ b/public/index.html.bak.r1234.26162
@@ -0,0 +1,968 @@
+<!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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Loma Studio — Soft as linen. Made slowly.</title>
+<meta name="description" content="Loma Studio is a curated atelier dedicated entirely to linen. We do one thing — and we do it carefully.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "Loma Studio",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* Loma Studio · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="Linen Wallpaper">
+<meta property="og:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="Linen 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="Linen Wallpaper">
+<meta name="twitter:description" content="Linen Wallpaper — Linen-textured wallcoverings — quiet, natural fiber.">
+<meta name="twitter:image" content="https://linenwallpaper.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">Loma Studio</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@linenwallpaper.com?subject=Loma%20Studio%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@linenwallpaper.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">Natural Fiber</div>
+ <div class="center-mark">Linen Wallpaper</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+
+
+<section class="section" id="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">Linen · Natural · Woven</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">Loma Studio</div>
+ <p class="footer-text">Loma Studio — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · Loma Studio</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.com</span></span>
+ </a>
+ </div>
+ </div>
+</div>
+
+<!-- Inquiry modal -->
+<div class="dwm" id="dwmInquiry" onclick="if(event.target===this)dwmClose('Inquiry')">
+ <div class="dwm-box">
+ <button class="dwm-close" onclick="dwmClose('Inquiry')" aria-label="Close">×</button>
+ <h3>Project Inquiry</h3>
+ <p class="sub">We'll respond within one business day</p>
+ <form id="dwmInquiryForm" onsubmit="return dwmSubmit(event,'inquiry')">
+ <div class="dwm-field"><label>Name</label><input type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Phone</label><input type="tel" name="phone"></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Project Name</label><input type="text" name="projectName" required></div>
+ <div class="dwm-field"><label>Project Scope</label><textarea name="projectScope" placeholder="Square footage · room count · timeline" required></textarea></div>
+ <div class="dwm-field"><label>Additional Information</label><textarea name="message" placeholder="Patterns of interest, finish preferences, budget"></textarea></div>
+ <button type="submit" class="dwm-submit">Send Inquiry</button>
+ <div class="dwm-status" id="dwmInquiryStatus"></div>
+ </form>
+ </div>
+</div>
+
+<!-- Sample-request modal -->
+<div class="dwm" id="dwmSample" onclick="if(event.target===this)dwmClose('Sample')">
+ <div class="dwm-box">
+ <button class="dwm-close" onclick="dwmClose('Sample')" aria-label="Close">×</button>
+ <h3>Request a Sample</h3>
+ <p class="sub">Memo sample · ships free · 3–5 business days</p>
+ <div class="dwm-preview" id="dwmSamplePreview" style="display:none">
+ <img id="dwmSampleImg" src="" alt="">
+ <div><div class="pn" id="dwmSampleName"></div><div class="pc" id="dwmSampleSku"></div></div>
+ </div>
+ <form id="dwmSampleForm" onsubmit="return dwmSubmit(event,'sample')">
+ <input type="hidden" name="sku" id="dwmSampleSkuInput">
+ <input type="hidden" name="title" id="dwmSampleTitleInput">
+ <input type="hidden" name="image_url" id="dwmSampleImageInput">
+ <div class="dwm-field"><label>Name</label><input type="text" name="name" required></div>
+ <div class="dwm-field"><label>Email</label><input type="email" name="email" required></div>
+ <div class="dwm-field"><label>Company / Trade</label><input type="text" name="company"></div>
+ <div class="dwm-field"><label>Shipping Address</label><input type="text" name="address" placeholder="Street" required></div>
+ <div class="dwm-field" style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px">
+ <input type="text" name="city" placeholder="City" required>
+ <input type="text" name="state" placeholder="State" required>
+ <input type="text" name="zip" placeholder="ZIP" required>
+ </div>
+ <div class="dwm-field"><label>Notes (optional)</label><textarea name="message" placeholder="Project, sq.ft., timing"></textarea></div>
+ <button type="submit" class="dwm-submit">Send Sample Request</button>
+ <div class="dwm-status" id="dwmSampleStatus"></div>
+ </form>
+ </div>
+</div>
+
+<script>
+ // Universal modal control
+ function dwmOpen(name, opts){
+ document.getElementById('dwm' + name).classList.add('open');
+ document.body.style.overflow = 'hidden';
+ if (name === 'Sample' && opts) {
+ document.getElementById('dwmSamplePreview').style.display = 'flex';
+ document.getElementById('dwmSampleImg').src = opts.image_url || '';
+ document.getElementById('dwmSampleName').textContent = opts.title || '';
+ document.getElementById('dwmSampleSku').textContent = opts.sku || '';
+ document.getElementById('dwmSampleSkuInput').value = opts.sku || '';
+ document.getElementById('dwmSampleTitleInput').value = opts.title || '';
+ document.getElementById('dwmSampleImageInput').value = opts.image_url || '';
+ } else if (name === 'Sample') {
+ document.getElementById('dwmSamplePreview').style.display = 'none';
+ document.getElementById('dwmSampleSkuInput').value = '';
+ document.getElementById('dwmSampleTitleInput').value = '';
+ document.getElementById('dwmSampleImageInput').value = '';
+ }
+ }
+ function dwmClose(name){ document.getElementById('dwm'+name).classList.remove('open'); document.body.style.overflow=''; }
+
+ // Universal form submit — POSTs to /api/send-{kind}
+ async function dwmSubmit(e, kind) {
+ e.preventDefault();
+ const form = e.target;
+ const data = Object.fromEntries(new FormData(form).entries());
+ const btn = form.querySelector('.dwm-submit');
+ const status = form.querySelector('.dwm-status');
+ btn.disabled = true; const orig = btn.textContent; btn.textContent = 'Sending…';
+ status.className = 'dwm-status';
+ try {
+ const r = await fetch('/api/send-' + kind, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
+ if (!r.ok) throw new Error('HTTP '+r.status);
+ const j = await r.json();
+ status.textContent = kind === 'sample' ? 'Sample request sent — ships within 3–5 business days' : 'Inquiry sent — we respond within 1 business day';
+ status.className = 'dwm-status ok';
+ btn.textContent = 'Sent ✓';
+ setTimeout(() => { dwmClose(kind === 'sample' ? 'Sample' : 'Inquiry'); btn.disabled = false; btn.textContent = orig; status.className = 'dwm-status'; form.reset(); }, 2800);
+ } catch (err) {
+ status.textContent = 'Send failed — please email info@designerwallcoverings.com';
+ status.className = 'dwm-status err';
+ btn.disabled = false; btn.textContent = orig;
+ }
+ return false;
+ }
+
+ // Wire up "Memo samples ship free" badge in hero to open sample modal directly
+ document.querySelectorAll('.hero .badge, [data-open-sample]').forEach(el => {
+ el.style.cursor = 'pointer';
+ el.addEventListener('click', () => dwmOpen('Sample'));
+ });
+
+ // Wire nav "Trade" link to inquiry modal
+ document.querySelectorAll('nav a[href*="designerwallcoverings.com"]').forEach(a => {
+ a.removeAttribute('target');
+ a.removeAttribute('href');
+ a.style.cursor = 'pointer';
+ a.addEventListener('click', e => { e.preventDefault(); dwmOpen('Contact'); });
+ });
+
+ // Override product card clicks — open Sample modal pre-filled instead of redirecting to DW
+ document.addEventListener('click', e => {
+ const card = e.target.closest('.card, .rail-card');
+ if (!card) return;
+ if (card.tagName === 'A' && (card.getAttribute('href') || '').startsWith('/sample/')) {
+ e.preventDefault();
+ const img = card.querySelector('img');
+ const pat = card.querySelector('.pat');
+ const sku = card.getAttribute('href').replace('/sample/', '');
+ dwmOpen('Sample', {
+ sku,
+ title: pat ? pat.textContent.trim() : '',
+ image_url: img ? img.src : ''
+ });
+ }
+ }, true);
+// DW STANDARD: hero pulls from products.json[0].image_url so dw-collections-viewer
+// drag-drop / hero-pin updates the live front page on next deploy.
+// Always upgrades to the largest Shopify variant (strips _NxN size suffix) so
+// the full-page hero stays sharp.
+// DW STANDARD: hero rotates through top-20 NEWEST products on each visit
+// (novasuede.com pattern). Fetches /api/products?limit=100, sorts by created_at desc,
+// takes the freshest 20, picks one per visit via localStorage counter.
+// Falls back to static /hero-bg.jpg on any error.
+(async function rotateHero() {
+ try {
+ const r = await fetch('/api/products?limit=100');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || [])
+ .filter(p => p && p.image_url && /^https:\/\/(?:cdn\.shopify\.com|designerwallcoverings\.com)\//.test(p.image_url))
+ .sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))
+ .slice(0, 20);
+ if (!items.length) return;
+ let visits = 0;
+ try {
+ visits = parseInt(localStorage.getItem('hero_visits') || '0', 10) || 0;
+ localStorage.setItem('hero_visits', String(visits + 1));
+ } catch(e) {}
+ const pick = items[visits % items.length];
+ // Strip _NxN size suffix to get the largest CDN variant
+ const url = pick.image_url.replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ const bg = document.getElementById('heroBg') || document.getElementById('cinemaBg');
+ if (bg) bg.style.backgroundImage = "url('" + url + "')";
+ } catch (e) {}
+})();
+
+
+// DW STANDARD sort-select wire-up (added by fleet patcher)
+(function(){
+ const sortSel = document.getElementById('sortSelect');
+ if (!sortSel) return;
+ const KEY = location.hostname.replace(/\./g, '_') + '_sort';
+ const saved = localStorage.getItem(KEY);
+ if (saved) { try { sortSel.value = saved; } catch(e){} }
+ if (typeof state !== 'undefined') state.sort = sortSel.value;
+ sortSel.addEventListener('change', e => {
+ const v = e.target.value;
+ try { localStorage.setItem(KEY, v); } catch(e){}
+ if (typeof state !== 'undefined') state.sort = v;
+ if (typeof resetGrid === 'function') resetGrid();
+ else location.reload();
+ });
+})();
+
+// graphics-loop pass 11: hydrate Ideas rails from /api/products
+(async function hydrateIdeas() {
+ try {
+ const r = await fetch('/api/products?limit=80');
+ if (!r.ok) return;
+ const d = await r.json();
+ const items = (d.items || []).filter(p => p && p.image_url);
+ if (!items.length) return;
+ const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ const newest = sorted.slice(0, 12);
+ // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
+ const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+ function card(p) {
+ const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
+ return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
+ '<img src="' + url + '" alt="' + (p.title || '').replace(/"/g, '"') + '" loading="lazy">' +
+ '<div class="rc-title">' + ((p.title || '').split('|')[0] || '').trim() + '</div>' +
+ '<div class="rc-meta">' + (p.vendor || '') + '</div>' +
+ '</a>';
+ }
+ const r1 = document.getElementById('railNewest');
+ const r2 = document.getElementById('railCurated');
+ if (r1) r1.innerHTML = newest.map(card).join('');
+ if (r2) r2.innerHTML = curated.map(card).join('');
+ } catch(e) {}
+})();
+</script>
+
+</body>
+</html>
diff --git a/public/index.html.pre-sort-frontend b/public/index.html.pre-sort-frontend
new file mode 100644
index 0000000..e1d4c13
--- /dev/null
+++ b/public/index.html.pre-sort-frontend
@@ -0,0 +1,902 @@
+<!doctype html>
+<html lang="en" data-hero-mode="huge">
+<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-DKS5X482LQ"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', 'G-DKS5X482LQ');
+</script>
+
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>linenwallpaper — curated wallcoverings, real samples ship free.</title>
+<meta name="description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta name="theme-color" content="#F2EDE2">
+<link rel="canonical" href="https://linenwallpaper.com/">
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<link rel="preload" as="image" href="/hero-bg.jpg" fetchpriority="high">
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ "name": "linenwallpaper",
+ "url": "https://linenwallpaper.com/",
+ "email": "info@linenwallpaper.com"
+}
+</script>
+<script src="/zd-loader.js" defer></script>
+<style>
+/* linenwallpaper · Loewe template — soft cream paper + sienna accent, hand-craft restraint */
+:root {
+ --bg: #F2EDE2;
+ --paper: #2C1F0F;
+ --muted: #8B7866;
+ --line: #C9BFA7;
+ --accent: #9B7B4F;
+ --bg-soft: #C9BFA7;
+ --cols: 6;
+}
+html[data-theme="dark"] {
+ --bg: #2C1F0F;
+ --paper: #F2EDE2;
+ --muted: #C9B89E;
+ --line: rgba(255,254,250,0.12);
+ --accent: #9B7B4F;
+ --bg-soft: #2a1f15;
+}
+* { margin:0; padding:0; box-sizing:border-box }
+html { scroll-behavior:smooth }
+body { font-family:'Inter', sans-serif; color:var(--paper); -webkit-font-smoothing:antialiased; background:var(--bg); min-height:100vh; overflow-x:hidden }
+/* DW-FASHION-TEMPLATE-OVERRIDE */
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif !important; font-weight:500 !important; }
+.section-title .accent, .footer-brand, .center-mark, h1, h2, h3 { font-family: 'Libre Baskerville', Georgia, serif !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 (R2: huge full-bleed photo + top-bar logo + below-photo wordmark) ===== */
+.cinema { position:relative; min-height:clamp(560px, 90vh, 1080px); 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.20) 0%, rgba(0,0,0,0) 35%, rgba(0,0,0,0) 65%, rgba(0,0,0,0.55) 100%) }
+/* Hero-mode hooks (R4): both modes share the same min-height — CLS-free */
+[data-hero-mode="four-square"] .cinema-bg { display:none }
+[data-hero-mode="four-square"] .cinema-grid { display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; position:absolute; inset:0 }
+[data-hero-mode="four-square"] .cinema-grid .cell { background-size:cover; background-position:center; border:1px solid rgba(0,0,0,0.4) }
+.cinema-grid { display:none }
+.cinema-logo { position:absolute; top:22px; left:32px; font-family: ui-serif, Georgia, serif; font-size:18px; font-weight:500; letter-spacing:0.02em; color:#fff; text-decoration:none; z-index:6; text-shadow:0 1px 4px rgba(0,0,0,0.5) }
+/* Below-photo wordmark (R2): brand display lives below the hero, not painted on it */
+.hero-wordmark-block { position:relative; padding:38px 32px 24px; text-align:center; background:var(--bg) }
+.hero-wordmark { font-family: ui-serif, Georgia, 'Cormorant Garamond', serif; font-weight:400; font-size:clamp(48px, 9vw, 128px); letter-spacing:-0.01em; line-height:1; color:var(--paper); margin:0; text-transform:none }
+.hero-wordmark-sub { display:block; font-family:'Playfair Display', Georgia, serif; font-style:italic; font-weight:400; font-size:clamp(13px, 1.4vw, 17px); letter-spacing:0.04em; text-transform:none; margin-top:14px; color:var(--muted) }
+@media (max-width:760px) { .hero-wordmark { font-size:clamp(32px, 12vw, 64px) } }
+.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 deprecated 2026-05-10 per R2 — hidden */
+.center-mark { display:none !important; 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('linen_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
+</script>
+
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://linenwallpaper.com/">
+<meta property="og:title" content="linenwallpaper — curated wallcoverings, real samples ship free.">
+<meta property="og:description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta property="og:image" content="https://linenwallpaper.com/hero-bg.jpg">
+<meta property="og:site_name" content="linenwallpaper">
+<meta property="article:publisher" content="https://www.facebook.com/SteveAbramsDesigns.DesignerWallcoverings">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="linenwallpaper — curated wallcoverings, real samples ship free.">
+<meta name="twitter:description" content="linenwallpaper — a curated wallcovering atelier. Free memo samples. Trade service.">
+<meta name="twitter:image" content="https://linenwallpaper.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">linenwallpaper</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@linenwallpaper.com?subject=linenwallpaper%20inquiry"><span class="ico">✉️</span><div class="txt"><b>Email Us</b><span>info@linenwallpaper.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" id="hero">
+ <a class="cinema-logo" href="/" aria-label="linenwallpaper home">linenwallpaper</a>
+ <div class="cinema-bg" id="heroBg"></div>
+ <div class="cinema-grid" id="heroGrid" aria-hidden="true"></div>
+ <div class="corner-mark">Natural Fiber</div>
+ <div class="meta-line">Linen · Natural · Woven<span class="num" id="heroNum"></span></div>
+ <a class="enter" href="#shop">Enter <svg viewBox="0 0 24 12" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M0 6h22M16 1l6 5-6 5"/></svg></a>
+</section>
+<div class="hero-wordmark-block">
+ <h1 class="hero-wordmark">linenwallpaper</h1>
+ <span class="hero-wordmark-sub" id="heroWordmarkSub"></span>
+</div>
+
+
+<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">Linen · Natural · Woven</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">linenwallpaper</div>
+ <p class="footer-text">linenwallpaper — 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>
+ <!-- mini-constellation: every storefront shows its position in the family -->
+ <div class="footer-bottom">
+ <span>linenwallpaper.com · linenwallpaper</span>
+ <span><a href="mailto:info@linenwallpaper.com" style="color:var(--paper);text-decoration:none;border-bottom:1px solid var(--accent);padding-bottom:1px">info@linenwallpaper.com</a></span>
+ <span id="footerStat"></span>
+ </div>
+</footer>
+
+<script>
+// mini-constellation — 43 niche-stars, current site highlighted in gold
+(function(){
+ const SLUG = "linenwallpaper";
+ 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('linen_theme_density', n); } catch(e){}
+}
+slider.addEventListener('input', e => setDensity(parseInt(e.target.value)));
+const savedDensity = parseInt(localStorage.getItem('linen_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('linen_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@linenwallpaper.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@linenwallpaper.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>
+/* R4 — Alternating hero modes (huge / four-square). Server-side default is huge (HTML attr).
+ Client-only flip starts on visit 2. Both modes share the same .cinema min-height (CLS-free).
+ Crawlers without JS always see huge. */
+(function(){
+ try {
+ var k='dw-hero-mode';
+ var prev=parseInt(localStorage.getItem(k)||'0',10);
+ var next=(prev+1)%2;
+ localStorage.setItem(k,String(next));
+ if (prev>0) {
+ var mode = next===0 ? 'huge' : 'four-square';
+ document.documentElement.dataset.heroMode = mode;
+ if (mode === 'four-square') {
+ fetch('/api/products?limit=4&page=1').then(r=>r.ok?r.json():null).then(j=>{
+ if (!j || !j.items || j.items.length < 4) {
+ document.documentElement.dataset.heroMode = 'huge';
+ console.warn('dw-hero-stale: insufficient items for four-square, falling back to huge');
+ return;
+ }
+ var grid = document.getElementById('heroGrid');
+ if (!grid) return;
+ j.items.slice(0,4).forEach(function(it){
+ var d = document.createElement('div');
+ d.className = 'cell';
+ var img = (it.image_url || it.image || '/hero-bg.jpg');
+ d.style.backgroundImage = "url('" + img + "')";
+ grid.appendChild(d);
+ });
+ }).catch(function(){
+ document.documentElement.dataset.heroMode = 'huge';
+ });
+ }
+ }
+ } catch(e) { /* no-op: keep server default */ }
+})();
+</script>
+</body>
+</html>
diff --git a/public/sourcing.html b/public/sourcing.html
new file mode 100644
index 0000000..aceca62
--- /dev/null
+++ b/public/sourcing.html
@@ -0,0 +1,59 @@
+<!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 — linenwallpaper.com</title>
+<meta name="description" content="How we source — Soft as linen. Made slowly.">
+<link rel="canonical" href="https://linenwallpaper.com/sourcing">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-serif;background:#F2EDE2;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#9B7B4F;margin-bottom:18px}
+h1{font-family:'Saol Display', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Saol Display', serif;font-size:22px;color:#9B7B4F;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#9B7B4F;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 · Loma Studio</div>
+<h1>How we source</h1>
+<p>We begin with the quiet rhythm of the loom, the whisper of threads meeting in the hands of artisans who have spent lifetimes mastering their craft. Our journey to curate linen patterns begins not in a showroom, but in the quiet corners of mills where tradition and precision coexist. Here, the air is thick with the scent of natural fibers and the soft hum of machines that have been fine-tuned over generations. These mills, many of which have been operating for decades, are the heartbeat of our selection process. They are not chosen for speed or scale, but for their dedication to the integrity of linen—a material that demands respect and care.</p>
+
+<h2>The Mills: A Legacy of Craft</h2>
+
+<p>Our partnerships with mills are rooted in a shared philosophy: to honor the material’s inherent beauty. We visit these mills not as buyers, but as collaborators. Each mill we consider has a history of working with linen, often passed down through families or maintained by small collectives. They understand that linen is not a commodity to be rushed, but a story to be told through texture and hue. We prioritize mills that use traditional methods, where the warp and weft are woven with a patience that mirrors our own.</p>
+
+<p>These mills are not found in trade catalogs meant for mass consumption. Instead, we seek out those that operate in the margins of the industry, where exclusivity is not a marketing tactic but a necessity. Their catalogs are not glossy or filled with fleeting trends; they are hand-bound tomes or digital files that feel like letters from an old friend. Each entry is a testament to the mill’s commitment to linen’s unique qualities—its breathability, its resilience, its capacity to age gracefully.</p>
+
+<h2>The Criteria: A Gentle but Rigorous Filter</h2>
+
+<p>When we receive samples, we apply a filter that is as much about intuition as it is about technicality. The first criterion is the substrate itself. Linen must be sourced from flax grown in regions where the climate and soil create fibers of exceptional strength and character. We reject any that feel synthetic, compromised, or lacking in the organic irregularity that defines true linen.</p>
+
+<ul>
+ <li><strong>Repeat and Scale:</strong> We favor patterns that are neither too small nor too large, allowing the design to breathe without overwhelming the space. The repeat must feel intentional, not forced.</li>
+ <li><strong>Washfastness:</strong> Our standards for color retention are high. We test each sample under conditions that mimic years of use, ensuring that the hues remain true and the fabric remains unyielding to the elements.</li>
+ <li><strong>Hand and Drape:</strong> The tactile experience of the linen is non-negotiable. It must feel alive in the hand, with a drape that suggests movement rather than stiffness.</li>
+</ul>
+
+<h2>What Gets Cut: The Art of Letting Go</h2>
+
+<p>Not every sample finds a home in our collection. Some are discarded for reasons that may seem minor but are, to us, deeply significant. A repeat that feels disjointed, a color that does not hold up under scrutiny, or a texture that lacks the warmth of handwoven linen—all are quietly set aside. We do not shy away from this process. It is a necessary part of curation, a way to ensure that what remains is not only beautiful but also enduring.</p>
+
+<p>Our criteria are not rigid, but they are clear. We cut what does not align with our vision, not out of cruelty, but out of loyalty to the material and the stories it carries. In the end, our collection is not about abundance, but about presence. Each linen we include is a choice made with care, a thread in the tapestry of what we believe is possible when time, skill, and material are allowed to speak freely.</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..d8f2fd4
--- /dev/null
+++ b/public/trade.html
@@ -0,0 +1,63 @@
+<!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 — linenwallpaper.com</title>
+<meta name="description" content="Trade program — Soft as linen. Made slowly.">
+<link rel="canonical" href="https://linenwallpaper.com/trade">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-serif;background:#F2EDE2;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#9B7B4F;margin-bottom:18px}
+h1{font-family:'Saol Display', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Saol Display', serif;font-size:22px;color:#9B7B4F;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#9B7B4F;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 · Loma Studio</div>
+<h1>Trade program</h1>
+<p>Welcome to the heart of a carefully considered practice, where every detail is shaped by time and intention. Loma Studio invites designers, architects, and contractors to explore a world of linen that is not rushed, but refined—a collaboration rooted in the quiet confidence of a material that has been woven, tested, and trusted for generations.</p>
+
+<h2>Who Qualifies</h2>
+<p>Loma Studio’s trade program is designed for professionals who approach their work with the same care that defines our linen. This includes:</p>
+<ul>
+ <li>Designers working on residential or commercial interiors</li>
+ <li>Architects seeking materials that align with a thoughtful, handcrafted vision</li>
+ <li>Contractors who value quality, consistency, and a curated selection of finishes</li>
+</ul>
+<p>Whether you’re refining a small project or curating a large-scale vision, our team is here to support your process with expertise and attention to detail.</p>
+
+<h2>Memo Samples</h2>
+<p>To help you find the perfect match, we offer memo samples—small, tactile swatches that capture the essence of each linen. These are sent directly to your inbox, allowing you to experience the weight, sheen, and character of our materials before committing to a larger order. Requests are handled with care, and we aim to dispatch samples within five business days of receipt.</p>
+
+<h2>Lead Times</h2>
+<p>Our commitment to quality means we take the time needed to ensure every roll meets our standards. For standard linen selections, lead times are typically 10–14 business days from order confirmation. Custom-print options may require additional time, which we will communicate clearly during the initial inquiry process.</p>
+
+<h2>NET-30 Terms</h2>
+<p>We believe in fostering long-term partnerships built on trust and transparency. As a trade partner, you’ll be eligible for NET-30 terms on all orders, allowing you the flexibility to manage your workflow without compromising on quality or timing.</p>
+
+<h2>Custom-Print Availability</h2>
+<p>While we curate a selection of linen that reflects our values, we also welcome the unique vision of our collaborators. Custom-print options are available for those who wish to create bespoke designs, ensuring your project stands out with a personal touch. Please note that custom requests require a minimum order quantity and will be reviewed on a case-by-case basis.</p>
+
+<h2>The Designer Wallcoverings Legacy</h2>
+<p>Loma Studio is part of Designer Wallcoverings, a name that has guided the design trade since 1996. As a family-owned entity, we have built a legacy of depth, integrity, and connection—offering a breadth of linen that is both accessible and exclusive. Our parent house ensures that every roll, weave, and finish is vetted with the same rigor that has defined our industry for decades. Whether you’re seeking a classic linen or a contemporary interpretation, you’ll find that our inventory is shaped by experience, and our team is here to support your journey with real, human connection.</p>
+
+<p>At Loma Studio, we believe in the power of a material that is both humble and enduring. Our trade program is not about speed or volume—it is about partnership, patience, and the quiet confidence that comes from working with something that has stood the test of time.</p>
+<nav class="foot">
+<a href="/">Browse</a>
+<a href="/history">History</a>
+<a href="/vocabulary">Vocabulary</a>
+<a href="/sourcing">Sourcing</a>
+<a href="/care">Care</a>
+<a href="/trade">Trade</a>
+<a href="/about">About</a>
+</nav>
+</div></body></html>
\ No newline at end of file
diff --git a/public/vocabulary.html b/public/vocabulary.html
new file mode 100644
index 0000000..562b9ed
--- /dev/null
+++ b/public/vocabulary.html
@@ -0,0 +1,46 @@
+<!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 — linenwallpaper.com</title>
+<meta name="description" content="Design vocabulary — Soft as linen. Made slowly.">
+<link rel="canonical" href="https://linenwallpaper.com/vocabulary">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:ui-serif;background:#F2EDE2;color:#1a1410;line-height:1.65;padding:60px 24px;min-height:100vh}
+.wrap{max-width:720px;margin:0 auto}
+.eyebrow{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:#9B7B4F;margin-bottom:18px}
+h1{font-family:'Saol Display', serif;font-style:italic;font-size:48px;line-height:1.1;letter-spacing:-0.02em;color:#1a1410;margin-bottom:32px}
+h2{font-family:'Saol Display', serif;font-size:22px;color:#9B7B4F;margin:36px 0 12px;font-weight:500}
+p{margin-bottom:16px;color:#3a322c}
+ul{margin:12px 0 20px 18px}
+li{margin-bottom:8px;color:#3a322c}
+strong{color:#1a1410}
+a{color:#9B7B4F;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 · Loma Studio</div>
+<h1>Design vocabulary</h1>
+<p>When selecting linen, understanding the language of the material is as important as knowing its origins. Linen is a material of patience and precision, and the terms that define it are rooted in centuries of craftsmanship. Here are key design terms that guide the selection and appreciation of linen, offering clarity to those who seek depth in texture and history in every thread.</p>
+
+<h2>Loom Weight</h2>
+<p>The measure of a linen’s thickness, determined by the number of threads per square inch. A heavier loom weight offers sturdier drape and a more substantial presence, while lighter weights are softer and more fluid. In our product, a medium loom weight is often chosen for its balance between durability and elegance, ideal for statement walls that hold their shape without feeling rigid.</p>
+
+<h2>Selvage</h2>
+<p>The tightly woven edge of a linen roll, designed to prevent fraying. It is a marker of the roll’s integrity and a natural boundary of the fabric. Our rolls are cut with clean, unaltered selvage, which can be left exposed to highlight the linen’s handwoven quality or hidden to create a seamless look.</p>
+
+<h2>Dye Bath</h2>
+<p>The process of immersing linen in liquid dye to achieve color. The depth and evenness of the dye bath influence the final hue and its longevity. A single dye bath ensures uniformity,
+<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..8516d27
--- /dev/null
+++ b/server.js.pre-sort-skill
@@ -0,0 +1,159 @@
+/**
+ * LINEN WALLPAPER — DW family vertical
+ * Curated slice from live designerwallcoverings.com Shopify catalog.
+ */
+const express = require('express');
+const helmet = require('helmet');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = process.env.PORT || 9842;
+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 = ["linen","natural","woven","flax"];
+const NICHE_NEG = ["solid","blank","plain"];
+function nicheFit(p) {
+ const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
+ if (NICHE_NEG.some(n => blob.includes(n.toLowerCase()))) return false;
+ return NICHE_POS.length === 0 || NICHE_POS.some(k => blob.includes(k.toLowerCase()));
+}
+const PRODUCTS_NICHE = PRODUCTS.filter(nicheFit);
+const NICHE_DROPPED = PRODUCTS.length - PRODUCTS_NICHE.length;
+console.log(`Niche filter: kept ${PRODUCTS_NICHE.length} of ${PRODUCTS.length}, dropped ${NICHE_DROPPED}`);
+// Replace PRODUCTS reference with the niche-filtered list for all downstream handlers.
+const PRODUCTS_ORIG = PRODUCTS;
+Object.defineProperty(global, '__products_niche_applied', { value: true });
+
+
+const app = express();
+// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
+// DW STANDARD: sort by color/style/sku/title for any product-rendering site.
+const COLOR_RANK = {
+ black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
+ red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+ yellow:30,gold:31,mustard:32,olive:33,
+ green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
+ blue:60,navy:61,indigo:62,periwinkle:63,
+ purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
+};
+function colorRank(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
+ return 999;
+}
+const STYLE_TAGS = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','mid century','rustic','industrial','farmhouse','bohemian','boho','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
+function styleKey(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
+ return 'zzz';
+}
+function sortProducts(list, mode) {
+ if (mode === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+ if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'price-asc') return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
+ if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+ return list;
+}
+
+
+app.use(helmet({ contentSecurityPolicy: false }));
+app.use(express.json({ limit: '256kb' }));
+// Universal contact module — modals, /api/send-inquiry, /api/send-sample, /zd-loader.js
+require('./_universal-contact')(app, { siteName: "linenwallpaper", zdColor: "#a89060", zdPosition: 'right' });
+require('./_universal-auth')(app, { siteName: "linenwallpaper" });
+
+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 = ["natural","woven","botanical","abstract","stripe","neutral"];
+ 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://linenwallpaper.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://linenwallpaper.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(`linenwallpaper listening on http://127.0.0.1:${PORT}`);
+});
← a483e2a sort-skill UI: add Light→Dark / Dark→Light / Color Wheel opt
·
back to Linenwallpaper
·
fleet-grid refine: chip_uniform_aspect (linenwallpaper) 6bac45f →