[object Object]

← back to Commercialdesignreps

initial scaffold + GA gtag

80b81392cefb0ff0bcf8ef2d6efdc79ab78b0e31 · 2026-05-10 02:40:14 -0700 · Steve Abrams

Files touched

Diff

commit 80b81392cefb0ff0bcf8ef2d6efdc79ab78b0e31
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 10 02:40:14 2026 -0700

    initial scaffold + GA gtag
---
 .gitignore              |    9 +
 README.md               |   29 +
 data/reps.json          |   42 ++
 package-lock.json       | 1418 +++++++++++++++++++++++++++++++++++++++++++++++
 package.json            |   24 +
 public/css/styles.css   |  196 +++++++
 public/favicon.svg      |   11 +
 public/js/main.js       |   45 ++
 public/robots.txt       |    3 +
 server.js               |  111 ++++
 views/404.ejs           |   10 +
 views/about.ejs         |   25 +
 views/index.ejs         |  100 ++++
 views/partials/foot.ejs |   29 +
 views/partials/head.ejs |   47 ++
 views/rep.ejs           |   69 +++
 16 files changed, 2168 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..b9b0a18
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# commercialdesignreps.com
+
+Commercial design sales-rep directory. Independent reps and showroom agencies covering U.S. commercial, hospitality, healthcare, and contract interior design lines.
+
+## Stack
+
+- Express + EJS (slim, no DB at MVP)
+- Seed data in `data/reps.json` (40 reps)
+- pm2 port 9922 on Kamatera
+- GA4 `G-PRJ51PTW5G`
+
+## Run locally
+
+```bash
+npm ci
+npm start          # → http://localhost:9922
+```
+
+## Deploy
+
+Rsync to `root@45.61.58.125:/root/Projects/commercialdesignreps/`, then:
+
+```
+npm ci
+pm2 start server.js --name commercialdesignreps
+pm2 save
+```
+
+nginx vhost at `/etc/nginx/sites-enabled/commercialdesignreps.com` proxies port 9922.
diff --git a/data/reps.json b/data/reps.json
new file mode 100644
index 0000000..fa371f8
--- /dev/null
+++ b/data/reps.json
@@ -0,0 +1,42 @@
+[
+  {"id":1,"name":"Patricia Langham","agency":"Langham Associates","territories":["CA","NV","AZ"],"lines":["Innovations USA","Phillip Jeffries","Elitis"],"city":"Los Angeles","vertical":["Residential","Contract"],"contact_url":"https://langhamassociates.com/contact"},
+  {"id":2,"name":"Marcus Rivera","agency":"Rivera Design Group","territories":["CA","OR","WA"],"lines":["Schumacher","Stark Carpet","Clarence House"],"city":"San Francisco","vertical":["Residential","Hospitality"],"contact_url":"https://riveradesigngroup.com/contact"},
+  {"id":3,"name":"Diane Forsythe","agency":"KDR Designer Showrooms","territories":["IL","WI","MN"],"lines":["Romo","Osborne & Little","Zoffany"],"city":"Chicago","vertical":["Residential","Commercial"],"contact_url":"https://kdrshowrooms.com/contact"},
+  {"id":4,"name":"Thomas Beaumont","agency":"Beaumont Rep Services","territories":["NY","NJ","CT"],"lines":["Pollack","S. Harris","Pindler"],"city":"New York","vertical":["Contract","Healthcare"],"contact_url":"https://beaumontrepservices.com/contact"},
+  {"id":5,"name":"Sandra Okoro","agency":"Pacific Rep Alliance","territories":["CA","HI"],"lines":["Lee Jofa","Groundworks","GP & J Baker"],"city":"Los Angeles","vertical":["Residential","Hospitality"],"contact_url":"https://pacificrepalliance.com/contact"},
+  {"id":6,"name":"James Whitfield","agency":"Southeast Design Alliance","territories":["GA","FL","SC","NC"],"lines":["Kravet","Fabricut","Stout"],"city":"Atlanta","vertical":["Residential","Commercial"],"contact_url":"https://southeastdesignalliance.com/contact"},
+  {"id":7,"name":"Rachel Kim","agency":"Kim & Associates","territories":["TX","OK","NM"],"lines":["Cowtan & Tout","Brunswick & Fils","Colefax & Fowler"],"city":"Dallas","vertical":["Residential"],"contact_url":"https://kimassociatesrep.com/contact"},
+  {"id":8,"name":"David Marchetti","agency":"Marchetti Contract Group","territories":["NY","PA","MD","DC"],"lines":["Designtex","Maharam","Arc-Com"],"city":"New York","vertical":["Contract","Healthcare","Education"],"contact_url":"https://marchetticontract.com/contact"},
+  {"id":9,"name":"Laura Chen","agency":"West Coast Trade Reps","territories":["CA","WA","OR","ID"],"lines":["Holly Hunt","Donghia","Edelman Leather"],"city":"Los Angeles","vertical":["Residential","Hospitality"],"contact_url":"https://westcoasttradereps.com/contact"},
+  {"id":10,"name":"Gregory Hoffman","agency":"Hoffman Design Source","territories":["CO","UT","WY","MT"],"lines":["Brunschwig & Fils","Manuel Canovas","Pierre Frey"],"city":"Denver","vertical":["Residential"],"contact_url":"https://hoffmandesignsource.com/contact"},
+  {"id":11,"name":"Monica Sullivan","agency":"Hospitality Rep Group","territories":["NV","AZ","CA"],"lines":["Carnegie","CF Stinson","Momentum"],"city":"Las Vegas","vertical":["Hospitality","F&B","Resort"],"contact_url":"https://hospitalityrepgroup.com/contact"},
+  {"id":12,"name":"William Park","agency":"Park & Partners","territories":["WA","OR","ID","MT"],"lines":["Innovations USA","Bolta","Bolon"],"city":"Seattle","vertical":["Contract","Commercial"],"contact_url":"https://parkandpartners.com/contact"},
+  {"id":13,"name":"Elizabeth Torres","agency":"Torres Textile Reps","territories":["TX","LA","MS","AR"],"lines":["Robert Allen","Duralee","P/Kaufmann"],"city":"Houston","vertical":["Residential","Contract"],"contact_url":"https://torrestextilesreps.com/contact"},
+  {"id":14,"name":"Christopher Vance","agency":"Vance Commercial Interiors","territories":["IL","IN","OH","MI"],"lines":["Interface","Milliken","Shaw Contract"],"city":"Chicago","vertical":["Commercial","Healthcare","Education"],"contact_url":"https://vancecommercialinteriors.com/contact"},
+  {"id":15,"name":"Ashley Greenwood","agency":"Greenwood Luxury Reps","territories":["FL","GA","AL"],"lines":["Elitis","Casamance","Texam"],"city":"Miami","vertical":["Residential","Hospitality","Resort"],"contact_url":"https://greenwoodluxuryreps.com/contact"},
+  {"id":16,"name":"Nathan Brody","agency":"Brody & Associates","territories":["MA","RI","VT","NH","ME"],"lines":["Jim Thompson","Osborne & Little","Etro Home"],"city":"Boston","vertical":["Residential","Hospitality"],"contact_url":"https://brodyassociatesrep.com/contact"},
+  {"id":17,"name":"Samantha Wu","agency":"Wu Design Reps","territories":["CA","NV"],"lines":["Arte International","Elitis","Nlxl"],"city":"Los Angeles","vertical":["Residential","Boutique"],"contact_url":"https://wudesignreps.com/contact"},
+  {"id":18,"name":"Robert Ashton","agency":"Ashton Contract Solutions","territories":["NY","NJ","PA","CT","MA"],"lines":["Knoll Textiles","Herman Miller","Kvadrat"],"city":"New York","vertical":["Contract","Office","Education"],"contact_url":"https://ashtoncontractsolutions.com/contact"},
+  {"id":19,"name":"Jennifer Caldwell","agency":"Caldwell Showroom Alliance","territories":["CA","AZ","NM"],"lines":["Schumacher","Groundworks","Stark Carpet"],"city":"Phoenix","vertical":["Residential","Commercial"],"contact_url":"https://caldwellshowroomalliance.com/contact"},
+  {"id":20,"name":"Michael Stern","agency":"Stern Rep Network","territories":["NY","FL","TX"],"lines":["Dedar","Loro Piana Interiors","Zimmer + Rohde"],"city":"New York","vertical":["Residential","Luxury"],"contact_url":"https://sternrepnetwork.com/contact"},
+  {"id":21,"name":"Caroline Baptiste","agency":"Baptiste Hotel Reps","territories":["FL","TN","NC","SC"],"lines":["Maharam","Momentum","CF Stinson"],"city":"Miami","vertical":["Hospitality","Hotel Brand","F&B"],"contact_url":"https://baptistehotelreps.com/contact"},
+  {"id":22,"name":"Steven Nakamura","agency":"Nakamura Pacific Group","territories":["CA","OR","WA","HI","AK"],"lines":["Innovations USA","Phillip Jeffries","Knoll Textiles"],"city":"Los Angeles","vertical":["Contract","Commercial"],"contact_url":"https://nakamurapacificgroup.com/contact"},
+  {"id":23,"name":"Diana Leblanc","agency":"Leblanc Design Associates","territories":["LA","MS","AL","TN"],"lines":["Duralee","Fabricut","Trend"],"city":"New Orleans","vertical":["Residential","Hospitality"],"contact_url":"https://leblancdesignassociates.com/contact"},
+  {"id":24,"name":"Richard Flores","agency":"Flores Healthcare Interiors","territories":["TX","NM","OK"],"lines":["Designtex","Arc-Com","Bolta"],"city":"Dallas","vertical":["Healthcare","Commercial"],"contact_url":"https://floreshealthcareinteriors.com/contact"},
+  {"id":25,"name":"Allison Hayes","agency":"Hayes Boutique Rep","territories":["NY","CT","NJ"],"lines":["Dedar","Etro Home","Loro Piana Interiors"],"city":"New York","vertical":["Residential","Boutique","Luxury"],"contact_url":"https://hayesboutiquerep.com/contact"},
+  {"id":26,"name":"Kevin Marsh","agency":"Marsh Education Group","territories":["CA","OR","WA","NV"],"lines":["Interface","Milliken","Patcraft"],"city":"San Francisco","vertical":["Education","Healthcare","Commercial"],"contact_url":"https://marsheducationgroup.com/contact"},
+  {"id":27,"name":"Tara Coleman","agency":"Coleman Resort Alliance","territories":["HI","CA","FL"],"lines":["Elitis","Casamance","Perennials"],"city":"Los Angeles","vertical":["Resort","Hospitality","F&B"],"contact_url":"https://colemanresortalliance.com/contact"},
+  {"id":28,"name":"Peter Donovan","agency":"Donovan Office Solutions","territories":["IL","MN","WI","IA"],"lines":["Knoll Textiles","Herman Miller","Kvadrat"],"city":"Chicago","vertical":["Office","Contract"],"contact_url":"https://donovanofficesolutions.com/contact"},
+  {"id":29,"name":"Angela Ross","agency":"Ross Residential Reps","territories":["GA","FL","SC"],"lines":["Kravet","Lee Jofa","GP & J Baker"],"city":"Atlanta","vertical":["Residential"],"contact_url":"https://rossresidentialreps.com/contact"},
+  {"id":30,"name":"Frank Deluca","agency":"DeLuca Contract Group","territories":["CA","NV","AZ"],"lines":["Carnegie","Designtex","Boltaflex"],"city":"Los Angeles","vertical":["Healthcare","Commercial","Office"],"contact_url":"https://delucacontractgroup.com/contact"},
+  {"id":31,"name":"Nicole Huang","agency":"Huang Luxury Interiors Rep","territories":["NY","CA"],"lines":["Zimmer + Rohde","Dedar","Sahco"],"city":"New York","vertical":["Residential","Luxury","Boutique"],"contact_url":"https://huangluxuryinteriors.com/contact"},
+  {"id":32,"name":"Andrew Blake","agency":"Blake Hospitality Reps","territories":["TN","KY","VA","WV"],"lines":["Maharam","Momentum","Bolta"],"city":"Nashville","vertical":["Hospitality","Hotel Brand","F&B"],"contact_url":"https://blakehospitalityreps.com/contact"},
+  {"id":33,"name":"Stephanie Osei","agency":"Osei Commercial Alliance","territories":["GA","AL","TN","SC"],"lines":["Interface","Shaw Contract","Patcraft"],"city":"Atlanta","vertical":["Commercial","Office","Retail"],"contact_url":"https://oseicommercialalliance.com/contact"},
+  {"id":34,"name":"Timothy Grant","agency":"Grant & Grant Design Reps","territories":["PA","DE","MD","VA"],"lines":["Schumacher","Brunschwig & Fils","Clarence House"],"city":"Philadelphia","vertical":["Residential","Contract"],"contact_url":"https://grantgrantdesignreps.com/contact"},
+  {"id":35,"name":"Lisa Johansson","agency":"Nordic West Reps","territories":["WA","OR","ID","MT","WY"],"lines":["Bolon","Baux","Kvadrat"],"city":"Seattle","vertical":["Commercial","Office","Education"],"contact_url":"https://nordicwestreps.com/contact"},
+  {"id":36,"name":"Omar Farouk","agency":"Farouk Luxury Reps","territories":["CA","TX","NY"],"lines":["Loro Piana Interiors","Dedar","Sahco"],"city":"Los Angeles","vertical":["Luxury","Residential","Boutique"],"contact_url":"https://faroukluxuryreps.com/contact"},
+  {"id":37,"name":"Catherine Moore","agency":"Moore Wallcovering Reps","territories":["IL","MO","KS","NE"],"lines":["Innovations USA","Arte International","Nlxl"],"city":"Chicago","vertical":["Residential","Contract"],"contact_url":"https://moorewallcoveringreps.com/contact"},
+  {"id":38,"name":"Henry Vasquez","agency":"Vasquez Southwest Reps","territories":["AZ","NM","TX","CO"],"lines":["Pollack","Pindler","Duralee"],"city":"Phoenix","vertical":["Residential","Commercial"],"contact_url":"https://vasquezSouthwestreps.com/contact"},
+  {"id":39,"name":"Pamela Nkosi","agency":"Nkosi Retail Design Reps","territories":["NY","NJ","CT","MA"],"lines":["Maharam","Momentum","Interface"],"city":"New York","vertical":["Retail","Commercial","Office"],"contact_url":"https://nkosiretaildesignreps.com/contact"},
+  {"id":40,"name":"Jonathan Reyes","agency":"Reyes Pacific Hospitality","territories":["CA","HI","NV"],"lines":["Perennials","Elitis","Casamance"],"city":"Los Angeles","vertical":["Hospitality","Resort","Boutique"],"contact_url":"https://reyespacifichospitality.com/contact"}
+]
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e9f6314
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1418 @@
+{
+  "name": "commercialdesignreps",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "commercialdesignreps",
+      "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..3c77dd6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+  "name": "commercialdesignreps",
+  "version": "0.1.0",
+  "private": true,
+  "description": "Commercial design sales rep directory — find independent reps for hospitality, healthcare, contract, and commercial interior design markets",
+  "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..b885a60
--- /dev/null
+++ b/public/css/styles.css
@@ -0,0 +1,196 @@
+:root {
+  --bg: #0b1220;
+  --bg-elev: #11192a;
+  --bg-card: #131c30;
+  --fg: #e6ecff;
+  --fg-muted: #8a96b3;
+  --accent: #d4a574;
+  --accent-fg: #1a120a;
+  --border: #232c44;
+  --tag-bg: #1c2540;
+  --shadow: 0 10px 30px rgba(0,0,0,.35);
+  --radius: 14px;
+  --cols-min: 320px;
+  font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif;
+}
+[data-theme="light"] {
+  --bg: #f7f5f0;
+  --bg-elev: #ffffff;
+  --bg-card: #ffffff;
+  --fg: #1a1d29;
+  --fg-muted: #5a6478;
+  --accent: #8b5a2b;
+  --accent-fg: #ffffff;
+  --border: #e3dfd5;
+  --tag-bg: #efeae0;
+  --shadow: 0 6px 22px rgba(40,30,15,.08);
+}
+* { 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: 8px;
+  background: linear-gradient(135deg, var(--accent), #b8845a);
+}
+.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: 50%;
+  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: 740px; margin: 0; }
+
+.filter-bar {
+  display: grid; grid-template-columns: minmax(220px,1.4fr) repeat(3, 1fr) auto auto;
+  gap: 10px; margin: 24px 0 16px;
+  background: var(--bg-elev); padding: 14px; border-radius: var(--radius);
+  border: 1px solid var(--border);
+}
+.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;
+}
+.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;
+}
+.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; }
+
+.rep-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: 44px; height: 44px; border-radius: 50%;
+  background: linear-gradient(135deg, var(--accent), #b8845a);
+  color: var(--accent-fg); font-weight: 700; font-size: 14px;
+  display: flex; align-items: center; justify-content: center;
+  flex-shrink: 0;
+}
+.rep-avatar.lg { width: 80px; height: 80px; font-size: 26px; }
+.rep-name { font-weight: 600; font-size: 16px; }
+.rep-agency { color: var(--fg-muted); font-size: 13px; }
+.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);
+}
+.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: center; margin-bottom: 24px; }
+.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; }
+.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: 720px; }
+.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: 820px; }
+.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(200px, 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); }
+
+@media (max-width: 720px) {
+  .filter-bar { grid-template-columns: 1fr 1fr; }
+  .filter-bar input[type=search] { grid-column: 1 / -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..bc779be
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+  <defs>
+    <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" stop-color="#d4a574"/>
+      <stop offset="100%" stop-color="#8b5a2b"/>
+    </linearGradient>
+  </defs>
+  <rect width="64" height="64" rx="14" fill="url(#g)"/>
+  <text x="50%" y="56%" font-family="Helvetica,Arial,sans-serif" font-size="28" font-weight="700"
+        text-anchor="middle" dominant-baseline="middle" fill="#1a120a">CR</text>
+</svg>
diff --git a/public/js/main.js b/public/js/main.js
new file mode 100644
index 0000000..b4990d8
--- /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('cdr-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('cdr-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('cdr-density', slider.value); } catch (e) {}
+    });
+  }
+
+  const sortSelect = document.getElementById('sort-select');
+  if (sortSelect) {
+    let savedSort = null;
+    try { savedSort = localStorage.getItem('cdr-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('cdr-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..f11f866
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Allow: /
+Sitemap: https://commercialdesignreps.com/sitemap.xml
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..73663b0
--- /dev/null
+++ b/server.js
@@ -0,0 +1,111 @@
+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 || 9922;
+const GA_ID = process.env.GA_ID || 'G-PRJ51PTW5G';
+
+const reps = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'reps.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 allTerritories = Array.from(new Set(reps.flatMap(r => r.territories))).sort();
+const allLines = Array.from(new Set(reps.flatMap(r => r.lines))).sort();
+const allVerticals = Array.from(new Set(reps.flatMap(r => r.vertical))).sort();
+const allCities = Array.from(new Set(reps.map(r => r.city))).sort();
+
+const SITE = {
+  domain: 'commercialdesignreps.com',
+  name: 'Commercial Design Reps',
+  tagline: 'Independent sales reps for commercial, hospitality, and contract interior design',
+  description: 'Find independent design reps and showroom agencies for commercial, hospitality, healthcare, and contract textile and wallcovering lines across the United States.',
+  email: 'info@commercialdesignreps.com',
+  phone: '888-373-4564',
+  address: 'Designer Wallcoverings, 15442 Ventura Bl #102, Sherman Oaks CA 91403',
+  ga_id: GA_ID,
+};
+
+function sortReps(list, sort) {
+  const copy = list.slice();
+  switch (sort) {
+    case 'name':
+      return copy.sort((a, b) => a.name.localeCompare(b.name));
+    case 'agency':
+      return copy.sort((a, b) => a.agency.localeCompare(b.agency));
+    case 'city':
+      return copy.sort((a, b) => a.city.localeCompare(b.city));
+    case 'territory':
+      return copy.sort((a, b) => (a.territories[0] || '').localeCompare(b.territories[0] || ''));
+    case 'newest':
+    default:
+      return copy.sort((a, b) => b.id - a.id);
+  }
+}
+
+app.get('/', (req, res) => {
+  const { q, territory, vertical, line, sort } = req.query;
+  let filtered = reps.slice();
+
+  if (q) {
+    const needle = String(q).toLowerCase();
+    filtered = filtered.filter(r =>
+      r.name.toLowerCase().includes(needle) ||
+      r.agency.toLowerCase().includes(needle) ||
+      r.city.toLowerCase().includes(needle) ||
+      r.lines.some(l => l.toLowerCase().includes(needle))
+    );
+  }
+  if (territory) filtered = filtered.filter(r => r.territories.includes(String(territory)));
+  if (vertical) filtered = filtered.filter(r => r.vertical.includes(String(vertical)));
+  if (line) filtered = filtered.filter(r => r.lines.includes(String(line)));
+
+  filtered = sortReps(filtered, sort);
+
+  res.render('index', {
+    site: SITE,
+    reps: filtered,
+    total: reps.length,
+    matched: filtered.length,
+    filters: { q: q || '', territory: territory || '', vertical: vertical || '', line: line || '', sort: sort || 'newest' },
+    facets: { territories: allTerritories, lines: allLines, verticals: allVerticals, cities: allCities },
+  });
+});
+
+app.get('/rep/:id', (req, res) => {
+  const id = Number(req.params.id);
+  const rep = reps.find(r => r.id === id);
+  if (!rep) return res.status(404).render('404', { site: SITE });
+  const related = reps.filter(r => r.id !== rep.id && r.vertical.some(v => rep.vertical.includes(v))).slice(0, 4);
+  res.render('rep', { site: SITE, rep, related });
+});
+
+app.get('/about', (req, res) => {
+  res.render('about', { site: SITE });
+});
+
+app.get('/healthz', (req, res) => res.json({ ok: true, port: PORT, reps: reps.length }));
+
+app.use((req, res) => res.status(404).render('404', { site: SITE }));
+
+app.listen(PORT, () => {
+  console.log(`commercialdesignreps listening on :${PORT} (${reps.length} reps 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..fb9217e
--- /dev/null
+++ b/views/about.ejs
@@ -0,0 +1,25 @@
+<%- include('partials/head', { title: 'About — ' + site.name, description: 'Commercial Design Reps is an industry directory of independent sales reps and showroom agencies in commercial, hospitality, healthcare, and contract interior design.' }) %>
+
+<main class="wrap main">
+  <a class="back-link" href="/">&larr; Back</a>
+
+  <article class="long-form">
+    <h1>About Commercial Design Reps</h1>
+
+    <p>Commercial Design Reps is a free, curated directory of independent sales-rep agencies serving the U.S. commercial design industry. The directory exists because specifiers, A&amp;D firms, contract dealers, and purchasing teams routinely struggle to figure out <em>who carries what, where</em> — especially in the contract, hospitality, healthcare, and commercial interiors space, where representation maps shift constantly and brand-by-brand "find a rep" pages are out of date or hard to reach.</p>
+
+    <h2>What's in the directory</h2>
+    <p>Each listing includes the agency principal, the agency name, the territories the rep covers, the manufacturer and mill lines they carry, the verticals they actively service (residential, contract, hospitality, healthcare, education, retail, F&amp;B, resort, office), the city the rep is based in, and a direct contact path. Filters above the directory let you narrow by territory, by vertical, and by line.</p>
+
+    <h2>Who maintains it</h2>
+    <p>The directory is operated by <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> as a free industry resource. We are a wallcovering distributor, so we have a vested interest in keeping rep-territory maps accurate — and we're publishing this index in the hope that it's useful well beyond our own product line. Listings are not pay-to-play and there is no advertising on the site.</p>
+
+    <h2>How to list your agency</h2>
+    <p>If you run a rep agency and would like to be added — or if you are listed and need to update territories, lines, or contact info — 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 rep name, agency, and the correction. We act on corrections promptly.</p>
+  </article>
+</main>
+
+<%- include('partials/foot') %>
diff --git a/views/index.ejs b/views/index.ejs
new file mode 100644
index 0000000..256c321
--- /dev/null
+++ b/views/index.ejs
@@ -0,0 +1,100 @@
+<%- include('partials/head', { title: site.name + ' — ' + site.tagline, description: site.description }) %>
+
+<main class="wrap main">
+  <section class="hero">
+    <h1>Commercial design reps, by territory and line.</h1>
+    <p class="lead">A working directory of independent sales reps, agency principals, and showroom partners covering the U.S. commercial, hospitality, healthcare, and contract design markets. <%= total %> agencies indexed.</p>
+  </section>
+
+  <form class="filter-bar" method="get" action="/">
+    <input type="search" name="q" value="<%= filters.q %>" placeholder="Search rep, agency, line, city…" aria-label="Search">
+
+    <select name="territory" aria-label="Territory">
+      <option value="">All territories</option>
+      <% facets.territories.forEach(function(t){ %>
+        <option value="<%= t %>" <%= filters.territory === t ? 'selected' : '' %>><%= t %></option>
+      <% }) %>
+    </select>
+
+    <select name="vertical" aria-label="Vertical">
+      <option value="">All verticals</option>
+      <% facets.verticals.forEach(function(v){ %>
+        <option value="<%= v %>" <%= filters.vertical === v ? 'selected' : '' %>><%= v %></option>
+      <% }) %>
+    </select>
+
+    <select name="line" aria-label="Line carried">
+      <option value="">All lines</option>
+      <% facets.lines.forEach(function(l){ %>
+        <option value="<%= l %>" <%= filters.line === l ? 'selected' : '' %>><%= l %></option>
+      <% }) %>
+    </select>
+
+    <button type="submit" class="btn-primary">Filter</button>
+    <% if (filters.q || filters.territory || filters.vertical || filters.line) { %>
+      <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="name" <%= filters.sort === 'name' ? 'selected' : '' %>>Name A→Z</option>
+          <option value="agency" <%= filters.sort === 'agency' ? 'selected' : '' %>>Agency A→Z</option>
+          <option value="city" <%= filters.sort === 'city' ? 'selected' : '' %>>City A→Z</option>
+          <option value="territory" <%= filters.sort === 'territory' ? 'selected' : '' %>>Territory</option>
+        </select>
+      </label>
+      <label class="control">
+        <span>Density</span>
+        <input id="density-slider" type="range" min="220" max="420" step="20" value="300">
+      </label>
+    </div>
+  </div>
+
+  <% if (!matched) { %>
+    <div class="empty">No reps match those filters. <a href="/">Reset</a>.</div>
+  <% } else { %>
+    <ul class="rep-grid" id="rep-grid">
+      <% reps.forEach(function(r){ %>
+        <li class="rep-card">
+          <a class="rep-link" href="/rep/<%= r.id %>">
+            <div class="rep-card-head">
+              <div class="rep-avatar"><%= r.name.split(' ').map(s => s[0]).join('').slice(0,2) %></div>
+              <div>
+                <div class="rep-name"><%= r.name %></div>
+                <div class="rep-agency"><%= r.agency %></div>
+              </div>
+            </div>
+            <div class="rep-meta">
+              <span class="chip city"><%= r.city %></span>
+              <% r.territories.slice(0,4).forEach(function(t){ %>
+                <span class="chip"><%= t %></span>
+              <% }) %>
+            </div>
+            <div class="rep-lines muted">
+              <%= r.lines.slice(0,3).join(' &middot; ') %>
+            </div>
+            <div class="rep-vertical">
+              <% r.vertical.forEach(function(v){ %>
+                <span class="tag"><%= v %></span>
+              <% }) %>
+            </div>
+          </a>
+        </li>
+      <% }) %>
+    </ul>
+  <% } %>
+
+  <section class="seo-block">
+    <h2>How this directory works</h2>
+    <p>Commercial Design Reps is a curated index of independent sales-rep agencies serving the contract, hospitality, and commercial interior design industries. Each listing maps the rep's territory coverage, the manufacturer lines they carry, and the verticals (residential, contract, hospitality, healthcare, education, F&amp;B, retail) they actively service. Use the filters above to find a rep by region, by line carried, or by project type. Specifiers, designers, architects, and purchasing teams can contact reps directly through the listing page.</p>
+    <p>This directory is operated by <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> as a free industry resource. To list your agency, 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..d6b3148
--- /dev/null
+++ b/views/partials/foot.ejs
@@ -0,0 +1,29 @@
+<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 reps</strong>
+      <p>
+        <a href="mailto:<%= site.email %>?subject=List%20our%20agency">List your agency</a><br>
+        <a href="mailto:<%= site.email %>?subject=Update%20our%20listing">Update a listing</a>
+      </p>
+    </div>
+  </div>
+  <div class="wrap copy">
+    <small>&copy; <%= new Date().getFullYear() %> <%= site.name %>. A directory project of Designer Wallcoverings.</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..f9abc2e
--- /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="#0f172a">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<link rel="stylesheet" href="/css/styles.css">
+<script>
+(function(){
+  try {
+    var t = localStorage.getItem('cdr-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">&#9789;</span><span class="sun">&#9728;</span>
+    </button>
+  </div>
+</header>
diff --git a/views/rep.ejs b/views/rep.ejs
new file mode 100644
index 0000000..1cd500c
--- /dev/null
+++ b/views/rep.ejs
@@ -0,0 +1,69 @@
+<%- include('partials/head', { title: rep.name + ' — ' + rep.agency + ' | ' + site.name, description: rep.agency + ' represents ' + rep.lines.join(', ') + ' in ' + rep.territories.join(', ') + '. Independent commercial design rep for ' + rep.vertical.join(', ') + '.' }) %>
+
+<main class="wrap main">
+  <a class="back-link" href="/">&larr; Back to directory</a>
+
+  <section class="rep-detail">
+    <div class="rep-header">
+      <div class="rep-avatar lg"><%= rep.name.split(' ').map(s => s[0]).join('').slice(0,2) %></div>
+      <div>
+        <h1><%= rep.name %></h1>
+        <div class="agency"><%= rep.agency %></div>
+        <div class="city muted"><%= rep.city %></div>
+      </div>
+    </div>
+
+    <div class="rep-grid-detail">
+      <div>
+        <h3>Territories</h3>
+        <div class="chips">
+          <% rep.territories.forEach(function(t){ %><span class="chip"><%= t %></span><% }) %>
+        </div>
+      </div>
+      <div>
+        <h3>Verticals</h3>
+        <div class="chips">
+          <% rep.vertical.forEach(function(v){ %><span class="tag"><%= v %></span><% }) %>
+        </div>
+      </div>
+      <div>
+        <h3>Lines carried</h3>
+        <ul class="lines-list">
+          <% rep.lines.forEach(function(l){ %><li><%= l %></li><% }) %>
+        </ul>
+      </div>
+    </div>
+
+    <div class="cta-row">
+      <a class="btn-primary" href="<%= rep.contact_url %>" target="_blank" rel="noopener">Contact agency &rarr;</a>
+      <a class="btn-ghost" href="mailto:<%= site.email %>?subject=Question%20about%20<%= encodeURIComponent(rep.agency) %>">Ask the directory</a>
+    </div>
+  </section>
+
+  <% if (related.length) { %>
+  <section class="related">
+    <h2>Other reps in similar verticals</h2>
+    <ul class="rep-grid" style="--cols-min: 280px">
+      <% related.forEach(function(r){ %>
+        <li class="rep-card">
+          <a class="rep-link" href="/rep/<%= r.id %>">
+            <div class="rep-card-head">
+              <div class="rep-avatar"><%= r.name.split(' ').map(s => s[0]).join('').slice(0,2) %></div>
+              <div>
+                <div class="rep-name"><%= r.name %></div>
+                <div class="rep-agency"><%= r.agency %></div>
+              </div>
+            </div>
+            <div class="rep-meta">
+              <span class="chip city"><%= r.city %></span>
+              <% r.territories.slice(0,3).forEach(function(t){ %><span class="chip"><%= t %></span><% }) %>
+            </div>
+          </a>
+        </li>
+      <% }) %>
+    </ul>
+  </section>
+  <% } %>
+</main>
+
+<%- include('partials/foot') %>

(oldest)  ·  back to Commercialdesignreps  ·  hamburger nav: apply canonical pattern from sister-site flee 2c0ea85 →