← back to Bleachresistantfabrics
untrack .bak snapshots + .gitignore catch-all + Express 404-guard for .bak/.pre-/.orig paths
0315bb3d826994629d2b7f5cf2eec0ae2e4cdb84 · 2026-05-19 15:13:40 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M .gitignoreD public/index.html.bak.20260513T194442ZM server.js
Diff
commit 0315bb3d826994629d2b7f5cf2eec0ae2e4cdb84
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 19 15:13:40 2026 -0700
untrack .bak snapshots + .gitignore catch-all + Express 404-guard for .bak/.pre-/.orig paths
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
.gitignore | 6 +
public/index.html.bak.20260513T194442Z | 469 ---------------------------------
server.js | 6 +
3 files changed, 12 insertions(+), 469 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4a0b3ea..f274559 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,9 @@ tmp/
dist/
build/
.next/
+# snapshot / backup files — never commit, never serve
+*.bak
+*.bak.*
+*.pre-*
+*.orig
+*~
diff --git a/public/index.html.bak.20260513T194442Z b/public/index.html.bak.20260513T194442Z
deleted file mode 100644
index 58bce4e..0000000
--- a/public/index.html.bak.20260513T194442Z
+++ /dev/null
@@ -1,469 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<!-- GA4 gtag (auto) -->
-<script async src="https://www.googletagmanager.com/gtag/js?id=G-5XZST5KTQC"></script>
-<script>
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', 'G-5XZST5KTQC');
-</script>
-<!-- /GA4 gtag -->
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>Bleach Resistant Fabrics — Crypton, Vinyl-Coated & Performance Contract Fabrics for Hospitality & Healthcare</title>
-<meta name="description" content="Crypton, vinyl-coated, and performance contract fabrics rated for 1:10 bleach cleanup. Hospitality + healthcare specifications, trade pricing, free samples.">
-<meta name="theme-color" content="#0F1A1F">
-<link rel="canonical" href="https://bleachresistantfabrics.com/">
-<link rel="icon" type="image/svg+xml" href="/favicon.svg">
-<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@300;400;500;700&display=swap" rel="stylesheet">
-<script type="application/ld+json">
-{
- "@context": "https://schema.org",
- "@type": "Organization",
- "name": "Bleach Resistant Fabrics",
- "url": "https://bleachresistantfabrics.com/",
- "email": "info@bleachresistantfabrics.com",
- "telephone": "+1-888-373-4564",
- "address": { "@type": "PostalAddress", "streetAddress": "15442 Ventura Bl #102", "addressLocality": "Sherman Oaks", "addressRegion": "CA", "postalCode": "91403", "addressCountry": "US" }
-}
-</script>
-<script>
-(function(){ try { var t = localStorage.getItem('brf_theme') || 'dark'; document.documentElement.dataset.theme = t; } catch(e){} })();
-</script>
-<style>
-:root {
- --bg: #0F1A1F;
- --paper: #EAF1F4;
- --muted: #8FA3AB;
- --line: rgba(63,182,187,0.22);
- --accent: #3FB6BB;
- --bg-soft: #16242B;
- --cols: 5;
-}
-html[data-theme="light"] {
- --bg: #F5F8F9;
- --paper: #0F1A1F;
- --muted: #4F6770;
- --line: rgba(15,26,31,0.16);
- --accent: #1D7B80;
- --bg-soft: #E1ECEF;
-}
-* { margin:0; padding:0; box-sizing:border-box }
-html { scroll-behavior:smooth }
-body { font-family:'Inter', sans-serif; color:var(--paper); background:var(--bg); -webkit-font-smoothing:antialiased; min-height:100vh; overflow-x:hidden }
-h1, h2, h3, .brand, .accent-italic { font-family:'Cormorant Garamond', Georgia, serif; font-weight:500 }
-a { color:inherit }
-
-header { position:sticky; top:0; z-index:100; display:flex; justify-content:space-between; align-items:center; padding:18px 28px; background:color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter:blur(10px); border-bottom:1px solid var(--line) }
-.brand { font-size:20px; letter-spacing:0.06em; text-transform:uppercase; text-decoration:none }
-.brand .dot { display:inline-block; width:10px; height:10px; border-radius:50%; background:var(--accent); margin-right:10px; vertical-align:middle }
-.h-actions { display:flex; gap:14px; align-items:center }
-.h-link { font-size:11px; letter-spacing:0.22em; text-transform:uppercase; font-weight:700; color:var(--paper); text-decoration:none; background:transparent; border:0; cursor:pointer; padding:6px 0 }
-.h-link:hover { color:var(--accent) }
-.theme-toggle { width:34px; height:34px; border:1px solid var(--line); background:transparent; color:var(--paper); cursor:pointer; display:inline-flex; align-items:center; justify-content:center; font-size:14px; line-height:1; transition:all .15s; padding:0; border-radius:50% }
-.theme-toggle:hover { border-color:var(--accent); color:var(--accent) }
-
-.hero { padding:112px 28px 64px; max-width:1500px; margin:0 auto }
-.eyebrow { font-size:10px; letter-spacing:0.4em; text-transform:uppercase; font-weight:700; color:var(--accent); margin-bottom:18px }
-h1.headline { font-size:clamp(40px, 6vw, 80px); line-height:1.05; letter-spacing:-0.01em; margin-bottom:20px }
-h1.headline .accent-italic { color:var(--accent); font-style:italic }
-.lede { font-size:18px; line-height:1.55; color:var(--muted); max-width:720px; margin-bottom:32px }
-.cta-row { display:flex; gap:14px; flex-wrap:wrap }
-.btn { display:inline-block; padding:14px 26px; font-size:11px; letter-spacing:0.28em; text-transform:uppercase; font-weight:700; text-decoration:none; cursor:pointer; border:1px solid var(--paper); color:var(--paper); background:transparent; transition:all .2s; font-family:inherit }
-.btn:hover { background:var(--paper); color:var(--bg) }
-.btn.primary { background:var(--accent); border-color:var(--accent); color:#0F1A1F }
-.btn.primary:hover { filter:brightness(1.1) }
-
-.specs { display:grid; grid-template-columns:repeat(4, 1fr); gap:0; max-width:1500px; margin:48px auto 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line) }
-.spec { padding:22px 26px; border-right:1px solid var(--line) }
-.spec:last-child { border-right:0 }
-.spec .k { font-size:10px; letter-spacing:0.3em; text-transform:uppercase; font-weight:700; color:var(--muted); margin-bottom:8px }
-.spec .v { font-size:15px; font-weight:500 }
-@media (max-width:780px) { .specs { grid-template-columns:repeat(2, 1fr) } .spec:nth-child(2) { border-right:0 } .spec:nth-child(1), .spec:nth-child(2) { border-bottom:1px solid var(--line) } }
-
-.section { padding:80px 28px; max-width:1500px; margin:0 auto }
-.section-head { display:flex; justify-content:space-between; align-items:flex-end; flex-wrap:wrap; gap:18px; margin-bottom:28px; padding-bottom:20px; border-bottom:1px solid var(--line) }
-.section-title { font-size:clamp(32px, 4.4vw, 56px); letter-spacing:-0.01em; line-height:1 }
-.section-title .accent-italic { color:var(--accent); font-style:italic }
-
-.controls { display:flex; gap:14px; flex-wrap:wrap; align-items:center; margin-bottom:18px }
-.chip { padding:8px 14px; font-size:11px; letter-spacing:0.2em; text-transform:uppercase; font-weight:700; color:var(--paper); background:transparent; border:1px solid var(--line); cursor:pointer; transition:all .2s; font-family:inherit }
-.chip:hover { border-color:var(--paper) }
-.chip.active { background:var(--paper); color:var(--bg); border-color:var(--paper) }
-.search { flex:1 1 240px; max-width:320px; display:flex; align-items:center; gap:8px; 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 }
-.search input::placeholder { color:var(--muted) }
-.toolbar { display:flex; gap:18px; align-items:center; flex-wrap:wrap; padding:14px 0 22px; border-bottom:1px solid var(--line); margin-bottom:24px }
-.toolbar label { font-size:10px; letter-spacing:0.3em; text-transform:uppercase; font-weight:700; color:var(--muted) }
-.toolbar select { padding:6px 10px; background:transparent; color:var(--paper); border:1px solid var(--line); font-family:inherit; font-size:12px; letter-spacing:0.04em; cursor:pointer }
-.toolbar select:focus { outline:none; border-color:var(--accent) }
-.toolbar .density { display:flex; align-items:center; gap:10px; flex:1 1 240px; max-width:340px }
-.toolbar input[type=range] { flex:1; -webkit-appearance:none; appearance:none; height:1px; background:var(--line); outline:none }
-.toolbar input[type=range]::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:14px; height:14px; background:var(--accent); cursor:pointer; border-radius:50% }
-.toolbar input[type=range]::-moz-range-thumb { width:14px; height:14px; background:var(--accent); cursor:pointer; border-radius:50%; border:0 }
-.toolbar .ct { font-size:10px; letter-spacing:0.28em; text-transform:uppercase; font-weight:700; color:var(--muted) }
-
-.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 .35s 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 }
-.card .overlay { position:absolute; inset:auto 0 0 0; padding:18px 14px 12px; background:linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.85) 100%); transform:translateY(48%); opacity:.85; transition:transform .3s ease, opacity .3s ease }
-.card:hover .overlay { transform:translateY(0); opacity:1 }
-.card .pat { font-size:13px; font-weight:600; 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.2em; text-transform:uppercase; color:#fff; opacity:.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; text-decoration:none; transition:all .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) } .toolbar .density { display:none } }
-
-.loading { text-align:center; color:var(--muted); padding:32px; font-size:10px; letter-spacing:0.3em; text-transform:uppercase; font-weight:700 }
-.stat-line { font-size:10px; letter-spacing:0.3em; text-transform:uppercase; font-weight:600; color:var(--muted); margin-bottom:18px }
-
-.spec-block { display:grid; grid-template-columns:repeat(3, 1fr); gap:0; border-top:1px solid var(--line); border-bottom:1px solid var(--line); margin:32px 0 0 }
-.spec-block .item { padding:28px 28px 26px; border-right:1px solid var(--line) }
-.spec-block .item:last-child { border-right:0 }
-.spec-block h3 { font-size:22px; margin-bottom:10px }
-.spec-block p { font-size:13px; color:var(--muted); line-height:1.6 }
-@media (max-width:780px) { .spec-block { grid-template-columns:1fr } .spec-block .item { border-right:0; border-bottom:1px solid var(--line) } .spec-block .item:last-child { border-bottom:0 } }
-
-footer { padding:64px 28px 28px; border-top:1px solid var(--line); margin-top:64px }
-.foot-grid { max-width:1500px; margin:0 auto; display:grid; grid-template-columns:2fr 1fr 1fr 1fr; gap:48px; margin-bottom:36px }
-.foot-brand { font-size:22px; letter-spacing:0.18em; text-transform:uppercase; margin-bottom:10px }
-.foot-text { font-size:13px; line-height:1.6; color:var(--muted); max-width:380px }
-.foot-col h4 { font-size:10px; letter-spacing:0.3em; text-transform:uppercase; font-weight:700; margin-bottom:14px }
-.foot-col a { display:block; font-size:13px; color:var(--muted); text-decoration:none; margin-bottom:6px }
-.foot-col a:hover { color:var(--accent) }
-.foot-bot { max-width:1500px; margin:0 auto; padding-top:20px; border-top:1px solid var(--line); display:flex; justify-content:space-between; flex-wrap:wrap; gap:14px; font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted) }
-@media (max-width:720px) { .foot-grid { grid-template-columns:1fr; gap:28px } }
-
-a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible { outline:2px solid var(--accent); outline-offset:2px }
-</style>
-<meta property="og:type" content="website">
-<meta property="og:title" content="Bleach Resistant Fabrics">
-<meta property="og:description" content="Crypton, vinyl-coated, and performance contract fabrics rated for 1:10 bleach cleanup. Hospitality + healthcare.">
-<meta property="og:url" content="https://bleachresistantfabrics.com/">
-</head>
-<body>
-
-<header>
- <a class="brand" href="/"><span class="dot"></span>Bleach Resistant Fabrics</a>
- <div class="h-actions">
- <a class="h-link" href="#catalog">Catalog</a>
- <a class="h-link" href="#specs">Specs</a>
- <a class="h-link" href="mailto:info@bleachresistantfabrics.com">Trade</a>
- <button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">☾</button>
- </div>
-</header>
-
-<section class="hero">
- <div class="eyebrow">DW Family · Hospitality + Healthcare Performance Fabrics</div>
- <h1 class="headline">Bleach. <span class="accent-italic">Cleaned.</span> Tested.</h1>
- <p class="lede">Contract textiles engineered to survive a 1:10 bleach solution — Crypton, vinyl-coated, and performance constructions for hotels, restaurants, ICU bays, and assisted-living suites. Trade pricing, full spec sheets, free memo samples.</p>
- <div class="cta-row">
- <a class="btn primary" href="#catalog">Browse Catalog</a>
- <a class="btn" href="mailto:info@bleachresistantfabrics.com?subject=Trade%20Account%20Inquiry">Open Trade Account</a>
- </div>
-
- <div class="specs">
- <div class="spec"><div class="k">Cleaning Code</div><div class="v">W/S · Bleach 1:10 OK</div></div>
- <div class="spec"><div class="k">Wear Test</div><div class="v">100k+ Wyzenbeek double-rubs</div></div>
- <div class="spec"><div class="k">Fire Code</div><div class="v">Class A · NFPA 260</div></div>
- <div class="spec"><div class="k">Lead Time</div><div class="v">Memos in 3-5 business days</div></div>
- </div>
-</section>
-
-<section class="section" id="catalog">
- <div class="section-head">
- <h2 class="section-title">The <span class="accent-italic">catalog</span></h2>
- <div class="search">
- <input id="q" type="search" placeholder="Search Crypton, vinyl, healthcare…" aria-label="Search catalog">
- </div>
- </div>
-
- <div class="controls" id="rails">
- <button class="chip active" data-rail="all">All</button>
- <button class="chip" data-rail="crypton">Crypton</button>
- <button class="chip" data-rail="vinyl-coated">Vinyl-Coated</button>
- <button class="chip" data-rail="performance">Performance</button>
- <button class="chip" data-rail="healthcare">Healthcare</button>
- <button class="chip" data-rail="moisture-barrier">Moisture-Barrier</button>
- <button class="chip" data-rail="antimicrobial">Antimicrobial</button>
- </div>
-
- <div class="toolbar">
- <label for="sort">Sort</label>
- <select id="sort">
- <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>
- <div class="density">
- <label for="cols">Density</label>
- <input id="cols" type="range" min="3" max="8" step="1" value="5">
- <span class="ct" id="colsv">5 cols</span>
- </div>
- </div>
-
- <div class="stat-line" id="stat">Loading catalog…</div>
- <div class="grid" id="grid"></div>
- <div class="loading" id="more" style="display:none">Loading more…</div>
-</section>
-
-<section class="section" id="specs">
- <div class="section-head">
- <h2 class="section-title">What <span class="accent-italic">bleach-resistant</span> means here</h2>
- </div>
- <div class="spec-block">
- <div class="item">
- <h3>1:10 cleanup</h3>
- <p>Every fabric in this catalog tolerates a 1-part-bleach to 10-parts-water cleaning solution — the protocol used in CDC-aligned healthcare environments and Marriott-grade hospitality housekeeping.</p>
- </div>
- <div class="item">
- <h3>Crypton + vinyl-coated</h3>
- <p>Crypton is moisture-barrier-fused to fiber. Vinyl-coated (Type-II) is a wipe-clean polymer face on a fabric scrim. Both pass the 1:10 test; differences are tactile and acoustic.</p>
- </div>
- <div class="item">
- <h3>Specs on demand</h3>
- <p>Trade portal includes full Wyzenbeek, NFPA 260, CA TB 117-2013, ASTM E84 reports per SKU. Email <a style="color:var(--accent);text-decoration:none" href="mailto:info@bleachresistantfabrics.com">info@bleachresistantfabrics.com</a>.</p>
- </div>
- </div>
-</section>
-
-<footer>
- <div class="foot-grid">
- <div>
- <div class="foot-brand">Bleach Resistant Fabrics</div>
- <div class="foot-text">A Designer Wallcoverings vertical. Curated contract textiles for hospitality, healthcare, and senior-living interiors. Bleach-cleanable, fire-rated, trade-priced.</div>
- </div>
- <div class="foot-col">
- <h4>Catalog</h4>
- <a href="#catalog">All Fabrics</a>
- <a href="#catalog">Crypton</a>
- <a href="#catalog">Vinyl-Coated</a>
- <a href="#catalog">Healthcare</a>
- </div>
- <div class="foot-col">
- <h4>Trade</h4>
- <a href="mailto:info@bleachresistantfabrics.com">Open an Account</a>
- <a href="mailto:info@bleachresistantfabrics.com?subject=Memo%20Sample%20Request">Memo Samples</a>
- <a href="mailto:info@bleachresistantfabrics.com?subject=Spec%20Sheet%20Request">Spec Sheets</a>
- </div>
- <div class="foot-col">
- <h4>Contact</h4>
- <a href="mailto:info@bleachresistantfabrics.com">info@bleachresistantfabrics.com</a>
- <a href="tel:+18883734564">888-373-4564</a>
- <span style="display:block; font-size:13px; color:var(--muted); line-height:1.55; margin-top:6px">Designer Wallcoverings<br>15442 Ventura Bl #102<br>Sherman Oaks CA 91403</span>
- </div>
- </div>
- <div class="foot-bot">
- <span>© <span id="yr"></span> Designer Wallcoverings · DW Family</span>
- <span>Bleach Resistant Fabrics</span>
- </div>
-</footer>
-
-<script>
-document.getElementById('yr').textContent = new Date().getFullYear();
-
-// Theme toggle
-(function(){
- const KEY = 'brf_theme';
- const root = document.documentElement;
- const btn = document.getElementById('themeToggle');
- function paint(){ btn.textContent = (root.dataset.theme === 'light') ? '☀' : '☾'; }
- btn.addEventListener('click', () => {
- const next = (root.dataset.theme === 'light') ? 'dark' : 'light';
- root.dataset.theme = next;
- try { localStorage.setItem(KEY, next); } catch(e){}
- paint();
- });
- paint();
-})();
-
-// Catalog state
-const state = { q: '', rail: 'all', sort: 'newest', page: 1, limit: 36, total: 0 };
-const $grid = document.getElementById('grid');
-const $stat = document.getElementById('stat');
-
-// Persisted UI
-(function(){
- const s = localStorage.getItem('brf_sort'); if (s) { state.sort = s; document.getElementById('sort').value = s; }
- const c = localStorage.getItem('brf_cols'); if (c) {
- document.documentElement.style.setProperty('--cols', c);
- document.getElementById('cols').value = c;
- document.getElementById('colsv').textContent = c + ' cols';
- }
-})();
-
-document.getElementById('cols').addEventListener('input', e => {
- const v = e.target.value;
- document.documentElement.style.setProperty('--cols', v);
- document.getElementById('colsv').textContent = v + ' cols';
- try { localStorage.setItem('brf_cols', v); } catch(e){}
-});
-
-document.getElementById('sort').addEventListener('change', e => {
- state.sort = e.target.value;
- state.page = 1;
- try { localStorage.setItem('brf_sort', state.sort); } catch(_) {}
- load(true);
-});
-
-document.getElementById('q').addEventListener('input', e => {
- state.q = e.target.value.trim();
- state.page = 1;
- clearTimeout(window.__qt);
- window.__qt = setTimeout(() => load(true), 220);
-});
-
-document.querySelectorAll('#rails .chip').forEach(c => c.addEventListener('click', () => {
- document.querySelectorAll('#rails .chip').forEach(x => x.classList.remove('active'));
- c.classList.add('active');
- state.rail = c.dataset.rail;
- state.page = 1;
- load(true);
-}));
-
-function card(p) {
- const handle = encodeURIComponent(p.handle || p.sku || '');
- const url = p.product_url || ('https://designerwallcoverings.com/products/' + handle);
- const sample = '/sample/' + handle;
- return `<a class="card" href="${url}" target="_blank" rel="noopener">
- <img loading="lazy" src="${p.image_url}" alt="${(p.title || '').replace(/"/g, '"')}">
- <div class="overlay">
- <div class="pat">${p.title || ''}</div>
- <div class="ven">${p.vendor || ''}</div>
- <div class="actions">
- <span class="sample-btn">Free Memo</span>
- </div>
- </div>
- </a>`;
-}
-
-async function load(reset) {
- if (reset) $grid.innerHTML = '';
- $stat.textContent = 'Loading…';
- const params = new URLSearchParams({ q: state.q, rail: state.rail, sort: state.sort, page: state.page, limit: state.limit });
- try {
- const r = await fetch('/api/products?' + params);
- const j = await r.json();
- state.total = j.total;
- j.items.forEach(p => $grid.insertAdjacentHTML('beforeend', card(p)));
- $stat.textContent = j.total + ' fabrics · page ' + j.page + ' of ' + j.pages;
- } catch (e) {
- $stat.textContent = 'Failed to load catalog. Refresh to retry.';
- }
-}
-
-load(true);
-</script>
-<!-- DW-HAMBURGER-NAV v1 (canonical 2026-05-10) -->
-<style id="dw-hamburger-styles">
- /* Canonical hamburger nav — always visible top-right, all viewports.
- Standing rule (Steve, 2026-05-08): hamburger is the primary nav. */
- .dw-burger-btn {
- position: fixed; top: 18px; right: 18px; z-index: 9999;
- width: 44px; height: 44px;
- background: rgba(20,20,20,0.78); color: #f5f1e8;
- backdrop-filter: blur(8px) saturate(140%);
- -webkit-backdrop-filter: blur(8px) saturate(140%);
- border: 1px solid rgba(245,241,232,0.18); border-radius: 50%;
- cursor: pointer; padding: 0;
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- gap: 4px;
- box-shadow: 0 6px 20px rgba(0,0,0,0.35);
- transition: background 200ms, transform 200ms;
- }
- .dw-burger-btn:hover { background: rgba(20,20,20,0.92); }
- .dw-burger-btn span {
- display: block; width: 20px; height: 1.5px; background: currentColor;
- transition: transform 220ms, opacity 220ms;
- }
- .dw-burger-btn[aria-expanded="true"] span:nth-child(1) {
- transform: translateY(5.5px) rotate(45deg);
- }
- .dw-burger-btn[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
- .dw-burger-btn[aria-expanded="true"] span:nth-child(3) {
- transform: translateY(-5.5px) rotate(-45deg);
- }
- .dw-burger-panel {
- position: fixed; top: 72px; right: 18px; z-index: 9998;
- min-width: 240px; max-width: 88vw;
- background: rgba(15,15,15,0.96);
- backdrop-filter: blur(14px) saturate(140%);
- -webkit-backdrop-filter: blur(14px) saturate(140%);
- border: 1px solid rgba(245,241,232,0.12);
- border-radius: 14px;
- box-shadow: 0 18px 50px rgba(0,0,0,0.55);
- padding: 10px 0;
- display: none;
- font-family: -apple-system, "SF Pro Text", system-ui, sans-serif;
- }
- .dw-burger-panel.open { display: block; }
- .dw-burger-panel a,
- .dw-burger-panel button {
- display: block; width: 100%;
- padding: 13px 22px;
- color: #f5f1e8 !important;
- background: transparent; border: 0; cursor: pointer;
- text-decoration: none !important;
- font-size: 15px; font-weight: 400;
- text-align: left;
- letter-spacing: 0;
- border-bottom: 1px solid rgba(245,241,232,0.06);
- transition: background 160ms, color 160ms;
- box-sizing: border-box;
- }
- .dw-burger-panel a:last-child,
- .dw-burger-panel button:last-child { border-bottom: 0; }
- .dw-burger-panel a:hover,
- .dw-burger-panel button:hover {
- background: rgba(245,241,232,0.06);
- color: #ffffff !important;
- }
- @media (max-width: 480px) {
- .dw-burger-btn { top: 12px; right: 12px; width: 40px; height: 40px; }
- .dw-burger-panel { top: 62px; right: 12px; }
- }
- /* Avoid double-print on sites already adopting a top-of-page header
- that overlaps z-index. Reserve safe space near top-right. */
-</style>
-<button type="button" class="dw-burger-btn" id="dwBurgerBtn"
- aria-label="Toggle menu" aria-controls="dwBurgerPanel"
- aria-expanded="false">
- <span></span><span></span><span></span>
-</button>
-<nav class="dw-burger-panel" id="dwBurgerPanel" aria-label="Primary">
- <a href="/">Bleach Resistant Fabrics</a>
- <a href="#catalog">Catalog</a>
- <a href="#specs">Specs</a>
- <a href="mailto:info@bleachresistantfabrics.com">Trade</a>
-</nav>
-<script>
-(function(){
- var btn = document.getElementById('dwBurgerBtn');
- var panel = document.getElementById('dwBurgerPanel');
- if (!btn || !panel) return;
- function close(){ panel.classList.remove('open'); btn.setAttribute('aria-expanded','false'); }
- function toggle(){
- var open = panel.classList.toggle('open');
- btn.setAttribute('aria-expanded', open ? 'true' : 'false');
- }
- btn.addEventListener('click', function(e){ e.stopPropagation(); toggle(); });
- document.addEventListener('click', function(e){
- if (panel.classList.contains('open') && !panel.contains(e.target) && e.target !== btn) close();
- });
- document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
-})();
-</script>
-<!-- /DW-HAMBURGER-NAV v1 -->
-
-</body>
-</html>
diff --git a/server.js b/server.js
index 49ad49c..483f596 100644
--- a/server.js
+++ b/server.js
@@ -112,6 +112,12 @@ const app = express();
app.use(helmet({ contentSecurityPolicy: false }));
app.use(express.json({ limit: '256kb' }));
app.use((req, res, next) => { res.set('Cache-Control', 'no-store, must-revalidate'); next(); });
+// 404-guard snapshot/backup paths so they NEVER serve from static root even
+// if a stale file ends up on disk (matches *.bak, *.bak.*, *.pre-*, *.orig, *~).
+app.use((req, res, next) => {
+ if (/\.(bak|orig)(\.|$)|\.pre-|~$/i.test(req.path)) return res.status(404).send('Not found');
+ next();
+});
app.use(express.static(path.join(__dirname, 'public')));
app.get('/api/products', (req, res) => {
← 36a208d Phase 2b: harden .card .pat against long-token title clippin
·
back to Bleachresistantfabrics
·
add rel=noreferrer to external product-card link (tabnabbing c2b9060 →