← 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
A .deploy.confA .gitignoreA agent/com.steve.fashion-style-guides-refresh.plistA agent/refresh.jsA data/brands.jsonA data/pantone.jsonA ecosystem.config.jsA package-lock.jsonA package.jsonA public/app.jsA public/index.htmlA public/styles.cssA server.js
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 →