[object Object]

← 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

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 →