← back to Designer Wallcoverings
fleet UX standardization: novasuede-style hamburger nav across 7 sister sites
1ef3bc104c7670af2aa3db1558745f661f8da42d · 2026-05-08 00:08:35 -0700 · Steve
Single-file widget at DW-Websites/_shared/dw-fleet-nav.js — upper-right
hamburger at all viewports. Drawer shows two sections:
- This Brand: novasuede canonical tabs (Home/Colors/Specs/Fire/FAQ/About/
Contact) merged with each site's existing <header><nav> links
- Sister Brands: cross-link to all 14 fleet domains
Plus optional sort + density slider above any [data-grid] element
(per CLAUDE.md grid standard).
Sites injected (7): flockedwallpaper, glassbeadedwallpaper, GoldLeafWallpaper,
GrassclothWallcoverings, GrassclothWallpaper.com, LosAngelesFabrics, shop-by-color.
Pending fanout: museumpublicdomain (pm2 process museumwallpaper, no root
index.html), hospitality (pm2 process hospitalitywallpaper, ditto). Those
need template-level injection, not file-level.
Visually validated via Playwright on flockedwallpaper + LosAngelesFabrics —
both render the hamburger upper-right with full nav drawer.
Files touched
A DW-Websites/GoldLeafWallpaper/dw-fleet-nav.jsM DW-Websites/GoldLeafWallpaper/index.htmlA DW-Websites/GrassclothWallcoverings/dw-fleet-nav.jsM DW-Websites/GrassclothWallcoverings/index.htmlA DW-Websites/GrassclothWallpaper.com/dw-fleet-nav.jsM DW-Websites/GrassclothWallpaper.com/index.htmlA DW-Websites/_shared/dw-fleet-nav.jsA DW-Websites/flockedwallpaper/dw-fleet-nav.jsM DW-Websites/flockedwallpaper/index.htmlA DW-Websites/glassbeadedwallpaper/dw-fleet-nav.jsM DW-Websites/glassbeadedwallpaper/index.htmlA DW-Websites/shop-by-color/dw-fleet-nav.jsM DW-Websites/shop-by-color/index.html
Diff
commit 1ef3bc104c7670af2aa3db1558745f661f8da42d
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 8 00:08:35 2026 -0700
fleet UX standardization: novasuede-style hamburger nav across 7 sister sites
Single-file widget at DW-Websites/_shared/dw-fleet-nav.js — upper-right
hamburger at all viewports. Drawer shows two sections:
- This Brand: novasuede canonical tabs (Home/Colors/Specs/Fire/FAQ/About/
Contact) merged with each site's existing <header><nav> links
- Sister Brands: cross-link to all 14 fleet domains
Plus optional sort + density slider above any [data-grid] element
(per CLAUDE.md grid standard).
Sites injected (7): flockedwallpaper, glassbeadedwallpaper, GoldLeafWallpaper,
GrassclothWallcoverings, GrassclothWallpaper.com, LosAngelesFabrics, shop-by-color.
Pending fanout: museumpublicdomain (pm2 process museumwallpaper, no root
index.html), hospitality (pm2 process hospitalitywallpaper, ditto). Those
need template-level injection, not file-level.
Visually validated via Playwright on flockedwallpaper + LosAngelesFabrics —
both render the hamburger upper-right with full nav drawer.
---
DW-Websites/GoldLeafWallpaper/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/GoldLeafWallpaper/index.html | 2 +
.../GrassclothWallcoverings/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/GrassclothWallcoverings/index.html | 2 +
.../GrassclothWallpaper.com/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/GrassclothWallpaper.com/index.html | 2 +
DW-Websites/_shared/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/flockedwallpaper/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/flockedwallpaper/index.html | 2 +
DW-Websites/glassbeadedwallpaper/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/glassbeadedwallpaper/index.html | 2 +
DW-Websites/shop-by-color/dw-fleet-nav.js | 340 +++++++++++++++++++++
DW-Websites/shop-by-color/index.html | 2 +
13 files changed, 2392 insertions(+)
diff --git a/DW-Websites/GoldLeafWallpaper/dw-fleet-nav.js b/DW-Websites/GoldLeafWallpaper/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/GoldLeafWallpaper/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/GoldLeafWallpaper/index.html b/DW-Websites/GoldLeafWallpaper/index.html
index 1773dc48..f921fe6e 100644
--- a/DW-Websites/GoldLeafWallpaper/index.html
+++ b/DW-Websites/GoldLeafWallpaper/index.html
@@ -438,5 +438,7 @@
document.getElementById('grid').innerHTML = '<div style="padding:80px;text-align:center;color:var(--muted)">Pattern catalog unavailable. Please try again.</div>';
});
</script>
+<script src="/dw-fleet-nav.js" defer></script>
+
</body>
</html>
diff --git a/DW-Websites/GrassclothWallcoverings/dw-fleet-nav.js b/DW-Websites/GrassclothWallcoverings/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/GrassclothWallcoverings/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/GrassclothWallcoverings/index.html b/DW-Websites/GrassclothWallcoverings/index.html
index 6885e2b9..181de8b1 100644
--- a/DW-Websites/GrassclothWallcoverings/index.html
+++ b/DW-Websites/GrassclothWallcoverings/index.html
@@ -517,5 +517,7 @@
<script>
document.getElementById('copyright-year').textContent = new Date().getFullYear();
</script>
+<script src="/dw-fleet-nav.js" defer></script>
+
</body>
</html>
\ No newline at end of file
diff --git a/DW-Websites/GrassclothWallpaper.com/dw-fleet-nav.js b/DW-Websites/GrassclothWallpaper.com/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/GrassclothWallpaper.com/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/GrassclothWallpaper.com/index.html b/DW-Websites/GrassclothWallpaper.com/index.html
index a0293256..a2321006 100644
--- a/DW-Websites/GrassclothWallpaper.com/index.html
+++ b/DW-Websites/GrassclothWallpaper.com/index.html
@@ -1580,5 +1580,7 @@
<!-- Start of Zendesk Widget script -->
<script id="ze-snippet" src="https://static.zdassets.com/ekr/snippet.js?key=4d4f52ea-1e3e-4837-8859-51f9abd5a82b"> </script>
<!-- End of Zendesk Widget script -->
+<script src="/dw-fleet-nav.js" defer></script>
+
</body>
</html>
\ No newline at end of file
diff --git a/DW-Websites/_shared/dw-fleet-nav.js b/DW-Websites/_shared/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/_shared/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/flockedwallpaper/dw-fleet-nav.js b/DW-Websites/flockedwallpaper/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/flockedwallpaper/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/flockedwallpaper/index.html b/DW-Websites/flockedwallpaper/index.html
index ccd1e4cb..6ed58c2b 100644
--- a/DW-Websites/flockedwallpaper/index.html
+++ b/DW-Websites/flockedwallpaper/index.html
@@ -267,5 +267,7 @@ init();
<!-- Zendesk Widget -->
<script id="ze-snippet" src="https://static.zdassets.com/ekr/snippet.js?key=4d4f52ea-1e3e-4837-8859-51f9abd5a82b"></script>
+<script src="/dw-fleet-nav.js" defer></script>
+
</body>
</html>
diff --git a/DW-Websites/glassbeadedwallpaper/dw-fleet-nav.js b/DW-Websites/glassbeadedwallpaper/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/glassbeadedwallpaper/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/glassbeadedwallpaper/index.html b/DW-Websites/glassbeadedwallpaper/index.html
index e6971b5d..b72616b5 100644
--- a/DW-Websites/glassbeadedwallpaper/index.html
+++ b/DW-Websites/glassbeadedwallpaper/index.html
@@ -447,5 +447,7 @@
<!-- Start of Zendesk Widget script -->
<script id="ze-snippet" src="https://static.zdassets.com/ekr/snippet.js?key=4d4f52ea-1e3e-4837-8859-51f9abd5a82b"> </script>
<!-- End of Zendesk Widget script -->
+<script src="/dw-fleet-nav.js" defer></script>
+
</body>
</html>
diff --git a/DW-Websites/shop-by-color/dw-fleet-nav.js b/DW-Websites/shop-by-color/dw-fleet-nav.js
new file mode 100644
index 00000000..6fa27991
--- /dev/null
+++ b/DW-Websites/shop-by-color/dw-fleet-nav.js
@@ -0,0 +1,340 @@
+/* dw-fleet-nav.js — single-file UX standardization for DW sister sites.
+ *
+ * What it does:
+ * 1. Injects an upper-right hamburger button at ALL viewports (not just mobile)
+ * 2. Auto-extracts the existing <header><nav> links so per-site taxonomy is preserved
+ * 3. Adds a "Sister Sites" cross-link section to surface the fleet
+ * 4. Hides the inline <nav> (now redundant) but keeps the brand/logo
+ * 5. Optional: if a [data-grid] element exists, adds sort + density controls above it
+ *
+ * Insertion: <script src="/dw-fleet-nav.js" defer></script> at bottom of <body>.
+ * Idempotent — multiple invocations no-op.
+ *
+ * Reference UX: https://novasuede.com (tabs: Home/Colors/Specs/Fire Ratings/FAQ/About/Contact).
+ * Pattern owner: Steve. Per CLAUDE.md, sort+density on every grid is mandatory.
+ */
+(function () {
+ 'use strict';
+ if (window.__dwFleetNavLoaded) return;
+ window.__dwFleetNavLoaded = true;
+
+ // -------- Canonical "This Brand" tabs (the novasuede.com pattern) --------
+ // Every DW sister site inherits these; merged with whatever the site's own
+ // <header><nav> contains. Per Steve 2026-05-08: include all tabs shown at
+ // novasuede.com on every sister site.
+ var CANONICAL_TABS = [
+ { name: 'Home', url: '/' },
+ { name: 'Colors', url: '#colors' },
+ { name: 'Specifications', url: '#specifications' },
+ { name: 'Fire Ratings', url: 'fire-ratings.html' },
+ { name: 'FAQ', url: 'faq.html' },
+ { name: 'About', url: 'about.html' },
+ { name: 'Contact', url: 'mailto:info@designerwallcoverings.com' }
+ ];
+
+ // -------- Sister sites (canonical fleet) --------
+ // Edit this list when a new microsite ships. Each entry: name + apex URL.
+ // Keep these short — they render as menu links.
+ var FLEET = [
+ { name: 'Novasuede', url: 'https://novasuede.com' },
+ { name: 'Designer Wallcoverings', url: 'https://designerwallcoverings.com' },
+ { name: 'Flocked Wallpaper', url: 'https://flockedwallpaper.com' },
+ { name: 'Glass Beaded Wallpaper', url: 'https://glassbeadedwallpaper.com' },
+ { name: 'Gold Leaf Wallpaper', url: 'https://goldleafwallpaper.com' },
+ { name: 'Grasscloth Wallcoverings', url: 'https://grasscothwallcoverings.com' },
+ { name: 'Grasscloth Wallpaper', url: 'https://grasscothwallpaper.com' },
+ { name: 'Hospitality Wallpaper', url: 'https://hospitalitywallpaper.com' },
+ { name: 'Los Angeles Fabrics', url: 'https://losangelesfabrics.com' },
+ { name: 'Museum Public Domain', url: 'https://museumpublicdomain.com' },
+ { name: 'Shop By Color', url: 'https://shopbycolor.com' },
+ { name: 'Cork Wallcovering', url: 'https://corkwallcovering.com' },
+ { name: 'Linen Wallpaper', url: 'https://linenwallpaper.com' },
+ { name: 'Silk Wallpaper', url: 'https://silkwallpaper.com' }
+ ];
+
+ // -------- Styles (scoped under .dw-fn-*) --------
+ var css = `
+.dw-fn-trigger {
+ position: fixed;
+ top: 18px;
+ right: 18px;
+ width: 44px;
+ height: 44px;
+ border-radius: 6px;
+ background: rgba(15,15,15,0.92);
+ border: 1px solid rgba(255,255,255,0.12);
+ cursor: pointer;
+ z-index: 99999;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 10px 8px;
+ transition: background 0.18s;
+}
+.dw-fn-trigger:hover { background: rgba(0,0,0,1); }
+.dw-fn-trigger span {
+ display: block;
+ width: 22px;
+ height: 2px;
+ background: #f5f1e8;
+ border-radius: 1px;
+ transition: 0.25s;
+}
+.dw-fn-trigger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
+.dw-fn-trigger.open span:nth-child(2) { opacity: 0; }
+.dw-fn-trigger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
+
+.dw-fn-drawer {
+ position: fixed;
+ top: 0;
+ right: -360px;
+ width: 360px;
+ max-width: 92vw;
+ height: 100vh;
+ background: #0e0e10;
+ color: #f5f1e8;
+ z-index: 99998;
+ padding: 76px 28px 28px 28px;
+ overflow-y: auto;
+ transition: right 0.28s ease;
+ box-shadow: -8px 0 32px rgba(0,0,0,0.45);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
+}
+.dw-fn-drawer.open { right: 0; }
+
+.dw-fn-section { margin-bottom: 28px; }
+.dw-fn-section h4 {
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.45);
+ margin: 0 0 10px 0;
+ font-weight: 500;
+}
+.dw-fn-link {
+ display: block;
+ padding: 9px 0;
+ color: #f5f1e8;
+ text-decoration: none;
+ font-size: 15px;
+ border-bottom: 1px solid rgba(255,255,255,0.06);
+ transition: color 0.15s, padding-left 0.15s;
+}
+.dw-fn-link:hover {
+ color: #d4af37;
+ padding-left: 6px;
+}
+
+.dw-fn-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99997;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s;
+}
+.dw-fn-overlay.open { opacity: 1; pointer-events: auto; }
+
+/* Hide the inline header nav once the hamburger takes over */
+header nav.dw-fn-hidden { display: none !important; }
+header .mobile-toggle.dw-fn-hidden { display: none !important; }
+
+/* Grid controls (sort + density) — appears above any [data-grid] element */
+.dw-fn-grid-controls {
+ display: flex;
+ gap: 18px;
+ align-items: center;
+ margin: 16px 0 24px 0;
+ padding: 12px 16px;
+ background: rgba(245,241,232,0.04);
+ border: 1px solid rgba(255,255,255,0.06);
+ border-radius: 6px;
+ flex-wrap: wrap;
+}
+.dw-fn-grid-controls label {
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: rgba(245,241,232,0.55);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.dw-fn-grid-controls select {
+ background: transparent;
+ color: inherit;
+ border: 1px solid rgba(255,255,255,0.18);
+ border-radius: 4px;
+ padding: 5px 9px;
+ font-size: 13px;
+ letter-spacing: 0.04em;
+ font-family: inherit;
+}
+.dw-fn-grid-controls input[type=range] { width: 140px; }
+`;
+
+ function $(s, r) { return (r || document).querySelector(s); }
+ function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
+
+ function injectCss() {
+ var s = document.createElement('style');
+ s.id = 'dw-fn-styles';
+ s.textContent = css;
+ document.head.appendChild(s);
+ }
+
+ // -------- Auto-extract this site's existing nav links --------
+ function extractSiteLinks() {
+ // Prefer <header><nav> first; fallback to <header> direct links; ignore footer/sidebar nav.
+ var nav = $('header nav') || $('header [role=navigation]') || $('nav.primary, nav#primary, nav.main-nav, nav#nsPrimaryNav');
+ if (!nav) return [];
+ var links = $$('a[href]', nav).map(function (a) {
+ return { name: (a.textContent || '').trim(), url: a.getAttribute('href') };
+ }).filter(function (l) { return l.name; });
+ // Hide the inline nav now that we own navigation
+ nav.classList.add('dw-fn-hidden');
+ var mobileToggle = $('header .mobile-toggle');
+ if (mobileToggle) mobileToggle.classList.add('dw-fn-hidden');
+ return links;
+ }
+
+ // -------- Build drawer content --------
+ function buildDrawer(siteLinks, host) {
+ var drawer = document.createElement('aside');
+ drawer.className = 'dw-fn-drawer';
+ drawer.setAttribute('aria-label', 'Site navigation');
+
+ // Merge canonical tabs with the site's own nav. Site-own takes precedence;
+ // canonical fills any gap (so a static site lacking its own nav still gets
+ // Home/Colors/Specs/Fire/FAQ/About/Contact per Steve's spec).
+ var seenNames = {};
+ var pages = [];
+ siteLinks.forEach(function (l) {
+ var key = (l.name || '').toLowerCase();
+ if (key && !seenNames[key]) { seenNames[key] = true; pages.push(l); }
+ });
+ CANONICAL_TABS.forEach(function (t) {
+ var key = t.name.toLowerCase();
+ if (!seenNames[key]) { seenNames[key] = true; pages.push(t); }
+ });
+
+ var s1 = '<div class="dw-fn-section"><h4>This Brand</h4>' +
+ pages.map(function (l) {
+ return '<a class="dw-fn-link" href="' + (l.url || '#').replace(/"/g, '"') + '">' + l.name + '</a>';
+ }).join('') + '</div>';
+
+ // Sister sites (exclude self if hostname matches)
+ var sister = FLEET.filter(function (f) {
+ try {
+ var u = new URL(f.url);
+ return u.hostname.replace(/^www\./, '') !== host.replace(/^www\./, '');
+ } catch (_) { return true; }
+ });
+ var s2 = '<div class="dw-fn-section"><h4>Sister Brands</h4>' +
+ sister.map(function (f) {
+ return '<a class="dw-fn-link" href="' + f.url + '" target="_blank" rel="noopener">' + f.name + '</a>';
+ }).join('') + '</div>';
+
+ drawer.innerHTML = s1 + s2;
+ return drawer;
+ }
+
+ // -------- Grid controls (sort + density) — only if [data-grid] is present --------
+ function injectGridControls() {
+ var grid = $('[data-grid]');
+ if (!grid) return;
+ if (grid.previousElementSibling && grid.previousElementSibling.classList && grid.previousElementSibling.classList.contains('dw-fn-grid-controls')) return;
+
+ var c = document.createElement('div');
+ c.className = 'dw-fn-grid-controls';
+ c.innerHTML =
+ '<label>Sort: <select id="dw-fn-sort">' +
+ '<option value="newest">Newest</option>' +
+ '<option value="title-asc">Title A→Z</option>' +
+ '<option value="sku-asc">SKU A→Z</option>' +
+ '<option value="color">Color</option>' +
+ '<option value="style">Style</option>' +
+ '<option value="price-asc">Price ↑</option>' +
+ '<option value="price-desc">Price ↓</option>' +
+ '</select></label>' +
+ '<label>Density: <input type="range" id="dw-fn-density" min="160" max="420" step="20" value="240"></label>';
+ grid.parentNode.insertBefore(c, grid);
+
+ var sort = $('#dw-fn-sort'), den = $('#dw-fn-density');
+ var saved = {};
+ try { saved = JSON.parse(localStorage.getItem('dw-fn-grid') || '{}'); } catch (_) {}
+ if (saved.sort) sort.value = saved.sort;
+ if (saved.density) den.value = saved.density;
+
+ function applyDensity() {
+ grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(' + den.value + 'px, 1fr))';
+ }
+ function applySort() {
+ // Best-effort: dispatch a CustomEvent so per-site code can react.
+ grid.dispatchEvent(new CustomEvent('dw-fn-sort', { detail: { sort: sort.value } }));
+ }
+ function persist() {
+ try { localStorage.setItem('dw-fn-grid', JSON.stringify({ sort: sort.value, density: den.value })); } catch (_) {}
+ }
+ sort.addEventListener('change', function () { applySort(); persist(); });
+ den.addEventListener('input', function () { applyDensity(); persist(); });
+ applyDensity();
+ applySort();
+ }
+
+ // -------- Wire it up --------
+ function init() {
+ if (document.getElementById('dw-fn-trigger')) return;
+ injectCss();
+
+ var trigger = document.createElement('button');
+ trigger.id = 'dw-fn-trigger';
+ trigger.className = 'dw-fn-trigger';
+ trigger.setAttribute('aria-label', 'Open menu');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.innerHTML = '<span></span><span></span><span></span>';
+
+ var overlay = document.createElement('div');
+ overlay.className = 'dw-fn-overlay';
+
+ var siteLinks = extractSiteLinks();
+ var drawer = buildDrawer(siteLinks, window.location.hostname);
+
+ function open() {
+ trigger.classList.add('open');
+ drawer.classList.add('open');
+ overlay.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ }
+ function close() {
+ trigger.classList.remove('open');
+ drawer.classList.remove('open');
+ overlay.classList.remove('open');
+ trigger.setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ }
+ function toggle() {
+ drawer.classList.contains('open') ? close() : open();
+ }
+
+ trigger.addEventListener('click', toggle);
+ overlay.addEventListener('click', close);
+ document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
+
+ document.body.appendChild(trigger);
+ document.body.appendChild(overlay);
+ document.body.appendChild(drawer);
+
+ injectGridControls();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/DW-Websites/shop-by-color/index.html b/DW-Websites/shop-by-color/index.html
index 21c18b6d..27ff4eb7 100644
--- a/DW-Websites/shop-by-color/index.html
+++ b/DW-Websites/shop-by-color/index.html
@@ -595,5 +595,7 @@ async function init() {
init();
</script>
+<script src="/dw-fleet-nav.js" defer></script>
+
</body>
</html>
← 74822dc9 novasuede chip: per-color data-dw-sample on Sample button +
·
back to Designer Wallcoverings
·
novasuede: add dark/light theme toggle in header upper-right 658722c4 →