← back to Small Business Builder
yolo: iter 2 trivial patches from claude-codex debate
980062b829f825b7f01635000d9a3f5c72a7221c · 2026-05-04 09:06:47 -0700 · yolo
Files touched
A design/north-star.mdA src/render/home.js
Diff
commit 980062b829f825b7f01635000d9a3f5c72a7221c
Author: yolo <yolo@local>
Date: Mon May 4 09:06:47 2026 -0700
yolo: iter 2 trivial patches from claude-codex debate
---
design/north-star.md | 82 +++++++++++++++
src/render/home.js | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 364 insertions(+)
diff --git a/design/north-star.md b/design/north-star.md
new file mode 100644
index 0000000..7e27e02
--- /dev/null
+++ b/design/north-star.md
@@ -0,0 +1,82 @@
+# LA Salon Directory — Design North Star
+
+Source: search-specialist competitor scan, 2026-05-04.
+
+## Positioning
+
+> Not a logistics platform (Booksy) nor a portfolio gallery (StyleSeat).
+> A **neighborhood-native editorial platform that happens to have booking.**
+> Kinfolk × Soho House × your neighborhood barber crew.
+
+Visual + neighborhood context is the moat. Booking is friction removal.
+
+## Brand
+
+| Token | Value |
+|---|---|
+| Primary | `#1A1A1A` warm black |
+| Off-white | `#F5F5F5` |
+| Headline serif | Cormorant Garamond (proxy for Lyon / Publico / Saol) |
+| Body sans | Inter |
+| Accent mono | ui-monospace (service codes, hours, neighborhood signage) |
+| **AVOID** | pastels, glassmorphism, gradients |
+
+### Per-neighborhood accent
+
+One high-saturation color per neighborhood:
+
+| Neighborhood | Accent |
+|---|---|
+| Silver Lake | `#1f6f70` teal |
+| Echo Park | `#c95a2c` burnt orange |
+| K-Town / Koreatown | `#c9a23a` gold |
+| Highland Park | `#7a3b3b` oxblood |
+| DTLA | `#3a3a3a` graphite |
+| West Hollywood | `#b6395f` rose |
+| Venice | `#2c5d8a` pacific blue |
+| Beverly Hills | `#8a7448` champagne |
+| Hollywood | `#7a2530` velvet |
+| Sherman Oaks | `#5a6e3a` olive |
+
+## Hero
+
+Full-bleed neighborhood photography. Neighborhood selector is **primary navigation, not a filter**. Subtitle: "Barber & Salon Directory. By Neighborhood."
+
+## Browse-by-neighborhood
+
+Card grid with area photography + shop count: "Silver Lake — 12 shops". Click → map view + linear top-picks editorial list. Cultural/historical narrative beats generic category sort:
+
+- "Silver Lake's Legacy Cuts"
+- "K-Town Fades"
+- "East Side Classics"
+
+## Salon-detail page
+
+- Hero: barber at work or interior shot — **not** a service-photo carousel
+- Byline-style intro: 1–2 sentences on who owns this, why they matter (editorial voice)
+- Services as visual chips, not dropdowns (fades, beard work, hot-towel shaves, balayage, manicure, pedicure, brow shaping, lash extension)
+- 4–6 photos organized by mood (workspace / result / community)
+- Booking CTA prominent but secondary to editorial
+
+## Booking flow
+
+3 steps max: Select Service → Select Time → Confirm. OAuth (Google/Apple), no form fields until payment. Inline availability, not modal.
+
+## Anchor tenants — homepage shortlist
+
+| # | Shop | Neighborhood | Vibe |
+|---|---|---|---|
+| 1 | Sweeney Todd's (1947) | Hollywood | 1950s time capsule, oldest continuously operating shop in LA |
+| 2 | Fellow Barber Silver Lake | Silver Lake | Brutalist mid-century in a converted 1920s auto-body shop |
+| 3 | Blind Barber Highland Park | Highland Park | Mod '70s retro, starlit dome ceiling |
+| 4 | No Filter Barbershop | Echo Park | East-side cool, 10+ yrs, community vibe |
+| 5 | Bolt Barbers | DTLA | Voted best DTLA, urban revitalization anchor |
+| 6 | House of AANUKO | Culver City | Design-forward, K-Town-adjacent |
+| 7 | The Fade Inn | West Hollywood | Upscale WeHo polish |
+| 8 | Service & Supply | Venice | Surf-adjacent cool, community-first |
+
+## Inspirational non-salon brands
+
+- **Kinfolk** — minimalist editorial discipline, whitespace as power
+- **Soho House app** — membership-era design authority, neighborhood clubhouse
+- **Monocle** — global-local curation, sophisticated serif+sans pairing
diff --git a/src/render/home.js b/src/render/home.js
new file mode 100644
index 0000000..8ba0b51
--- /dev/null
+++ b/src/render/home.js
@@ -0,0 +1,282 @@
+// Public editorial homepage — LA salon directory.
+// North star: Kinfolk × Soho House × neighborhood barber crew.
+// Neighborhood-led primary nav, Barber/Beard featured first, no booking-platform vibes.
+
+import { esc } from '../lib/escape.js';
+
+// Per-neighborhood accent palette from design/north-star.md.
+const NEIGHBORHOOD_ACCENT = {
+ 'Silver Lake': '#1f6f70',
+ 'Echo Park': '#c95a2c',
+ 'Koreatown': '#c9a23a',
+ 'K-Town': '#c9a23a',
+ 'Highland Park': '#7a3b3b',
+ 'DTLA': '#3a3a3a',
+ 'West Hollywood': '#b6395f',
+ 'Venice': '#2c5d8a',
+ 'Beverly Hills': '#8a7448',
+ 'Hollywood': '#7a2530',
+ 'Sherman Oaks': '#5a6e3a',
+ 'Santa Monica': '#3a5a6e',
+ 'Culver City': '#6e5a3a',
+ 'Los Feliz': '#4a6e3a',
+ 'Westwood': '#5a3a6e',
+ 'West Adams': '#6e3a4a',
+ 'Los Angeles': '#3a3a3a',
+};
+
+const NEIGHBORHOOD_TAGLINE = {
+ 'Silver Lake': "Silver Lake's Legacy Cuts",
+ 'Echo Park': 'East Side Classics',
+ 'Highland Park': 'Highland Park Heritage',
+ 'Koreatown': 'K-Town Fades',
+ 'K-Town': 'K-Town Fades',
+ 'DTLA': 'Downtown Modern',
+ 'West Hollywood': 'WeHo Polish',
+ 'Venice': 'Salt-Air Sessions',
+ 'Beverly Hills': 'Beverly Hills Standard',
+ 'Hollywood': 'Hollywood, Established',
+ 'Sherman Oaks': 'Valley Quiet',
+ 'Santa Monica': 'Westside Routine',
+ 'Culver City': 'Culver Craft',
+ 'Los Feliz': 'Los Feliz Living',
+};
+
+// Order neighborhoods for nav by editorial priority, falling back to alphabetical.
+const NAV_ORDER = [
+ 'Silver Lake', 'Echo Park', 'Highland Park', 'Koreatown', 'DTLA',
+ 'West Hollywood', 'Venice', 'Beverly Hills', 'Hollywood',
+ 'Sherman Oaks', 'Santa Monica', 'Culver City', 'Los Feliz',
+];
+
+const CATEGORY_LABEL = {
+ barbershop: 'Barber & Beard',
+ beard: 'Barber & Beard',
+ salon: 'Hair',
+ hair: 'Hair',
+ nail: 'Nails',
+ nail_salon: 'Nails',
+ lash: 'Lash',
+ brow: 'Brow',
+ spa: 'Spa',
+ blowout: 'Blowout',
+ waxing: 'Waxing',
+ generic: 'Salon',
+};
+
+function neighborhoodSlug(n) {
+ return String(n || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+}
+
+function accent(neighborhood) {
+ return NEIGHBORHOOD_ACCENT[neighborhood] || '#1f6f70';
+}
+
+function tagline(neighborhood) {
+ return NEIGHBORHOOD_TAGLINE[neighborhood] || `${neighborhood}, by appointment`;
+}
+
+function categoryLabel(c) {
+ return CATEGORY_LABEL[c] || (c ? c[0].toUpperCase() + c.slice(1) : 'Salon');
+}
+
+// Render a single shop card. Editorial — image-led, accent rule, sparse copy.
+function renderShopCard(b) {
+ const a = accent(b.neighborhood);
+ const hero = b.hero_image_url || '';
+ const cat = categoryLabel(b.category);
+ return `
+ <a class="shop" href="/biz/${esc(b.slug)}" style="--accent:${a}">
+ <div class="shop-img">${hero ? `<img loading="lazy" src="${esc(hero)}" alt="${esc(b.name)}">` : `<div class="shop-img-placeholder"></div>`}</div>
+ <div class="shop-meta">
+ <span class="shop-cat">${esc(cat)}</span>
+ <h3 class="shop-name">${esc(b.name)}</h3>
+ <p class="shop-loc">${esc(b.neighborhood || b.city || 'Los Angeles')}</p>
+ </div>
+ </a>`;
+}
+
+function renderNeighborhoodTile(n) {
+ const a = accent(n.neighborhood);
+ const slug = neighborhoodSlug(n.neighborhood);
+ return `
+ <a class="hood" href="/n/${esc(slug)}" style="--accent:${a}">
+ <span class="hood-name">${esc(n.neighborhood)}</span>
+ <span class="hood-meta"><em>${esc(tagline(n.neighborhood))}</em> · ${n.count} shop${n.count === 1 ? '' : 's'}</span>
+ </a>`;
+}
+
+// businesses: full list, neighborhoods: [{neighborhood, count}], featuredBarbers: shops where category in barbershop/beard
+export function renderHome({ featuredBarbers = [], otherShops = [], neighborhoods = [], totalCount = 0 } = {}) {
+ // Sort neighborhood list by editorial priority, then by count desc, then alphabetical
+ const orderIdx = (n) => {
+ const i = NAV_ORDER.indexOf(n);
+ return i === -1 ? 999 : i;
+ };
+ const sortedHoods = [...neighborhoods].sort((a, b) => {
+ const ai = orderIdx(a.neighborhood);
+ const bi = orderIdx(b.neighborhood);
+ if (ai !== bi) return ai - bi;
+ if (a.count !== b.count) return b.count - a.count;
+ return a.neighborhood.localeCompare(b.neighborhood);
+ });
+
+ return `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>LA Barber & Salon Directory · By Neighborhood</title>
+<meta name="description" content="Barber, beard, hair, nails, lashes, brows. By neighborhood. Curated.">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
+<style>
+:root {
+ --ink:#1a1a1a;
+ --paper:#f5f5f5;
+ --rule:#e6e3dc;
+ --mute:#7a766f;
+ --accent:#1f6f70;
+ --serif:"Cormorant Garamond",Georgia,serif;
+ --sans:"Inter",-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
+ --mono:ui-monospace,"SF Mono",Menlo,monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html,body{background:var(--paper);color:var(--ink);font-family:var(--sans);font-weight:400;-webkit-font-smoothing:antialiased}
+a{color:inherit;text-decoration:none}
+img{display:block;max-width:100%;height:auto}
+
+/* ==== Topbar ==================================================== */
+.topbar{padding:22px 32px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rule);position:sticky;top:0;background:var(--paper);z-index:50}
+.brand{font-family:var(--serif);font-size:22px;letter-spacing:-.01em;font-weight:500}
+.brand em{font-style:italic;color:var(--accent)}
+.nav-mini{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);display:flex;gap:24px}
+.nav-mini a:hover{color:var(--ink)}
+
+/* ==== Hero ====================================================== */
+.hero{padding:120px 32px 80px;border-bottom:1px solid var(--rule);max-width:1240px;margin:0 auto}
+.hero-eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--mute);margin-bottom:20px}
+.hero h1{font-family:var(--serif);font-weight:500;font-size:clamp(48px,7.5vw,108px);line-height:.96;letter-spacing:-.02em;margin-bottom:28px;max-width:14ch}
+.hero h1 em{font-style:italic;color:var(--accent)}
+.hero-lede{font-size:18px;line-height:1.55;color:#3a3a3a;max-width:54ch;margin-bottom:48px}
+.hero-stat{display:flex;gap:48px;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
+.hero-stat b{display:block;font-family:var(--serif);font-style:italic;font-size:34px;letter-spacing:-.01em;color:var(--ink);font-weight:500;margin-top:4px;text-transform:none}
+
+/* ==== Section heading =========================================== */
+.section{padding:80px 32px;max-width:1240px;margin:0 auto}
+.section-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:40px;border-bottom:1px solid var(--rule);padding-bottom:18px}
+.section-head h2{font-family:var(--serif);font-weight:500;font-size:clamp(32px,4vw,52px);letter-spacing:-.015em;line-height:1}
+.section-head .kicker{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--mute)}
+
+/* ==== Neighborhood tiles ======================================== */
+.hoods{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1px;background:var(--rule);border:1px solid var(--rule)}
+.hood{background:var(--paper);padding:28px 24px;display:flex;flex-direction:column;justify-content:flex-end;min-height:180px;border-left:4px solid var(--accent);transition:background 200ms ease,transform 200ms ease}
+.hood:hover{background:#fff;transform:translateY(-2px)}
+.hood-name{font-family:var(--serif);font-size:28px;font-weight:500;letter-spacing:-.01em;line-height:1.1;margin-bottom:6px}
+.hood-meta{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
+.hood-meta em{font-style:italic;color:var(--accent);text-transform:none;letter-spacing:0;font-family:var(--serif);font-size:13px}
+
+/* ==== Shop grid ================================================= */
+.shops{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:32px}
+.shop{display:block;color:var(--ink)}
+.shop-img{aspect-ratio:4/5;overflow:hidden;background:#222;margin-bottom:14px;position:relative}
+.shop-img img{width:100%;height:100%;object-fit:cover;transition:transform 600ms ease}
+.shop:hover .shop-img img{transform:scale(1.04)}
+.shop-img-placeholder{position:absolute;inset:0;background:linear-gradient(135deg,var(--ink) 0%,#2a2a2a 100%);display:flex;align-items:center;justify-content:center}
+.shop-img-placeholder::before{content:"";width:1px;height:36%;background:var(--accent)}
+.shop-cat{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin-bottom:6px;display:inline-block}
+.shop-name{font-family:var(--serif);font-size:22px;font-weight:500;letter-spacing:-.01em;line-height:1.15;margin-bottom:4px}
+.shop-loc{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--mute)}
+
+/* ==== Manifesto strip =========================================== */
+.manifesto{padding:80px 32px;background:var(--ink);color:var(--paper);border-top:1px solid var(--ink)}
+.manifesto-inner{max-width:880px;margin:0 auto}
+.manifesto p{font-family:var(--serif);font-style:italic;font-size:clamp(28px,3.5vw,42px);line-height:1.25;letter-spacing:-.01em;font-weight:400}
+.manifesto p em{color:#c9a23a;font-style:normal}
+.manifesto .sig{font-family:var(--mono);font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:#aaa;margin-top:32px}
+
+/* ==== Footer ==================================================== */
+.footer{padding:48px 32px;border-top:1px solid var(--rule);font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--mute);display:flex;justify-content:space-between;flex-wrap:wrap;gap:16px}
+
+@media (max-width:720px){
+ .hero{padding:64px 24px 48px}
+ .section{padding:48px 24px}
+ .topbar{padding:16px 20px}
+ .nav-mini{display:none}
+ .hero-stat{flex-wrap:wrap;gap:24px}
+}
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <div class="brand">LA <em>Salon</em> Directory</div>
+ <nav class="nav-mini">
+ <a href="#neighborhoods">Neighborhoods</a>
+ <a href="#barber">Barber & Beard</a>
+ <a href="#all">All Shops</a>
+ </nav>
+</header>
+
+<section class="hero">
+ <div class="hero-eyebrow">Barber · Beard · Hair · Nails · Lash · Brow · Spa</div>
+ <h1>Barber & Salon, <em>by neighborhood.</em></h1>
+ <p class="hero-lede">A directory of the LA shops worth knowing — the legacy cuts, the East-side classics, the K-Town fades, the WeHo polish. Curated by the people who actually live here. Booking optional. Authority required.</p>
+ <div class="hero-stat">
+ <div>Shops indexed<b>${totalCount}</b></div>
+ <div>Neighborhoods<b>${neighborhoods.length}</b></div>
+ <div>Categories<b>${new Set(featuredBarbers.concat(otherShops).map(s => s.category).filter(Boolean)).size}</b></div>
+ </div>
+</section>
+
+<section class="section" id="neighborhoods">
+ <div class="section-head">
+ <h2>Browse by neighborhood</h2>
+ <span class="kicker">${sortedHoods.length} areas covered</span>
+ </div>
+ <div class="hoods">
+ ${sortedHoods.map(renderNeighborhoodTile).join('')}
+ </div>
+</section>
+
+${featuredBarbers.length ? `
+<section class="section" id="barber">
+ <div class="section-head">
+ <h2>Barber & Beard</h2>
+ <span class="kicker">Top of the list</span>
+ </div>
+ <div class="shops">
+ ${featuredBarbers.map(renderShopCard).join('')}
+ </div>
+</section>
+` : ''}
+
+<section class="manifesto">
+ <div class="manifesto-inner">
+ <p>Not a booking platform. Not a portfolio gallery. <em>An editorial directory</em> for the LA shops worth your standing appointment.</p>
+ <div class="sig">— Editor's note</div>
+ </div>
+</section>
+
+${otherShops.length ? `
+<section class="section" id="all">
+ <div class="section-head">
+ <h2>The full list</h2>
+ <span class="kicker">Hair · Nails · Lash · Brow · Spa</span>
+ </div>
+ <div class="shops">
+ ${otherShops.map(renderShopCard).join('')}
+ </div>
+</section>
+` : ''}
+
+<footer class="footer">
+ <span>LA Salon Directory · ${new Date().getFullYear()}</span>
+ <span>Curated, not crowdsourced</span>
+ <span><a href="/admin">Admin</a> · <a href="/builder">Builder</a></span>
+</footer>
+
+</body>
+</html>`;
+}
← ff56b97 yolo: baseline before overnight
·
back to Small Business Builder
·
yolo: iter 3 trivial patches from claude-codex debate 035afe2 →