[object Object]

← back to Stars of Design

initial scaffold + GA gtag (G-YCT0F2Q692) + 27 designer profiles

aa14a0298ae1251b39c33e36eec00ca38eda046e · 2026-05-10 03:50:34 -0700 · Steve Abrams

Files touched

Diff

commit aa14a0298ae1251b39c33e36eec00ca38eda046e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 10 03:50:34 2026 -0700

    initial scaffold + GA gtag (G-YCT0F2Q692) + 27 designer profiles
---
 .gitignore                       |   9 +
 README.md                        |  56 +++
 data/designers.json              | 444 +++++++++++++++++
 ecosystem.config.js              |  16 +
 lib/data.js                      |  55 +++
 lib/sort.js                      |  43 ++
 package-lock.json                | 995 +++++++++++++++++++++++++++++++++++++++
 package.json                     |  20 +
 public/css/site.css              | 222 +++++++++
 public/css/theme.css             |  41 ++
 public/favicon.svg               |   5 +
 public/js/grid-controls.js       |  31 ++
 public/js/theme-toggle.js        |  11 +
 public/robots.txt                |   3 +
 routes/public.js                 |  75 +++
 server.js                        |  53 +++
 views/partials/footer.ejs        |  41 ++
 views/partials/grid-controls.ejs |  21 +
 views/partials/head.ejs          |  42 ++
 views/partials/header.ejs        |  17 +
 views/public/404.ejs             |  11 +
 views/public/about.ejs           |  62 +++
 views/public/designer.ejs        |  62 +++
 views/public/designers.ejs       |  68 +++
 views/public/error.ejs           |  12 +
 views/public/home.ejs            |  76 +++
 views/public/submit.ejs          |  46 ++
 27 files changed, 2537 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..579d5c2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+# Stars of Design
+
+Editorial profile directory of notable interior designers — historic and
+contemporary — paired with the Designer Wallcoverings collections that match
+each designer's signature style.
+
+- **Domain**: starsofdesign.com
+- **Local port (Mac2 dev)**: `9928`
+- **Kamatera pm2 name**: `starsofdesign`
+- **Stack**: Express + EJS, JSON-backed (no PG dependency)
+- **GA4**: `G-YCT0F2Q692`
+
+## Run locally
+
+```bash
+npm ci
+PORT=9928 node server.js
+# → http://localhost:9928
+```
+
+## Add a designer
+
+Edit `data/designers.json`. Each entry:
+
+```json
+{
+  "slug": "first-last",
+  "name": "First Last",
+  "headline": "One-line tagline",
+  "era": "Mid-Century | Late 20th Century | Contemporary",
+  "era_sort": 1985,
+  "city": "City, ST",
+  "styles": ["Style A", "Style B"],
+  "active_years": "1990–present",
+  "bio": "One paragraph profile…",
+  "signature": "One sentence visual language.",
+  "preferred_dw": [
+    { "label": "Collection name", "url": "https://designerwallcoverings.com/collections/handle" }
+  ]
+}
+```
+
+The JSON is hot-reloaded on every request via mtime check — no restart
+needed when editing in place.
+
+## Deploy
+
+`ecosystem.config.js` is configured for Kamatera pm2:
+
+```bash
+rsync -av --exclude node_modules /Users/stevestudio2/Projects/starsofdesign/ root@45.61.58.125:/root/Projects/starsofdesign/
+ssh root@45.61.58.125 "cd /root/Projects/starsofdesign && npm ci && pm2 start ecosystem.config.js && pm2 save"
+```
+
+Nginx vhost lives at `/etc/nginx/sites-enabled/starsofdesign.com`, currently
+listening on `:80` only (TLS will be added when DNS flips).
diff --git a/data/designers.json b/data/designers.json
new file mode 100644
index 0000000..bd6d712
--- /dev/null
+++ b/data/designers.json
@@ -0,0 +1,444 @@
+[
+  {
+    "slug": "dorothy-draper",
+    "name": "Dorothy Draper",
+    "headline": "The mother of modern American interior design",
+    "era": "Mid-Century",
+    "era_sort": 1940,
+    "city": "New York, NY",
+    "styles": ["Maximalist", "Baroque Modern", "Hospitality"],
+    "active_years": "1923–1969",
+    "bio": "Dorothy Draper founded the first commercial interior design firm in the United States in 1923. Her bold use of color — most famously the Greenbrier resort in West Virginia — invented the concept of \"hospitality design\" as we now know it. Plaster moldings painted in carnival colors, oversized chintz, black-and-white checkerboard floors. Anti-good-taste. Anti-restraint.",
+    "signature": "Cabbage rose chintz, supersized scale, Wedgwood plaster ceilings.",
+    "preferred_dw": [
+      { "label": "Floral & Chintz Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" },
+      { "label": "Damask Wallcoverings", "url": "https://designerwallcoverings.com/collections/damask" },
+      { "label": "Maya Romanoff Beaded", "url": "https://designerwallcoverings.com/collections/maya-romanoff" }
+    ]
+  },
+  {
+    "slug": "sister-parish",
+    "name": "Sister Parish",
+    "headline": "Inventor of the American Country style",
+    "era": "Mid-Century",
+    "era_sort": 1950,
+    "city": "New York, NY",
+    "styles": ["American Country", "Traditional", "Layered"],
+    "active_years": "1933–1994",
+    "bio": "Dorothy May Kinnicutt — \"Sister\" Parish — founded Parish-Hadley with Albert Hadley and decorated the Kennedy White House. She is credited with codifying the American Country aesthetic: hooked rugs, painted floors, four-poster beds, English chintz layered against simple American antiques.",
+    "signature": "Patchwork quilts, painted floorboards, faded chintz, hand-painted floral wallpaper.",
+    "preferred_dw": [
+      { "label": "Floral Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" },
+      { "label": "Toile Wallcoverings", "url": "https://designerwallcoverings.com/collections/toile" },
+      { "label": "Schumacher Hand-Prints", "url": "https://designerwallcoverings.com/collections/schumacher" }
+    ]
+  },
+  {
+    "slug": "albert-hadley",
+    "name": "Albert Hadley",
+    "headline": "The dean of American decorating",
+    "era": "Mid-Century",
+    "era_sort": 1955,
+    "city": "New York, NY",
+    "styles": ["Traditional", "Editorial", "American Modern"],
+    "active_years": "1956–2012",
+    "bio": "Albert Hadley joined Parish-Hadley in 1962 and served as the firm's design lead until 1994. Where Sister Parish layered, Hadley pared down — a single perfect Mark Rothko, a Louis XVI chair, a pencil-stripe wall. He decorated for Brooke Astor, Babe Paley, and Vice President Rockefeller.",
+    "signature": "Library red, lacquered walls, geometric stripe wallpaper, restraint.",
+    "preferred_dw": [
+      { "label": "Striped Wallcoverings", "url": "https://designerwallcoverings.com/collections/stripe" },
+      { "label": "Solid Lacquer Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Phillip Jeffries Grasscloth", "url": "https://designerwallcoverings.com/collections/phillip-jeffries" }
+    ]
+  },
+  {
+    "slug": "billy-baldwin",
+    "name": "Billy Baldwin",
+    "headline": "Decorator to American café society",
+    "era": "Mid-Century",
+    "era_sort": 1960,
+    "city": "New York, NY",
+    "styles": ["Tailored", "Mid-Century Modern", "Editorial"],
+    "active_years": "1935–1983",
+    "bio": "Billy Baldwin decorated for Cole Porter, Jacqueline Kennedy, Diana Vreeland, and Bunny Mellon. He preached editing — \"be faithful to your own taste, no one will ever like what you do unless you do it yourself\" — and is credited with making the brown lacquered library a permanent fixture of the Manhattan apartment.",
+    "signature": "Brown-lacquer libraries, brass étagères, slipper chairs, plain off-white walls.",
+    "preferred_dw": [
+      { "label": "Solid & Tonal Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Grasscloth Wallcoverings", "url": "https://designerwallcoverings.com/collections/grasscloth" },
+      { "label": "Lacquered Wallcoverings", "url": "https://designerwallcoverings.com/collections/lacquer" }
+    ]
+  },
+  {
+    "slug": "david-hicks",
+    "name": "David Hicks",
+    "headline": "British modernism, the geometric pattern made famous",
+    "era": "Mid-Century",
+    "era_sort": 1965,
+    "city": "London, UK",
+    "styles": ["Geometric", "British Modernism", "Color-Block"],
+    "active_years": "1954–1998",
+    "bio": "David Hicks broke from English country-house decoration by combining antiques with hard-edge geometric carpets and tablescapes of objects grouped by color. The hexagonal Hicks pattern is still in production. He decorated for Helena Rubinstein, the Prince of Wales, and his own family at Britwell Salome.",
+    "signature": "Hexagonal hex carpets, magenta-and-orange color-blocking, tablescapes.",
+    "preferred_dw": [
+      { "label": "Geometric Wallcoverings", "url": "https://designerwallcoverings.com/collections/geometric" },
+      { "label": "Trellis & Lattice", "url": "https://designerwallcoverings.com/collections/trellis" },
+      { "label": "Cole & Son", "url": "https://designerwallcoverings.com/collections/cole-and-son" }
+    ]
+  },
+  {
+    "slug": "mario-buatta",
+    "name": "Mario Buatta",
+    "headline": "The Prince of Chintz",
+    "era": "Late 20th Century",
+    "era_sort": 1980,
+    "city": "New York, NY",
+    "styles": ["English Country", "Chintz", "Maximalist"],
+    "active_years": "1963–2018",
+    "bio": "Mario Buatta brought English country-house decoration to American clients — Barbara Walters, Mariah Carey, Henry Ford II, and the Blair House restoration. Cabbage roses, glazed walls in Yellow #11, dog portraits, swag valances, and a refusal to ever make a room look new.",
+    "signature": "Glazed yellow walls, cabbage-rose chintz, swag drapery, dog portraits.",
+    "preferred_dw": [
+      { "label": "Floral Chintz Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" },
+      { "label": "Toile de Jouy", "url": "https://designerwallcoverings.com/collections/toile" },
+      { "label": "Schumacher", "url": "https://designerwallcoverings.com/collections/schumacher" }
+    ]
+  },
+  {
+    "slug": "mark-hampton",
+    "name": "Mark Hampton",
+    "headline": "Decorator to four U.S. Presidents",
+    "era": "Late 20th Century",
+    "era_sort": 1985,
+    "city": "New York, NY",
+    "styles": ["Traditional", "American Federal", "Library Style"],
+    "active_years": "1976–1998",
+    "bio": "Mark Hampton decorated the private quarters of the Bush, Clinton, and Carter White Houses, Blair House, and Gracie Mansion. His look — anglophile, library-heavy, scholarly — defined Reagan-era American taste. He drew every project in his own watercolors before construction.",
+    "signature": "Library bookcases painted putty, English antiques, herringbone floors, faded glaze.",
+    "preferred_dw": [
+      { "label": "Library Wallcoverings", "url": "https://designerwallcoverings.com/collections/library" },
+      { "label": "Damask Wallcoverings", "url": "https://designerwallcoverings.com/collections/damask" },
+      { "label": "Cowtan & Tout", "url": "https://designerwallcoverings.com/collections/cowtan-and-tout" }
+    ]
+  },
+  {
+    "slug": "bunny-williams",
+    "name": "Bunny Williams",
+    "headline": "Layered American country",
+    "era": "Late 20th Century",
+    "era_sort": 1990,
+    "city": "Falls Village, CT",
+    "styles": ["Layered", "American Country", "Garden"],
+    "active_years": "1989–present",
+    "bio": "Bunny Williams left Parish-Hadley in 1988 to open her own firm. Her work is patient — rooms that look like they were assembled over generations rather than installed in a weekend. She publishes seven books and runs Treillage, the New York antiques shop she co-founded with John Rosselli.",
+    "signature": "Botanical prints, garden rooms, generous slipcovers, mixed Continental antiques.",
+    "preferred_dw": [
+      { "label": "Botanical Wallcoverings", "url": "https://designerwallcoverings.com/collections/botanical" },
+      { "label": "Floral Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" },
+      { "label": "Brunschwig & Fils", "url": "https://designerwallcoverings.com/collections/brunschwig-and-fils" }
+    ]
+  },
+  {
+    "slug": "kelly-wearstler",
+    "name": "Kelly Wearstler",
+    "headline": "California glamour-modernism",
+    "era": "Contemporary",
+    "era_sort": 2005,
+    "city": "Los Angeles, CA",
+    "styles": ["Hollywood Regency", "Glamour", "Sculptural"],
+    "active_years": "1995–present",
+    "bio": "Kelly Wearstler invented the language of \"new Hollywood Regency\" through Viceroy Hotels and her own line of furniture, lighting, fabrics, and wallcoverings. Her work is sculptural, tactile, color-saturated, and often architectural in scale — Greek key floors, Carrara coffee tables, brass everywhere.",
+    "signature": "Greek key motifs, malachite walls, brass and travertine, theatrical color.",
+    "preferred_dw": [
+      { "label": "Geometric Wallcoverings", "url": "https://designerwallcoverings.com/collections/geometric" },
+      { "label": "Glass Beaded Wallcoverings", "url": "https://designerwallcoverings.com/collections/glass-beaded" },
+      { "label": "Maya Romanoff", "url": "https://designerwallcoverings.com/collections/maya-romanoff" }
+    ]
+  },
+  {
+    "slug": "jonathan-adler",
+    "name": "Jonathan Adler",
+    "headline": "Happy chic, modern American pop",
+    "era": "Contemporary",
+    "era_sort": 2008,
+    "city": "New York, NY",
+    "styles": ["Pop", "Modern", "Maximalist"],
+    "active_years": "1993–present",
+    "bio": "Jonathan Adler started with handmade pottery sold to Barneys and built it into a 30-store retail chain. His interiors — Parker Palm Springs, Eau Spa, Le Royal Monceau — are unapologetically optimistic: bold geometric rugs, brass figurines, lucite, and saturated color.",
+    "signature": "Lucite, brass animals, mod geometric prints, candy-shop palettes.",
+    "preferred_dw": [
+      { "label": "Geometric Wallcoverings", "url": "https://designerwallcoverings.com/collections/geometric" },
+      { "label": "Pop & Novelty", "url": "https://designerwallcoverings.com/collections/novelty" },
+      { "label": "Cole & Son", "url": "https://designerwallcoverings.com/collections/cole-and-son" }
+    ]
+  },
+  {
+    "slug": "nate-berkus",
+    "name": "Nate Berkus",
+    "headline": "Lived-in modernism, story-driven rooms",
+    "era": "Contemporary",
+    "era_sort": 2010,
+    "city": "New York, NY",
+    "styles": ["Layered Modern", "Collected", "Warm Minimalism"],
+    "active_years": "2002–present",
+    "bio": "Nate Berkus built his name on Oprah's couch and translated the on-camera warmth into a design vocabulary that prizes objects-with-meaning over symmetry. He runs his namesake firm with husband Jeremiah Brent and lines at Target and Living Spaces.",
+    "signature": "Travel-collected objects, warm woods, stacked books, plaster walls.",
+    "preferred_dw": [
+      { "label": "Grasscloth Wallcoverings", "url": "https://designerwallcoverings.com/collections/grasscloth" },
+      { "label": "Plaster & Limestone Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Phillip Jeffries", "url": "https://designerwallcoverings.com/collections/phillip-jeffries" }
+    ]
+  },
+  {
+    "slug": "miles-redd",
+    "name": "Miles Redd",
+    "headline": "Color, drama, the American country house reimagined",
+    "era": "Contemporary",
+    "era_sort": 2012,
+    "city": "New York, NY",
+    "styles": ["Maximalist", "American Country", "Color-Saturated"],
+    "active_years": "1998–present",
+    "bio": "Miles Redd worked under John Rosselli before opening his own firm and now leads Redd Kaihoi with David Kaihoi. His work is fearlessly colored — lacquered scarlet libraries, leopard stair-runners, malachite breakfast rooms — and informed by the great mid-century decorators he openly cites.",
+    "signature": "Lacquered scarlet, leopard prints, hand-painted murals, glossy ceilings.",
+    "preferred_dw": [
+      { "label": "Hand-Painted Murals", "url": "https://designerwallcoverings.com/collections/mural" },
+      { "label": "Animal Print Wallcoverings", "url": "https://designerwallcoverings.com/collections/animal-print" },
+      { "label": "Schumacher", "url": "https://designerwallcoverings.com/collections/schumacher" }
+    ]
+  },
+  {
+    "slug": "martyn-lawrence-bullard",
+    "name": "Martyn Lawrence Bullard",
+    "headline": "Hollywood-celebrity glamour",
+    "era": "Contemporary",
+    "era_sort": 2010,
+    "city": "Los Angeles, CA",
+    "styles": ["Hollywood Regency", "Eclectic", "Bohemian Luxe"],
+    "active_years": "1996–present",
+    "bio": "Martyn Lawrence Bullard's client list reads like a TMZ index — Cher, Elton John, Kim Kardashian, the Osbournes. He moves easily between Moroccan riads, Beverly Hills compounds, and Aman resorts, with a signature use of pattern and global reference.",
+    "signature": "Moroccan stars, Suzani prints, layered pattern, big-scale chandeliers.",
+    "preferred_dw": [
+      { "label": "Moroccan & Suzani", "url": "https://designerwallcoverings.com/collections/moroccan" },
+      { "label": "Hand-Beaded", "url": "https://designerwallcoverings.com/collections/glass-beaded" },
+      { "label": "Schumacher", "url": "https://designerwallcoverings.com/collections/schumacher" }
+    ]
+  },
+  {
+    "slug": "michael-s-smith",
+    "name": "Michael S. Smith",
+    "headline": "California classicism, Obama White House decorator",
+    "era": "Contemporary",
+    "era_sort": 2009,
+    "city": "Los Angeles, CA",
+    "styles": ["Classical", "California Mediterranean", "Restrained"],
+    "active_years": "1990–present",
+    "bio": "Michael S. Smith decorated the Obama White House family quarters and the Oval Office (2010 redesign), as well as homes for Steven Spielberg, Cindy Crawford, and Rupert Murdoch. His look fuses Mediterranean classicism with California ease — limestone, plaster, faded antiques, no flash.",
+    "signature": "Plastered walls, limestone floors, blue-and-white porcelain, faded textiles.",
+    "preferred_dw": [
+      { "label": "Plaster & Limestone Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Blue & White", "url": "https://designerwallcoverings.com/collections/chinoiserie" },
+      { "label": "Cowtan & Tout", "url": "https://designerwallcoverings.com/collections/cowtan-and-tout" }
+    ]
+  },
+  {
+    "slug": "rose-tarlow",
+    "name": "Rose Tarlow",
+    "headline": "Old-world materiality, hand-aged everything",
+    "era": "Contemporary",
+    "era_sort": 1995,
+    "city": "Los Angeles, CA",
+    "styles": ["European Classical", "Aged Patina", "Architectural"],
+    "active_years": "1976–present",
+    "bio": "Rose Tarlow runs Melrose House on La Cienega, the showroom and furniture line that supplies the most architectural decorators in America. Her own home — published in every shelter magazine — is a study in plaster, oak beams, and the absence of anything that looks new.",
+    "signature": "Plaster walls, hand-aged oak, antique linen slipcovers, minimal color.",
+    "preferred_dw": [
+      { "label": "Plaster Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Linen Wallcoverings", "url": "https://designerwallcoverings.com/collections/linen" },
+      { "label": "Phillip Jeffries", "url": "https://designerwallcoverings.com/collections/phillip-jeffries" }
+    ]
+  },
+  {
+    "slug": "axel-vervoordt",
+    "name": "Axel Vervoordt",
+    "headline": "Wabi-sabi for the European collector",
+    "era": "Contemporary",
+    "era_sort": 2000,
+    "city": "Antwerp, Belgium",
+    "styles": ["Wabi-Sabi", "Monastic", "Patina"],
+    "active_years": "1969–present",
+    "bio": "Axel Vervoordt blends Belgian classicism with Japanese wabi-sabi — battered linen, plaster walls, Khmer stone heads on Belgian oak tables. Clients include Robert De Niro, Calvin Klein, and Kanye West (who described Vervoordt's aesthetic as the foundation for his own \"Ye home\" minimalism).",
+    "signature": "Lime-washed walls, monastic furniture, Asian artifacts on European tables.",
+    "preferred_dw": [
+      { "label": "Plaster & Lime-Wash Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Linen Wallcoverings", "url": "https://designerwallcoverings.com/collections/linen" },
+      { "label": "Grasscloth Wallcoverings", "url": "https://designerwallcoverings.com/collections/grasscloth" }
+    ]
+  },
+  {
+    "slug": "jean-louis-deniot",
+    "name": "Jean-Louis Deniot",
+    "headline": "Parisian neo-classical-modernism",
+    "era": "Contemporary",
+    "era_sort": 2014,
+    "city": "Paris, France",
+    "styles": ["Neoclassical", "Parisian Modern", "Architectural"],
+    "active_years": "2000–present",
+    "bio": "Jean-Louis Deniot designs from his 7th arrondissement studio and ships projects to New York, Mumbai, Aspen, Capri, and Bogotá. His work fuses 18th-century French classicism with hard-edged contemporary geometry — fluted limestone walls, brass inlay floors, plaster ceiling reliefs.",
+    "signature": "Fluted limestone, brass inlay, plaster reliefs, all-tone palettes.",
+    "preferred_dw": [
+      { "label": "Solid & Tonal Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Metallic Wallcoverings", "url": "https://designerwallcoverings.com/collections/metallic" },
+      { "label": "Pierre Frey", "url": "https://designerwallcoverings.com/collections/pierre-frey" }
+    ]
+  },
+  {
+    "slug": "kelly-hoppen",
+    "name": "Kelly Hoppen",
+    "headline": "British modern minimalism, taupe-on-taupe",
+    "era": "Contemporary",
+    "era_sort": 2002,
+    "city": "London, UK",
+    "styles": ["Modern Minimalism", "Taupe", "Asian-Influenced"],
+    "active_years": "1976–present",
+    "bio": "Kelly Hoppen built her name on a tightly disciplined neutral palette — taupe, cream, charcoal, the occasional gold leaf — and the rigorous use of texture in place of color. She has designed yachts for Sir Philip Green, hotels in Beijing, and her own line at QVC.",
+    "signature": "Taupe-on-taupe, gold leaf, Asian-influenced symmetry, layered texture.",
+    "preferred_dw": [
+      { "label": "Solid & Tonal Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Gold Leaf Wallcoverings", "url": "https://designerwallcoverings.com/collections/gold-leaf" },
+      { "label": "Grasscloth Wallcoverings", "url": "https://designerwallcoverings.com/collections/grasscloth" }
+    ]
+  },
+  {
+    "slug": "studio-mcgee",
+    "name": "Shea McGee (Studio McGee)",
+    "headline": "Modern transitional, the Instagram era",
+    "era": "Contemporary",
+    "era_sort": 2018,
+    "city": "Lehi, UT",
+    "styles": ["Modern Transitional", "Light & Airy", "California Casual"],
+    "active_years": "2014–present",
+    "bio": "Shea and Syd McGee launched Studio McGee from a basement and turned it into the most-followed interior design firm on Instagram. Their look — white shiplap, brass sconces, oak floors, neutral linen — defined the Netflix-era American family home and seeded the Threshold line at Target.",
+    "signature": "White shiplap, brass sconces, oak floors, neutral linen, woven baskets.",
+    "preferred_dw": [
+      { "label": "Grasscloth Wallcoverings", "url": "https://designerwallcoverings.com/collections/grasscloth" },
+      { "label": "Faux Shiplap & Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Floral Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" }
+    ]
+  },
+  {
+    "slug": "amber-lewis",
+    "name": "Amber Lewis (Amber Interiors)",
+    "headline": "California-warm boho-modernism",
+    "era": "Contemporary",
+    "era_sort": 2019,
+    "city": "Calabasas, CA",
+    "styles": ["California Casual", "Boho-Modern", "Earthy"],
+    "active_years": "2011–present",
+    "bio": "Amber Lewis runs Amber Interiors and Shoppe Amber Interiors, the Calabasas brick-and-mortar that anchors a generation of California-warm interiors — bouclé sofas, vintage Berber rugs, oak beams, plaster fireplaces, weathered woods.",
+    "signature": "Vintage rugs, bouclé sofas, earthy plaster, mixed wood tones.",
+    "preferred_dw": [
+      { "label": "Plaster & Lime-Wash Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Grasscloth Wallcoverings", "url": "https://designerwallcoverings.com/collections/grasscloth" },
+      { "label": "Boho & Tribal", "url": "https://designerwallcoverings.com/collections/boho" }
+    ]
+  },
+  {
+    "slug": "athena-calderone",
+    "name": "Athena Calderone",
+    "headline": "Tonal modernism, the artful interior",
+    "era": "Contemporary",
+    "era_sort": 2020,
+    "city": "Brooklyn, NY",
+    "styles": ["Tonal Modern", "Sculptural", "Editorial"],
+    "active_years": "2015–present",
+    "bio": "Athena Calderone built EyeSwoon, a content brand that became an interiors and product practice. She publishes Live Beautiful and Cook Beautiful, and her own Brooklyn brownstone (renovated with husband DJ Aoki) is the model for tonal modernism — limestone, oak, plaster, no color, all sculpture.",
+    "signature": "Tonal palettes, sculptural seating, plaster fireplaces, oak millwork.",
+    "preferred_dw": [
+      { "label": "Plaster Texture", "url": "https://designerwallcoverings.com/collections/texture" },
+      { "label": "Solid Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Linen Wallcoverings", "url": "https://designerwallcoverings.com/collections/linen" }
+    ]
+  },
+  {
+    "slug": "nicole-hollis",
+    "name": "Nicole Hollis",
+    "headline": "Dark-luxe California minimalism",
+    "era": "Contemporary",
+    "era_sort": 2016,
+    "city": "San Francisco, CA",
+    "styles": ["Dark Modern", "Sculptural Minimalism", "Sculptural"],
+    "active_years": "2003–present",
+    "bio": "Nicole Hollis runs a 30-person San Francisco firm specializing in serene, monochromatic interiors with a strong sculptural edge. Recent projects include the Four Seasons Resort Lanai and a Pacific Heights mansion published in AD's October cover story.",
+    "signature": "Black-and-bronze palettes, sculptural seating, single statement pendant.",
+    "preferred_dw": [
+      { "label": "Solid Dark Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Metallic Wallcoverings", "url": "https://designerwallcoverings.com/collections/metallic" },
+      { "label": "Maya Romanoff", "url": "https://designerwallcoverings.com/collections/maya-romanoff" }
+    ]
+  },
+  {
+    "slug": "charlotte-moss",
+    "name": "Charlotte Moss",
+    "headline": "Garden-room romanticism",
+    "era": "Late 20th Century",
+    "era_sort": 1995,
+    "city": "New York, NY",
+    "styles": ["Romantic", "Garden", "Anglo-French"],
+    "active_years": "1985–present",
+    "bio": "Charlotte Moss launched her Madison Avenue shop in 1985 and built her name on highly feminine, garden-influenced rooms with a strong English country undercurrent. She's published 11 books and decorated for clients across the U.S. and Europe.",
+    "signature": "Botanical prints, painted floors, scalloped fabrics, garden-room palettes.",
+    "preferred_dw": [
+      { "label": "Botanical Wallcoverings", "url": "https://designerwallcoverings.com/collections/botanical" },
+      { "label": "Floral Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" },
+      { "label": "Scalamandre", "url": "https://designerwallcoverings.com/collections/scalamandre" }
+    ]
+  },
+  {
+    "slug": "thomas-obrien",
+    "name": "Thomas O'Brien",
+    "headline": "American modern, layered with antiques",
+    "era": "Contemporary",
+    "era_sort": 2003,
+    "city": "New York, NY",
+    "styles": ["American Modern", "Layered", "Tailored"],
+    "active_years": "1992–present",
+    "bio": "Thomas O'Brien founded Aero Studios in 1992 and built a parallel retail life through Aero on Bleecker. He pioneered the look of \"warm modernism\" — twentieth-century industrial pieces sitting comfortably alongside 18th-century French antiques — and his Target line shifted American taste in the early 2000s.",
+    "signature": "Industrial pendants, 1940s leather, bleached oak, warm whites.",
+    "preferred_dw": [
+      { "label": "Solid Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Stripe Wallcoverings", "url": "https://designerwallcoverings.com/collections/stripe" },
+      { "label": "Phillip Jeffries", "url": "https://designerwallcoverings.com/collections/phillip-jeffries" }
+    ]
+  },
+  {
+    "slug": "steven-gambrel",
+    "name": "Steven Gambrel",
+    "headline": "Color-block American architectural",
+    "era": "Contemporary",
+    "era_sort": 2011,
+    "city": "New York, NY",
+    "styles": ["Architectural", "Saturated Color", "Detail-Driven"],
+    "active_years": "1995–present",
+    "bio": "Steven Gambrel runs SR Gambrel from his Bond Street office and is known for the level of architectural millwork in every project — paneled walls, custom lighting, color-block libraries. His Sag Harbor and West Village houses have appeared in every major shelter title.",
+    "signature": "Saturated wall color, custom millwork, antique brass, plaster ceilings.",
+    "preferred_dw": [
+      { "label": "Solid Saturated Wallcoverings", "url": "https://designerwallcoverings.com/collections/solid" },
+      { "label": "Damask Wallcoverings", "url": "https://designerwallcoverings.com/collections/damask" },
+      { "label": "Schumacher", "url": "https://designerwallcoverings.com/collections/schumacher" }
+    ]
+  },
+  {
+    "slug": "suzanne-kasler",
+    "name": "Suzanne Kasler",
+    "headline": "Atlanta-French, garden-soft",
+    "era": "Contemporary",
+    "era_sort": 2008,
+    "city": "Atlanta, GA",
+    "styles": ["French Country", "Soft Modern", "Light Palette"],
+    "active_years": "1994–present",
+    "bio": "Suzanne Kasler runs Suzanne Kasler Interiors and lines with Hickory Chair, Visual Comfort, and Ballard Designs. Her interiors mix French antiques with American softness — dove-grey walls, garden roses, antique mirrors — and have built a national retail following.",
+    "signature": "Dove-grey walls, French antiques, garden florals, antique mirrors.",
+    "preferred_dw": [
+      { "label": "Floral Wallcoverings", "url": "https://designerwallcoverings.com/collections/floral" },
+      { "label": "Toile de Jouy", "url": "https://designerwallcoverings.com/collections/toile" },
+      { "label": "Brunschwig & Fils", "url": "https://designerwallcoverings.com/collections/brunschwig-and-fils" }
+    ]
+  }
+]
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..e97663f
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,16 @@
+module.exports = {
+  apps: [
+    {
+      name: 'starsofdesign',
+      script: 'server.js',
+      cwd: __dirname,
+      env: {
+        NODE_ENV: 'production',
+        PORT: 9928,
+        PUBLIC_URL: 'https://starsofdesign.com',
+      },
+      max_memory_restart: '300M',
+      time: true,
+    },
+  ],
+};
diff --git a/lib/data.js b/lib/data.js
new file mode 100644
index 0000000..0698991
--- /dev/null
+++ b/lib/data.js
@@ -0,0 +1,55 @@
+// JSON-backed designer registry. Loaded once at boot, refreshed on each
+// request via fs.statSync mtime check (cheap), so editing data/designers.json
+// in place is picked up without a restart.
+
+const fs = require('fs');
+const path = require('path');
+
+const FILE = path.join(__dirname, '..', 'data', 'designers.json');
+
+let _cache = null;
+let _mtime = 0;
+
+function load() {
+  const stat = fs.statSync(FILE);
+  if (!_cache || stat.mtimeMs !== _mtime) {
+    const raw = JSON.parse(fs.readFileSync(FILE, 'utf8'));
+    _cache = raw.map((d, idx) => ({ id: idx + 1, ...d }));
+    _mtime = stat.mtimeMs;
+  }
+  return _cache;
+}
+
+function bySlug(slug) {
+  return load().find((d) => d.slug === slug) || null;
+}
+
+function allStyles() {
+  const set = new Set();
+  for (const d of load()) (d.styles || []).forEach((s) => set.add(s));
+  return Array.from(set).sort();
+}
+
+function allEras() {
+  const set = new Set();
+  for (const d of load()) if (d.era) set.add(d.era);
+  return Array.from(set).sort();
+}
+
+function filter(opts = {}) {
+  let rows = load();
+  if (opts.style) rows = rows.filter((d) => (d.styles || []).includes(opts.style));
+  if (opts.era)   rows = rows.filter((d) => d.era === opts.era);
+  if (opts.q) {
+    const q = String(opts.q).toLowerCase();
+    rows = rows.filter((d) =>
+      (d.name || '').toLowerCase().includes(q) ||
+      (d.city || '').toLowerCase().includes(q) ||
+      (d.bio || '').toLowerCase().includes(q) ||
+      (d.styles || []).some((s) => s.toLowerCase().includes(q))
+    );
+  }
+  return rows;
+}
+
+module.exports = { load, bySlug, filter, allStyles, allEras };
diff --git a/lib/sort.js b/lib/sort.js
new file mode 100644
index 0000000..80511a9
--- /dev/null
+++ b/lib/sort.js
@@ -0,0 +1,43 @@
+// Sort + density helpers for the designer grid.
+// Select <option> values must match SORTS keys exactly.
+
+const SORTS = {
+  newest:    'newest',
+  era:       'era',
+  city:      'city',
+  name_az:   'name_az',
+  style:     'style',
+};
+
+function sortDesigners(designers, key) {
+  const k = SORTS[key] ? key : 'newest';
+  const out = designers.slice();
+  switch (k) {
+    case 'name_az':
+      out.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
+      break;
+    case 'city':
+      out.sort((a, b) =>
+        (a.city || '').localeCompare(b.city || '') ||
+        (a.name || '').localeCompare(b.name || '')
+      );
+      break;
+    case 'era':
+      // Mid-century → contemporary → current. Sort by era_sort numeric.
+      out.sort((a, b) => (a.era_sort || 9999) - (b.era_sort || 9999));
+      break;
+    case 'style':
+      out.sort((a, b) => {
+        const av = (a.styles && a.styles[0]) || 'zzz';
+        const bv = (b.styles && b.styles[0]) || 'zzz';
+        return av.localeCompare(bv) || (a.name || '').localeCompare(b.name || '');
+      });
+      break;
+    case 'newest':
+    default:
+      out.sort((a, b) => (b.id || 0) - (a.id || 0));
+  }
+  return out;
+}
+
+module.exports = { SORTS, sortDesigners };
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c645ca5
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,995 @@
+{
+  "name": "starsofdesign",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "starsofdesign",
+      "version": "0.1.0",
+      "dependencies": {
+        "dotenv": "^16.4.5",
+        "ejs": "^3.1.10",
+        "express": "^4.21.0",
+        "morgan": "^1.10.0"
+      },
+      "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/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/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/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/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/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "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/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/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/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-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/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/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/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.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/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/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.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/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/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/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/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/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/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/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/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/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..f8d5313
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "starsofdesign",
+  "version": "0.1.0",
+  "private": true,
+  "description": "Stars of Design — profile directory of notable interior designers and the wallcoverings that match their signature style.",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js",
+    "dev": "node server.js"
+  },
+  "dependencies": {
+    "dotenv": "^16.4.5",
+    "ejs": "^3.1.10",
+    "express": "^4.21.0",
+    "morgan": "^1.10.0"
+  },
+  "engines": {
+    "node": ">=20"
+  }
+}
diff --git a/public/css/site.css b/public/css/site.css
new file mode 100644
index 0000000..608229a
--- /dev/null
+++ b/public/css/site.css
@@ -0,0 +1,222 @@
+* { box-sizing: border-box; }
+
+html, body { margin: 0; padding: 0; }
+body {
+  background: var(--bg);
+  color: var(--text);
+  font-family: var(--font-sans);
+  font-size: 16px;
+  line-height: 1.55;
+  -webkit-font-smoothing: antialiased;
+}
+
+a { color: var(--link); text-decoration: none; }
+a:hover { color: var(--link-hover); text-decoration: underline; }
+
+h1, h2, h3 { font-family: var(--font-serif); font-weight: 500; letter-spacing: -0.01em; }
+h1 { font-size: clamp(2.2rem, 4.5vw, 3.4rem); margin: 0 0 .5rem; line-height: 1.1; }
+h2 { font-size: 1.65rem; margin: 1.75rem 0 .75rem; }
+h3 { font-size: 1.2rem; margin: 0 0 .25rem; }
+
+.muted { color: var(--text-muted); }
+.small { font-size: .85rem; }
+
+.wrap { max-width: var(--max-w); margin: 0 auto; padding: 0 1.5rem; }
+.wrap.narrow { max-width: 760px; }
+.section { padding: 3.5rem 0; }
+.section.alt { background: var(--bg-alt); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
+
+/* Header */
+.site-header {
+  background: var(--surface);
+  border-bottom: 1px solid var(--border);
+  position: sticky; top: 0; z-index: 30;
+  backdrop-filter: saturate(140%) blur(6px);
+}
+.site-header .wrap {
+  display: flex; align-items: center; gap: 1.5rem;
+  padding-top: .9rem; padding-bottom: .9rem;
+}
+.brand { display: inline-flex; align-items: center; gap: .65rem; color: var(--text); }
+.brand:hover { text-decoration: none; }
+.brand-mark {
+  font-family: var(--font-serif); font-weight: 600;
+  background: var(--gold); color: var(--accent-fg);
+  width: 34px; height: 34px; border-radius: 50%;
+  display: inline-flex; align-items: center; justify-content: center;
+  font-size: 1.1rem; letter-spacing: 0;
+}
+.brand-name { font-family: var(--font-serif); font-size: 1.2rem; letter-spacing: 0.01em; }
+.nav { margin-left: auto; display: flex; gap: 1.25rem; }
+.nav a { color: var(--text); }
+.theme-toggle {
+  background: transparent; border: 1px solid var(--border);
+  border-radius: 999px; padding: .35rem .65rem;
+  color: var(--text); cursor: pointer; font-size: 1rem;
+  display: inline-flex; gap: .25rem;
+}
+html[data-theme='light'] .theme-toggle-moon { display: none; }
+html[data-theme='dark']  .theme-toggle-sun  { display: none; }
+
+/* Hero */
+.hero {
+  padding: 5.5rem 0 3.5rem;
+  border-bottom: 1px solid var(--border);
+  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-alt) 100%);
+}
+.hero .kicker {
+  text-transform: uppercase; letter-spacing: .14em;
+  font-size: .8rem; color: var(--gold); margin: 0 0 .75rem; font-weight: 500;
+}
+.hero h1 { max-width: 22ch; }
+.hero .lede { max-width: 64ch; font-size: 1.13rem; color: var(--text-muted); margin-top: 1.25rem; }
+.hero-cta { margin-top: 2rem; display: flex; gap: .75rem; flex-wrap: wrap; }
+
+.btn {
+  display: inline-flex; align-items: center; justify-content: center;
+  padding: .75rem 1.25rem; border-radius: var(--radius);
+  font-family: var(--font-sans); font-size: .95rem; font-weight: 500;
+  cursor: pointer; border: 1px solid transparent;
+  text-decoration: none;
+}
+.btn.primary { background: var(--accent); color: var(--accent-fg); }
+.btn.primary:hover { background: var(--gold); color: var(--accent-fg); text-decoration: none; }
+.btn.ghost { background: transparent; color: var(--text); border-color: var(--border); }
+.btn.ghost:hover { background: var(--surface); text-decoration: none; }
+
+/* Section heads */
+.section-head {
+  display: flex; align-items: baseline; justify-content: space-between;
+  gap: 1rem; margin-bottom: 1.25rem;
+}
+
+/* Two-col layout */
+.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem; }
+@media (max-width: 720px) { .two-col { grid-template-columns: 1fr; } }
+
+/* Filter form */
+.filter-form {
+  display: flex; gap: .75rem; flex-wrap: wrap;
+  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+  padding: 1rem; margin: 1.25rem 0;
+}
+.filter-form .control { display: flex; flex-direction: column; gap: .25rem; min-width: 150px; }
+.filter-form input, .filter-form select {
+  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
+  color: var(--text); padding: .5rem .65rem; font: inherit;
+}
+.filter-form .btn { align-self: flex-end; }
+
+/* Grid controls */
+.grid-controls {
+  display: flex; align-items: center; justify-content: space-between;
+  gap: 1rem; margin: 1.25rem 0; flex-wrap: wrap;
+}
+.grid-controls-right { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
+.grid-controls .control { display: inline-flex; align-items: center; gap: .5rem; font-size: .9rem; color: var(--text-muted); }
+.grid-controls .control select,
+.grid-controls .control input[type=range] {
+  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+  color: var(--text); padding: .35rem .5rem; font: inherit;
+}
+.grid-controls input[type=range] { padding: 0; width: 160px; }
+.grid-count { font-weight: 600; font-size: 1.05rem; color: var(--text); }
+
+/* Designer grid */
+.designer-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(var(--cols-min), 1fr));
+  gap: var(--gap);
+}
+.designer-card {
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: var(--radius);
+  padding: 1.25rem;
+  box-shadow: var(--shadow);
+  display: flex; flex-direction: column; gap: .55rem;
+  transition: transform .15s ease, box-shadow .15s ease;
+}
+.designer-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(40,30,15,0.08), 0 12px 28px rgba(40,30,15,0.08); }
+.designer-card header { display: flex; gap: .9rem; align-items: flex-start; }
+.designer-card h3 { font-family: var(--font-serif); font-size: 1.25rem; }
+.designer-card .headline { font-style: italic; color: var(--text-muted); margin: .25rem 0; font-size: .95rem; }
+.designer-card .tags { display: flex; flex-wrap: wrap; gap: .35rem; padding: 0; margin: .25rem 0 0; list-style: none; }
+.designer-card .tags li {
+  font-size: .72rem; padding: .15rem .55rem; border-radius: 999px;
+  background: var(--bg-alt); color: var(--text-muted); border: 1px solid var(--border);
+}
+
+.card-mark, .detail-mark {
+  font-family: var(--font-serif); font-weight: 600;
+  background: var(--gold); color: var(--accent-fg);
+  width: 44px; height: 44px; border-radius: 50%;
+  display: inline-flex; align-items: center; justify-content: center;
+  font-size: 1rem; letter-spacing: 0; flex-shrink: 0;
+}
+.detail-mark {
+  width: 64px; height: 64px; font-size: 1.3rem;
+}
+.detail-header {
+  display: flex; gap: 1.25rem; align-items: flex-start;
+  margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border);
+}
+.detail-header h1 { margin: 0 0 .25rem; }
+.detail-header .lede { font-size: 1.15rem; color: var(--text-muted); margin: 0 0 .25rem; font-style: italic; }
+
+.empty { grid-column: 1 / -1; color: var(--text-muted); padding: 2rem 0; text-align: center; }
+
+/* Designer detail */
+.designer-detail .pills { display: flex; flex-wrap: wrap; gap: .4rem; padding: 0; list-style: none; }
+.designer-detail .pills li {
+  background: var(--bg-alt); color: var(--text); border: 1px solid var(--border);
+  padding: .35rem .8rem; border-radius: 999px; font-size: .9rem;
+}
+.detail-disclaim { margin-top: 2.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border); }
+
+/* Pairing grid (DW collections) */
+.pairing-grid {
+  display: grid; gap: .75rem;
+  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+  margin: 1rem 0 1.5rem;
+}
+.pairing-card {
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 1rem 1.15rem;
+  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+  color: var(--text); transition: border-color .15s ease, transform .15s ease;
+}
+.pairing-card:hover {
+  text-decoration: none; border-color: var(--gold); transform: translateY(-2px);
+  color: var(--text);
+}
+.pairing-label { font-weight: 500; }
+.pairing-arrow { color: var(--gold); font-size: 1.1rem; margin-left: .75rem; }
+
+/* Prose / About / Submit */
+.prose { padding-left: 1.25rem; }
+.prose li { margin: .35rem 0; }
+.claim-callout {
+  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+  padding: 1.25rem 1.5rem; margin: 1.5rem 0;
+}
+
+/* Footer */
+.site-footer {
+  background: var(--bg-alt);
+  border-top: 1px solid var(--border);
+  margin-top: 3rem;
+  padding: 2.75rem 0 1.25rem;
+  font-size: .92rem;
+}
+.site-footer .wrap {
+  display: grid; grid-template-columns: repeat(4, 1fr); gap: 2rem;
+}
+@media (max-width: 720px) { .site-footer .wrap { grid-template-columns: 1fr 1fr; } }
+.site-footer .footer-col strong { display: block; margin-bottom: .35rem; font-family: var(--font-serif); font-size: 1.05rem; }
+.site-footer address { font-style: normal; color: var(--text-muted); }
+.site-footer .legal {
+  margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border);
+  display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
+  font-size: .82rem; color: var(--text-muted);
+}
diff --git a/public/css/theme.css b/public/css/theme.css
new file mode 100644
index 0000000..ac5af04
--- /dev/null
+++ b/public/css/theme.css
@@ -0,0 +1,41 @@
+/* Theme tokens — light + dark, switched via [data-theme] on <html>. */
+/* Stars of Design palette: ink-and-bronze, gallery-catalog feel. */
+
+:root {
+  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  --font-serif: 'Playfair Display', Georgia, 'Times New Roman', serif;
+  --max-w: 1180px;
+  --radius: 4px;
+  --gap: 1.25rem;
+  --cols-min: 320px;
+}
+
+html[data-theme='light'] {
+  --bg:        #f7f4ee;
+  --bg-alt:    #ece6d8;
+  --surface:   #ffffff;
+  --border:    #d8cfbb;
+  --text:      #1a1815;
+  --text-muted:#6b6354;
+  --link:      #7a4d12;
+  --link-hover:#a86b21;
+  --accent:    #1a1815;
+  --accent-fg: #f7f4ee;
+  --gold:      #b88a3d;
+  --shadow:    0 1px 2px rgba(40,30,15,0.06), 0 6px 18px rgba(40,30,15,0.05);
+}
+
+html[data-theme='dark'] {
+  --bg:        #0b0a08;
+  --bg-alt:    #15130e;
+  --surface:   #1c1914;
+  --border:    #2d281f;
+  --text:      #f0e9d6;
+  --text-muted:#a59c87;
+  --link:      #d9a868;
+  --link-hover:#f0c890;
+  --accent:    #d9a868;
+  --accent-fg: #0b0a08;
+  --gold:      #d9a868;
+  --shadow:    0 1px 2px rgba(0,0,0,0.4), 0 6px 18px rgba(0,0,0,0.4);
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..6dcecee
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+  <rect width="64" height="64" rx="12" fill="#1a1815"/>
+  <path d="M32 12 L37 26 L52 26 L40 35 L44 50 L32 41 L20 50 L24 35 L12 26 L27 26 Z"
+        fill="#d9a868" stroke="#d9a868" stroke-width="1.5" stroke-linejoin="round"/>
+</svg>
diff --git a/public/js/grid-controls.js b/public/js/grid-controls.js
new file mode 100644
index 0000000..fc8de1a
--- /dev/null
+++ b/public/js/grid-controls.js
@@ -0,0 +1,31 @@
+// Density slider + sort select. Sort hits the server via querystring (matches lib/sort.js).
+// Density adjusts CSS var --cols-min on the grid; both values persist in localStorage.
+(function () {
+  var KEY_SORT = 'sod-sort';
+  var KEY_DENS = 'sod-density';
+
+  var grid = document.querySelector('[data-designer-grid]');
+  var sortEl = document.querySelector('[data-sort]');
+  var densEl = document.querySelector('[data-density]');
+
+  if (densEl && grid) {
+    try {
+      var saved = localStorage.getItem(KEY_DENS);
+      if (saved) { densEl.value = saved; }
+    } catch (e) {}
+    grid.style.setProperty('--cols-min', densEl.value + 'px');
+    densEl.addEventListener('input', function () {
+      grid.style.setProperty('--cols-min', densEl.value + 'px');
+      try { localStorage.setItem(KEY_DENS, densEl.value); } catch (e) {}
+    });
+  }
+
+  if (sortEl) {
+    sortEl.addEventListener('change', function () {
+      try { localStorage.setItem(KEY_SORT, sortEl.value); } catch (e) {}
+      var url = new URL(window.location.href);
+      url.searchParams.set('sort', sortEl.value);
+      window.location.href = url.toString();
+    });
+  }
+})();
diff --git a/public/js/theme-toggle.js b/public/js/theme-toggle.js
new file mode 100644
index 0000000..3c1e642
--- /dev/null
+++ b/public/js/theme-toggle.js
@@ -0,0 +1,11 @@
+(function () {
+  var KEY = 'sod-theme';
+  var btn = document.getElementById('theme-toggle');
+  if (!btn) return;
+  btn.addEventListener('click', function () {
+    var cur = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
+    var next = cur === 'dark' ? 'light' : 'dark';
+    document.documentElement.setAttribute('data-theme', next);
+    try { localStorage.setItem(KEY, next); } catch (e) {}
+  });
+})();
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..a545e38
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Allow: /
+Sitemap: https://starsofdesign.com/sitemap.xml
diff --git a/routes/public.js b/routes/public.js
new file mode 100644
index 0000000..ee08542
--- /dev/null
+++ b/routes/public.js
@@ -0,0 +1,75 @@
+const express = require('express');
+const router = express.Router();
+
+const data = require('../lib/data');
+const { sortDesigners, SORTS } = require('../lib/sort');
+
+const SITE_TITLE = 'Stars of Design';
+const META_DESC = 'Stars of Design — a curated profile directory of the world\'s most influential interior designers, decade by decade, with the wallcoverings that match each designer\'s signature style.';
+
+router.use((req, res, next) => {
+  res.locals.site_title = SITE_TITLE;
+  res.locals.meta_desc = META_DESC;
+  res.locals.canonical = `https://starsofdesign.com${req.path}`;
+  res.locals.path = req.path;
+  next();
+});
+
+router.get('/', (req, res, next) => {
+  try {
+    const all = data.load();
+    const sortKey = SORTS[req.query.sort] ? req.query.sort : 'newest';
+    res.render('public/home', {
+      title: 'Stars of Design — A Profile Directory of the Designers Who Shape Interiors',
+      designers: sortDesigners(all, sortKey),
+      total: all.length,
+      sortKey,
+      filter: {},
+    });
+  } catch (e) { next(e); }
+});
+
+router.get('/designers', (req, res, next) => {
+  try {
+    const filt = {
+      style: req.query.style,
+      era:   req.query.era,
+      q:     req.query.q,
+    };
+    const rows = data.filter(filt);
+    const sortKey = SORTS[req.query.sort] ? req.query.sort : 'newest';
+    res.render('public/designers', {
+      title: 'Browse Designers — Stars of Design',
+      designers: sortDesigners(rows, sortKey),
+      total: rows.length,
+      sortKey,
+      filter: filt,
+      styles: data.allStyles(),
+      eras: data.allEras(),
+    });
+  } catch (e) { next(e); }
+});
+
+router.get('/designers/:slug', (req, res, next) => {
+  try {
+    const d = data.bySlug(req.params.slug);
+    if (!d) return res.status(404).render('public/404', { title: 'Not found — Stars of Design' });
+    res.render('public/designer', {
+      title: `${d.name} — Profile, Signature Style & Wallcovering Pairings | Stars of Design`,
+      meta_desc_override: `${d.name} — ${d.headline || (d.styles || []).join(', ')} interior designer based in ${d.city || 'unknown'}. Profile, era, signature works, and wallcoverings that pair with their style.`,
+      designer: d,
+    });
+  } catch (e) { next(e); }
+});
+
+router.get('/about', (req, res) => {
+  res.render('public/about', { title: 'About — Stars of Design' });
+});
+
+router.get('/submit', (req, res) => {
+  res.render('public/submit', { title: 'Submit a designer — Stars of Design' });
+});
+
+router.get('/healthz', (req, res) => res.type('text').send('ok'));
+
+module.exports = router;
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..5b7f73b
--- /dev/null
+++ b/server.js
@@ -0,0 +1,53 @@
+require('dotenv').config();
+
+const express = require('express');
+const path = require('path');
+const morgan = require('morgan');
+
+const publicRoutes = require('./routes/public');
+
+const app = express();
+const PORT = parseInt(process.env.PORT || '9928', 10);
+const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`;
+const IS_PROD = process.env.NODE_ENV === 'production';
+
+app.set('trust proxy', 1);
+app.set('view engine', 'ejs');
+app.set('views', path.join(__dirname, 'views'));
+app.disable('x-powered-by');
+
+app.use(morgan(IS_PROD ? 'combined' : 'dev'));
+app.use(express.json({ limit: '256kb' }));
+app.use(express.urlencoded({ extended: true, limit: '256kb' }));
+
+app.use(express.static(path.join(__dirname, 'public'), {
+  maxAge: '1h',
+  setHeaders(res, p) {
+    if (p.endsWith('.html')) res.setHeader('Cache-Control', 'no-store, must-revalidate');
+  },
+}));
+
+app.use((req, res, next) => {
+  res.locals.GA_ID = process.env.GA_ID || 'G-YCT0F2Q692';
+  res.locals.PUBLIC_URL = PUBLIC_URL;
+  next();
+});
+
+app.use('/', publicRoutes);
+
+app.use((req, res) => {
+  res.status(404).render('public/404', { title: 'Not found — Stars of Design' });
+});
+
+// eslint-disable-next-line no-unused-vars
+app.use((err, req, res, next) => {
+  console.error('[error]', err && err.stack ? err.stack : err);
+  res.status(500).render('public/error', {
+    title: 'Server error — Stars of Design',
+    message: IS_PROD ? 'Something went wrong.' : (err && err.message) || 'Unknown error',
+  });
+});
+
+app.listen(PORT, () => {
+  console.log(`starsofdesign listening on :${PORT} (${IS_PROD ? 'prod' : 'dev'})`);
+});
diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs
new file mode 100644
index 0000000..4dd112b
--- /dev/null
+++ b/views/partials/footer.ejs
@@ -0,0 +1,41 @@
+<footer class="site-footer">
+  <div class="wrap">
+    <div class="footer-col">
+      <strong>Stars of Design</strong>
+      <p class="muted">
+        A curated profile directory of the world's most influential interior
+        designers — and the wallcoverings that match each designer's signature
+        style.
+      </p>
+    </div>
+    <div class="footer-col">
+      <strong>Contact</strong>
+      <p>
+        <a href="mailto:info@starsofdesign.com">info@starsofdesign.com</a><br>
+        <a href="tel:+18883734564">888-373-4564</a>
+      </p>
+    </div>
+    <div class="footer-col">
+      <strong>Mailing</strong>
+      <address>
+        Designer Wallcoverings<br>
+        15442 Ventura Bl #102<br>
+        Sherman Oaks, CA 91403
+      </address>
+    </div>
+    <div class="footer-col">
+      <strong>Browse</strong>
+      <p>
+        <a href="/designers">All designers</a><br>
+        <a href="/submit">Submit a designer</a><br>
+        <a href="/about">About this directory</a>
+      </p>
+    </div>
+  </div>
+  <div class="wrap legal">
+    <span>&copy; <%= new Date().getFullYear() %> Stars of Design</span>
+    <span class="muted">Editorial profiles for reference only. Stars of Design is independent of the designers and brands named on these pages.</span>
+  </div>
+</footer>
+</body>
+</html>
diff --git a/views/partials/grid-controls.ejs b/views/partials/grid-controls.ejs
new file mode 100644
index 0000000..cf45538
--- /dev/null
+++ b/views/partials/grid-controls.ejs
@@ -0,0 +1,21 @@
+<div class="grid-controls" data-grid-controls>
+  <div class="grid-controls-left">
+    <span class="grid-count" data-grid-count><%= total %></span> <span class="muted">designers</span>
+  </div>
+  <div class="grid-controls-right">
+    <label class="control">
+      <span>Sort</span>
+      <select id="sort-select" data-sort>
+        <option value="newest"  <%= sortKey === 'newest'  ? 'selected' : '' %>>Newest added</option>
+        <option value="era"     <%= sortKey === 'era'     ? 'selected' : '' %>>By era</option>
+        <option value="city"    <%= sortKey === 'city'    ? 'selected' : '' %>>By city</option>
+        <option value="name_az" <%= sortKey === 'name_az' ? 'selected' : '' %>>Name A→Z</option>
+        <option value="style"   <%= sortKey === 'style'   ? 'selected' : '' %>>By style</option>
+      </select>
+    </label>
+    <label class="control">
+      <span>Density</span>
+      <input id="density-slider" type="range" min="220" max="440" step="20" value="320" data-density>
+    </label>
+  </div>
+</div>
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
new file mode 100644
index 0000000..f7630c5
--- /dev/null
+++ b/views/partials/head.ejs
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title><%= typeof title !== 'undefined' ? title : site_title %></title>
+  <meta name="description" content="<%= typeof meta_desc_override !== 'undefined' ? meta_desc_override : meta_desc %>">
+  <link rel="canonical" href="<%= canonical %>">
+  <meta property="og:type" content="website">
+  <meta property="og:url" content="<%= canonical %>">
+  <meta property="og:site_name" content="<%= site_title %>">
+  <meta property="og:title" content="<%= typeof title !== 'undefined' ? title : site_title %>">
+  <meta property="og:description" content="<%= typeof meta_desc_override !== 'undefined' ? meta_desc_override : meta_desc %>">
+  <meta name="twitter:card" content="summary_large_image">
+  <meta name="theme-color" content="#0e0e0e">
+  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
+  <link rel="preconnect" href="https://fonts.googleapis.com">
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+  <link rel="stylesheet" href="/css/theme.css">
+  <link rel="stylesheet" href="/css/site.css">
+  <!-- GA4 gtag (auto) -->
+  <script async src="https://www.googletagmanager.com/gtag/js?id=<%= GA_ID %>"></script>
+  <script>
+    window.dataLayer = window.dataLayer || [];
+    function gtag(){dataLayer.push(arguments);}
+    gtag('js', new Date());
+    gtag('config', '<%= GA_ID %>');
+  </script>
+  <!-- /GA4 gtag -->
+  <script>
+    (function () {
+      try {
+        var t = localStorage.getItem('sod-theme');
+        if (!t) t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+        document.documentElement.setAttribute('data-theme', t);
+      } catch (e) {}
+    })();
+  </script>
+  <script src="/js/theme-toggle.js" defer></script>
+</head>
+<body>
diff --git a/views/partials/header.ejs b/views/partials/header.ejs
new file mode 100644
index 0000000..05a78dd
--- /dev/null
+++ b/views/partials/header.ejs
@@ -0,0 +1,17 @@
+<header class="site-header">
+  <div class="wrap">
+    <a class="brand" href="/">
+      <span class="brand-mark">★</span>
+      <span class="brand-name">Stars of Design</span>
+    </a>
+    <nav class="nav">
+      <a href="/designers">Designers</a>
+      <a href="/about">About</a>
+      <a href="/submit">Submit</a>
+    </nav>
+    <button id="theme-toggle" class="theme-toggle" type="button" aria-label="Toggle dark / light theme">
+      <span class="theme-toggle-sun">☀</span>
+      <span class="theme-toggle-moon">☾</span>
+    </button>
+  </div>
+</header>
diff --git a/views/public/404.ejs b/views/public/404.ejs
new file mode 100644
index 0000000..aa99f60
--- /dev/null
+++ b/views/public/404.ejs
@@ -0,0 +1,11 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+<main class="error-page">
+  <section class="section">
+    <div class="wrap narrow">
+      <h1>Not found</h1>
+      <p>The page you tried to reach is not in the directory. Try the <a href="/designers">full designer index</a> or <a href="/">the homepage</a>.</p>
+    </div>
+  </section>
+</main>
+<%- include('../partials/footer') %>
diff --git a/views/public/about.ejs b/views/public/about.ejs
new file mode 100644
index 0000000..e284832
--- /dev/null
+++ b/views/public/about.ejs
@@ -0,0 +1,62 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+
+<main class="about-page">
+  <section class="section">
+    <div class="wrap narrow">
+      <h1>About Stars of Design</h1>
+      <p class="lede">
+        Stars of Design indexes the working interior designers — historic and
+        contemporary — whose published interiors have shaped how rooms get
+        decorated. Every profile is paired with the wallcoverings that fit
+        the designer's signature style.
+      </p>
+
+      <h2>What you'll find here</h2>
+      <ul class="prose">
+        <li><strong>Profiles.</strong> One page per designer, with era, city, signature visual language, and a one-paragraph profile of why their work matters.</li>
+        <li><strong>Pairings.</strong> Three to five Designer Wallcoverings collections per designer, curated to match the look — not a generic catalog dump.</li>
+        <li><strong>Filters.</strong> Sort and filter by era, style, or city. The grid honors a density slider and a sort menu, both persisted across visits.</li>
+      </ul>
+
+      <h2>How profiles are written</h2>
+      <p>
+        Profiles are sourced from publicly available material: published
+        interviews, shelter-magazine features, monographs, and the designer's
+        own studio site. We do not copy designer images or reproduce
+        copyrighted material. If a designer wants their profile updated,
+        replaced, or removed, the <a href="/submit">submission form</a> is the
+        path.
+      </p>
+
+      <h2>How wallcovering pairings are chosen</h2>
+      <p>
+        For each designer, a Designer Wallcoverings editor reviews the
+        designer's published interiors and selects the three collections that
+        best match their visual language — Buatta gets chintz, Vervoordt gets
+        plaster, Hicks gets hexagonal geometric. The link sends you to the
+        relevant collection page on
+        <a href="https://designerwallcoverings.com" rel="noopener">designerwallcoverings.com</a>
+        for sampling and quotes.
+      </p>
+
+      <h2>Who runs this</h2>
+      <p>
+        Stars of Design is an editorial sister property of
+        <a href="https://designerwallcoverings.com" rel="noopener">Designer Wallcoverings</a>,
+        a Sherman Oaks, California–based wallcovering and textile dealer. We
+        do not represent a single line; we map the designers who shape
+        interiors across every line.
+      </p>
+
+      <h2>What this directory is not</h2>
+      <ul class="prose">
+        <li>Not a portfolio site — we do not host designer images.</li>
+        <li>Not a hire-a-designer marketplace — we don't gate intros or charge for placement.</li>
+        <li>Not a paid-ad surface — there is no sponsored placement above organic results.</li>
+      </ul>
+    </div>
+  </section>
+</main>
+
+<%- include('../partials/footer') %>
diff --git a/views/public/designer.ejs b/views/public/designer.ejs
new file mode 100644
index 0000000..be64287
--- /dev/null
+++ b/views/public/designer.ejs
@@ -0,0 +1,62 @@
+<%- include('../partials/head', { title, meta_desc_override: typeof meta_desc_override !== 'undefined' ? meta_desc_override : undefined }) %>
+<%- include('../partials/header') %>
+
+<main class="designer-detail">
+  <section class="section">
+    <div class="wrap narrow">
+      <p class="muted"><a href="/designers">← Back to all designers</a></p>
+
+      <header class="detail-header">
+        <span class="detail-mark" aria-hidden="true"><%= (designer.name || '?').split(' ').slice(0,2).map(function (w) { return w[0]; }).join('') %></span>
+        <div>
+          <h1><%= designer.name %></h1>
+          <% if (designer.headline) { %><p class="lede"><%= designer.headline %></p><% } %>
+          <p class="muted">
+            <% if (designer.era) { %><%= designer.era %><% } %>
+            <% if (designer.active_years) { %> · <%= designer.active_years %><% } %>
+            <% if (designer.city) { %> · <%= designer.city %><% } %>
+          </p>
+        </div>
+      </header>
+
+      <% if (designer.styles && designer.styles.length) { %>
+        <h2>Signature styles</h2>
+        <ul class="pills">
+          <% designer.styles.forEach(function (s) { %><li><%= s %></li><% }) %>
+        </ul>
+      <% } %>
+
+      <% if (designer.bio) { %>
+        <h2>Profile</h2>
+        <p><%= designer.bio %></p>
+      <% } %>
+
+      <% if (designer.signature) { %>
+        <h2>Visual language</h2>
+        <p><%= designer.signature %></p>
+      <% } %>
+
+      <% if (designer.preferred_dw && designer.preferred_dw.length) { %>
+        <h2>Wallcoverings that match this designer</h2>
+        <p class="muted small">Curated by Designer Wallcoverings — collections that map to <%= designer.name.split(' ')[0] %>'s published interiors.</p>
+        <div class="pairing-grid">
+          <% designer.preferred_dw.forEach(function (p) { %>
+            <a class="pairing-card" href="<%= p.url %>" rel="noopener" target="_blank">
+              <span class="pairing-label"><%= p.label %></span>
+              <span class="pairing-arrow" aria-hidden="true">→</span>
+            </a>
+          <% }); %>
+        </div>
+      <% } %>
+
+      <p class="muted small detail-disclaim">
+        Stars of Design is an editorial profile directory. We are not affiliated with <%= designer.name %> or
+        their studio. Wallcovering pairings are curated based on publicly available work and are sold by
+        <a href="https://designerwallcoverings.com" rel="noopener">Designer Wallcoverings</a>, the directory's
+        publisher.
+      </p>
+    </div>
+  </section>
+</main>
+
+<%- include('../partials/footer') %>
diff --git a/views/public/designers.ejs b/views/public/designers.ejs
new file mode 100644
index 0000000..d3e2866
--- /dev/null
+++ b/views/public/designers.ejs
@@ -0,0 +1,68 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+
+<main class="designers-page">
+  <section class="section">
+    <div class="wrap">
+      <h1>Browse Designers</h1>
+      <p class="muted">Filter by style, era, or search by name, city, or signature.</p>
+
+      <form class="filter-form" method="get" action="/designers">
+        <label class="control">
+          <span>Search</span>
+          <input type="search" name="q" placeholder="name, city, or style" value="<%= filter.q || '' %>">
+        </label>
+        <label class="control">
+          <span>Style</span>
+          <select name="style">
+            <option value="">All</option>
+            <% styles.forEach(function (s) { %>
+              <option value="<%= s %>" <%= filter.style === s ? 'selected' : '' %>><%= s %></option>
+            <% }) %>
+          </select>
+        </label>
+        <label class="control">
+          <span>Era</span>
+          <select name="era">
+            <option value="">All</option>
+            <% eras.forEach(function (e) { %>
+              <option value="<%= e %>" <%= filter.era === e ? 'selected' : '' %>><%= e %></option>
+            <% }) %>
+          </select>
+        </label>
+        <button class="btn primary" type="submit">Filter</button>
+        <a class="btn ghost" href="/designers">Reset</a>
+      </form>
+
+      <%- include('../partials/grid-controls', { total, sortKey }) %>
+
+      <div class="designer-grid" data-designer-grid>
+        <% designers.forEach(function (d) { %>
+          <article class="designer-card">
+            <header>
+              <span class="card-mark" aria-hidden="true"><%= (d.name || '?').split(' ').slice(0,2).map(function (w) { return w[0]; }).join('') %></span>
+              <div>
+                <h3><a href="/designers/<%= d.slug %>"><%= d.name %></a></h3>
+                <p class="muted"><%= d.era %><% if (d.city) { %> · <%= d.city %><% } %></p>
+              </div>
+            </header>
+            <% if (d.headline) { %>
+              <p class="headline"><%= d.headline %></p>
+            <% } %>
+            <% if (d.styles && d.styles.length) { %>
+              <ul class="tags">
+                <% d.styles.forEach(function (s) { %><li><%= s %></li><% }) %>
+              </ul>
+            <% } %>
+          </article>
+        <% }); %>
+        <% if (!designers.length) { %>
+          <p class="empty">No designers match those filters yet. Try resetting or <a href="/submit">submit one</a>.</p>
+        <% } %>
+      </div>
+    </div>
+  </section>
+</main>
+
+<%- include('../partials/footer') %>
+<script src="/js/grid-controls.js" defer></script>
diff --git a/views/public/error.ejs b/views/public/error.ejs
new file mode 100644
index 0000000..2e847ac
--- /dev/null
+++ b/views/public/error.ejs
@@ -0,0 +1,12 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+<main class="error-page">
+  <section class="section">
+    <div class="wrap narrow">
+      <h1>Server error</h1>
+      <p><%= typeof message !== 'undefined' ? message : 'Something went wrong.' %></p>
+      <p><a href="/">Return home</a></p>
+    </div>
+  </section>
+</main>
+<%- include('../partials/footer') %>
diff --git a/views/public/home.ejs b/views/public/home.ejs
new file mode 100644
index 0000000..4fed30b
--- /dev/null
+++ b/views/public/home.ejs
@@ -0,0 +1,76 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+
+<main class="home">
+  <section class="hero">
+    <div class="wrap">
+      <p class="kicker">Profile directory · designers, eras, signature styles</p>
+      <h1>The designers who shape interiors — and the walls behind them.</h1>
+      <p class="lede">
+        Stars of Design is a curated profile directory of the world's most
+        influential interior designers, from the mid-century pioneers who
+        invented the profession through the contemporary studios shaping
+        rooms today. Each profile names the wallcoverings that fit the
+        designer's signature style.
+      </p>
+      <div class="hero-cta">
+        <a class="btn primary" href="/designers">Browse all designers</a>
+        <a class="btn ghost"   href="/about">How this directory works</a>
+      </div>
+    </div>
+  </section>
+
+  <section class="section">
+    <div class="wrap">
+      <div class="section-head">
+        <h2>The directory</h2>
+        <a class="muted" href="/designers">View all designers →</a>
+      </div>
+
+      <%- include('../partials/grid-controls', { total, sortKey }) %>
+
+      <div class="designer-grid" data-designer-grid>
+        <% designers.slice(0, 24).forEach(function (d) { %>
+          <article class="designer-card">
+            <header>
+              <span class="card-mark" aria-hidden="true"><%= (d.name || '?').split(' ').slice(0,2).map(function (w) { return w[0]; }).join('') %></span>
+              <div>
+                <h3><a href="/designers/<%= d.slug %>"><%= d.name %></a></h3>
+                <p class="muted"><%= d.era %><% if (d.city) { %> · <%= d.city %><% } %></p>
+              </div>
+            </header>
+            <% if (d.headline) { %>
+              <p class="headline"><%= d.headline %></p>
+            <% } %>
+            <% if (d.styles && d.styles.length) { %>
+              <ul class="tags">
+                <% d.styles.forEach(function (s) { %><li><%= s %></li><% }) %>
+              </ul>
+            <% } %>
+          </article>
+        <% }); %>
+      </div>
+    </div>
+  </section>
+
+  <section class="section alt">
+    <div class="wrap two-col">
+      <div>
+        <h2>How to read these profiles</h2>
+        <ol class="prose">
+          <li><strong>Era and city.</strong> Every profile is anchored to a working decade and a working city — Dorothy Draper in 1940s New York is a different designer than Athena Calderone in 2020s Brooklyn.</li>
+          <li><strong>Signature.</strong> One sentence on the visual language a designer is known for. The thing you'd recognize across the room.</li>
+          <li><strong>Wallcovering pairings.</strong> Three Designer Wallcoverings collections that map to the designer's published interiors — chintz for Buatta, plaster for Vervoordt, hexagonal geometric for Hicks.</li>
+        </ol>
+      </div>
+      <div>
+        <h2>For designers and PR</h2>
+        <p>If you'd like to be added to the directory or update an existing profile, use the <a href="/submit">submission form</a>. Profiles are free, editorial, and never paid placement.</p>
+        <p class="muted">We do not sell contact data and we do not run ads above organic results.</p>
+      </div>
+    </div>
+  </section>
+</main>
+
+<%- include('../partials/footer') %>
+<script src="/js/grid-controls.js" defer></script>
diff --git a/views/public/submit.ejs b/views/public/submit.ejs
new file mode 100644
index 0000000..4cd7966
--- /dev/null
+++ b/views/public/submit.ejs
@@ -0,0 +1,46 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+
+<main class="submit-page">
+  <section class="section">
+    <div class="wrap narrow">
+      <h1>Submit a designer</h1>
+      <p class="lede">
+        Stars of Design is editorial — we accept submissions from designers,
+        their studios, and PR representatives, and from anyone who'd like to
+        nominate a designer we've missed.
+      </p>
+
+      <div class="claim-callout">
+        <h2>How to submit</h2>
+        <p>
+          The fastest path is email. Send the following to
+          <a href="mailto:info@starsofdesign.com">info@starsofdesign.com</a> and an editor
+          will respond within five business days:
+        </p>
+        <ul class="prose">
+          <li>Designer name and studio</li>
+          <li>City, era, and active years</li>
+          <li>One short paragraph (≤ 150 words) on the designer's signature style and best-known work</li>
+          <li>Up to five links to published interviews, shelter-magazine features, or the designer's own studio site</li>
+          <li>Optional: three to five wallcovering categories the designer is known for using (we'll match these against the Designer Wallcoverings catalog)</li>
+        </ul>
+      </div>
+
+      <h2>What we don't accept</h2>
+      <ul class="prose">
+        <li>Promotional copy. Profiles are editorial, not marketing.</li>
+        <li>Copyrighted images we don't have rights to host.</li>
+        <li>Paid-placement inquiries. There is no sponsored placement.</li>
+      </ul>
+
+      <h2>Questions</h2>
+      <p>
+        Email <a href="mailto:info@starsofdesign.com">info@starsofdesign.com</a> or call
+        <a href="tel:+18883734564">888-373-4564</a>.
+      </p>
+    </div>
+  </section>
+</main>
+
+<%- include('../partials/footer') %>

(oldest)  ·  back to Stars of Design  ·  add /sitemap.xml route + JSON-LD Person schema on designer d 70b7912 →