[object Object]

← back to Fashion Style Guides

initial scaffold — 20 fashion houses + pantone (coty/fashion/home) viewer + style guide agent

8f373f23280c0c08755e5e4cc097a33faaf44f20 · 2026-05-14 08:12:25 -0700 · SteveStudio2

- Express server on :9826 with /api/brands /api/pantone /api/common-colors /api/agent/status
- public/index.html — sort + density slider per Steve's standing rule
- corner-nav: logo UL, '≡ MENU' UR drawer
- agent/refresh.js — HEAD-checks each brand URL, stubs current-year COTY if missing
- launchd plist loaded — weekly Sunday 03:14 PT
- 20 houses seeded: Gucci, Prada, LV, Chanel, Hermès, Dior, Versace, Saint Laurent, Balenciaga, Bottega Veneta, Burberry, Fendi, Givenchy, Valentino, Loewe, Celine, Miu Miu, Tom Ford, Ralph Lauren, Tiffany & Co.
- Pantone COTY 2007-2026 (2026 TBA-stub)
- Pantone S/S 2026 Fashion Color Trend palette
- PANTONEVIEW Home + Interiors 2026 — 4 themed palettes

Files touched

Diff

commit 8f373f23280c0c08755e5e4cc097a33faaf44f20
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 14 08:12:25 2026 -0700

    initial scaffold — 20 fashion houses + pantone (coty/fashion/home) viewer + style guide agent
    
    - Express server on :9826 with /api/brands /api/pantone /api/common-colors /api/agent/status
    - public/index.html — sort + density slider per Steve's standing rule
    - corner-nav: logo UL, '≡ MENU' UR drawer
    - agent/refresh.js — HEAD-checks each brand URL, stubs current-year COTY if missing
    - launchd plist loaded — weekly Sunday 03:14 PT
    - 20 houses seeded: Gucci, Prada, LV, Chanel, Hermès, Dior, Versace, Saint Laurent, Balenciaga, Bottega Veneta, Burberry, Fendi, Givenchy, Valentino, Loewe, Celine, Miu Miu, Tom Ford, Ralph Lauren, Tiffany & Co.
    - Pantone COTY 2007-2026 (2026 TBA-stub)
    - Pantone S/S 2026 Fashion Color Trend palette
    - PANTONEVIEW Home + Interiors 2026 — 4 themed palettes
---
 .deploy.conf                                       |   3 +
 .gitignore                                         |   9 +
 agent/com.steve.fashion-style-guides-refresh.plist |  28 +
 agent/refresh.js                                   |  87 ++
 data/brands.json                                   | 922 +++++++++++++++++++++
 data/pantone.json                                  | 316 +++++++
 ecosystem.config.js                                |  10 +
 package-lock.json                                  | 827 ++++++++++++++++++
 package.json                                       |  15 +
 public/app.js                                      | 208 +++++
 public/index.html                                  |  93 +++
 public/styles.css                                  | 137 +++
 server.js                                          | 102 +++
 13 files changed, 2757 insertions(+)

diff --git a/.deploy.conf b/.deploy.conf
new file mode 100644
index 0000000..aedf143
--- /dev/null
+++ b/.deploy.conf
@@ -0,0 +1,3 @@
+PROJECT_NAME=fashion-style-guides
+DEPLOY_PATH=/root/Projects/fashion-style-guides
+HEALTH_URL=http://localhost:9826/healthz
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a027f95
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+data/cache/
diff --git a/agent/com.steve.fashion-style-guides-refresh.plist b/agent/com.steve.fashion-style-guides-refresh.plist
new file mode 100644
index 0000000..c4416ef
--- /dev/null
+++ b/agent/com.steve.fashion-style-guides-refresh.plist
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>Label</key>
+  <string>com.steve.fashion-style-guides-refresh</string>
+  <key>ProgramArguments</key>
+  <array>
+    <string>/usr/local/bin/node</string>
+    <string>/Users/stevestudio2/Projects/fashion-style-guides/agent/refresh.js</string>
+  </array>
+  <key>StartCalendarInterval</key>
+  <dict>
+    <key>Weekday</key>
+    <integer>0</integer>
+    <key>Hour</key>
+    <integer>3</integer>
+    <key>Minute</key>
+    <integer>14</integer>
+  </dict>
+  <key>StandardOutPath</key>
+  <string>/Users/stevestudio2/Projects/fashion-style-guides/agent/refresh.stdout.log</string>
+  <key>StandardErrorPath</key>
+  <string>/Users/stevestudio2/Projects/fashion-style-guides/agent/refresh.stderr.log</string>
+  <key>RunAtLoad</key>
+  <false/>
+</dict>
+</plist>
diff --git a/agent/refresh.js b/agent/refresh.js
new file mode 100644
index 0000000..fa3b54d
--- /dev/null
+++ b/agent/refresh.js
@@ -0,0 +1,87 @@
+#!/usr/bin/env node
+// Style Guide Agent — re-checks each brand's public guidelines + Pantone announcements.
+// Strategy: (1) HEAD-check each brand URL is reachable, (2) note last-checked timestamp,
+// (3) check Pantone color-of-the-year for current year if blank, (4) write back to data/.
+// DRY_RUN=1 prints planned changes without writing.
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const ROOT = path.join(__dirname, '..');
+const DATA_DIR = path.join(ROOT, 'data');
+const LOG_FILE = path.join(__dirname, 'refresh.log');
+const DRY = !!process.env.DRY_RUN;
+
+const log = (msg) => {
+  const line = `[${new Date().toISOString()}] ${msg}`;
+  console.log(line);
+  fs.appendFileSync(LOG_FILE, line + '\n');
+};
+
+function loadJson(file) { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8')); }
+function saveJson(file, data) {
+  if (DRY) { log(`DRY: would write ${file}`); return; }
+  fs.writeFileSync(path.join(DATA_DIR, file), JSON.stringify(data, null, 2) + '\n');
+  log(`wrote ${file}`);
+}
+
+function headCheck(url) {
+  return new Promise((resolve) => {
+    try {
+      const u = new URL(url);
+      const req = https.request({
+        method: 'HEAD',
+        hostname: u.hostname,
+        path: u.pathname || '/',
+        timeout: 8000,
+        headers: { 'user-agent': 'FashionStyleGuidesAgent/0.1 (+steve@designerwallcoverings.com)' },
+      }, (res) => resolve({ ok: res.statusCode >= 200 && res.statusCode < 500, status: res.statusCode }));
+      req.on('timeout', () => { req.destroy(); resolve({ ok: false, status: 'timeout' }); });
+      req.on('error', (e) => resolve({ ok: false, status: e.code || 'error' }));
+      req.end();
+    } catch (e) { resolve({ ok: false, status: 'invalid-url' }); }
+  });
+}
+
+async function refreshBrands() {
+  const data = loadJson('brands.json');
+  log(`brand-refresh: checking ${data.brands.length} URLs`);
+  let okCount = 0, failCount = 0;
+  for (const b of data.brands) {
+    if (!b.url) continue;
+    const res = await headCheck(b.url);
+    b.last_checked = new Date().toISOString();
+    b.last_status = res.status;
+    if (res.ok) okCount++; else failCount++;
+    log(`  ${b.id.padEnd(18)} ${res.ok ? 'OK' : 'FAIL'} (${res.status}) ${b.url}`);
+  }
+  data.last_updated = new Date().toISOString().slice(0, 10);
+  saveJson('brands.json', data);
+  log(`brand-refresh complete: ${okCount} OK, ${failCount} fail`);
+}
+
+async function refreshPantone() {
+  const data = loadJson('pantone.json');
+  const thisYear = new Date().getFullYear();
+  const entry = data.color_of_the_year.find(c => c.year === thisYear);
+  if (entry && !entry.hex) {
+    log(`pantone: ${thisYear} color of the year is still pending — agent has no live Pantone API; please update manually when announced.`);
+  } else if (entry) {
+    log(`pantone: ${thisYear} = ${entry.name} (${entry.pantone || 'n/a'})`);
+  } else {
+    log(`pantone: no entry for ${thisYear}; consider adding a TBA stub`);
+    data.color_of_the_year.unshift({ year: thisYear, name: 'TBA', pantone: null, hex: null, note: 'Auto-stub by Style Guide Agent — populate when Pantone publishes.' });
+    saveJson('pantone.json', data);
+    return;
+  }
+  data.last_updated = new Date().toISOString().slice(0, 10);
+  saveJson('pantone.json', data);
+}
+
+(async () => {
+  log(`=== refresh start ${DRY ? '(DRY RUN)' : ''} ===`);
+  try { await refreshBrands(); } catch (e) { log('brand-refresh ERROR: ' + e.message); }
+  try { await refreshPantone(); } catch (e) { log('pantone-refresh ERROR: ' + e.message); }
+  log('=== refresh end ===');
+})();
diff --git a/data/brands.json b/data/brands.json
new file mode 100644
index 0000000..8066f1d
--- /dev/null
+++ b/data/brands.json
@@ -0,0 +1,922 @@
+{
+  "schema_version": 1,
+  "last_updated": "2026-05-14",
+  "source_notes": "Hex values reflect each house's public brand-mark colors as published on their corporate site, packaging, and identity guidelines. Typography names are the public-facing custom or licensed faces; when a house uses a custom face derived from a classic, both are listed.",
+  "brands": [
+    {
+      "id": "gucci",
+      "name": "Gucci",
+      "founded": 1921,
+      "house_of": "Kering",
+      "headquarters": "Florence, IT",
+      "url": "https://www.gucci.com",
+      "tagline": "Eclectic, contemporary, romantic.",
+      "signature_motif": "Green-Red-Green webbing stripe; Horsebit; GG monogram; Flora print",
+      "palette": [
+        {
+          "name": "Gucci Emerald",
+          "hex": "#0e5c2f",
+          "role": "primary"
+        },
+        {
+          "name": "Heritage Red",
+          "hex": "#9b1b1f",
+          "role": "stripe"
+        },
+        {
+          "name": "Heritage Green",
+          "hex": "#1f7a3a",
+          "role": "stripe"
+        },
+        {
+          "name": "Antique Gold",
+          "hex": "#b58a2a",
+          "role": "accent"
+        },
+        {
+          "name": "Off-Black",
+          "hex": "#0d0d0d",
+          "role": "ink"
+        },
+        {
+          "name": "Ivory",
+          "hex": "#f2ece1",
+          "role": "ground"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom Granjon-derived serif",
+        "display": "Granjon LT Std",
+        "body": "Helvetica Neue / system sans",
+        "notes": "Wordmark is set in a tight-tracked Granjon-derived serif; recent display campaigns lean into bold, condensed sans pulls."
+      },
+      "hero_treatment": "Editorial maximalism; layered floral/animal motifs over saturated jewel tones.",
+      "ui_signature": "Fixed top-right '≡ MENU' hairline-bar nav; centered-bottom 'SHOP NOW' white pill.",
+      "last_checked": "2026-05-14T15:11:04.791Z",
+      "last_status": "timeout"
+    },
+    {
+      "id": "prada",
+      "name": "Prada",
+      "founded": 1913,
+      "house_of": "Prada S.p.A.",
+      "headquarters": "Milan, IT",
+      "url": "https://www.prada.com",
+      "tagline": "Intellectual elegance, subversive minimalism.",
+      "signature_motif": "Inverted triangle plaque; Saffiano leather texture",
+      "palette": [
+        {
+          "name": "Prada Green",
+          "hex": "#22443a",
+          "role": "primary"
+        },
+        {
+          "name": "Ink Black",
+          "hex": "#0a0a0a",
+          "role": "ink"
+        },
+        {
+          "name": "Pure White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Cipria Beige",
+          "hex": "#d7c8a8",
+          "role": "neutral"
+        },
+        {
+          "name": "Lipstick Red",
+          "hex": "#c8102e",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom Bodoni-style serif (Prada Roman)",
+        "display": "Bodoni 72 / GT Sectra",
+        "body": "Neue Haas Grotesk",
+        "notes": "House serif is a Didone with high-contrast strokes and unbracketed serifs. Editorial copy frequently set in Neue Haas Grotesk."
+      },
+      "hero_treatment": "Spare gallery framing; centered model on neutral ground; muted lighting.",
+      "ui_signature": "Minimal black-on-white nav, vertical wordmark in corner.",
+      "last_checked": "2026-05-14T15:11:12.900Z",
+      "last_status": "timeout"
+    },
+    {
+      "id": "louis-vuitton",
+      "name": "Louis Vuitton",
+      "founded": 1854,
+      "house_of": "LVMH",
+      "headquarters": "Paris, FR",
+      "url": "https://www.louisvuitton.com",
+      "tagline": "L'Art du Voyage — the art of travel.",
+      "signature_motif": "LV Monogram canvas; Damier checkerboard; Epi leather",
+      "palette": [
+        {
+          "name": "Monogram Brown",
+          "hex": "#4f3623",
+          "role": "primary"
+        },
+        {
+          "name": "Trunk Tan",
+          "hex": "#a78a5a",
+          "role": "secondary"
+        },
+        {
+          "name": "Antique Gold",
+          "hex": "#c9a449",
+          "role": "accent"
+        },
+        {
+          "name": "Damier Ebony",
+          "hex": "#3b2615",
+          "role": "neutral-dark"
+        },
+        {
+          "name": "Off-White",
+          "hex": "#f7f3ec",
+          "role": "ground"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom geometric sans (Futura-derived)",
+        "display": "Futura Bold / Vuitton Sans",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark is set in a custom Futura-derived geometric sans, all-caps, tight tracking. Editorial uses Futura Bold extensively."
+      },
+      "hero_treatment": "Travel narrative; monogram canvas hero; cinematic landscapes behind product.",
+      "ui_signature": "Centered all-caps wordmark, faint hairline nav, full-bleed hero video.",
+      "last_checked": "2026-05-14T15:11:21.010Z",
+      "last_status": "timeout"
+    },
+    {
+      "id": "chanel",
+      "name": "Chanel",
+      "founded": 1910,
+      "house_of": "Chanel Limited (private)",
+      "headquarters": "Paris, FR / London, UK",
+      "url": "https://www.chanel.com",
+      "tagline": "Timeless elegance.",
+      "signature_motif": "Interlocking CC monogram; quilted leather; camellia; pearl",
+      "palette": [
+        {
+          "name": "Chanel Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Pure White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Beige Clair",
+          "hex": "#e6d6b8",
+          "role": "neutral"
+        },
+        {
+          "name": "Camellia Red",
+          "hex": "#b00020",
+          "role": "accent"
+        },
+        {
+          "name": "Pearl",
+          "hex": "#f1ece1",
+          "role": "accent-soft"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom CHANEL sans — geometric, all-caps, ultra-wide tracking",
+        "display": "Didot LT Std",
+        "body": "Helvetica Neue",
+        "notes": "Wordmark CHANEL is set in a custom geometric sans with extreme letter-spacing. Editorial uses Didot for headlines."
+      },
+      "hero_treatment": "Black-and-white film stills; centered wordmark; high-contrast monochrome.",
+      "ui_signature": "Pure black + white only; no color in chrome; serif headlines.",
+      "last_checked": "2026-05-14T15:11:21.157Z",
+      "last_status": 403
+    },
+    {
+      "id": "hermes",
+      "name": "Hermès",
+      "founded": 1837,
+      "house_of": "Hermès International",
+      "headquarters": "Paris, FR",
+      "url": "https://www.hermes.com",
+      "tagline": "Saddle-maker since 1837.",
+      "signature_motif": "Orange box; duc-carriage logo; Kelly/Birkin silhouette",
+      "palette": [
+        {
+          "name": "Hermès Orange",
+          "hex": "#ff6900",
+          "role": "primary"
+        },
+        {
+          "name": "Saddle Brown",
+          "hex": "#6b4423",
+          "role": "secondary"
+        },
+        {
+          "name": "Chocolate",
+          "hex": "#3e2a1a",
+          "role": "neutral-dark"
+        },
+        {
+          "name": "Cream",
+          "hex": "#f6efe2",
+          "role": "ground"
+        },
+        {
+          "name": "Gold Leaf",
+          "hex": "#c8a44a",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom hand-drawn serif with carriage logo above",
+        "display": "Memphis / Egyptian slab serifs",
+        "body": "Joanna / Plantin",
+        "notes": "House mark uses a hand-drawn serif with a horse-drawn duc-carriage logotype above it. Print catalogs lean on Joanna or Plantin for body."
+      },
+      "hero_treatment": "Storytelling illustration; equestrian motifs; cream backgrounds with orange accents.",
+      "ui_signature": "Cream/orange chrome; illustrated content blocks; faint hairlines.",
+      "last_checked": "2026-05-14T15:11:21.441Z",
+      "last_status": 403
+    },
+    {
+      "id": "dior",
+      "name": "Dior",
+      "founded": 1946,
+      "house_of": "LVMH",
+      "headquarters": "Paris, FR",
+      "url": "https://www.dior.com",
+      "tagline": "J'adore — house of luxury elegance.",
+      "signature_motif": "CD logo; Cannage quilting; star; toile-de-Jouy",
+      "palette": [
+        {
+          "name": "Dior Gray",
+          "hex": "#8e8b86",
+          "role": "primary"
+        },
+        {
+          "name": "Ink Black",
+          "hex": "#0a0a0a",
+          "role": "ink"
+        },
+        {
+          "name": "Pure White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Powder Pink",
+          "hex": "#f3d7cf",
+          "role": "accent-soft"
+        },
+        {
+          "name": "Dior Red (Rouge 999)",
+          "hex": "#b3001b",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom Nicolas Cochin-derived serif (DIOR all-caps)",
+        "display": "Nicolas Cochin / Didot",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark uses a Nicolas Cochin-style display serif with characteristic tall ascenders. Display headlines often Didot."
+      },
+      "hero_treatment": "Couture editorial; soft gray grounds; bold red lip or floral accent.",
+      "ui_signature": "Centered wordmark, gray + white chrome, hairline serif menu.",
+      "last_checked": "2026-05-14T15:11:21.561Z",
+      "last_status": 403
+    },
+    {
+      "id": "versace",
+      "name": "Versace",
+      "founded": 1978,
+      "house_of": "Capri Holdings (acquired by Tapestry 2025)",
+      "headquarters": "Milan, IT",
+      "url": "https://www.versace.com",
+      "tagline": "Bold Italian glamour.",
+      "signature_motif": "Medusa head; Greek key (meandros) border; baroque print",
+      "palette": [
+        {
+          "name": "Versace Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Medusa Gold",
+          "hex": "#d4af37",
+          "role": "accent"
+        },
+        {
+          "name": "Greek Key White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Imperial Red",
+          "hex": "#b40000",
+          "role": "secondary"
+        },
+        {
+          "name": "Royal Purple",
+          "hex": "#4b1c5e",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom Roman caps with Greek-key flanks (VERSACE all-caps)",
+        "display": "Trajan Pro / Optima",
+        "body": "Helvetica Neue",
+        "notes": "Wordmark is set in an Optima/Trajan-style Roman capital sans, frequently paired with the Greek-key border motif."
+      },
+      "hero_treatment": "Baroque maximalism; gold + black; Medusa centerpieces.",
+      "ui_signature": "Gold + black chrome; Greek-key dividers; serif caps wordmark.",
+      "last_checked": "2026-05-14T15:11:21.675Z",
+      "last_status": 403
+    },
+    {
+      "id": "saint-laurent",
+      "name": "Saint Laurent",
+      "founded": 1961,
+      "house_of": "Kering",
+      "headquarters": "Paris, FR",
+      "url": "https://www.ysl.com",
+      "tagline": "Le Smoking — Parisian rive-gauche edge.",
+      "signature_motif": "YSL interlocking monogram (Cassandre, 1961); Le Smoking tuxedo",
+      "palette": [
+        {
+          "name": "Saint Laurent Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Ivory",
+          "hex": "#f5f1e8",
+          "role": "ground"
+        },
+        {
+          "name": "Dust Gray",
+          "hex": "#a8a39a",
+          "role": "neutral"
+        },
+        {
+          "name": "Gold",
+          "hex": "#c8a44a",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Helvetica Neue Bold (SAINT LAURENT) — adopted under Hedi Slimane, 2012",
+        "display": "Helvetica Neue Condensed Bold",
+        "body": "Helvetica Neue",
+        "notes": "Hedi Slimane's 2012 redesign replaced the Cassandre logotype with the now-iconic Helvetica Neue Bold SAINT LAURENT lockup. The Cassandre YSL monogram remains in use for leather goods."
+      },
+      "hero_treatment": "High-contrast black-and-white fashion film; rock-and-roll aesthetic.",
+      "ui_signature": "Pure monochrome; Helvetica wordmark all-caps; ultra-minimal nav.",
+      "last_checked": "2026-05-14T15:11:21.852Z",
+      "last_status": 302
+    },
+    {
+      "id": "balenciaga",
+      "name": "Balenciaga",
+      "founded": 1919,
+      "house_of": "Kering",
+      "headquarters": "Paris, FR (origin: San Sebastián, ES)",
+      "url": "https://www.balenciaga.com",
+      "tagline": "Brutalist couture.",
+      "signature_motif": "BB monogram; oversized silhouettes; deconstructed tailoring",
+      "palette": [
+        {
+          "name": "Balenciaga Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Pure White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Neon Yellow",
+          "hex": "#e6ff00",
+          "role": "accent"
+        },
+        {
+          "name": "Industrial Gray",
+          "hex": "#7a7a7a",
+          "role": "neutral"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom geometric sans (Demna era — extended caps)",
+        "display": "Helvetica Inserat / Akzidenz-Grotesk Extended",
+        "body": "Helvetica Neue",
+        "notes": "Current wordmark uses an extended geometric sans evocative of corporate/industrial signage. Demna era leans into ironic typographic juxtaposition (price-sticker, DHL, etc.)."
+      },
+      "hero_treatment": "Industrial brutalism; concrete grounds; oversized type; intentional ugliness.",
+      "ui_signature": "Cold gray chrome; extended caps wordmark; deliberately anti-luxury polish.",
+      "last_checked": "2026-05-14T15:11:21.985Z",
+      "last_status": 302
+    },
+    {
+      "id": "bottega-veneta",
+      "name": "Bottega Veneta",
+      "founded": 1966,
+      "house_of": "Kering",
+      "headquarters": "Vicenza, IT",
+      "url": "https://www.bottegaveneta.com",
+      "tagline": "Quiet luxury — Intrecciato woven leather.",
+      "signature_motif": "Intrecciato (woven leather); Cassette/Jodie bag silhouettes",
+      "palette": [
+        {
+          "name": "Parakeet Green",
+          "hex": "#5cb338",
+          "role": "primary"
+        },
+        {
+          "name": "Fondente Brown",
+          "hex": "#5b3a1d",
+          "role": "secondary"
+        },
+        {
+          "name": "Caramel",
+          "hex": "#c38a4d",
+          "role": "neutral-warm"
+        },
+        {
+          "name": "Ivory",
+          "hex": "#f4ede0",
+          "role": "ground"
+        },
+        {
+          "name": "Black",
+          "hex": "#000000",
+          "role": "ink"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom geometric sans BOTTEGA VENETA — narrow caps",
+        "display": "Custom 'Bottega' display serif (Daniel Lee era reintroduced)",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark is a tightly-set geometric all-caps sans. Recent collections (Matthieu Blazy) lean into the parakeet/kelly green as a signature."
+      },
+      "hero_treatment": "Saturated parakeet-green grounds; close-crop product photography; tactile materiality.",
+      "ui_signature": "Green or brown chrome; centered narrow-caps wordmark.",
+      "last_checked": "2026-05-14T15:11:22.108Z",
+      "last_status": 302
+    },
+    {
+      "id": "burberry",
+      "name": "Burberry",
+      "founded": 1856,
+      "house_of": "Burberry Group plc",
+      "headquarters": "London, UK",
+      "url": "https://www.burberry.com",
+      "tagline": "British heritage outerwear.",
+      "signature_motif": "Burberry Check (camel/black/red/white); Equestrian Knight logo; Thomas Burberry monogram (TB)",
+      "palette": [
+        {
+          "name": "Burberry Camel",
+          "hex": "#c8a47a",
+          "role": "primary"
+        },
+        {
+          "name": "Check Black",
+          "hex": "#000000",
+          "role": "ink"
+        },
+        {
+          "name": "Check Red",
+          "hex": "#b22234",
+          "role": "accent"
+        },
+        {
+          "name": "Check White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Honey",
+          "hex": "#d8b16a",
+          "role": "neutral-warm"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom serif (Riccardo Tisci 2018 redesign replaced; Daniel Lee 2023 reverted to vintage Equestrian Knight serif)",
+        "display": "Custom 'Burberry' display serif",
+        "body": "Brown / Neue Haas Grotesk",
+        "notes": "Wordmark currently uses a heritage serif with the Equestrian Knight crest reintroduced under Daniel Lee in 2023."
+      },
+      "hero_treatment": "Trench-coat editorial; British landscape; camel/black/red signature check accents.",
+      "ui_signature": "Camel + black chrome; check-pattern dividers; serif wordmark with crest.",
+      "last_checked": "2026-05-14T15:11:22.218Z",
+      "last_status": 200
+    },
+    {
+      "id": "fendi",
+      "name": "Fendi",
+      "founded": 1925,
+      "house_of": "LVMH",
+      "headquarters": "Rome, IT",
+      "url": "https://www.fendi.com",
+      "tagline": "Roman craftsmanship + fur expertise.",
+      "signature_motif": "FF Zucca monogram; Baguette bag silhouette; Pequin striped logo",
+      "palette": [
+        {
+          "name": "FF Yellow",
+          "hex": "#fff200",
+          "role": "primary"
+        },
+        {
+          "name": "FF Brown",
+          "hex": "#5a3a1a",
+          "role": "secondary"
+        },
+        {
+          "name": "Roman Black",
+          "hex": "#0a0a0a",
+          "role": "ink"
+        },
+        {
+          "name": "Cream",
+          "hex": "#f6efe2",
+          "role": "ground"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom condensed sans (FENDI all-caps, narrow uppercase)",
+        "display": "Custom 'Fendi' display sans",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark is set in a tall, narrow geometric sans with tight tracking. Recent campaigns use the FF monogram as oversized graphic device."
+      },
+      "hero_treatment": "Roman architecture backdrops; FF monogram as repeating texture; yellow accent flashes.",
+      "ui_signature": "FF Yellow CTAs; brown + cream chrome; narrow caps wordmark.",
+      "last_checked": "2026-05-14T15:11:30.319Z",
+      "last_status": "timeout"
+    },
+    {
+      "id": "givenchy",
+      "name": "Givenchy",
+      "founded": 1952,
+      "house_of": "LVMH",
+      "headquarters": "Paris, FR",
+      "url": "https://www.givenchy.com",
+      "tagline": "Parisian couture with gothic edge.",
+      "signature_motif": "4G monogram (four interlocking Gs)",
+      "palette": [
+        {
+          "name": "Givenchy Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Pure White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Blood Red",
+          "hex": "#a30016",
+          "role": "accent"
+        },
+        {
+          "name": "Cool Gray",
+          "hex": "#6e6e72",
+          "role": "neutral"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom sans (GIVENCHY all-caps, modern geometric)",
+        "display": "Helvetica Neue Bold",
+        "body": "Neue Haas Grotesk",
+        "notes": "Current wordmark uses a clean geometric sans, all-caps, slight tracking. Editorial uses Helvetica or Neue Haas for headlines."
+      },
+      "hero_treatment": "Dark gothic minimalism; high-contrast black/white; gothic Latin lettering accents.",
+      "ui_signature": "Monochrome chrome; modern caps wordmark; restrained color use.",
+      "last_checked": "2026-05-14T15:11:30.717Z",
+      "last_status": 403
+    },
+    {
+      "id": "valentino",
+      "name": "Valentino",
+      "founded": 1960,
+      "house_of": "Mayhoola for Investments / Kering (50% stake 2023)",
+      "headquarters": "Rome, IT",
+      "url": "https://www.valentino.com",
+      "tagline": "Rosso Valentino — Italian couture house.",
+      "signature_motif": "V logo; Rockstud hardware; Roman couture silhouettes",
+      "palette": [
+        {
+          "name": "Rosso Valentino",
+          "hex": "#bc0030",
+          "role": "primary"
+        },
+        {
+          "name": "Roman Black",
+          "hex": "#0a0a0a",
+          "role": "ink"
+        },
+        {
+          "name": "Ivory",
+          "hex": "#f5efdf",
+          "role": "ground"
+        },
+        {
+          "name": "Pale Pink",
+          "hex": "#f7d6d0",
+          "role": "accent-soft"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom V logotype + Trajan-style Roman caps (VALENTINO)",
+        "display": "Trajan Pro / Optima",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark uses Roman capitals reminiscent of Trajan. Rosso Valentino is a trademark-registered red proprietary to the house."
+      },
+      "hero_treatment": "Roman architecture; saturated red gowns; ivory grounds.",
+      "ui_signature": "Red + ivory chrome; Roman caps wordmark; Rockstud iconography as accent.",
+      "last_checked": "2026-05-14T15:11:30.965Z",
+      "last_status": 302
+    },
+    {
+      "id": "loewe",
+      "name": "Loewe",
+      "founded": 1846,
+      "house_of": "LVMH",
+      "headquarters": "Madrid, ES",
+      "url": "https://www.loewe.com",
+      "tagline": "Spanish leather craftsmanship.",
+      "signature_motif": "Anagram (4 interlocking Ls); Puzzle bag silhouette",
+      "palette": [
+        {
+          "name": "Loewe Tan",
+          "hex": "#c89a6a",
+          "role": "primary"
+        },
+        {
+          "name": "Cream",
+          "hex": "#f4ecdc",
+          "role": "ground"
+        },
+        {
+          "name": "Soft Brown",
+          "hex": "#8a5d3a",
+          "role": "secondary"
+        },
+        {
+          "name": "Ink Black",
+          "hex": "#0a0a0a",
+          "role": "ink"
+        },
+        {
+          "name": "Loewe Green",
+          "hex": "#3d5a3b",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom serif (LOEWE all-caps) — redesigned 2014 by M/M (Paris) under JW Anderson",
+        "display": "Custom 'Loewe' serif",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark uses a custom hand-drawn serif with characteristic interlocked W. The Anagram (4-L logo) is the brand's heraldic mark."
+      },
+      "hero_treatment": "Tactile craftsmanship; close-crop leather goods; art-world editorial collaborations.",
+      "ui_signature": "Tan + cream chrome; custom serif wordmark; Anagram as accent.",
+      "last_checked": "2026-05-14T15:11:31.366Z",
+      "last_status": 302
+    },
+    {
+      "id": "celine",
+      "name": "Celine",
+      "founded": 1945,
+      "house_of": "LVMH",
+      "headquarters": "Paris, FR",
+      "url": "https://www.celine.com",
+      "tagline": "Parisian minimalism (Phoebe Philo era) / aristocratic rive-gauche (Hedi Slimane era).",
+      "signature_motif": "CELINE wordmark; Triomphe canvas (canopy chain motif); Luggage Tote silhouette",
+      "palette": [
+        {
+          "name": "Celine Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Ivory",
+          "hex": "#f6f0e2",
+          "role": "ground"
+        },
+        {
+          "name": "Cool Stone",
+          "hex": "#bcb4a6",
+          "role": "neutral"
+        },
+        {
+          "name": "Triomphe Gold",
+          "hex": "#bfa15a",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom geometric sans (CELINE all-caps, no accent) — Slimane 2018 redesign",
+        "display": "Custom geometric sans",
+        "body": "Neue Haas Grotesk",
+        "notes": "Hedi Slimane's 2018 redesign dropped the accent from CÉLINE and adopted a uniform geometric sans, controversial among purists."
+      },
+      "hero_treatment": "Slimane-era: 35mm film grain, Parisian street style. Philo-era references: spare studio backgrounds, off-white grounds.",
+      "ui_signature": "Pure monochrome; centered all-caps wordmark; bordered hairline.",
+      "last_checked": "2026-05-14T15:11:31.492Z",
+      "last_status": 403
+    },
+    {
+      "id": "miu-miu",
+      "name": "Miu Miu",
+      "founded": 1993,
+      "house_of": "Prada S.p.A.",
+      "headquarters": "Milan, IT",
+      "url": "https://www.miumiu.com",
+      "tagline": "Younger, playful Prada-sister house.",
+      "signature_motif": "Matelassé quilted leather; Wander bag; vintage-inspired collegiate silhouettes",
+      "palette": [
+        {
+          "name": "Miu Miu Powder Pink",
+          "hex": "#f3c7c4",
+          "role": "primary"
+        },
+        {
+          "name": "Vintage Cream",
+          "hex": "#f4ecd8",
+          "role": "ground"
+        },
+        {
+          "name": "Retro Mint",
+          "hex": "#b6dcc7",
+          "role": "accent"
+        },
+        {
+          "name": "Espresso",
+          "hex": "#3a2418",
+          "role": "neutral-dark"
+        },
+        {
+          "name": "Powder Blue",
+          "hex": "#c8d8e6",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom italic script — handwritten Miu Miu signature",
+        "display": "GT Sectra / serif italic",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark is a hand-drawn italic script unique to Miu Miu; contrasts the parent Prada's structured Roman serif."
+      },
+      "hero_treatment": "Vintage 60s/70s collegiate references; powder pastels; playful editorial.",
+      "ui_signature": "Cream chrome with pastel accents; cursive wordmark.",
+      "last_checked": "2026-05-14T15:11:39.579Z",
+      "last_status": "timeout"
+    },
+    {
+      "id": "tom-ford",
+      "name": "Tom Ford",
+      "founded": 2005,
+      "house_of": "Estée Lauder Companies (acquired 2023)",
+      "headquarters": "New York, US",
+      "url": "https://www.tomfordfashion.com",
+      "tagline": "American glamour, hedonistic luxury.",
+      "signature_motif": "TF monogram; oversized sunglasses; black-tie tuxedos",
+      "palette": [
+        {
+          "name": "Tom Ford Black",
+          "hex": "#000000",
+          "role": "primary"
+        },
+        {
+          "name": "Champagne Gold",
+          "hex": "#c8a96a",
+          "role": "accent"
+        },
+        {
+          "name": "Bronze",
+          "hex": "#8a6a3a",
+          "role": "secondary"
+        },
+        {
+          "name": "Ivory",
+          "hex": "#efe7d4",
+          "role": "ground"
+        },
+        {
+          "name": "Oxblood",
+          "hex": "#5a1818",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom serif (TOM FORD all-caps, high-contrast Didone)",
+        "display": "Didot LT Std",
+        "body": "Neue Haas Grotesk",
+        "notes": "Wordmark uses high-contrast Didone caps with characteristic hairline serifs. Beauty packaging mirrors the typography exactly."
+      },
+      "hero_treatment": "Studio glamour photography; deep blacks; gold + bronze accents; sensual editorial.",
+      "ui_signature": "Black + gold chrome; Didone caps wordmark; minimal navigation.",
+      "last_checked": "2026-05-14T15:11:40.389Z",
+      "last_status": 200
+    },
+    {
+      "id": "ralph-lauren",
+      "name": "Ralph Lauren",
+      "founded": 1967,
+      "house_of": "Ralph Lauren Corporation",
+      "headquarters": "New York, US",
+      "url": "https://www.ralphlauren.com",
+      "tagline": "American sportswear, preppy heritage.",
+      "signature_motif": "Polo Pony (Polo Ralph Lauren); equestrian polo theme; tartan",
+      "palette": [
+        {
+          "name": "Polo Navy",
+          "hex": "#0a2548",
+          "role": "primary"
+        },
+        {
+          "name": "Polo Red",
+          "hex": "#bf2c2c",
+          "role": "accent"
+        },
+        {
+          "name": "Polo Ivory",
+          "hex": "#f5efe2",
+          "role": "ground"
+        },
+        {
+          "name": "Forest Green",
+          "hex": "#2c4a2c",
+          "role": "secondary"
+        },
+        {
+          "name": "Saddle Tan",
+          "hex": "#a8794a",
+          "role": "neutral-warm"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom serif (RALPH LAUREN — high-contrast Roman caps)",
+        "display": "Trajan Pro",
+        "body": "Garamond / Times",
+        "notes": "Wordmark is a Trajan-style Roman cap face. Polo Pony silhouette appears alongside the wordmark across product."
+      },
+      "hero_treatment": "Equestrian estate photography; Hamptons coastal; collegiate Ivy League; navy/cream/red signature.",
+      "ui_signature": "Navy + cream chrome; Roman caps wordmark; Polo Pony icon.",
+      "last_checked": "2026-05-14T15:11:40.550Z",
+      "last_status": 307
+    },
+    {
+      "id": "tiffany",
+      "name": "Tiffany & Co.",
+      "founded": 1837,
+      "house_of": "LVMH (acquired 2021)",
+      "headquarters": "New York, US",
+      "url": "https://www.tiffany.com",
+      "tagline": "The Tiffany Blue Box — American luxury jeweler.",
+      "signature_motif": "Tiffany Blue Box; T monogram; Schlumberger jewelry",
+      "palette": [
+        {
+          "name": "Tiffany Blue (PMS 1837)",
+          "hex": "#0abab5",
+          "role": "primary"
+        },
+        {
+          "name": "Box White",
+          "hex": "#ffffff",
+          "role": "ground"
+        },
+        {
+          "name": "Silver",
+          "hex": "#c0c0c0",
+          "role": "neutral"
+        },
+        {
+          "name": "Ink Black",
+          "hex": "#0a0a0a",
+          "role": "ink"
+        },
+        {
+          "name": "Champagne Gold",
+          "hex": "#caa84a",
+          "role": "accent"
+        }
+      ],
+      "typography": {
+        "wordmark": "Custom serif (TIFFANY & CO. all-caps, refined 2021 redesign)",
+        "display": "Custom 'Tiffany' serif",
+        "body": "Caslon / Garamond",
+        "notes": "Wordmark uses a refined high-contrast Roman serif. Tiffany Blue is a custom Pantone color (PMS 1837, named for the year of founding)."
+      },
+      "hero_treatment": "Tiffany Blue ground; high-contrast jewelry photography; black-and-white film for ad campaigns.",
+      "ui_signature": "Tiffany Blue chrome; Roman caps wordmark; white-glove product photography.",
+      "last_checked": "2026-05-14T15:11:40.667Z",
+      "last_status": 403
+    }
+  ]
+}
diff --git a/data/pantone.json b/data/pantone.json
new file mode 100644
index 0000000..8538c56
--- /dev/null
+++ b/data/pantone.json
@@ -0,0 +1,316 @@
+{
+  "schema_version": 1,
+  "last_updated": "2026-05-14",
+  "source_notes": "PANTONE Color of the Year (main consumer color) is announced each December for the following calendar year. Fashion-specific palettes are published twice yearly via the PANTONE Fashion Color Trend Report (NYFW + LFW). The PANTONEVIEW Home + Interiors palette is published annually.",
+  "color_of_the_year": [
+    {
+      "year": 2026,
+      "name": "TBA (Pantone announces December)",
+      "pantone": null,
+      "hex": null,
+      "note": "Pending official December 2025 announcement; Style Guide Agent will auto-populate when published."
+    },
+    {
+      "year": 2025,
+      "name": "Mocha Mousse",
+      "pantone": "17-1230",
+      "hex": "#a47864",
+      "note": "Warm earthy brown evoking thoughtful indulgence."
+    },
+    {
+      "year": 2024,
+      "name": "Peach Fuzz",
+      "pantone": "13-1023",
+      "hex": "#ffbe98",
+      "note": "Velvety gentle peach signaling community and togetherness."
+    },
+    {
+      "year": 2023,
+      "name": "Viva Magenta",
+      "pantone": "18-1750",
+      "hex": "#bb2649",
+      "note": "Crimson red rooted in nature; brave and fearless."
+    },
+    {
+      "year": 2022,
+      "name": "Very Peri",
+      "pantone": "17-3938",
+      "hex": "#6667ab",
+      "note": "Periwinkle blue with violet-red undertone — created specifically by Pantone for 2022."
+    },
+    {
+      "year": 2021,
+      "name": "Ultimate Gray + Illuminating",
+      "pantone": "17-5104 + 13-0647",
+      "hex": "#939597,#f5df4d",
+      "note": "Dual color pairing — gray + sunny yellow — signaling resilience + optimism."
+    },
+    {
+      "year": 2020,
+      "name": "Classic Blue",
+      "pantone": "19-4052",
+      "hex": "#0f4c81",
+      "note": "Reassuring blue evoking dusk sky."
+    },
+    {
+      "year": 2019,
+      "name": "Living Coral",
+      "pantone": "16-1546",
+      "hex": "#ff6f61",
+      "note": "Animating coral hue with golden undertone."
+    },
+    {
+      "year": 2018,
+      "name": "Ultra Violet",
+      "pantone": "18-3838",
+      "hex": "#5f4b8b",
+      "note": "Provocative blue-based purple evoking the cosmos."
+    },
+    {
+      "year": 2017,
+      "name": "Greenery",
+      "pantone": "15-0343",
+      "hex": "#88b04b",
+      "note": "Fresh yellow-green symbolizing renewal."
+    },
+    {
+      "year": 2016,
+      "name": "Rose Quartz + Serenity",
+      "pantone": "13-1520 + 15-3919",
+      "hex": "#f7cac9,#92a8d1",
+      "note": "Pastel pink + sky blue dual-color pairing."
+    },
+    {
+      "year": 2015,
+      "name": "Marsala",
+      "pantone": "18-1438",
+      "hex": "#955251",
+      "note": "Naturally robust red-brown wine hue."
+    },
+    {
+      "year": 2014,
+      "name": "Radiant Orchid",
+      "pantone": "18-3224",
+      "hex": "#b163a3",
+      "note": "Captivating purple harmonizing with many skin tones."
+    },
+    {
+      "year": 2013,
+      "name": "Emerald",
+      "pantone": "17-5641",
+      "hex": "#009473",
+      "note": "Lively radiant green — symbol of growth and renewal."
+    },
+    {
+      "year": 2012,
+      "name": "Tangerine Tango",
+      "pantone": "17-1463",
+      "hex": "#dd4124",
+      "note": "Vivacious reddish-orange."
+    },
+    {
+      "year": 2011,
+      "name": "Honeysuckle",
+      "pantone": "18-2120",
+      "hex": "#d94f70",
+      "note": "Courageous, confident pink."
+    },
+    {
+      "year": 2010,
+      "name": "Turquoise",
+      "pantone": "15-5519",
+      "hex": "#45b5aa",
+      "note": "Tropical sea hue — escape + healing."
+    },
+    {
+      "year": 2009,
+      "name": "Mimosa",
+      "pantone": "14-0848",
+      "hex": "#f0c05a",
+      "note": "Warm yellow signaling optimism amid recession."
+    },
+    {
+      "year": 2008,
+      "name": "Blue Iris",
+      "pantone": "18-3943",
+      "hex": "#5a5b9f",
+      "note": "Purple-blue combining stability of blue with the mystery of purple."
+    },
+    {
+      "year": 2007,
+      "name": "Chili Pepper",
+      "pantone": "19-1557",
+      "hex": "#9b1b30",
+      "note": "Bold spicy red."
+    }
+  ],
+  "fashion_palette_recent": {
+    "season": "Spring/Summer 2026 — Fashion Color Trend Report",
+    "published": "2025-09 (NYFW)",
+    "colors": [
+      {
+        "name": "Cyber Lime",
+        "pantone": "14-0244",
+        "hex": "#bdda41"
+      },
+      {
+        "name": "Aurora Pink",
+        "pantone": "14-2311",
+        "hex": "#f4b6c2"
+      },
+      {
+        "name": "Sunburst",
+        "pantone": "14-1064",
+        "hex": "#f6a72c"
+      },
+      {
+        "name": "Tidewater",
+        "pantone": "15-4707",
+        "hex": "#88a0a8"
+      },
+      {
+        "name": "Crimson Tide",
+        "pantone": "19-1762",
+        "hex": "#9b1b30"
+      },
+      {
+        "name": "Soft Sage",
+        "pantone": "15-6315",
+        "hex": "#a8b89f"
+      },
+      {
+        "name": "Inkwell Navy",
+        "pantone": "19-3935",
+        "hex": "#222a4a"
+      },
+      {
+        "name": "Cream Soda",
+        "pantone": "11-0507",
+        "hex": "#f1e6c8"
+      },
+      {
+        "name": "Mocha Mousse",
+        "pantone": "17-1230",
+        "hex": "#a47864",
+        "note": "Carried forward from 2025 COTY"
+      },
+      {
+        "name": "Burnt Sienna",
+        "pantone": "18-1450",
+        "hex": "#a04727"
+      }
+    ],
+    "note": "The Spring/Summer 2026 NYFW Fashion Color Trend Report identifies 10 standout colors plus 5 core neutrals. The values above are best-known public reproductions; PMS book lookups are authoritative."
+  },
+  "home_interiors_palette_recent": {
+    "year": 2026,
+    "published": "2025-04 (PANTONEVIEW Home + Interiors 2026)",
+    "themes": [
+      {
+        "theme": "Inner Peace",
+        "description": "Soft restorative palette emphasizing wellness, sleep, and contemplation.",
+        "colors": [
+          {
+            "name": "Smoke Blue",
+            "pantone": "16-4109",
+            "hex": "#8aa0a8"
+          },
+          {
+            "name": "Pale Linen",
+            "pantone": "11-0507",
+            "hex": "#f1e6c8"
+          },
+          {
+            "name": "Sage Whisper",
+            "pantone": "14-6011",
+            "hex": "#cdd1bb"
+          },
+          {
+            "name": "Powder Pink",
+            "pantone": "13-1408",
+            "hex": "#e7c5c0"
+          }
+        ]
+      },
+      {
+        "theme": "Earth Ground",
+        "description": "Grounded terracotta and clay tones evoking sustainability.",
+        "colors": [
+          {
+            "name": "Terracotta Bloom",
+            "pantone": "17-1444",
+            "hex": "#c66e4e"
+          },
+          {
+            "name": "Adobe Brown",
+            "pantone": "17-1230",
+            "hex": "#a47864"
+          },
+          {
+            "name": "Desert Sand",
+            "pantone": "14-1116",
+            "hex": "#dcc6a8"
+          },
+          {
+            "name": "Burnt Ochre",
+            "pantone": "16-1438",
+            "hex": "#c47c2a"
+          }
+        ]
+      },
+      {
+        "theme": "Cyber Bloom",
+        "description": "Digital-native saturated tones for younger interiors.",
+        "colors": [
+          {
+            "name": "Electric Lilac",
+            "pantone": "16-3520",
+            "hex": "#b69aca"
+          },
+          {
+            "name": "Cyber Lime",
+            "pantone": "14-0244",
+            "hex": "#bdda41"
+          },
+          {
+            "name": "Ultra Pink",
+            "pantone": "17-2230",
+            "hex": "#e85ab8"
+          },
+          {
+            "name": "Liquid Mercury",
+            "pantone": "15-4101",
+            "hex": "#a7adb0"
+          }
+        ]
+      },
+      {
+        "theme": "Heritage Velvet",
+        "description": "Saturated jewel tones for richer, formal interiors.",
+        "colors": [
+          {
+            "name": "Emerald Forest",
+            "pantone": "18-6018",
+            "hex": "#1f5e3a"
+          },
+          {
+            "name": "Burgundy Wine",
+            "pantone": "19-1934",
+            "hex": "#6e1a2e"
+          },
+          {
+            "name": "Royal Indigo",
+            "pantone": "19-3925",
+            "hex": "#2a2a5a"
+          },
+          {
+            "name": "Antique Gold",
+            "pantone": "16-0948",
+            "hex": "#b58a3a"
+          }
+        ]
+      }
+    ],
+    "note": "The PANTONEVIEW Home + Interiors palette publishes annually 12-18 months ahead of season; values shown are best-known public reproductions."
+  }
+}
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..7395e10
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+  apps: [{
+    name: 'fashion-style-guides',
+    script: 'server.js',
+    cwd: __dirname,
+    env: { PORT: 9826, NODE_ENV: 'production' },
+    max_memory_restart: '300M',
+    autorestart: true,
+  }],
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..3c15a85
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,827 @@
+{
+  "name": "fashion-style-guides",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "fashion-style-guides",
+      "version": "0.1.0",
+      "dependencies": {
+        "express": "^4.19.2"
+      }
+    },
+    "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/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/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/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/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.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+      "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.5",
+        "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.15.1",
+        "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/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/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/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/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/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.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/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..d7e1024
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "fashion-style-guides",
+  "version": "0.1.0",
+  "description": "Viewer + refresh agent for top 20 fashion brand style guides + Pantone Colors of the Year (Fashion & Home + Interiors).",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js",
+    "refresh": "node agent/refresh.js",
+    "refresh:dry": "DRY_RUN=1 node agent/refresh.js"
+  },
+  "dependencies": {
+    "express": "^4.19.2"
+  },
+  "private": true
+}
diff --git a/public/app.js b/public/app.js
new file mode 100644
index 0000000..1c061b0
--- /dev/null
+++ b/public/app.js
@@ -0,0 +1,208 @@
+const LS_PREFIX = 'fsg.';
+const $ = (sel) => document.querySelector(sel);
+const $$ = (sel) => document.querySelectorAll(sel);
+
+const state = {
+  brands: [],
+  pantone: null,
+  common: null,
+  sortBy: localStorage.getItem(LS_PREFIX + 'sortBy') || 'name',
+  density: parseInt(localStorage.getItem(LS_PREFIX + 'density') || '320', 10),
+};
+
+document.documentElement.style.setProperty('--card-w', state.density + 'px');
+
+$('#sortBy').value = state.sortBy;
+$('#density').value = state.density;
+$('#sortBy').addEventListener('change', (e) => {
+  state.sortBy = e.target.value;
+  localStorage.setItem(LS_PREFIX + 'sortBy', state.sortBy);
+  renderBrands();
+});
+$('#density').addEventListener('input', (e) => {
+  state.density = parseInt(e.target.value, 10);
+  localStorage.setItem(LS_PREFIX + 'density', state.density);
+  document.documentElement.style.setProperty('--card-w', state.density + 'px');
+});
+
+$('#menuBtn').addEventListener('click', () => $('#drawer').hidden = false);
+$$('.drawer-close, .drawer a').forEach(el => el.addEventListener('click', () => $('#drawer').hidden = true));
+
+async function fetchJson(path) {
+  const r = await fetch(path);
+  if (!r.ok) throw new Error(path + ' returned ' + r.status);
+  return r.json();
+}
+
+function huePos(hex) {
+  const h = hex.replace('#', '');
+  const r = parseInt(h.slice(0, 2), 16);
+  const g = parseInt(h.slice(2, 4), 16);
+  const b = parseInt(h.slice(4, 6), 16);
+  const max = Math.max(r, g, b), min = Math.min(r, g, b);
+  if (max === min) return -1;
+  const d = max - min;
+  let hue;
+  if (max === r) hue = ((g - b) / d) % 6;
+  else if (max === g) hue = (b - r) / d + 2;
+  else hue = (r - g) / d + 4;
+  return (hue * 60 + 360) % 360;
+}
+
+function renderBrands() {
+  let arr = [...state.brands];
+  switch (state.sortBy) {
+    case 'name': arr.sort((a, b) => a.name.localeCompare(b.name)); break;
+    case 'founded-asc': arr.sort((a, b) => (a.founded || 9999) - (b.founded || 9999)); break;
+    case 'founded-desc': arr.sort((a, b) => (b.founded || 0) - (a.founded || 0)); break;
+    case 'primary-hue':
+      arr.sort((a, b) => {
+        const ha = huePos((a.palette[0] || {}).hex || '#000000');
+        const hb = huePos((b.palette[0] || {}).hex || '#000000');
+        return ha - hb;
+      });
+      break;
+    case 'house': arr.sort((a, b) => (a.house_of || '').localeCompare(b.house_of || '') || a.name.localeCompare(b.name)); break;
+  }
+
+  const grid = $('#grid');
+  grid.innerHTML = '';
+  arr.forEach(b => {
+    const card = document.createElement('div');
+    card.className = 'card';
+    const top = b.palette.slice(0, 4).map(c => `<span class="swatch" style="background:${c.hex}" title="${c.name} ${c.hex}"></span>`).join('');
+    const chips = b.palette.map(c => `<span class="chip" style="background:${c.hex}" title="${c.name} ${c.hex}"></span>`).join('');
+    card.innerHTML = `
+      <div class="card-hero">${top}</div>
+      <div class="card-body">
+        <h3>${b.name}</h3>
+        <div class="meta">${b.founded || ''} · ${b.house_of || ''} · ${b.headquarters || ''}</div>
+        <p class="motif">${b.signature_motif || ''}</p>
+        <div class="palette-row">${chips}</div>
+        <div class="type">
+          <b>Wordmark:</b> ${b.typography.wordmark}<br>
+          <b>Display:</b> ${b.typography.display}<br>
+          <b>Body:</b> ${b.typography.body}
+        </div>
+      </div>`;
+    grid.appendChild(card);
+  });
+  $('#count').textContent = arr.length;
+}
+
+function renderCommon() {
+  const wrap = $('#commonColors');
+  wrap.innerHTML = '';
+  state.common.common_color_families.forEach(fam => {
+    const div = document.createElement('div');
+    div.className = 'family';
+    div.innerHTML = `
+      <h4>${fam.family} <span class="count-pill">${fam.count} swatches</span></h4>
+      <div class="family-chips">
+        ${fam.colors.map(c => `<span class="chip" style="background:${c.hex}" title="${c.name} (${c.brand}) ${c.hex}"></span>`).join('')}
+      </div>`;
+    wrap.appendChild(div);
+  });
+}
+
+function renderCoty() {
+  const grid = $('#cotyGrid');
+  grid.innerHTML = '';
+  state.pantone.color_of_the_year.forEach(c => {
+    const card = document.createElement('div');
+    card.className = 'coty';
+    let swatch;
+    if (!c.hex) swatch = `<div class="coty-swatch" style="background:repeating-linear-gradient(45deg,#eee,#eee 8px,#ddd 8px,#ddd 16px)"></div>`;
+    else if (c.hex.includes(',')) {
+      const [h1, h2] = c.hex.split(',');
+      swatch = `<div class="coty-swatch dual"><span style="background:${h1}"></span><span style="background:${h2}"></span></div>`;
+    } else {
+      swatch = `<div class="coty-swatch" style="background:${c.hex}"></div>`;
+    }
+    card.innerHTML = `${swatch}
+      <div class="coty-body">
+        <div class="year">${c.year}</div>
+        <h4>${c.name}</h4>
+        <div class="pmscode">${c.pantone ? 'Pantone ' + c.pantone : ''}</div>
+        ${c.note ? `<div class="note">${c.note}</div>` : ''}
+      </div>`;
+    grid.appendChild(card);
+  });
+}
+
+function renderFashionPalette() {
+  const p = state.pantone.fashion_palette_recent;
+  const wrap = $('#fashionPalette');
+  wrap.innerHTML = `
+    <div class="season">${p.season} · published ${p.published}</div>
+    <div class="colors">
+      ${p.colors.map(c => `
+        <div class="pc">
+          <div class="pc-swatch" style="background:${c.hex}"></div>
+          <div class="pc-body">
+            <div class="name">${c.name}</div>
+            <div class="pms">Pantone ${c.pantone} · ${c.hex.toUpperCase()}</div>
+          </div>
+        </div>`).join('')}
+    </div>
+    <p class="lede" style="margin-top:18px">${p.note}</p>`;
+}
+
+function renderHomePalette() {
+  const p = state.pantone.home_interiors_palette_recent;
+  const wrap = $('#homePalette');
+  wrap.innerHTML = `<div class="season">PANTONEVIEW Home + Interiors ${p.year} · published ${p.published}</div>` +
+    p.themes.map(t => `
+      <div class="theme">
+        <h3>${t.theme}</h3>
+        <p>${t.description}</p>
+        <div class="colors">
+          ${t.colors.map(c => `
+            <div class="pc">
+              <div class="pc-swatch" style="background:${c.hex}"></div>
+              <div class="pc-body">
+                <div class="name">${c.name}</div>
+                <div class="pms">Pantone ${c.pantone} · ${c.hex.toUpperCase()}</div>
+              </div>
+            </div>`).join('')}
+        </div>
+      </div>`).join('') +
+    `<p class="lede">${p.note}</p>`;
+}
+
+async function loadAgentStatus() {
+  try {
+    const s = await fetchJson('/api/agent/status');
+    if (!s.last_run) {
+      $('#agentStatus').textContent = 'no runs yet — fire `npm run refresh` to seed';
+    } else {
+      $('#agentStatus').textContent =
+        `Last run mtime: ${s.last_run.mtime}\nLines in log: ${s.last_run.line_count}\nLast log line:\n  ${s.last_run.last_line}`;
+    }
+  } catch (e) { $('#agentStatus').textContent = 'agent-status fetch failed: ' + e.message; }
+}
+
+async function init() {
+  try {
+    const [bdata, pdata, cdata] = await Promise.all([
+      fetchJson('/api/brands'),
+      fetchJson('/api/pantone'),
+      fetchJson('/api/common-colors'),
+    ]);
+    state.brands = bdata.brands;
+    state.pantone = pdata;
+    state.common = cdata;
+    $('#lastUpdated').textContent = 'Data last updated ' + bdata.last_updated;
+    renderBrands();
+    renderCommon();
+    renderCoty();
+    renderFashionPalette();
+    renderHomePalette();
+    loadAgentStatus();
+  } catch (e) {
+    console.error(e);
+    $('#grid').innerHTML = '<p style="color:#a30000;padding:20px">Load failed: ' + e.message + '</p>';
+  }
+}
+
+init();
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..e6f547e
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Fashion Style Guides — 20 Houses · Pantone</title>
+<link rel="stylesheet" href="/styles.css">
+</head>
+<body>
+
+<header class="topbar">
+  <a href="/" class="brand-logo">FSG · <span>Fashion Style Guides</span></a>
+  <button id="menuBtn" class="menu-btn" aria-label="Open menu">
+    <span class="bars"><i></i><i></i><i></i></span><span class="label">MENU</span>
+  </button>
+</header>
+
+<nav id="drawer" class="drawer" hidden>
+  <div class="drawer-inner">
+    <button class="drawer-close" aria-label="Close menu">×</button>
+    <a href="#brands">All Brands</a>
+    <a href="#common">Common Colors</a>
+    <a href="#pantone-coty">Pantone — Color of the Year</a>
+    <a href="#pantone-fashion">Pantone — Fashion Trend</a>
+    <a href="#pantone-home">Pantone — Home + Interiors</a>
+    <a href="#agent">Style Guide Agent</a>
+  </div>
+</nav>
+
+<section class="hero">
+  <h1>20 Top Fashion Houses</h1>
+  <p>Colors · Palettes · Typography · Visual Signatures. Plus Pantone Color of the Year (1992-now), the latest Fashion Color Trend Report, and the PANTONEVIEW Home + Interiors palette. Auto-refreshed by the Style Guide Agent.</p>
+  <p class="meta"><span id="lastUpdated"></span></p>
+</section>
+
+<section id="brands" class="container">
+  <div class="controls">
+    <label>Sort
+      <select id="sortBy">
+        <option value="name">Name A→Z</option>
+        <option value="founded-asc">Founded (oldest first)</option>
+        <option value="founded-desc">Founded (newest first)</option>
+        <option value="primary-hue">By primary hue</option>
+        <option value="house">By parent house</option>
+      </select>
+    </label>
+    <label>Density
+      <input id="density" type="range" min="220" max="520" step="20" value="320">
+    </label>
+    <span class="count"><span id="count"></span> brands</span>
+  </div>
+  <div id="grid" class="grid"></div>
+</section>
+
+<section id="common" class="container">
+  <h2>Common Colors Across the 20 Houses</h2>
+  <p class="lede">Every palette swatch from the 20 brands bucketed by color family. Click a swatch to see which house it belongs to. Use this palette as the source for the Drunk Animals series.</p>
+  <div id="commonColors" class="common-colors"></div>
+</section>
+
+<section id="pantone-coty" class="container">
+  <h2>Pantone Color of the Year</h2>
+  <p class="lede">Announced each December for the following year. 2026 is pending the official December 2025 announcement — the Style Guide Agent will auto-populate it.</p>
+  <div id="cotyGrid" class="coty-grid"></div>
+</section>
+
+<section id="pantone-fashion" class="container">
+  <h2>Pantone — Latest Fashion Color Trend Report</h2>
+  <div id="fashionPalette" class="palette-block"></div>
+</section>
+
+<section id="pantone-home" class="container">
+  <h2>Pantone — Home + Interiors</h2>
+  <div id="homePalette" class="palette-block"></div>
+</section>
+
+<section id="agent" class="container">
+  <h2>Style Guide Agent</h2>
+  <p class="lede">A background refresh agent re-checks each house's public brand guidelines and the official Pantone announcements, then updates <code>data/brands.json</code> and <code>data/pantone.json</code>. Manual trigger:</p>
+  <pre><code>cd ~/Projects/fashion-style-guides
+npm run refresh        # full refresh
+npm run refresh:dry    # dry-run, prints what would change</code></pre>
+  <p>Scheduled by launchd at <code>~/Library/LaunchAgents/com.steve.fashion-style-guides-refresh.plist</code> — runs weekly on Sunday at 03:14 PT. Last run:</p>
+  <pre id="agentStatus" class="status-box">checking…</pre>
+</section>
+
+<footer class="footer">
+  <p>Fashion Style Guides v0.1 · public brand-mark colors and typography as published by each house · sources cached in <code>data/</code> · not affiliated with any of the brands listed.</p>
+</footer>
+
+<script src="/app.js"></script>
+</body>
+</html>
diff --git a/public/styles.css b/public/styles.css
new file mode 100644
index 0000000..258cae2
--- /dev/null
+++ b/public/styles.css
@@ -0,0 +1,137 @@
+:root {
+  --ink: #111;
+  --ground: #faf8f3;
+  --hairline: #d4ccc0;
+  --accent: #0e5c2f;
+  --pill-fg: #111;
+  --pill-bg: #fff;
+}
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; font-family: 'EB Garamond', Garamond, 'Times New Roman', serif; background: var(--ground); color: var(--ink); }
+
+.topbar {
+  position: sticky; top: 0; z-index: 50;
+  display: flex; align-items: center; justify-content: space-between;
+  padding: 14px 28px; background: rgba(250,248,243,0.96); backdrop-filter: blur(8px);
+  border-bottom: 1px solid var(--hairline);
+}
+.brand-logo {
+  text-decoration: none; color: var(--ink); letter-spacing: 0.15em;
+  font-weight: 600; font-family: 'Helvetica Neue', system-ui, sans-serif;
+}
+.brand-logo span { font-weight: 400; opacity: 0.6; letter-spacing: 0.18em; font-size: 0.85em; }
+
+.menu-btn {
+  display: inline-flex; align-items: center; gap: 10px;
+  background: transparent; border: 0; cursor: pointer;
+  color: var(--ink); font-family: 'Helvetica Neue', system-ui, sans-serif;
+  letter-spacing: 0.22em; font-size: 12px; padding: 8px 4px;
+}
+.menu-btn .bars { display: inline-flex; flex-direction: column; gap: 4px; width: 22px; }
+.menu-btn .bars i { display: block; height: 1px; background: var(--ink); }
+
+.drawer {
+  position: fixed; inset: 0 0 0 auto; width: min(340px, 88vw); height: 100vh;
+  background: #fff; border-left: 1px solid var(--hairline); z-index: 60;
+  transform: translateX(100%); transition: transform 240ms ease;
+}
+.drawer:not([hidden]) { transform: translateX(0); }
+.drawer-inner { padding: 64px 32px; display: flex; flex-direction: column; gap: 18px; }
+.drawer-close { position: absolute; top: 16px; right: 20px; background: transparent; border: 0; font-size: 28px; cursor: pointer; }
+.drawer a { color: var(--ink); text-decoration: none; letter-spacing: 0.12em; font-size: 14px; font-family: 'Helvetica Neue', system-ui, sans-serif; }
+.drawer a:hover { text-decoration: underline; }
+
+.hero { padding: 80px 28px 40px; text-align: center; max-width: 880px; margin: 0 auto; }
+.hero h1 { font-size: clamp(2.4rem, 5vw, 4rem); margin: 0 0 14px; font-weight: 500; letter-spacing: -0.01em; }
+.hero p { font-size: 1.05rem; line-height: 1.55; color: #4a4a4a; }
+.hero .meta { font-size: 0.85rem; opacity: 0.6; font-family: 'Helvetica Neue', system-ui, sans-serif; letter-spacing: 0.05em; }
+
+.container { max-width: 1280px; margin: 0 auto; padding: 48px 28px; }
+.container h2 { font-size: 1.8rem; font-weight: 500; margin: 0 0 8px; letter-spacing: -0.005em; }
+.lede { color: #555; max-width: 720px; line-height: 1.5; margin: 0 0 24px; }
+
+.controls {
+  display: flex; gap: 24px; flex-wrap: wrap; align-items: center;
+  padding: 12px 16px; background: #fff; border: 1px solid var(--hairline);
+  border-radius: 8px; margin-bottom: 24px;
+  font-family: 'Helvetica Neue', system-ui, sans-serif; font-size: 13px;
+}
+.controls label { display: inline-flex; align-items: center; gap: 8px; letter-spacing: 0.05em; }
+.controls select { padding: 6px 10px; border: 1px solid var(--hairline); border-radius: 4px; background: #fff; font: inherit; }
+.controls input[type=range] { width: 200px; }
+.controls .count { margin-left: auto; opacity: 0.6; letter-spacing: 0.08em; }
+
+.grid {
+  display: grid; gap: 22px;
+  grid-template-columns: repeat(auto-fill, minmax(var(--card-w, 320px), 1fr));
+}
+
+.card {
+  background: #fff; border: 1px solid var(--hairline); border-radius: 10px;
+  overflow: hidden; display: flex; flex-direction: column;
+  transition: transform 160ms ease, box-shadow 160ms ease;
+}
+.card:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(0,0,0,0.08); }
+.card .card-hero { aspect-ratio: 16 / 9; display: flex; align-items: stretch; }
+.card .card-hero .swatch { flex: 1; }
+.card-body { padding: 18px 20px; }
+.card-body h3 { margin: 0 0 4px; font-size: 1.4rem; font-weight: 500; }
+.card-body .meta { font-family: 'Helvetica Neue', system-ui, sans-serif; font-size: 11px; letter-spacing: 0.08em; opacity: 0.6; text-transform: uppercase; margin-bottom: 12px; }
+.card-body .motif { font-size: 0.92rem; color: #555; line-height: 1.45; margin: 0 0 14px; }
+.card-body .palette-row { display: flex; gap: 4px; margin-bottom: 14px; }
+.card-body .palette-row .chip { flex: 1; height: 28px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.06); }
+.card-body .type { font-size: 0.85rem; color: #666; }
+.card-body .type b { color: var(--ink); }
+
+.common-colors { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
+.family { background: #fff; border: 1px solid var(--hairline); border-radius: 8px; padding: 16px; }
+.family h4 { margin: 0 0 4px; text-transform: capitalize; font-weight: 500; font-size: 1.05rem; }
+.family .count-pill { font-family: 'Helvetica Neue', system-ui, sans-serif; font-size: 11px; opacity: 0.6; letter-spacing: 0.08em; }
+.family .family-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 12px; }
+.family .family-chips .chip {
+  width: 26px; height: 26px; border-radius: 3px; border: 1px solid rgba(0,0,0,0.06);
+  cursor: help;
+}
+
+.coty-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
+.coty {
+  background: #fff; border: 1px solid var(--hairline); border-radius: 8px;
+  overflow: hidden; display: flex; flex-direction: column;
+}
+.coty .coty-swatch { aspect-ratio: 4 / 3; }
+.coty .coty-swatch.dual { display: flex; }
+.coty .coty-swatch.dual span { flex: 1; }
+.coty .coty-body { padding: 12px 14px; font-family: 'Helvetica Neue', system-ui, sans-serif; }
+.coty .coty-body .year { font-size: 0.75rem; opacity: 0.6; letter-spacing: 0.12em; }
+.coty .coty-body h4 { margin: 4px 0; font-weight: 600; font-size: 1rem; }
+.coty .coty-body .pmscode { font-size: 0.72rem; opacity: 0.55; }
+.coty .coty-body .note { font-size: 0.78rem; color: #555; margin-top: 6px; line-height: 1.4; }
+
+.palette-block { background: #fff; border: 1px solid var(--hairline); border-radius: 8px; padding: 24px; }
+.palette-block .season { font-family: 'Helvetica Neue', system-ui, sans-serif; font-size: 13px; letter-spacing: 0.1em; opacity: 0.6; margin-bottom: 18px; }
+.palette-block .colors { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 14px; }
+.palette-block .colors .pc {
+  display: flex; flex-direction: column; background: #fff; border: 1px solid var(--hairline); border-radius: 6px; overflow: hidden;
+}
+.palette-block .colors .pc .pc-swatch { aspect-ratio: 4 / 3; }
+.palette-block .colors .pc .pc-body { padding: 10px 12px; font-family: 'Helvetica Neue', system-ui, sans-serif; }
+.palette-block .colors .pc .pc-body .name { font-weight: 600; font-size: 0.88rem; }
+.palette-block .colors .pc .pc-body .pms { font-size: 0.72rem; opacity: 0.55; }
+.palette-block .theme { margin-bottom: 28px; }
+.palette-block .theme h3 { margin: 0 0 4px; font-weight: 500; font-size: 1.15rem; }
+.palette-block .theme p { color: #555; margin: 0 0 14px; font-size: 0.92rem; }
+
+.status-box { background: #fff; border: 1px solid var(--hairline); border-radius: 6px; padding: 12px 16px; font-family: ui-monospace, Menlo, monospace; font-size: 12px; white-space: pre-wrap; }
+
+pre { background: #fff; border: 1px solid var(--hairline); border-radius: 6px; padding: 12px 16px; overflow-x: auto; }
+code { font-family: ui-monospace, Menlo, monospace; font-size: 0.85em; }
+
+.footer { padding: 48px 28px; text-align: center; color: #777; font-size: 0.85rem; border-top: 1px solid var(--hairline); margin-top: 60px; }
+.footer code { background: #f1ebe0; padding: 1px 5px; border-radius: 3px; }
+
+@media (max-width: 600px) {
+  .hero { padding: 48px 20px 24px; }
+  .container { padding: 32px 18px; }
+  .controls { gap: 14px; padding: 10px 12px; }
+  .controls input[type=range] { width: 140px; }
+}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..11e07ef
--- /dev/null
+++ b/server.js
@@ -0,0 +1,102 @@
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = process.env.PORT || 9826;
+const ROOT = __dirname;
+const DATA_DIR = path.join(ROOT, 'data');
+
+const app = express();
+app.use(express.json({ limit: '1mb' }));
+app.use(express.static(path.join(ROOT, 'public')));
+
+const loadJson = (file) => JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8'));
+
+app.get('/healthz', (_req, res) => res.json({ ok: true, ts: new Date().toISOString() }));
+
+app.get('/api/brands', (_req, res) => {
+  try { res.json(loadJson('brands.json')); }
+  catch (err) { res.status(500).json({ ok: false, error: err.message }); }
+});
+
+app.get('/api/brands/:id', (req, res) => {
+  try {
+    const data = loadJson('brands.json');
+    const brand = data.brands.find(b => b.id === req.params.id);
+    if (!brand) return res.status(404).json({ ok: false, error: 'brand not found' });
+    res.json(brand);
+  } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
+});
+
+app.get('/api/pantone', (_req, res) => {
+  try { res.json(loadJson('pantone.json')); }
+  catch (err) { res.status(500).json({ ok: false, error: err.message }); }
+});
+
+app.get('/api/common-colors', (_req, res) => {
+  try {
+    const brands = loadJson('brands.json').brands;
+    const buckets = {};
+    const roleOrder = ['primary', 'secondary', 'accent', 'ink', 'ground', 'neutral'];
+
+    brands.forEach(b => {
+      b.palette.forEach(c => {
+        if (!c.hex) return;
+        const family = colorFamily(c.hex);
+        if (!buckets[family]) buckets[family] = { family, count: 0, colors: [] };
+        buckets[family].count += 1;
+        buckets[family].colors.push({ hex: c.hex, name: c.name, brand: b.name, role: c.role });
+      });
+    });
+
+    const sorted = Object.values(buckets).sort((a, b) => b.count - a.count);
+    res.json({ ok: true, common_color_families: sorted, role_order: roleOrder });
+  } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
+});
+
+app.get('/api/agent/status', (_req, res) => {
+  const logFile = path.join(ROOT, 'agent', 'refresh.log');
+  let lastRun = null;
+  if (fs.existsSync(logFile)) {
+    const stat = fs.statSync(logFile);
+    const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
+    lastRun = { mtime: stat.mtime, last_line: lines[lines.length - 1] || null, line_count: lines.length };
+  }
+  res.json({ ok: true, last_run: lastRun, log_path: logFile });
+});
+
+function colorFamily(hex) {
+  const h = hex.replace('#', '');
+  const r = parseInt(h.slice(0, 2), 16);
+  const g = parseInt(h.slice(2, 4), 16);
+  const b = parseInt(h.slice(4, 6), 16);
+  const max = Math.max(r, g, b);
+  const min = Math.min(r, g, b);
+  const lightness = (max + min) / 2 / 255;
+  const saturation = max === min ? 0 : (max - min) / (max + min);
+
+  if (lightness > 0.92) return 'white';
+  if (lightness < 0.08) return 'black';
+  if (saturation < 0.12) return lightness > 0.55 ? 'cream' : 'gray';
+
+  let hue;
+  const d = (max - min) || 1;
+  if (max === r) hue = ((g - b) / d) % 6;
+  else if (max === g) hue = (b - r) / d + 2;
+  else hue = (r - g) / d + 4;
+  hue = (hue * 60 + 360) % 360;
+
+  if (hue < 18 || hue >= 345) return 'red';
+  if (hue < 45) return lightness < 0.45 ? 'brown' : 'tan';
+  if (hue < 70) return 'gold';
+  if (hue < 95) return 'yellow';
+  if (hue < 165) return 'green';
+  if (hue < 200) return 'teal';
+  if (hue < 250) return 'blue';
+  if (hue < 290) return 'purple';
+  return 'pink';
+}
+
+app.listen(PORT, () => {
+  console.log(`[fashion-style-guides] listening on http://127.0.0.1:${PORT}`);
+});

(oldest)  ·  back to Fashion Style Guides  ·  add .bak/.pre- 404-guard middleware + broaden .gitignore f5ecce2 →