← back to Wallpapercontractor
initial scaffold + GA gtag G-LC83F70YHV
94e0e826e895d3f5d15680d257188dc7eb9b78cd · 2026-05-10 04:13:20 -0700 · Steve Abrams
Files touched
A .gitignoreA README.mdA data/contractors.jsonA package-lock.jsonA package.jsonA public/css/styles.cssA public/favicon.svgA public/js/main.jsA public/robots.txtA server.jsA views/404.ejsA views/about.ejsA views/contractor.ejsA views/index.ejsA views/partials/foot.ejsA views/partials/head.ejs
Diff
commit 94e0e826e895d3f5d15680d257188dc7eb9b78cd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 10 04:13:20 2026 -0700
initial scaffold + GA gtag G-LC83F70YHV
---
.gitignore | 9 +
README.md | 35 ++
data/contractors.json | 34 ++
package-lock.json | 1418 +++++++++++++++++++++++++++++++++++++++++++++++
package.json | 24 +
public/css/styles.css | 229 ++++++++
public/favicon.svg | 7 +
public/js/main.js | 45 ++
public/robots.txt | 4 +
server.js | 126 +++++
views/404.ejs | 10 +
views/about.ejs | 31 ++
views/contractor.ejs | 85 +++
views/index.ejs | 113 ++++
views/partials/foot.ejs | 30 +
views/partials/head.ejs | 47 ++
16 files changed, 2247 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a0b3ea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5c5345e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+# wallpapercontractor.com
+
+Commercial wallpaper contractor directory — bonded, insured, large-format wallcovering installation contractors for hospitality, retail, corporate, healthcare, multifamily, and government projects across the United States.
+
+Distinct from `nationalpaperhangers.com` which lists single-installer residential paperhangers. This directory lists **companies** with crews of 8–30+, surety bonding, and general liability $2M–$10M.
+
+## Run locally
+
+```
+npm install
+PORT=9930 npm start
+```
+
+Open http://localhost:9930.
+
+## Deploy
+
+- **Local Mac2 path**: `/Users/stevestudio2/Projects/wallpapercontractor`
+- **Kamatera path**: `/root/Projects/wallpapercontractor`
+- **pm2 process name**: `wallpapercontractor` on port `9930`
+- **GA4 measurement ID**: `G-LC83F70YHV`
+
+## Stack
+
+- Express 4 + EJS, Helmet, Compression, Morgan
+- Static `data/contractors.json` (no DB) — read-only seed of ~30 commercial wallcovering installers
+
+## Standing rules baked in
+
+- GA4 gtag in `<head>` with `<!-- GA4 gtag (auto) -->` marker (idempotent)
+- Dark/light theme toggle, localStorage persisted, anti-flash inline script
+- Sort + density slider above the grid, localStorage persisted
+- `Cache-Control: no-store, must-revalidate` on every response
+- Footer with `info@wallpapercontractor.com`, `888-373-4564`, Sherman Oaks address
+- Per-page `<title>` and meta description
diff --git a/data/contractors.json b/data/contractors.json
new file mode 100644
index 0000000..cb0d0b0
--- /dev/null
+++ b/data/contractors.json
@@ -0,0 +1,34 @@
+[
+ {"id":1,"company":"Meridian Commercial Wallcovering","principal":"Andre Meridian","city":"Las Vegas, NV","regions":["NV","AZ","UT","CA"],"segments":["Hospitality","Casino","Retail"],"specialties":["Vinyl Type II","Digital Murals","Type III Heavy-Duty","Acoustic Wallcovering"],"crew_max":24,"years_in_business":18,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://meridiancommercialwallcovering.com/contact"},
+ {"id":2,"company":"Skyline Wallcovering Contractors","principal":"Patricia Skyline","city":"Atlanta, GA","regions":["GA","SC","NC","TN","FL"],"segments":["Hospitality","Multifamily","Corporate"],"specialties":["Vinyl Type II","Grasscloth","Wallpaper Removal","Wall Prep"],"crew_max":18,"years_in_business":22,"bonded":true,"insured":true,"liability_limit":"$3M","union":false,"contact_url":"https://skylinewallcoveringcontractors.com/contact"},
+ {"id":3,"company":"Iron Bridge Wall Systems","principal":"Daniel Brennan","city":"Chicago, IL","regions":["IL","WI","IN","MI","OH"],"segments":["Healthcare","Corporate","Hospitality"],"specialties":["Antimicrobial Wallcovering","Type II Commercial Vinyl","Acoustic Wallcovering","Digital Murals"],"crew_max":32,"years_in_business":28,"bonded":true,"insured":true,"liability_limit":"$10M","union":true,"contact_url":"https://ironbridgewallsystems.com/contact"},
+ {"id":4,"company":"Pacific Crest Wallcovering","principal":"Yvette Tanaka","city":"Seattle, WA","regions":["WA","OR","CA","ID"],"segments":["Hospitality","Corporate","Retail"],"specialties":["Grasscloth","Silk","Hand-Painted","Type II Commercial Vinyl"],"crew_max":14,"years_in_business":15,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://pacificcrestwallcovering.com/contact"},
+ {"id":5,"company":"Eastline Hospitality Wall Group","principal":"Marcus Lyle","city":"New York, NY","regions":["NY","NJ","CT","PA","MA"],"segments":["Hospitality","Luxury Hotel","Restaurant"],"specialties":["Hand-Painted Murals","Silk","Leather Wall Panels","Type II Commercial Vinyl"],"crew_max":28,"years_in_business":31,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://eastlinehospitalitywallgroup.com/contact"},
+ {"id":6,"company":"Sunbelt Wallcovering Installers","principal":"Olivia Cardoza","city":"Dallas, TX","regions":["TX","OK","NM","LA","AR"],"segments":["Hospitality","Multifamily","Healthcare"],"specialties":["Type II Commercial Vinyl","Type III Heavy-Duty","Antimicrobial Wallcovering","Wall Prep"],"crew_max":22,"years_in_business":17,"bonded":true,"insured":true,"liability_limit":"$3M","union":false,"contact_url":"https://sunbeltwallcoveringinstallers.com/contact"},
+ {"id":7,"company":"Granite State Wallcovering","principal":"Bryan O'Donnell","city":"Boston, MA","regions":["MA","NH","ME","VT","RI"],"segments":["Healthcare","Corporate","Education"],"specialties":["Antimicrobial Wallcovering","Acoustic Wallcovering","Type II Commercial Vinyl","Wallpaper Removal"],"crew_max":12,"years_in_business":14,"bonded":true,"insured":true,"liability_limit":"$2M","union":true,"contact_url":"https://granitestatewallcovering.com/contact"},
+ {"id":8,"company":"Coastal Hospitality Wall Group","principal":"Sofia Renaldi","city":"Miami, FL","regions":["FL","GA","SC"],"segments":["Hospitality","Resort","Cruise"],"specialties":["Grasscloth","Silk","Type II Commercial Vinyl","Marine-Grade Wallcovering"],"crew_max":20,"years_in_business":19,"bonded":true,"insured":true,"liability_limit":"$5M","union":false,"contact_url":"https://coastalhospitalitywallgroup.com/contact"},
+ {"id":9,"company":"Cornerstone Commercial Wall","principal":"Frederick Holm","city":"Minneapolis, MN","regions":["MN","WI","ND","SD","IA"],"segments":["Corporate","Healthcare","Education"],"specialties":["Type II Commercial Vinyl","Acoustic Wallcovering","Antimicrobial Wallcovering","Wall Prep"],"crew_max":16,"years_in_business":24,"bonded":true,"insured":true,"liability_limit":"$3M","union":true,"contact_url":"https://cornerstonecommercialwall.com/contact"},
+ {"id":10,"company":"Bayview Wallcovering Contractors","principal":"Jasmine Pelletier","city":"San Francisco, CA","regions":["CA","NV","HI"],"segments":["Hospitality","Corporate","Retail"],"specialties":["Hand-Painted Murals","Digital Murals","Grasscloth","Type II Commercial Vinyl"],"crew_max":18,"years_in_business":20,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://bayviewwallcoveringcontractors.com/contact"},
+ {"id":11,"company":"Heartland Wall Systems","principal":"Gregory Voss","city":"Kansas City, MO","regions":["MO","KS","NE","IA","OK"],"segments":["Multifamily","Healthcare","Education"],"specialties":["Type II Commercial Vinyl","Antimicrobial Wallcovering","Wallpaper Removal","Wall Prep"],"crew_max":14,"years_in_business":12,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://heartlandwallsystems.com/contact"},
+ {"id":12,"company":"Emerald City Wallcovering","principal":"Naomi Aaltonen","city":"Portland, OR","regions":["OR","WA","ID"],"segments":["Hospitality","Corporate","Restaurant"],"specialties":["Grasscloth","Cork","Bamboo Wallcovering","Type II Commercial Vinyl"],"crew_max":10,"years_in_business":11,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://emeraldcitywallcovering.com/contact"},
+ {"id":13,"company":"Liberty Commercial Wallcovering","principal":"Vincent Carusone","city":"Philadelphia, PA","regions":["PA","NJ","DE","MD"],"segments":["Hospitality","Healthcare","Corporate"],"specialties":["Type II Commercial Vinyl","Type III Heavy-Duty","Antimicrobial Wallcovering","Acoustic Wallcovering"],"crew_max":24,"years_in_business":26,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://libertycommercialwallcovering.com/contact"},
+ {"id":14,"company":"Mountain West Wall Group","principal":"Hannah Briscoe","city":"Denver, CO","regions":["CO","UT","WY","MT","NM"],"segments":["Hospitality","Resort","Multifamily"],"specialties":["Type II Commercial Vinyl","Grasscloth","Hand-Painted Murals","Wall Prep"],"crew_max":12,"years_in_business":15,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://mountainwestwallgroup.com/contact"},
+ {"id":15,"company":"Crescent City Wallcovering","principal":"Etienne Roux","city":"New Orleans, LA","regions":["LA","MS","AL","TX"],"segments":["Hospitality","Restaurant","Casino"],"specialties":["Hand-Painted Murals","Silk","Type II Commercial Vinyl","Historic Restoration"],"crew_max":14,"years_in_business":23,"bonded":true,"insured":true,"liability_limit":"$3M","union":false,"contact_url":"https://crescentcitywallcovering.com/contact"},
+ {"id":16,"company":"Buckeye Wallcovering Installers","principal":"Roland Heffernan","city":"Columbus, OH","regions":["OH","IN","KY","WV","PA"],"segments":["Healthcare","Education","Multifamily"],"specialties":["Antimicrobial Wallcovering","Type II Commercial Vinyl","Wall Prep","Wallpaper Removal"],"crew_max":18,"years_in_business":21,"bonded":true,"insured":true,"liability_limit":"$3M","union":true,"contact_url":"https://buckeyewallcoveringinstallers.com/contact"},
+ {"id":17,"company":"Sun Valley Wallcovering","principal":"Elena Vargas","city":"Phoenix, AZ","regions":["AZ","NV","NM","CA"],"segments":["Hospitality","Multifamily","Resort"],"specialties":["Type II Commercial Vinyl","Grasscloth","Hand-Painted Murals","Wall Prep"],"crew_max":16,"years_in_business":13,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://sunvalleywallcovering.com/contact"},
+ {"id":18,"company":"Atlantic Wall Group","principal":"Marcus Whitfield","city":"Norfolk, VA","regions":["VA","NC","MD","DC","DE"],"segments":["Government","Healthcare","Hospitality"],"specialties":["Antimicrobial Wallcovering","Type III Heavy-Duty","Acoustic Wallcovering","Type II Commercial Vinyl"],"crew_max":20,"years_in_business":17,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://atlanticwallgroup.com/contact"},
+ {"id":19,"company":"Great Lakes Commercial Wall","principal":"Luca Ferraro","city":"Detroit, MI","regions":["MI","OH","IN","IL"],"segments":["Healthcare","Corporate","Multifamily"],"specialties":["Antimicrobial Wallcovering","Type II Commercial Vinyl","Wall Prep","Wallpaper Removal"],"crew_max":22,"years_in_business":29,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://greatlakescommercialwall.com/contact"},
+ {"id":20,"company":"Tidewater Wallcovering","principal":"Camille Beauregard","city":"Charleston, SC","regions":["SC","NC","GA","FL"],"segments":["Hospitality","Resort","Multifamily"],"specialties":["Grasscloth","Silk","Type II Commercial Vinyl","Hand-Painted Murals"],"crew_max":12,"years_in_business":16,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://tidewaterwallcovering.com/contact"},
+ {"id":21,"company":"Capitol Wall Contractors","principal":"Reginald Pemberton","city":"Washington, DC","regions":["DC","MD","VA","DE"],"segments":["Government","Corporate","Hospitality"],"specialties":["Type III Heavy-Duty","Acoustic Wallcovering","Type II Commercial Vinyl","Antimicrobial Wallcovering"],"crew_max":26,"years_in_business":33,"bonded":true,"insured":true,"liability_limit":"$10M","union":true,"contact_url":"https://capitolwallcontractors.com/contact"},
+ {"id":22,"company":"Quincy Hospitality Wall Co","principal":"Annabelle Quincy","city":"Nashville, TN","regions":["TN","KY","AL","GA"],"segments":["Hospitality","Restaurant","Multifamily"],"specialties":["Type II Commercial Vinyl","Hand-Painted Murals","Grasscloth","Wall Prep"],"crew_max":14,"years_in_business":11,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://quincyhospitalitywallco.com/contact"},
+ {"id":23,"company":"Front Range Wallcovering","principal":"Tobias Ekstrom","city":"Salt Lake City, UT","regions":["UT","CO","ID","WY"],"segments":["Hospitality","Resort","Corporate"],"specialties":["Grasscloth","Type II Commercial Vinyl","Acoustic Wallcovering","Wall Prep"],"crew_max":10,"years_in_business":9,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://frontrangewallcovering.com/contact"},
+ {"id":24,"company":"Magnolia Commercial Wall","principal":"Daphne Aldridge","city":"Birmingham, AL","regions":["AL","MS","GA","FL","TN"],"segments":["Healthcare","Education","Multifamily"],"specialties":["Antimicrobial Wallcovering","Type II Commercial Vinyl","Wallpaper Removal","Wall Prep"],"crew_max":16,"years_in_business":18,"bonded":true,"insured":true,"liability_limit":"$3M","union":false,"contact_url":"https://magnoliacommercialwall.com/contact"},
+ {"id":25,"company":"Yankee Wall Group","principal":"Nathaniel Bricker","city":"Hartford, CT","regions":["CT","MA","NY","RI"],"segments":["Corporate","Healthcare","Hospitality"],"specialties":["Type II Commercial Vinyl","Antimicrobial Wallcovering","Acoustic Wallcovering","Type III Heavy-Duty"],"crew_max":20,"years_in_business":25,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://yankeewallgroup.com/contact"},
+ {"id":26,"company":"Big Sky Wall Contractors","principal":"Linnea Hagstrom","city":"Bozeman, MT","regions":["MT","WY","ID","ND"],"segments":["Hospitality","Resort","Multifamily"],"specialties":["Grasscloth","Type II Commercial Vinyl","Hand-Painted Murals","Wall Prep"],"crew_max":8,"years_in_business":10,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://bigskywallcontractors.com/contact"},
+ {"id":27,"company":"Citrus Coast Wallcovering","principal":"Diego Salgado","city":"Tampa, FL","regions":["FL","GA","AL"],"segments":["Hospitality","Resort","Multifamily"],"specialties":["Type II Commercial Vinyl","Grasscloth","Marine-Grade Wallcovering","Wall Prep"],"crew_max":14,"years_in_business":13,"bonded":true,"insured":true,"liability_limit":"$2M","union":false,"contact_url":"https://citruscoastwallcovering.com/contact"},
+ {"id":28,"company":"Highline Commercial Wall","principal":"Margaux Cleary","city":"Brooklyn, NY","regions":["NY","NJ","CT"],"segments":["Hospitality","Luxury Hotel","Restaurant"],"specialties":["Hand-Painted Murals","Silk","Leather Wall Panels","Digital Murals"],"crew_max":18,"years_in_business":14,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://highlinecommercialwall.com/contact"},
+ {"id":29,"company":"Lone Star Wall Systems","principal":"Caleb Strickland","city":"Houston, TX","regions":["TX","LA","OK"],"segments":["Healthcare","Corporate","Multifamily"],"specialties":["Antimicrobial Wallcovering","Type II Commercial Vinyl","Acoustic Wallcovering","Wall Prep"],"crew_max":24,"years_in_business":20,"bonded":true,"insured":true,"liability_limit":"$5M","union":false,"contact_url":"https://lonestarwallsystems.com/contact"},
+ {"id":30,"company":"Beacon Hill Wallcovering","principal":"Theodora Whitcomb","city":"Providence, RI","regions":["RI","MA","CT"],"segments":["Hospitality","Education","Healthcare"],"specialties":["Type II Commercial Vinyl","Antimicrobial Wallcovering","Hand-Painted Murals","Wall Prep"],"crew_max":12,"years_in_business":17,"bonded":true,"insured":true,"liability_limit":"$3M","union":true,"contact_url":"https://beaconhillwallcovering.com/contact"},
+ {"id":31,"company":"Twin Cities Wall Group","principal":"Sigrid Thorvaldsen","city":"St. Paul, MN","regions":["MN","WI","ND"],"segments":["Healthcare","Education","Corporate"],"specialties":["Antimicrobial Wallcovering","Acoustic Wallcovering","Type II Commercial Vinyl","Wall Prep"],"crew_max":14,"years_in_business":16,"bonded":true,"insured":true,"liability_limit":"$3M","union":true,"contact_url":"https://twincitieswallgroup.com/contact"},
+ {"id":32,"company":"Old Dominion Wall Co","principal":"Wesley Carrington","city":"Richmond, VA","regions":["VA","NC","WV","MD"],"segments":["Government","Healthcare","Hospitality"],"specialties":["Antimicrobial Wallcovering","Type II Commercial Vinyl","Type III Heavy-Duty","Wall Prep"],"crew_max":18,"years_in_business":22,"bonded":true,"insured":true,"liability_limit":"$5M","union":true,"contact_url":"https://olddominionwallco.com/contact"}
+]
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..0ee438a
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1418 @@
+{
+ "name": "wallpapercontractor",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "wallpapercontractor",
+ "version": "0.1.0",
+ "dependencies": {
+ "compression": "^1.7.4",
+ "ejs": "^3.1.10",
+ "express": "^4.21.0",
+ "helmet": "^8.1.0",
+ "morgan": "^1.10.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.7"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+ "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+ "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/nodemon/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f71cc8e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "wallpapercontractor",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Commercial wallpaper contractor directory — bonded, insured, large-format wallcovering installation contractors for hospitality, retail, corporate, healthcare, and multifamily projects",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "compression": "^1.7.4",
+ "ejs": "^3.1.10",
+ "express": "^4.21.0",
+ "helmet": "^8.1.0",
+ "morgan": "^1.10.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.7"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/public/css/styles.css b/public/css/styles.css
new file mode 100644
index 0000000..84b7e33
--- /dev/null
+++ b/public/css/styles.css
@@ -0,0 +1,229 @@
+:root {
+ --bg: #0e1c26;
+ --bg-elev: #14283a;
+ --bg-card: #182f44;
+ --fg: #e8eef5;
+ --fg-muted: #8ea3b8;
+ --accent: #c97842;
+ --accent-fg: #1a0f06;
+ --ok: #4f9d6e;
+ --ok-fg: #0d2418;
+ --union: #6b8db5;
+ --union-fg: #0d1a2a;
+ --border: #1f3a52;
+ --tag-bg: #1a3147;
+ --shadow: 0 10px 30px rgba(0,0,0,.45);
+ --radius: 12px;
+ --cols-min: 320px;
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif;
+}
+[data-theme="light"] {
+ --bg: #f4ede4;
+ --bg-elev: #ffffff;
+ --bg-card: #ffffff;
+ --fg: #1a2333;
+ --fg-muted: #586879;
+ --accent: #a85a26;
+ --accent-fg: #ffffff;
+ --ok: #2e7a4d;
+ --ok-fg: #ffffff;
+ --union: #355680;
+ --union-fg: #ffffff;
+ --border: #e1d8c8;
+ --tag-bg: #efe6d5;
+ --shadow: 0 6px 22px rgba(40,30,15,.10);
+}
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+body {
+ background: var(--bg);
+ color: var(--fg);
+ line-height: 1.55;
+ -webkit-font-smoothing: antialiased;
+ min-height: 100vh;
+}
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+.wrap { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
+.muted { color: var(--fg-muted); }
+
+.site-header {
+ background: var(--bg-elev);
+ border-bottom: 1px solid var(--border);
+ position: sticky; top: 0; z-index: 30;
+}
+.header-row { display: flex; align-items: center; gap: 24px; padding: 14px 24px; }
+.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; color: var(--fg); }
+.brand:hover { text-decoration: none; }
+.brand-mark {
+ width: 28px; height: 28px; border-radius: 6px;
+ background: linear-gradient(135deg, var(--accent), #8a4a22);
+ position: relative;
+}
+.brand-mark::after {
+ content: ""; position: absolute; inset: 4px;
+ border: 2px solid rgba(255,255,255,.5); border-radius: 3px;
+}
+.brand-name { font-size: 18px; letter-spacing: -0.01em; }
+.site-nav { display: flex; gap: 18px; margin-left: auto; }
+.site-nav a { color: var(--fg); font-size: 14px; }
+.theme-toggle {
+ width: 36px; height: 36px; border-radius: 8px;
+ border: 1px solid var(--border); background: var(--bg-card);
+ color: var(--fg); cursor: pointer; font-size: 16px;
+ display: inline-flex; align-items: center; justify-content: center;
+}
+.theme-toggle .sun { display: none; }
+.theme-toggle .moon { display: inline; }
+[data-theme="light"] .theme-toggle .sun { display: inline; }
+[data-theme="light"] .theme-toggle .moon { display: none; }
+
+.main { padding: 32px 24px 80px; }
+.hero { padding: 30px 0 24px; }
+.hero h1 { font-size: clamp(28px, 4vw, 44px); margin: 0 0 12px; letter-spacing: -0.02em; line-height: 1.1; }
+.hero .lead { color: var(--fg-muted); font-size: 17px; max-width: 820px; margin: 0; }
+
+.filter-bar {
+ display: grid;
+ grid-template-columns: minmax(220px,1.4fr) repeat(3, 1fr) auto auto auto auto;
+ gap: 10px; margin: 24px 0 16px;
+ background: var(--bg-elev); padding: 14px; border-radius: var(--radius);
+ border: 1px solid var(--border);
+ align-items: center;
+}
+.filter-bar input[type=search], .filter-bar select {
+ background: var(--bg-card); color: var(--fg);
+ border: 1px solid var(--border); padding: 10px 12px;
+ border-radius: 10px; font-size: 14px; min-width: 0;
+}
+.filter-bar input[type=search]:focus, .filter-bar select:focus {
+ outline: 2px solid var(--accent); outline-offset: 1px;
+}
+.filter-bar .check {
+ display: inline-flex; gap: 6px; align-items: center;
+ font-size: 13px; color: var(--fg);
+ background: var(--bg-card); border: 1px solid var(--border);
+ padding: 8px 12px; border-radius: 10px;
+ white-space: nowrap; cursor: pointer;
+}
+.filter-bar .check input { margin: 0; accent-color: var(--accent); }
+.btn-primary, .btn-ghost {
+ display: inline-flex; align-items: center; justify-content: center;
+ padding: 10px 16px; border-radius: 10px; font-weight: 600;
+ font-size: 14px; cursor: pointer; border: 1px solid transparent;
+ text-decoration: none; white-space: nowrap;
+}
+.btn-primary { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
+.btn-primary:hover { filter: brightness(1.05); text-decoration: none; }
+.btn-ghost { background: transparent; color: var(--fg); border-color: var(--border); }
+.btn-ghost:hover { background: var(--bg-card); text-decoration: none; }
+
+.result-bar {
+ display: flex; align-items: center; justify-content: space-between;
+ margin: 14px 0 16px; gap: 14px; flex-wrap: wrap;
+}
+.controls { display: flex; gap: 14px; align-items: center; }
+.control { display: flex; gap: 8px; align-items: center; font-size: 13px; color: var(--fg-muted); }
+.control select, .control input[type=range] {
+ background: var(--bg-card); color: var(--fg);
+ border: 1px solid var(--border); padding: 6px 10px;
+ border-radius: 8px; font-size: 13px;
+}
+.control input[type=range] { padding: 0; accent-color: var(--accent); }
+
+.card-grid {
+ list-style: none; padding: 0; margin: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(var(--cols-min), 1fr));
+ gap: 16px;
+}
+.rep-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
+ overflow: hidden;
+}
+.rep-card:hover { transform: translateY(-2px); border-color: var(--accent); box-shadow: var(--shadow); }
+.rep-link { display: block; padding: 18px; color: var(--fg); }
+.rep-link:hover { text-decoration: none; }
+.rep-card-head { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
+.rep-avatar {
+ width: 48px; height: 48px; border-radius: 8px;
+ background: linear-gradient(135deg, var(--accent), #8a4a22);
+ color: var(--accent-fg); font-weight: 700; font-size: 13px;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0; letter-spacing: 0.02em;
+}
+.rep-avatar.lg { width: 84px; height: 84px; font-size: 22px; border-radius: 12px; }
+.rep-name { font-weight: 600; font-size: 16px; line-height: 1.25; }
+.rep-agency { color: var(--fg-muted); font-size: 13px; margin-top: 2px; }
+.rep-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
+.chip {
+ display: inline-block; font-size: 11px; padding: 3px 9px;
+ background: var(--tag-bg); color: var(--fg);
+ border-radius: 99px; border: 1px solid var(--border);
+ white-space: nowrap;
+}
+.chip.ok { background: var(--ok); color: var(--ok-fg); border-color: var(--ok); font-weight: 600; }
+.chip.union { background: var(--union); color: var(--union-fg); border-color: var(--union); font-weight: 600; }
+.chip.city { background: transparent; color: var(--fg-muted); }
+.rep-lines { font-size: 13px; margin-bottom: 10px; }
+.rep-vertical { display: flex; flex-wrap: wrap; gap: 4px; }
+.tag {
+ font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
+ padding: 2px 8px; background: transparent; border: 1px solid var(--border);
+ color: var(--fg-muted); border-radius: 4px;
+}
+
+.empty { text-align: center; padding: 60px 20px; color: var(--fg-muted); }
+
+.back-link { font-size: 13px; color: var(--fg-muted); display: inline-block; margin-bottom: 14px; }
+.rep-detail { background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; }
+.rep-header { display: flex; gap: 18px; align-items: flex-start; margin-bottom: 24px; flex-wrap: wrap; }
+.rep-header h1 { margin: 0; font-size: 28px; letter-spacing: -0.01em; }
+.rep-header .agency { color: var(--accent); font-weight: 600; margin-top: 4px; }
+.rep-header .city { font-size: 14px; }
+.rep-grid-detail {
+ display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ padding: 22px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
+}
+.rep-grid-detail h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-muted); }
+.chips { display: flex; flex-wrap: wrap; gap: 6px; }
+.lines-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; font-size: 14px; }
+.lines-list strong { color: var(--fg-muted); font-weight: 500; }
+.cta-row { display: flex; gap: 10px; padding-top: 22px; flex-wrap: wrap; }
+
+.related { margin-top: 40px; }
+.related h2 { font-size: 20px; margin: 0 0 14px; }
+
+.long-form { max-width: 740px; }
+.long-form h1 { font-size: 32px; margin: 0 0 20px; }
+.long-form h2 { font-size: 18px; margin-top: 28px; }
+.long-form p { color: var(--fg); }
+
+.seo-block { margin-top: 60px; padding-top: 30px; border-top: 1px solid var(--border); max-width: 860px; }
+.seo-block h2 { font-size: 18px; margin: 0 0 12px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: .06em; }
+.seo-block p { color: var(--fg-muted); font-size: 14px; }
+
+.site-footer { background: var(--bg-elev); border-top: 1px solid var(--border); margin-top: 60px; padding: 36px 0 24px; }
+.footer-grid {
+ display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ padding-bottom: 24px;
+}
+.site-footer strong { display: block; margin-bottom: 6px; font-size: 14px; }
+.site-footer p { font-size: 13px; line-height: 1.7; color: var(--fg-muted); margin: 0; }
+.site-footer a { color: var(--fg); }
+.copy { padding-top: 18px; border-top: 1px solid var(--border); color: var(--fg-muted); }
+.copy small { font-size: 12px; line-height: 1.6; display: block; }
+
+@media (max-width: 920px) {
+ .filter-bar { grid-template-columns: 1fr 1fr 1fr; }
+ .filter-bar input[type=search] { grid-column: 1 / -1; }
+}
+@media (max-width: 600px) {
+ .filter-bar { grid-template-columns: 1fr 1fr; }
+ .filter-bar input[type=search] { grid-column: 1 / -1; }
+ .filter-bar .check { grid-column: span 1; }
+ .site-nav a:nth-child(1) { display: none; }
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..a2ed339
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+ <rect width="64" height="64" rx="10" fill="#0e1c26"/>
+ <rect x="10" y="10" width="44" height="44" rx="4" fill="#c97842"/>
+ <rect x="14" y="14" width="36" height="36" rx="2" fill="none" stroke="#fff8ec" stroke-width="2.5" stroke-opacity=".6"/>
+ <path d="M14 22 H50 M14 32 H50 M14 42 H50" stroke="#fff8ec" stroke-width="1.5" stroke-opacity=".35"/>
+ <path d="M22 14 V50 M32 14 V50 M42 14 V50" stroke="#fff8ec" stroke-width="1.5" stroke-opacity=".35"/>
+</svg>
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..0b6face
--- /dev/null
+++ b/public/js/main.js
@@ -0,0 +1,45 @@
+(function () {
+ const root = document.documentElement;
+ const toggle = document.getElementById('theme-toggle');
+ if (toggle) {
+ toggle.addEventListener('click', () => {
+ const next = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
+ if (next === 'light') root.setAttribute('data-theme', 'light');
+ else root.removeAttribute('data-theme');
+ try { localStorage.setItem('wpc-theme', next); } catch (e) {}
+ });
+ }
+
+ const grid = document.getElementById('rep-grid');
+ const slider = document.getElementById('density-slider');
+ if (slider && grid) {
+ let saved = null;
+ try { saved = localStorage.getItem('wpc-density'); } catch (e) {}
+ if (saved) {
+ slider.value = saved;
+ grid.style.setProperty('--cols-min', saved + 'px');
+ }
+ slider.addEventListener('input', () => {
+ grid.style.setProperty('--cols-min', slider.value + 'px');
+ try { localStorage.setItem('wpc-density', slider.value); } catch (e) {}
+ });
+ }
+
+ const sortSelect = document.getElementById('sort-select');
+ if (sortSelect) {
+ let savedSort = null;
+ try { savedSort = localStorage.getItem('wpc-sort'); } catch (e) {}
+ const url = new URL(window.location.href);
+ if (savedSort && !url.searchParams.get('sort') && sortSelect.value === 'newest' && savedSort !== 'newest') {
+ url.searchParams.set('sort', savedSort);
+ window.location.replace(url.toString());
+ return;
+ }
+ sortSelect.addEventListener('change', () => {
+ try { localStorage.setItem('wpc-sort', sortSelect.value); } catch (e) {}
+ const u = new URL(window.location.href);
+ u.searchParams.set('sort', sortSelect.value);
+ window.location.assign(u.toString());
+ });
+ }
+})();
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..42584ed
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://wallpapercontractor.com/sitemap.xml
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..05f8487
--- /dev/null
+++ b/server.js
@@ -0,0 +1,126 @@
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+const helmet = require('helmet');
+const compression = require('compression');
+const morgan = require('morgan');
+
+const app = express();
+const PORT = process.env.PORT || 9930;
+const GA_ID = process.env.GA_ID || 'G-LC83F70YHV';
+
+const contractors = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'contractors.json'), 'utf8'));
+
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+
+app.use(helmet({
+ contentSecurityPolicy: false,
+ crossOriginEmbedderPolicy: false,
+}));
+app.use(compression());
+app.use(morgan('tiny'));
+
+app.use((req, res, next) => {
+ res.set('Cache-Control', 'no-store, must-revalidate');
+ next();
+});
+
+app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h' }));
+
+const allRegions = Array.from(new Set(contractors.flatMap(c => c.regions))).sort();
+const allSegments = Array.from(new Set(contractors.flatMap(c => c.segments))).sort();
+const allSpecialties = Array.from(new Set(contractors.flatMap(c => c.specialties))).sort();
+const allCities = Array.from(new Set(contractors.map(c => c.city))).sort();
+
+const SITE = {
+ domain: 'wallpapercontractor.com',
+ name: 'Wallpaper Contractor',
+ tagline: 'Bonded, insured, commercial-scale wallpaper installation contractors',
+ description: 'Find bonded and insured commercial wallpaper installation contractors for hospitality, retail, corporate, healthcare, and multifamily projects across the United States. Vetted contractors who carry general liability, workers comp, and surety bonding for large-format commercial wallcovering installs.',
+ email: 'info@wallpapercontractor.com',
+ phone: '888-373-4564',
+ address: 'Designer Wallcoverings, 15442 Ventura Bl #102, Sherman Oaks CA 91403',
+ ga_id: GA_ID,
+};
+
+function sortContractors(list, sort) {
+ const copy = list.slice();
+ switch (sort) {
+ case 'company':
+ return copy.sort((a, b) => a.company.localeCompare(b.company));
+ case 'city':
+ return copy.sort((a, b) => a.city.localeCompare(b.city));
+ case 'crew_max':
+ return copy.sort((a, b) => b.crew_max - a.crew_max);
+ case 'years':
+ return copy.sort((a, b) => b.years_in_business - a.years_in_business);
+ case 'region':
+ return copy.sort((a, b) => (a.regions[0] || '').localeCompare(b.regions[0] || ''));
+ case 'newest':
+ default:
+ return copy.sort((a, b) => b.id - a.id);
+ }
+}
+
+app.get('/', (req, res) => {
+ const { q, region, segment, specialty, bonded, insured, sort } = req.query;
+ let filtered = contractors.slice();
+
+ if (q) {
+ const needle = String(q).toLowerCase();
+ filtered = filtered.filter(c =>
+ c.company.toLowerCase().includes(needle) ||
+ c.principal.toLowerCase().includes(needle) ||
+ c.city.toLowerCase().includes(needle) ||
+ c.specialties.some(s => s.toLowerCase().includes(needle)) ||
+ c.segments.some(s => s.toLowerCase().includes(needle))
+ );
+ }
+ if (region) filtered = filtered.filter(c => c.regions.includes(String(region)));
+ if (segment) filtered = filtered.filter(c => c.segments.includes(String(segment)));
+ if (specialty) filtered = filtered.filter(c => c.specialties.includes(String(specialty)));
+ if (bonded === '1') filtered = filtered.filter(c => c.bonded);
+ if (insured === '1') filtered = filtered.filter(c => c.insured);
+
+ filtered = sortContractors(filtered, sort);
+
+ res.render('index', {
+ site: SITE,
+ contractors: filtered,
+ total: contractors.length,
+ matched: filtered.length,
+ filters: {
+ q: q || '',
+ region: region || '',
+ segment: segment || '',
+ specialty: specialty || '',
+ bonded: bonded || '',
+ insured: insured || '',
+ sort: sort || 'newest',
+ },
+ facets: { regions: allRegions, segments: allSegments, specialties: allSpecialties, cities: allCities },
+ });
+});
+
+app.get('/contractor/:id', (req, res) => {
+ const id = Number(req.params.id);
+ const contractor = contractors.find(c => c.id === id);
+ if (!contractor) return res.status(404).render('404', { site: SITE });
+ const related = contractors
+ .filter(c => c.id !== contractor.id && c.segments.some(s => contractor.segments.includes(s)))
+ .slice(0, 4);
+ res.render('contractor', { site: SITE, contractor, related });
+});
+
+app.get('/about', (req, res) => {
+ res.render('about', { site: SITE });
+});
+
+app.get('/healthz', (req, res) => res.json({ ok: true, port: PORT, contractors: contractors.length }));
+
+app.use((req, res) => res.status(404).render('404', { site: SITE }));
+
+app.listen(PORT, () => {
+ console.log(`wallpapercontractor listening on :${PORT} (${contractors.length} contractors loaded, GA ${GA_ID})`);
+});
diff --git a/views/404.ejs b/views/404.ejs
new file mode 100644
index 0000000..86b285f
--- /dev/null
+++ b/views/404.ejs
@@ -0,0 +1,10 @@
+<%- include('partials/head', { title: 'Not found — ' + site.name, description: 'Page not found.' }) %>
+
+<main class="wrap main">
+ <section class="empty">
+ <h1>Page not found</h1>
+ <p>The page you tried to load isn't here. <a href="/">Return to the directory</a>.</p>
+ </section>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/about.ejs b/views/about.ejs
new file mode 100644
index 0000000..d2222d1
--- /dev/null
+++ b/views/about.ejs
@@ -0,0 +1,31 @@
+<%- include('partials/head', { title: 'About — ' + site.name, description: 'Wallpaper Contractor is an industry directory of bonded, insured, commercial-scale wallcovering installation contractors serving hospitality, healthcare, multifamily, retail, corporate, and government projects.' }) %>
+
+<main class="wrap main">
+ <a class="back-link" href="/">← Back</a>
+
+ <article class="long-form">
+ <h1>About Wallpaper Contractor</h1>
+
+ <p>Wallpaper Contractor is a free, curated directory of <em>commercial-scale</em> wallcovering installation contractors operating in the United States. The directory exists because procurement teams, general contractors, FF&E purchasing groups, hotel ownership groups, and design firms routinely need to find an installation contractor capable of delivering a 30,000-square-foot, 100,000-square-foot, or larger commercial wallcovering scope — and the existing options online are mostly residential paperhanger directories that cannot serve that scale.</p>
+
+ <h2>Commercial-scale, not residential</h2>
+ <p>The contractors listed here are <strong>companies</strong>, not individual installers. Each carries general liability insurance (typically $2M–$10M aggregate), most carry surety bonding for performance-bonded public work, and each can field a peak crew of 8 to 30+ wallcovering installers. Many are union-affiliated through the IUPAT, which is required for many federal, state, and large-format commercial projects. For single-installer residential and small-commercial paperhangers, see our sister directory at <a href="https://nationalpaperhangers.com">NationalPaperHangers.com</a>.</p>
+
+ <h2>What's in each listing</h2>
+ <p>Each listing includes the company name, the principal's name, the metro the company is based in, the regions the company services, the project segments they actively work in (hospitality, luxury hotel, casino, resort, restaurant, multifamily, healthcare, education, corporate, retail, cruise, government), the wallcovering specialties they install (Type II commercial vinyl, Type III heavy-duty, antimicrobial, acoustic, grasscloth, silk, hand-painted murals, digital murals, leather wall panels, marine-grade, historic restoration), peak crew size, years in business, surety bonding status, general-liability insurance limits, union affiliation, and a direct contact path.</p>
+
+ <h2>Verify coverage before contracting</h2>
+ <p>Listings reflect contractor-supplied bonding and insurance information at the time of indexing. Coverage limits, named-insured parties, and policy currency change over time. <strong>Verify current coverage and limits with the contractor before contracting work.</strong> Most procurement teams will require an Acord 25 certificate of insurance and a current bond capacity letter before contract award; this directory is a starting point, not a substitute for those documents.</p>
+
+ <h2>Who maintains the directory</h2>
+ <p>The directory is operated by <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> as a free industry resource. Designer Wallcoverings is a wallcovering distributor with an extended hospitality and commercial-FF&E history, so we have a direct interest in keeping commercial-installer information accurate and accessible. Listings are not pay-to-play and there is no advertising on the site.</p>
+
+ <h2>How to list your company</h2>
+ <p>If you run a commercial wallcovering installation company and would like to be added — or if you are listed and need to update regions, segments, specialties, crew size, bonding, or insurance information — email <a href="mailto:<%= site.email %>"><%= site.email %></a> with the details. We typically update the directory weekly.</p>
+
+ <h2>How to flag a correction</h2>
+ <p>If a listing is wrong or out of date, email <a href="mailto:<%= site.email %>"><%= site.email %></a> with the company name and the correction. Bonding and insurance updates are processed on the same business day where possible.</p>
+ </article>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/contractor.ejs b/views/contractor.ejs
new file mode 100644
index 0000000..77eb8e1
--- /dev/null
+++ b/views/contractor.ejs
@@ -0,0 +1,85 @@
+<%- include('partials/head', { title: contractor.company + ' — ' + contractor.city + ' commercial wallpaper contractor | ' + site.name, description: contractor.company + ' is a commercial wallpaper installation contractor based in ' + contractor.city + '. Bonded: ' + (contractor.bonded ? 'yes' : 'no') + '. Insured: ' + (contractor.insured ? contractor.liability_limit : 'no') + '. Crew up to ' + contractor.crew_max + '. Specialties: ' + contractor.specialties.join(', ') + '.' }) %>
+
+<main class="wrap main">
+ <a class="back-link" href="/">← Back to directory</a>
+
+ <section class="rep-detail">
+ <div class="rep-header">
+ <div class="rep-avatar lg"><%= contractor.company.split(' ').map(s => s[0]).join('').slice(0,3).toUpperCase() %></div>
+ <div>
+ <h1><%= contractor.company %></h1>
+ <div class="agency"><%= contractor.principal %></div>
+ <div class="city muted"><%= contractor.city %></div>
+ <div class="rep-meta" style="margin-top:10px;">
+ <% if (contractor.bonded) { %><span class="chip ok">Bonded</span><% } %>
+ <% if (contractor.insured) { %><span class="chip ok">Insured <%= contractor.liability_limit %></span><% } %>
+ <% if (contractor.union) { %><span class="chip union">Union</span><% } %>
+ </div>
+ </div>
+ </div>
+
+ <div class="rep-grid-detail">
+ <div>
+ <h3>Regions covered</h3>
+ <div class="chips">
+ <% contractor.regions.forEach(function(r){ %><span class="chip"><%= r %></span><% }) %>
+ </div>
+ </div>
+ <div>
+ <h3>Project segments</h3>
+ <div class="chips">
+ <% contractor.segments.forEach(function(s){ %><span class="tag"><%= s %></span><% }) %>
+ </div>
+ </div>
+ <div>
+ <h3>Specialties</h3>
+ <ul class="lines-list">
+ <% contractor.specialties.forEach(function(sp){ %><li><%= sp %></li><% }) %>
+ </ul>
+ </div>
+ <div>
+ <h3>Capacity & tenure</h3>
+ <ul class="lines-list">
+ <li><strong>Peak crew size:</strong> <%= contractor.crew_max %> installers</li>
+ <li><strong>Years in business:</strong> <%= contractor.years_in_business %></li>
+ <li><strong>General liability:</strong> <%= contractor.insured ? contractor.liability_limit : 'Not on file' %></li>
+ <li><strong>Surety bonded:</strong> <%= contractor.bonded ? 'Yes' : 'Not on file' %></li>
+ <li><strong>Union affiliation:</strong> <%= contractor.union ? 'Yes' : 'Open shop' %></li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="cta-row">
+ <a class="btn-primary" href="<%= contractor.contact_url %>" target="_blank" rel="noopener">Contact <%= contractor.company %> →</a>
+ <a class="btn-ghost" href="mailto:<%= site.email %>?subject=Question%20about%20<%= encodeURIComponent(contractor.company) %>">Ask the directory</a>
+ </div>
+ </section>
+
+ <% if (related.length) { %>
+ <section class="related">
+ <h2>Other contractors in similar segments</h2>
+ <ul class="card-grid" style="--cols-min: 280px">
+ <% related.forEach(function(r){ %>
+ <li class="rep-card">
+ <a class="rep-link" href="/contractor/<%= r.id %>">
+ <div class="rep-card-head">
+ <div class="rep-avatar"><%= r.company.split(' ').map(s => s[0]).join('').slice(0,3).toUpperCase() %></div>
+ <div>
+ <div class="rep-name"><%= r.company %></div>
+ <div class="rep-agency"><%= r.city %></div>
+ </div>
+ </div>
+ <div class="rep-meta">
+ <% if (r.bonded) { %><span class="chip ok">Bonded</span><% } %>
+ <% if (r.insured) { %><span class="chip ok">Insured</span><% } %>
+ <span class="chip">Crew <%= r.crew_max %></span>
+ </div>
+ </a>
+ </li>
+ <% }) %>
+ </ul>
+ </section>
+ <% } %>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/index.ejs b/views/index.ejs
new file mode 100644
index 0000000..297e06b
--- /dev/null
+++ b/views/index.ejs
@@ -0,0 +1,113 @@
+<%- include('partials/head', { title: site.name + ' — ' + site.tagline, description: site.description }) %>
+
+<main class="wrap main">
+ <section class="hero">
+ <h1>Bonded, insured, commercial-scale wallpaper contractors.</h1>
+ <p class="lead">A working directory of commercial wallcovering installation contractors — companies that carry general liability insurance, surety bonding, and crews of 8–30+ for hospitality, retail, corporate, healthcare, multifamily, and government projects. <%= total %> contractors indexed. This is not a paperhanger directory; for single-installer residential paperhangers see <a href="https://nationalpaperhangers.com">NationalPaperHangers</a>.</p>
+ </section>
+
+ <form class="filter-bar" method="get" action="/">
+ <input type="search" name="q" value="<%= filters.q %>" placeholder="Search company, principal, city, specialty…" aria-label="Search">
+
+ <select name="region" aria-label="Region">
+ <option value="">All regions</option>
+ <% facets.regions.forEach(function(r){ %>
+ <option value="<%= r %>" <%= filters.region === r ? 'selected' : '' %>><%= r %></option>
+ <% }) %>
+ </select>
+
+ <select name="segment" aria-label="Project segment">
+ <option value="">All segments</option>
+ <% facets.segments.forEach(function(s){ %>
+ <option value="<%= s %>" <%= filters.segment === s ? 'selected' : '' %>><%= s %></option>
+ <% }) %>
+ </select>
+
+ <select name="specialty" aria-label="Specialty">
+ <option value="">All specialties</option>
+ <% facets.specialties.forEach(function(s){ %>
+ <option value="<%= s %>" <%= filters.specialty === s ? 'selected' : '' %>><%= s %></option>
+ <% }) %>
+ </select>
+
+ <label class="check">
+ <input type="checkbox" name="bonded" value="1" <%= filters.bonded === '1' ? 'checked' : '' %>>
+ <span>Bonded only</span>
+ </label>
+
+ <label class="check">
+ <input type="checkbox" name="insured" value="1" <%= filters.insured === '1' ? 'checked' : '' %>>
+ <span>Insured only</span>
+ </label>
+
+ <button type="submit" class="btn-primary">Filter</button>
+ <% if (filters.q || filters.region || filters.segment || filters.specialty || filters.bonded || filters.insured) { %>
+ <a class="btn-ghost" href="/">Reset</a>
+ <% } %>
+ </form>
+
+ <div class="result-bar">
+ <div class="muted"><%= matched %> of <%= total %> matching</div>
+ <div class="controls">
+ <label class="control">
+ <span>Sort</span>
+ <select id="sort-select" name="sort">
+ <option value="newest" <%= filters.sort === 'newest' ? 'selected' : '' %>>Newest</option>
+ <option value="company" <%= filters.sort === 'company' ? 'selected' : '' %>>Company A→Z</option>
+ <option value="city" <%= filters.sort === 'city' ? 'selected' : '' %>>City A→Z</option>
+ <option value="region" <%= filters.sort === 'region' ? 'selected' : '' %>>Region</option>
+ <option value="crew_max" <%= filters.sort === 'crew_max' ? 'selected' : '' %>>Crew size ↓</option>
+ <option value="years" <%= filters.sort === 'years' ? 'selected' : '' %>>Years in business ↓</option>
+ </select>
+ </label>
+ <label class="control">
+ <span>Density</span>
+ <input id="density-slider" type="range" min="240" max="440" step="20" value="320">
+ </label>
+ </div>
+ </div>
+
+ <% if (!matched) { %>
+ <div class="empty">No contractors match those filters. <a href="/">Reset</a>.</div>
+ <% } else { %>
+ <ul class="card-grid" id="rep-grid">
+ <% contractors.forEach(function(c){ %>
+ <li class="rep-card">
+ <a class="rep-link" href="/contractor/<%= c.id %>">
+ <div class="rep-card-head">
+ <div class="rep-avatar"><%= c.company.split(' ').map(s => s[0]).join('').slice(0,3).toUpperCase() %></div>
+ <div>
+ <div class="rep-name"><%= c.company %></div>
+ <div class="rep-agency"><%= c.principal %> · <%= c.city %></div>
+ </div>
+ </div>
+ <div class="rep-meta">
+ <% if (c.bonded) { %><span class="chip ok" title="Surety bonded">Bonded</span><% } %>
+ <% if (c.insured) { %><span class="chip ok" title="Carries general liability">Insured <%= c.liability_limit %></span><% } %>
+ <% if (c.union) { %><span class="chip union">Union</span><% } %>
+ <span class="chip">Crew up to <%= c.crew_max %></span>
+ <span class="chip"><%= c.years_in_business %> yrs</span>
+ </div>
+ <div class="rep-lines muted">
+ <%= c.specialties.slice(0,3).join(' · ') %>
+ </div>
+ <div class="rep-vertical">
+ <% c.segments.forEach(function(s){ %>
+ <span class="tag"><%= s %></span>
+ <% }) %>
+ </div>
+ </a>
+ </li>
+ <% }) %>
+ </ul>
+ <% } %>
+
+ <section class="seo-block">
+ <h2>How this directory works</h2>
+ <p>Wallpaper Contractor is a curated index of commercial wallcovering installation contractors operating in the United States. Each listing maps the contractor's regional coverage, the project segments they actively work in (hospitality, luxury hotel, casino, resort, restaurant, multifamily, healthcare, education, corporate, retail, cruise, government), the wallcovering specialties they install (Type II commercial vinyl, Type III heavy-duty, antimicrobial, acoustic, grasscloth, silk, hand-painted murals, digital murals, leather wall panels, marine-grade), peak crew size, years in business, surety bonding status, general-liability insurance limits, and union affiliation. Use the filters above to narrow by region, segment, specialty, bonding status, or insurance status.</p>
+ <p>This directory exists because procurement teams, general contractors, FF&E purchasing groups, and design firms routinely struggle to identify wallcovering installers who can actually take on a 30,000-square-foot commercial scope. Single-installer paperhangers — ideal for residential and small commercial work — generally cannot bond, insure, or staff that volume. The contractors listed here can.</p>
+ <p>This directory is operated by <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> as a free industry resource. Listings are not pay-to-play. To list your company, update an existing listing, or correct an entry, email <a href="mailto:<%= site.email %>"><%= site.email %></a>.</p>
+ </section>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/partials/foot.ejs b/views/partials/foot.ejs
new file mode 100644
index 0000000..ff5bdc8
--- /dev/null
+++ b/views/partials/foot.ejs
@@ -0,0 +1,30 @@
+<footer class="site-footer">
+ <div class="wrap footer-grid">
+ <div>
+ <strong><%= site.name %></strong>
+ <p class="muted"><%= site.tagline %></p>
+ </div>
+ <div>
+ <strong>Contact</strong>
+ <p>
+ <a href="mailto:<%= site.email %>"><%= site.email %></a><br>
+ <a href="tel:+1<%= site.phone.replace(/-/g,'') %>"><%= site.phone %></a><br>
+ <span class="muted"><%= site.address %></span>
+ </p>
+ </div>
+ <div>
+ <strong>For contractors</strong>
+ <p>
+ <a href="mailto:<%= site.email %>?subject=List%20our%20company">List your company</a><br>
+ <a href="mailto:<%= site.email %>?subject=Update%20our%20listing">Update a listing</a><br>
+ <a href="mailto:<%= site.email %>?subject=Bonding%20%2F%20insurance%20update">Update bonding/insurance</a>
+ </p>
+ </div>
+ </div>
+ <div class="wrap copy">
+ <small>© <%= new Date().getFullYear() %> <%= site.name %>. A directory project of Designer Wallcoverings. Listings reflect contractor-supplied bonding and insurance information; verify current coverage and limits with the contractor before contracting work.</small>
+ </div>
+</footer>
+<script src="/js/main.js" defer></script>
+</body>
+</html>
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
new file mode 100644
index 0000000..fbde87e
--- /dev/null
+++ b/views/partials/head.ejs
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title><%= title || (site.name + ' — ' + site.tagline) %></title>
+<meta name="description" content="<%= description || site.description %>">
+<meta name="theme-color" content="#0e1c26">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<link rel="stylesheet" href="/css/styles.css">
+<script>
+(function(){
+ try {
+ var t = localStorage.getItem('wpc-theme');
+ if (t === 'light' || (t == null && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches)) {
+ document.documentElement.setAttribute('data-theme','light');
+ }
+ } catch(e){}
+})();
+</script>
+<!-- GA4 gtag (auto) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=<%= site.ga_id %>"></script>
+<script>
+window.dataLayer = window.dataLayer || [];
+function gtag(){dataLayer.push(arguments);}
+gtag('js', new Date());
+gtag('config', '<%= site.ga_id %>');
+</script>
+<!-- /GA4 gtag -->
+</head>
+<body>
+<header class="site-header">
+ <div class="wrap header-row">
+ <a class="brand" href="/">
+ <span class="brand-mark"></span>
+ <span class="brand-name"><%= site.name %></span>
+ </a>
+ <nav class="site-nav">
+ <a href="/">Directory</a>
+ <a href="/about">About</a>
+ <a href="mailto:<%= site.email %>">Contact</a>
+ </nav>
+ <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" title="Toggle dark/light">
+ <span class="moon">☽</span><span class="sun">☀</span>
+ </button>
+ </div>
+</header>
(oldest)
·
back to Wallpapercontractor
·
add 3-step consumer lead funnel + /api/lead → nationalpaperh 1ab8a5f →