[object Object]

← back to Professional Directory

add apply-templates.js — deterministic mockup template-rotation

a5d87d572afdd96ec6f3b6f7e84350f5274c35f2 · 2026-05-02 06:59:31 -0700 · Steve

Companion to generate-mockups.js (LLM-driven). This script applies the
10 hand-built design templates (a..j) from
data/mockups/{22006,32287,32309}-{a..j}.html to a list of new businesses,
picking 3 designs per business via a prime-step rotation:

  - no two adjacent businesses share the same trio
  - each business's three designs are different from each other
  - across the run, all 10 design directions get used roughly evenly

Specialty-aware copy hooks (HOOKS) keep the variant text reading native
to each business type (nail / hair / lash / aesthetics / dds) instead of
robotic Mad-Libs substitution. Each specialty has its own:
  - hero line, kicker, story title + 2 paragraphs
  - 5 services with prices and provider notes
  - quote + author
  - book-cadence sentence, keyword tags, type label

First run output: 10 businesses × 3 designs = 30 mockups in 0.3 sec.
Add to BUSINESSES array and re-run to scale further.

Run: node scripts/apply-templates.js

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a5d87d572afdd96ec6f3b6f7e84350f5274c35f2
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat May 2 06:59:31 2026 -0700

    add apply-templates.js — deterministic mockup template-rotation
    
    Companion to generate-mockups.js (LLM-driven). This script applies the
    10 hand-built design templates (a..j) from
    data/mockups/{22006,32287,32309}-{a..j}.html to a list of new businesses,
    picking 3 designs per business via a prime-step rotation:
    
      - no two adjacent businesses share the same trio
      - each business's three designs are different from each other
      - across the run, all 10 design directions get used roughly evenly
    
    Specialty-aware copy hooks (HOOKS) keep the variant text reading native
    to each business type (nail / hair / lash / aesthetics / dds) instead of
    robotic Mad-Libs substitution. Each specialty has its own:
      - hero line, kicker, story title + 2 paragraphs
      - 5 services with prices and provider notes
      - quote + author
      - book-cadence sentence, keyword tags, type label
    
    First run output: 10 businesses × 3 designs = 30 mockups in 0.3 sec.
    Add to BUSINESSES array and re-run to scale further.
    
    Run: node scripts/apply-templates.js
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 scripts/apply-templates.js | 323 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 323 insertions(+)

diff --git a/scripts/apply-templates.js b/scripts/apply-templates.js
new file mode 100644
index 0000000..118e14f
--- /dev/null
+++ b/scripts/apply-templates.js
@@ -0,0 +1,323 @@
+#!/usr/bin/env node
+/**
+ * apply-templates.js — deterministic template-rotation mockup generator.
+ *
+ * Distinct from generate-mockups.js (which calls a local LLM per org). This
+ * script applies the 10 hand-built design templates (a..j) from
+ * data/mockups/{22006,32287,32309}-{a..j}.html to a list of new businesses,
+ * picking 3 designs per business via a prime-step rotation so:
+ *   - no two businesses share the same trio
+ *   - each business's three designs are different from each other
+ *   - across the run, all 10 designs get used roughly evenly
+ *
+ * Specialty-aware copy hooks (HOOKS) keep the variant text reading native to
+ * each business type (nail, hair, lash, aesthetics, dds) instead of
+ * Mad-Libs-style robotic substitution.
+ *
+ * Usage: node scripts/apply-templates.js
+ *   Writes <id>-<a..j>.html files into data/mockups/
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..', 'data', 'mockups');
+
+const BUSINESSES = [
+  { id: '73302', name: 'Oasis Nail Spa',          city: 'Burbank',          st: 'CA', zip: '91505', addr: '3119 W Magnolia Blvd',   phone: '(818) 845-0112', specialty: 'nail' },
+  { id: '73327', name: 'Union Salon',             city: 'Pasadena',         st: 'CA', zip: '91103', addr: '57 N Hill Ave',          phone: '(626) 304-9700', specialty: 'hair' },
+  { id: '73330', name: 'Collective Hair',         city: 'Pasadena',         st: 'CA', zip: '91101', addr: '85 N Raymond Ave',       phone: '(626) 535-9595', specialty: 'hair' },
+  { id: '73331', name: "Nail'd It London",        city: 'Pasadena',         st: 'CA', zip: '91101', addr: '24 W Colorado Blvd',     phone: '(626) 510-0085', specialty: 'nail' },
+  { id: '74415', name: 'Hair by Casey Smernoff',  city: 'Long Beach',       st: 'CA', zip: '90803', addr: '5366 E 2nd St',          phone: '(562) 433-7144', specialty: 'hair' },
+  { id: '74459', name: 'Blink Bliss Lash',        city: 'Torrance',         st: 'CA', zip: '90505', addr: '24631 Crenshaw Blvd',    phone: '(310) 530-2255', specialty: 'lash' },
+  { id: '74798', name: 'Skintimacy Aesthetics',   city: 'Santa Clarita',    st: 'CA', zip: '91355', addr: '24655 Town Center Dr',   phone: '(661) 388-4400', specialty: 'aesthetics' },
+  { id: '74833', name: 'Amanda Taylor Esthetics', city: 'Westlake Village', st: 'CA', zip: '91361', addr: '4854 E Thousand Oaks Bl',phone: '(805) 449-7100', specialty: 'aesthetics' },
+  { id: '74944', name: 'Ashley G at Hairitage',   city: 'Los Angeles',      st: 'CA', zip: '90046', addr: '7660 Beverly Blvd',      phone: '(323) 651-1111', specialty: 'hair' },
+  { id: '75645', name: 'Ian Hamel, DDS Inc.',     city: 'Burbank',          st: 'CA', zip: '91505', addr: '2625 W Alameda Ave',     phone: '(818) 841-3601', specialty: 'dds' },
+];
+
+const HOOKS = {
+  nail: {
+    nounSing: 'manicure', kicker: 'A neighborhood nail studio',
+    heroLine: 'Hands you took care of someone with.',
+    storyTitle: 'Why we use only 5-free polish',
+    storyP1: 'Most nail polish on the market still contains formaldehyde, toluene, DBP, formaldehyde resin, and camphor — five ingredients linked to long-term liver and reproductive harm. The salon down the block from us uses them because they cost half as much per bottle.',
+    storyP2: "We don't. Every bottle in this studio is 5-free or 7-free, screened against the EWG Skin Deep database. That choice cost us about $400 a month in supplies the first year. It bought us patients who drive in from Glendale because their dermatologist sent them.",
+    services: [
+      { name: 'Classic manicure', sub: 'Shape, cuticle, polish — 30 min', price: '$28', who: '30 min · Esme' },
+      { name: 'Gel manicure', sub: '5-free gel · 2-week wear', price: '$45', who: '45 min · Esme' },
+      { name: 'Spa pedicure', sub: 'Soak, scrub, mask, massage, polish', price: '$55', who: '60 min · Esme' },
+      { name: 'Mani + pedi combo', sub: 'Both services back-to-back', price: '$70', who: '90 min · Esme' },
+      { name: 'Nail art per finger', sub: 'Hand-painted, freehand designs', price: '+$5', who: '5 min · Marina' },
+    ],
+    quote: '"They use the products my dermatologist actually recommends. I have not had a contact reaction once in three years here."',
+    quoteAuthor: '— Diana K., patient since 2021',
+    bookCadence: 'Walk-ins welcome when a chair is open. Otherwise book 2-3 days out.',
+    keywords: ['5-FREE', 'NON-TOXIC', 'EWG VERIFIED', 'NEIGHBORHOOD', 'BY APPOINTMENT'],
+    typeLabel: 'NAIL STUDIO',
+  },
+  hair: {
+    nounSing: 'cut', kicker: 'A small hair salon',
+    heroLine: 'Hair you walk out feeling like yourself in.',
+    storyTitle: 'Why we kept the chair count at four',
+    storyP1: 'Most salons in this town now run six to ten chairs with rotating stylists. The math works on paper. It also produces the haircut equivalent of a chain-restaurant entree: technically correct, totally forgettable.',
+    storyP2: 'We stayed at four chairs. Every stylist owns their book. The same person cuts your hair every visit. We charge what that costs ($95 for a cut, $180 for color), and we see roughly 25 clients a week per stylist, total. That is the bar we will defend.',
+    services: [
+      { name: 'Haircut + style', sub: 'Consultation, cut, blow-dry', price: '$95', who: '60 min · Casey' },
+      { name: 'Single-process color', sub: 'Root touch-up or all-over', price: '$180', who: '90 min · Casey' },
+      { name: 'Highlights / balayage', sub: 'Hand-painted, dimensional', price: '$280', who: '3 hr · Casey' },
+      { name: 'Color correction', sub: 'Brassy to neutral, fixing chain-job', price: 'From $400', who: '4-6 hr · Casey' },
+      { name: 'Bridal trial + day-of', sub: 'Trial run + wedding-day style', price: '$425', who: '3 hr trial' },
+    ],
+    quote: '"First time in years I left a salon and did not spend an hour at home re-doing it."',
+    quoteAuthor: '— Marisa K., longtime client',
+    bookCadence: 'Saturdays book 3 weeks out. Weekday slots open 5-7 days ahead.',
+    keywords: ['SMALL CHAIRS', 'OWNER-OPERATED', 'BOOK YOUR STYLIST', 'BY APPOINTMENT'],
+    typeLabel: 'HAIR SALON',
+  },
+  lash: {
+    nounSing: 'lash set', kicker: 'A specialty lash studio',
+    heroLine: 'Lashes that move when you blink. Not a costume.',
+    storyTitle: 'Why we will not do mega-volume',
+    storyP1: 'Mega-volume is what most lash bars push because they charge double. The technique fans 8-14 thin lashes onto each natural lash, which looks dramatic in selfies and destroys your real lashes within six months. We have rebuilt enough lash lines that we said no.',
+    storyP2: 'What we do instead: classic 1:1 sets and considered hybrid sets. The look is more natural, the cost is half what mega-volume runs at, and your real lashes stay intact. If you walked in asking for "lots of volume" we will probably talk you down to a 0.10 hybrid.',
+    services: [
+      { name: 'Classic full set', sub: '1:1 application, natural look', price: '$165', who: '90 min' },
+      { name: 'Hybrid full set', sub: 'Mix of classic + light volume', price: '$215', who: '2 hr' },
+      { name: 'Fill within 3 weeks', sub: 'Maintenance, classic or hybrid', price: '$85', who: '60 min' },
+      { name: 'Lash lift + tint', sub: 'No extensions — your own lashes', price: '$110', who: '60 min' },
+      { name: 'Lash removal', sub: 'Safe gel-based removal, no damage', price: '$45', who: '30 min' },
+    ],
+    quote: '"I had three years of mega-volume damage. Six weeks of lash lifts here and my real lashes are back."',
+    quoteAuthor: '— Anika P., recovering client',
+    bookCadence: 'Fills must be within 3 weeks of last appointment. Walk-ins not accepted.',
+    keywords: ['NO MEGA-VOLUME', 'CLASSIC + HYBRID', 'BY APPOINTMENT', 'RETENTION-FOCUSED'],
+    typeLabel: 'LASH STUDIO',
+  },
+  aesthetics: {
+    nounSing: 'facial', kicker: 'A medical-grade aesthetics studio',
+    heroLine: 'Skin you do not have to filter.',
+    storyTitle: 'Why we will not sell you a package',
+    storyP1: 'Most aesthetics studios push 6 and 12-treatment packages because the math heavily favors the studio (cancelled = forfeited). It also creates pressure to oversell treatments you do not need.',
+    storyP2: 'We sell single sessions only. Every visit starts with a 15-minute skin assessment under our magnifying lamp, and the recommendation that comes out is honest: you may need three more visits, you may need none. Pricing is published. Surprises are not part of the protocol.',
+    services: [
+      { name: 'Custom signature facial', sub: 'Tailored to your skin that day', price: '$165', who: '75 min' },
+      { name: 'Chemical peel', sub: 'TCA, glycolic, or salicylic', price: '$140', who: '45 min' },
+      { name: 'Microneedling', sub: 'Collagen induction, 0.5-1.5mm', price: '$285', who: '90 min' },
+      { name: 'Dermaplaning + peel', sub: 'Manual exfoliation + acid layer', price: '$185', who: '60 min' },
+      { name: 'Skin assessment only', sub: 'No treatment, just consultation', price: '$65', who: '30 min' },
+    ],
+    quote: '"She told me NOT to buy three of the products she stocks. That is when I knew this was different."',
+    quoteAuthor: '— Stephanie R., 2-year client',
+    bookCadence: 'Initial assessment recommended before first treatment. Slots typically 4-7 days out.',
+    keywords: ['NO PACKAGES', 'SINGLE-SESSION ONLY', 'PUBLISHED PRICING', 'MEDICAL-GRADE'],
+    typeLabel: 'AESTHETICS',
+  },
+  dds: {
+    nounSing: 'cleaning', kicker: 'A neighborhood dental practice',
+    heroLine: 'A dentist who looks at you, not at your phone screen.',
+    storyTitle: 'Why we run on schedule',
+    storyP1: 'If your dentist is running 45 minutes behind it is because they overbooked. Three patients in a chair at the same time, hygienists rotating between them, dentist popping in for the 4-minute exam at the end. It is the strip-mall-dentistry model and it produces predictable outcomes: rushed exams, missed early decay, sold restorations you did not need.',
+    storyP2: 'We see one patient at a time. Cleanings are 60 minutes, exams are 30 minutes with the doctor in the room the whole time. It costs us throughput. It buys us the ability to actually see what is happening in your mouth before it becomes a $2,400 problem.',
+    services: [
+      { name: 'Comprehensive exam + cleaning', sub: 'X-rays, oral cancer screen, full perio', price: '$245', who: '90 min · Dr. Hamel' },
+      { name: 'Cleaning only', sub: 'Routine 6-month cleaning', price: '$135', who: '60 min · hygienist' },
+      { name: 'Composite filling', sub: 'Tooth-colored, single tooth', price: 'From $215', who: '60 min · Dr. Hamel' },
+      { name: 'Crown via CEREC', sub: 'In-house, same-day delivery', price: '$1,485', who: '2 visits' },
+      { name: 'Pediatric exam', sub: 'Children 4 and up', price: '$165', who: '45 min' },
+    ],
+    quote: '"I waited 28 years to find a dentist I trusted. He has had me for 12 years now."',
+    quoteAuthor: '— Robert C., longtime patient',
+    bookCadence: 'New-patient comprehensive exam books 2-3 weeks out. Cleanings 5-7 days.',
+    keywords: ['ON SCHEDULE', 'IN-HOUSE CEREC', 'NO UPSELLING', 'INSURANCE ACCEPTED'],
+    typeLabel: 'DENTAL PRACTICE',
+  },
+};
+
+const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
+const phoneTel = p => p.replace(/[^\d]/g, '');
+
+// ── 10 templates ──────────────────────────────────────────────────────
+
+function tplA(b) {
+  const h = HOOKS[b.specialty];
+  const treatRows = h.services.map(s => `<div class="t"><h3>${esc(s.name)}</h3><p>${esc(s.sub)}</p><span class="pr">${esc(s.price)}</span></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name)} — ${esc(b.city)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400&family=Inter:wght@300;400&display=swap" rel="stylesheet">
+<style>:root{--ink:#1a1a18;--paper:#f4f1ea;--paper-2:#ebe6dc;--rule:#b8a888;--accent:#6b3410;--quiet:#6b6358}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;line-height:1.7;font-size:17px}h1,h2,h3{font-family:'Cormorant Garamond',serif;font-weight:300;line-height:1.1}.banner{border-bottom:1px solid var(--ink);padding:14px 40px;display:flex;justify-content:space-between;align-items:baseline;font-size:12px;text-transform:uppercase;letter-spacing:.18em}.banner .name{font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:0;text-transform:none;font-style:italic}.banner .vol{color:var(--quiet)}.hero{padding:80px 40px 60px;max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 380px;gap:60px;align-items:end}.hero h1{font-size:clamp(48px,7vw,92px);margin:0;font-style:italic}.hero .kicker{font-size:13px;text-transform:uppercase;letter-spacing:.22em;color:var(--accent);margin:0 0 20px}.hero .lede{font-size:20px;line-height:1.5;margin-top:28px;max-width:540px}.hero .meta{border-top:1px solid var(--rule);padding-top:16px}.hero .meta dt{font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--quiet);margin-top:14px}.hero .meta dd{margin:4px 0 0;font-size:16px}.story{max-width:720px;margin:100px auto 80px;padding:0 40px}.story h2{font-size:36px;margin:0 0 36px;font-style:italic;text-align:center;color:var(--accent)}.story h2::before,.story h2::after{content:" ✦ ";color:var(--rule);font-size:18px}.story p{margin:0 0 22px}.story p:first-of-type::first-letter{font-family:'Cormorant Garamond',serif;font-size:64px;float:left;line-height:.85;margin:6px 12px 0 0;color:var(--accent)}.svc{background:var(--paper-2);padding:80px 40px}.svc-i{max-width:880px;margin:0 auto}.svc h2{font-size:42px;margin:0 0 8px;font-style:italic}.svc .sub{color:var(--quiet);margin:0 0 48px;font-size:15px;text-transform:uppercase;letter-spacing:.15em}.t{border-top:1px solid var(--rule);padding:28px 0;display:grid;grid-template-columns:240px 1fr 100px;gap:40px;align-items:baseline}.t:last-child{border-bottom:1px solid var(--rule)}.t h3{font-size:22px;margin:0;font-style:italic}.t p{margin:0;color:var(--quiet);font-size:15px}.t .pr{font-family:'Cormorant Garamond',serif;font-size:24px;text-align:right;color:var(--accent)}.q{max-width:760px;margin:100px auto;padding:0 40px;text-align:center}.q blockquote{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:clamp(28px,4vw,42px);line-height:1.3;margin:0 0 28px}.q cite{font-style:normal;font-size:13px;text-transform:uppercase;letter-spacing:.18em;color:var(--quiet)}.visit{background:var(--ink);color:var(--paper);padding:80px 40px}.visit-i{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 1fr;gap:80px}.visit h2{font-size:56px;margin:0 0 24px;font-style:italic}.visit a.cta{display:inline-block;margin-top:24px;padding:16px 28px;border:1px solid var(--paper);color:var(--paper);text-decoration:none;font-size:13px;text-transform:uppercase;letter-spacing:.18em}.visit dl{margin:0}.visit dt{font-size:11px;text-transform:uppercase;letter-spacing:.2em;color:rgba(244,241,234,.6);margin-top:24px}.visit dd{margin:6px 0 0;font-family:'Cormorant Garamond',serif;font-size:22px;font-style:italic}footer{padding:40px;text-align:center;font-size:12px;text-transform:uppercase;letter-spacing:.18em;color:var(--quiet)}@media(max-width:720px){.hero,.visit-i{grid-template-columns:1fr;gap:40px}.t{grid-template-columns:1fr;gap:12px}.t .pr{text-align:left}}</style></head><body>
+<header class="banner"><span class="vol">${esc(h.typeLabel)} · ${esc(b.city)}</span><span class="name">${esc(b.name.toLowerCase())}.</span><span class="vol">By appointment</span></header>
+<section class="hero"><div><p class="kicker">${esc(h.kicker)}</p><h1>${esc(h.heroLine)}</h1><p class="lede">${esc(b.name)} is a small, owner-run ${esc(h.typeLabel.toLowerCase())} on ${esc(b.addr.split(' ').slice(1).join(' '))} in ${esc(b.city)}. By appointment, with the same practitioner each visit.</p></div><dl class="meta"><dt>Practice</dt><dd>${esc(b.name)}</dd><dt>Studio</dt><dd>${esc(b.addr)}, ${esc(b.city)}</dd><dt>Hours</dt><dd>Tue-Sat, by appointment</dd><dt>To book</dt><dd>${esc(b.phone)}</dd></dl></section>
+<article class="story"><h2>${esc(h.storyTitle)}</h2><p>${esc(h.storyP1)}</p><p>${esc(h.storyP2)}</p></article>
+<section class="svc"><div class="svc-i"><h2>What we do</h2><p class="sub">A short, considered menu</p>${treatRows}</div></section>
+<section class="q"><blockquote>${esc(h.quote)}</blockquote><cite>${esc(h.quoteAuthor)}</cite></section>
+<section class="visit"><div class="visit-i"><div><h2>Come visit.</h2><p>${esc(h.bookCadence)}</p><a class="cta" href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)}</a></div><dl><dt>Address</dt><dd>${esc(b.addr)}<br>${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)}</dd><dt>Hours</dt><dd>Tuesday-Saturday<br>by appointment</dd></dl></div></section>
+<footer>© ${esc(b.name)} · ${esc(b.city)}, ${esc(b.st)}</footer>
+</body></html>`;
+}
+
+function tplB(b) {
+  const h = HOOKS[b.specialty];
+  const cards = h.services.slice(0, 3).map((s, i) => `<div class="price-card${i === 1 ? ' featured' : ''}"><h3>${esc(s.name)}</h3><p class="amount">${esc(s.price)}</p><p class="freq">${esc(s.who)}</p><ul><li>${esc(s.sub)}</li><li>Same practitioner each visit</li><li>Published pricing — no upsell</li></ul><a class="cta" href="tel:${phoneTel(b.phone)}">Book this →</a></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name)} — Book a ${esc(h.nounSing)} this week</title>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>:root{--brand:#0c63e7;--brand-soft:#eaf2fe;--accent:#ff6b4a;--ink:#0b1220;--ink-2:#475467;--line:#e4e7ec;--bg:#fff;--bg-alt:#f8fafc;--ok:#16a34a}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:'Inter',sans-serif;line-height:1.55;font-size:16px}.topbar{position:sticky;top:0;z-index:50;background:var(--bg);border-bottom:1px solid var(--line);padding:12px 24px;display:flex;align-items:center;justify-content:space-between}.logo{font-weight:800;font-size:18px}.logo span{color:var(--brand)}.topbar nav{display:flex;gap:28px;font-size:14px}.topbar nav a{color:var(--ink-2);text-decoration:none}.book-now{background:var(--brand);color:#fff;padding:10px 18px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px}.hero{padding:80px 24px 60px;text-align:center;background:linear-gradient(180deg,var(--brand-soft) 0%,var(--bg) 100%)}.badge{display:inline-flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--line);padding:6px 14px;border-radius:999px;font-size:13px;color:var(--ink-2);margin-bottom:24px}.badge .dot{width:8px;height:8px;background:var(--ok);border-radius:50%}.hero h1{font-size:clamp(36px,5.5vw,60px);line-height:1.05;margin:0 0 20px;letter-spacing:-.025em;font-weight:700}.hero h1 em{font-style:normal;color:var(--brand)}.hero .lede{font-size:19px;color:var(--ink-2);max-width:620px;margin:0 auto 36px}.ctas{display:flex;justify-content:center;gap:12px;flex-wrap:wrap}.btn-primary,.btn-secondary{padding:14px 24px;border-radius:10px;text-decoration:none;font-weight:600;font-size:15px}.btn-primary{background:var(--brand);color:#fff;box-shadow:0 8px 24px rgba(12,99,231,.28)}.btn-secondary{background:#fff;color:var(--ink);border:1px solid var(--line)}.trust{padding:32px 24px;border-bottom:1px solid var(--line);text-align:center}.trust .label{font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:var(--ink-2);margin-bottom:16px}.trust-grid{display:flex;justify-content:center;gap:40px;flex-wrap:wrap;color:var(--ink-2);font-weight:500;font-size:14px}.pricing{padding:80px 24px;max-width:1100px;margin:0 auto}.pricing h2{font-size:38px;text-align:center;margin:0 0 12px;letter-spacing:-.02em}.pricing .sub{text-align:center;color:var(--ink-2);margin:0 0 48px}.price-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.price-card{background:#fff;border:1px solid var(--line);border-radius:16px;padding:32px 28px}.price-card.featured{border:2px solid var(--brand);box-shadow:0 12px 32px rgba(12,99,231,.12);position:relative}.price-card.featured::before{content:'Most booked';position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:var(--brand);color:#fff;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:600}.price-card h3{font-size:18px;margin:0 0 12px}.price-card .amount{font-size:44px;font-weight:700;margin:0 0 4px;letter-spacing:-.03em}.price-card .freq{color:var(--ink-2);font-size:14px;margin:0 0 24px}.price-card ul{list-style:none;padding:0;margin:0 0 24px}.price-card li{padding:8px 0;font-size:14px;color:var(--ink-2);display:flex;gap:10px}.price-card li::before{content:'✓';color:var(--ok);font-weight:700}.price-card .cta{display:block;text-align:center;background:var(--bg-alt);color:var(--ink);padding:12px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px}.price-card.featured .cta{background:var(--brand);color:#fff}.big-cta{background:var(--ink);color:#fff;padding:80px 24px;text-align:center}.big-cta h2{font-size:44px;margin:0 0 16px;letter-spacing:-.02em}.big-cta p{color:rgba(255,255,255,.7);max-width:540px;margin:0 auto 32px}.big-cta .btn-primary{background:var(--accent);box-shadow:0 8px 24px rgba(255,107,74,.3)}footer{background:var(--ink);color:rgba(255,255,255,.5);text-align:center;padding:28px 24px;font-size:13px}@media(max-width:720px){.topbar nav{display:none}.price-grid{grid-template-columns:1fr}}</style></head><body>
+<header class="topbar"><div class="logo">${esc(b.name.split(' ')[0].toLowerCase())} <span>${esc((b.name.split(' ')[1] || h.typeLabel).toLowerCase())}</span></div><nav><a href="#pricing">Pricing</a><a href="#book">Book</a></nav><a href="#book" class="book-now">Book online →</a></header>
+<section class="hero"><div class="badge"><span class="dot"></span>Next available: <strong style="color:var(--ink);margin-left:4px">tomorrow 2:00pm</strong></div><h1>A ${esc(h.nounSing)} <em>this</em> week.<br>Not in three weeks.</h1><p class="lede">${esc(h.heroLine)} Online booking, transparent pricing, ${esc(h.keywords.slice(0,2).join(', ').toLowerCase())}.</p><div class="ctas"><a class="btn-primary" href="tel:${phoneTel(b.phone)}">Book online →</a><a class="btn-secondary" href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)}</a></div></section>
+<section class="trust"><p class="label">What sets us apart</p><div class="trust-grid">${h.keywords.map(k => `<span>${esc(k)}</span>`).join('')}</div></section>
+<section class="pricing" id="pricing"><h2>Transparent pricing</h2><p class="sub">No surprise fees. No package upsells.</p><div class="price-grid">${cards}</div></section>
+<section class="big-cta" id="book"><h2>Ready to book?</h2><p>${esc(h.bookCadence)}</p><a class="btn-primary" href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)}</a></section>
+<footer>${esc(b.name)} · ${esc(b.addr)}, ${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)}</footer>
+</body></html>`;
+}
+
+function tplC(b) {
+  const h = HOOKS[b.specialty];
+  const items = h.services.map(s => `<div class="menu-item"><span class="name">${esc(s.name)}</span><span class="meta">${esc(s.who)} · ${esc(s.price)}</span></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name.split(' ')[0])} — ${esc(h.typeLabel)}, ${esc(b.city)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Tenor+Sans&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
+<style>:root{--canvas:#faf7f2;--ink:#1c1c1c;--quiet:#8c8a86;--terra:#c08a64;--rule:#e6e2da}*{box-sizing:border-box}body{margin:0;background:var(--canvas);color:var(--ink);font-family:'Inter',sans-serif;font-weight:300;line-height:1.6;font-size:15px}.frame{padding:24px;min-height:100vh;display:flex;flex-direction:column}.top{display:flex;justify-content:space-between;align-items:baseline;border-bottom:1px solid var(--rule);padding-bottom:16px;font-size:11px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet)}.top .brand{font-family:'Tenor Sans',serif;font-size:16px;color:var(--ink);letter-spacing:.08em}.top nav{display:flex;gap:28px}.top nav a{color:var(--quiet);text-decoration:none}.statement{flex:1;display:flex;flex-direction:column;justify-content:center;padding:80px 0}.statement h1{font-family:'Tenor Sans',serif;font-weight:400;font-size:clamp(64px,11vw,160px);line-height:.92;letter-spacing:-.02em;margin:0}.statement h1 em{font-style:italic;color:var(--terra);font-family:'Tenor Sans',serif}.statement .lede{margin:40px 0 0;max-width:480px;font-size:17px;color:var(--quiet)}.statement .ctaline{margin-top:28px;font-family:'Tenor Sans',serif;font-size:16px;text-transform:uppercase;letter-spacing:.18em}.statement .ctaline a{color:var(--ink);border-bottom:1px solid var(--ink);text-decoration:none;padding-bottom:4px}.bot{border-top:1px solid var(--rule);padding-top:16px;display:flex;justify-content:space-between;font-size:11px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet)}.menu-section{padding:120px 24px;max-width:1080px;margin:0 auto}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet);margin:0 0 16px}.menu-section h2{font-family:'Tenor Sans',serif;font-size:clamp(36px,5vw,56px);margin:0 0 56px;max-width:720px;line-height:1.1}.menu-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 64px}.menu-item{border-bottom:1px solid var(--rule);padding:18px 0;display:flex;justify-content:space-between;gap:16px;font-family:'Tenor Sans',serif;font-size:17px}.menu-item .meta{color:var(--quiet);font-size:13px;font-family:'Inter',sans-serif}.about{background:var(--ink);color:var(--canvas);padding:120px 24px;text-align:center}.about-i{max-width:680px;margin:0 auto}.about p{font-family:'Tenor Sans',serif;font-size:clamp(20px,2.4vw,26px);line-height:1.5;margin:0 0 32px}.about .signature{font-family:'Tenor Sans',serif;font-style:italic;font-size:18px;color:var(--terra)}.booking{padding:80px 24px;text-align:center}.booking h3{font-family:'Tenor Sans',serif;font-size:clamp(40px,6vw,72px);margin:0 0 16px;line-height:1}.booking .sub{color:var(--quiet);margin:0 0 32px;font-size:15px}.booking a.book{display:inline-block;padding:18px 36px;background:var(--ink);color:var(--canvas);text-decoration:none;font-family:'Tenor Sans',serif;font-size:14px;text-transform:uppercase;letter-spacing:.18em}footer.copyright{text-align:center;padding:24px;font-size:11px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet);border-top:1px solid var(--rule)}@media(max-width:720px){.menu-grid{grid-template-columns:1fr}}</style></head><body>
+<div class="frame"><header class="top"><span class="brand">${esc(b.name)}</span><nav><a href="#menu">Menu</a><a href="#about">About</a><a href="#book">Book</a></nav><span>${esc(b.city)}, California</span></header>
+<div class="statement"><h1>${esc(h.heroLine.split(' ').slice(0,3).join(' '))}<br>is <em>${esc(h.heroLine.split(' ').slice(3,5).join(' ') || 'considered')}</em>,<br>not ${esc(h.heroLine.split(' ').slice(-3).join(' '))}.</h1><p class="lede">${esc(b.name)} is a small, owner-run ${esc(h.typeLabel.toLowerCase())} on ${esc(b.addr.split(' ').slice(1).join(' '))}. ${esc(h.keywords[0])}, ${esc(h.keywords[1] ? h.keywords[1].toLowerCase() : 'by appointment')}.</p><p class="ctaline"><a href="#book">Book a ${esc(h.nounSing)} →</a></p></div>
+<footer class="bot"><span>By appointment</span><span>${esc(b.city)}, ${esc(b.st)}</span><span>${esc(b.phone)}</span></footer></div>
+<section class="menu-section" id="menu"><p class="eyebrow">The menu</p><h2>A short list, intentionally.</h2><div class="menu-grid">${items}</div></section>
+<section class="about" id="about"><div class="about-i"><p>${esc(h.quote)}</p><p class="signature">${esc(h.quoteAuthor)}</p></div></section>
+<section class="booking" id="book"><h3>Book a visit.</h3><p class="sub">${esc(h.bookCadence)}</p><a class="book" href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)}</a></section>
+<footer class="copyright">© ${esc(b.name)} · ${esc(b.addr)}, ${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)}</footer>
+</body></html>`;
+}
+
+function tplD(b) {
+  const h = HOOKS[b.specialty];
+  const rows = h.services.map((s, i) => `<div class="row grid"><span class="n">${String(i+1).padStart(2,'0')}</span><span class="nm">${esc(s.name)}</span><span class="who">${esc(s.who)}</span><span class="pr">${esc(s.price)}</span></div>`).join('');
+  const issueNum = String((parseInt(b.id, 10) % 200) + 11).padStart(3, '0');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name.split(' ')[0])} · ${esc(issueNum)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>:root{--bg:#ededeb;--ink:#0a0a0a;--rule:#c8c8c4;--mute:#555;--hot:#ff4d2e;--paper:#fafaf8}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:'Space Grotesk',sans-serif;line-height:1.4;font-size:16px}.grid{max-width:1280px;margin:0 auto;padding:0 32px;display:grid;grid-template-columns:repeat(12,1fr);gap:16px}.idx{border-bottom:1px solid var(--ink);padding:14px 0;font-family:'JetBrains Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em}.idx .a{grid-column:1/4}.idx .b{grid-column:4/9;color:var(--mute)}.idx .c{grid-column:9/13;text-align:right;color:var(--mute)}.num{padding:48px 0 24px}.num h1{grid-column:1/5;font-family:'JetBrains Mono',monospace;font-size:clamp(120px,22vw,360px);line-height:.85;margin:0;letter-spacing:-.04em}.num .meta{grid-column:6/13;align-self:end;font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--mute);text-transform:uppercase;letter-spacing:.16em}.num .meta strong{color:var(--ink);display:block;font-size:22px;margin-top:8px;text-transform:none;letter-spacing:-.01em;font-family:'Space Grotesk',sans-serif;font-weight:400}.man{padding:64px 0;border-top:1px solid var(--ink)}.man .l{grid-column:1/5;font-family:'JetBrains Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--mute)}.man .r{grid-column:5/13;font-size:clamp(28px,3.4vw,44px);line-height:1.2;margin:0}.man .r em{background:var(--hot);color:var(--paper);font-style:normal;padding:0 8px}.list{border-top:1px solid var(--ink);padding:48px 0 24px}.row{border-top:1px solid var(--rule);padding:22px 0}.row .n{grid-column:1/2;font-family:'JetBrains Mono',monospace;font-size:32px;color:var(--mute);font-weight:500}.row .nm{grid-column:2/8;font-size:22px;letter-spacing:-.015em}.row .who{grid-column:8/11;font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--mute);text-transform:uppercase;letter-spacing:.15em}.row .pr{grid-column:11/13;text-align:right;font-family:'JetBrains Mono',monospace;font-size:20px}.book{border-top:1px solid var(--ink);padding:80px 0 64px}.book .l{grid-column:1/5;font-family:'JetBrains Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--mute)}.book .r{grid-column:5/13}.book h3{font-size:clamp(40px,6vw,68px);line-height:1;letter-spacing:-.025em;margin:0 0 32px;font-weight:400}.book dl{display:grid;grid-template-columns:160px 1fr;gap:14px 24px;margin:0}.book dt{font-family:'JetBrains Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.15em;color:var(--mute)}.book dd{margin:0;font-size:17px}.book dd a{color:var(--ink);text-decoration:underline;text-underline-offset:4px;text-decoration-thickness:2px}.col{border-top:1px solid var(--ink);padding:24px 0;font-family:'JetBrains Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em}.col .a{grid-column:1/5}.col .b{grid-column:5/9;color:var(--mute)}.col .c{grid-column:9/13;text-align:right;color:var(--mute)}@media(max-width:720px){.grid{grid-template-columns:repeat(6,1fr);padding:0 20px}.idx .a,.idx .b,.idx .c,.man .l,.man .r,.row .n,.row .nm,.row .who,.row .pr,.book .l,.book .r,.col .a,.col .b,.col .c{grid-column:span 6;padding:4px 0;text-align:left}.row .pr{text-align:left}.num h1{grid-column:span 6;font-size:120px}.num .meta{grid-column:span 6}}</style></head><body>
+<div class="idx grid"><span class="a">${esc(b.name)}</span><span class="b">A document of the work · Issue ${esc(issueNum)} · ${esc(b.city)}, ${esc(b.st)}</span><span class="c">By appointment</span></div>
+<section class="num grid"><h1>${esc(issueNum)}</h1><p class="meta">Volume ${esc(issueNum.split('').join(' '))}<strong>The current menu, the current rates,<br>the current room.</strong></p></section>
+<section class="man grid"><p class="l">A note from the studio</p><p class="r">${esc(h.heroLine.split('. ')[0])}. <em>${esc(h.keywords[0])}</em>. We tell you what's in it.</p></section>
+<section class="list"><div class="grid" style="margin-bottom:24px"><p style="grid-column:1/5;font-family:'JetBrains Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--mute)">The Menu</p><p style="grid-column:5/13;font-size:24px">A short list, intentionally.</p></div>${rows}</section>
+<section class="book grid"><p class="l">Reserve a session</p><div class="r"><h3>By telephone, ideally.</h3><dl><dt>Telephone</dt><dd><a href="tel:${phoneTel(b.phone)}">${esc(b.phone)}</a> — answered Tue–Sat 10–6</dd><dt>Address</dt><dd>${esc(b.addr)} · ${esc(b.city)}</dd><dt>Cancellation</dt><dd>24 hours please. After that, half-fee.</dd></dl></div></section>
+<footer class="col grid"><span class="a">© ${esc(b.name)}</span><span class="b">${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)}</span><span class="c">Issue ${esc(issueNum)} · 2026</span></footer>
+</body></html>`;
+}
+
+function tplE(b) {
+  const h = HOOKS[b.specialty];
+  const cases = h.services.slice(0, 3).map(s => `<div class="case"><div class="case-header"><p class="who">${esc(s.name)} — ${esc(s.who)}</p><span class="meta">${esc(s.price)}</span></div><p class="summary">${esc(s.sub)}. ${esc(h.storyP1.split('. ')[0])}.</p><ul class="case-tags"><li>${esc(h.keywords[0])}</li><li>${esc(h.keywords[1] || 'By appointment')}</li></ul></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Notes from the studio · ${esc(b.name)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
+<style>:root{--paper:#fbf8f1;--paper-2:#f3eee2;--ink:#1f1d18;--quiet:#6b6358;--rule:#d8cfb8;--sepia:#8b5a2b;--green:#4a6b3a}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:'Lora',Georgia,serif;font-size:18px;line-height:1.65}.author-header{border-bottom:1px solid var(--rule);padding:32px 24px 28px}.author-header-inner{max-width:760px;margin:0 auto;display:flex;align-items:center;gap:20px}.author-avatar{width:72px;height:72px;border-radius:50%;background:linear-gradient(135deg,var(--sepia),var(--green));flex-shrink:0;display:flex;align-items:center;justify-content:center;color:var(--paper);font-family:'Lora',serif;font-style:italic;font-size:28px}.author-meta{flex:1}.author-name{font-family:'Lora',serif;font-size:22px;font-weight:600;margin:0}.author-pub{font-family:'IBM Plex Sans',sans-serif;font-size:13px;color:var(--quiet);margin:2px 0 0}.author-pub strong{color:var(--ink)}.subscribe-btn{background:var(--ink);color:var(--paper);padding:9px 18px;border-radius:999px;font-family:'IBM Plex Sans',sans-serif;font-size:13px;font-weight:500;text-decoration:none}.issue-head{max-width:720px;margin:64px auto 48px;padding:0 24px}.issue-tag{font-family:'IBM Plex Sans',sans-serif;font-size:12px;text-transform:uppercase;letter-spacing:.18em;color:var(--sepia);margin:0 0 18px;font-weight:500}.issue-head h1{font-family:'Lora',serif;font-size:clamp(36px,5vw,52px);line-height:1.1;margin:0 0 18px;font-weight:500}.issue-byline{font-family:'IBM Plex Sans',sans-serif;font-size:14px;color:var(--quiet)}.issue-byline strong{color:var(--ink);font-weight:500}.intro{max-width:660px;margin:0 auto 56px;padding:0 24px;font-size:19px;line-height:1.65}.intro p{margin:0 0 18px}.intro em{color:var(--sepia);font-style:italic}.cases{background:var(--paper-2);padding:80px 24px;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}.cases-i{max-width:760px;margin:0 auto}.cases-eyebrow{font-family:'IBM Plex Sans',sans-serif;font-size:12px;text-transform:uppercase;letter-spacing:.18em;color:var(--sepia);text-align:center;margin:0 0 12px}.cases h2{font-family:'Lora',serif;font-size:36px;text-align:center;margin:0 0 48px;font-weight:500}.case{background:var(--paper);border:1px solid var(--rule);border-radius:4px;padding:32px 36px;margin-bottom:16px}.case-header{display:flex;justify-content:space-between;align-items:baseline;border-bottom:1px solid var(--rule);padding-bottom:16px;margin-bottom:18px}.case-header .who{font-family:'Lora',serif;font-size:20px;font-style:italic;margin:0}.case-header .meta{font-family:'IBM Plex Sans',sans-serif;font-size:12px;text-transform:uppercase;letter-spacing:.15em;color:var(--quiet)}.case .summary{font-size:17px;line-height:1.6;margin:0 0 16px}.case-tags{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}.case-tags li{font-family:'IBM Plex Sans',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:var(--green);background:rgba(74,107,58,.08);padding:4px 10px;border-radius:3px}.cta-block{background:var(--ink);color:var(--paper);padding:80px 24px;text-align:center}.cta-block-i{max-width:580px;margin:0 auto}.cta-block h2{font-family:'Lora',serif;font-size:32px;margin:0 0 16px;font-weight:500}.cta-block p{color:rgba(251,248,241,.65);margin:0 0 28px;font-size:16px}.or{margin:32px 0 0;font-family:'IBM Plex Sans',sans-serif;font-size:13px;color:rgba(251,248,241,.5);text-transform:uppercase;letter-spacing:.16em}.or a{color:var(--paper);text-decoration:underline}footer{padding:32px 24px;text-align:center;font-family:'IBM Plex Sans',sans-serif;font-size:13px;color:var(--quiet);border-top:1px solid var(--rule)}</style></head><body>
+<header class="author-header"><div class="author-header-inner"><div class="author-avatar">${esc(b.name.split(' ').map(w => w[0]).join('').slice(0,2).toUpperCase())}</div><div class="author-meta"><p class="author-name">${esc(b.name)}</p><p class="author-pub"><strong>Notes from the studio</strong> · A ${esc(h.typeLabel.toLowerCase())} in ${esc(b.city)} · ${esc(b.addr)}</p></div><a class="subscribe-btn" href="#subscribe">Subscribe</a></div></header>
+<section class="issue-head"><p class="issue-tag">Issue No. 12 · The Working Notebook</p><h1>${esc(h.storyTitle)}</h1><p class="issue-byline"><strong>${esc(b.name)}</strong> · 6 min read · Updated this week</p></section>
+<div class="intro"><p>${esc(h.storyP1)}</p><p>${esc(h.storyP2)}</p></div>
+<section class="cases"><div class="cases-i"><p class="cases-eyebrow">Three of the things we do</p><h2>What we charge, what's included, who does it.</h2>${cases}</div></section>
+<section class="cta-block" id="subscribe"><div class="cta-block-i"><h2>Subscribe to Notes from the studio</h2><p>Once a month: a case study, a service breakdown, what we're learning. No marketing, ever.</p><p class="or">Ready to come in? <a href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)}</a></p></div></section>
+<footer>${esc(b.name)} · ${esc(b.addr)}, ${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)}</footer>
+</body></html>`;
+}
+
+function tplF(b) {
+  const h = HOOKS[b.specialty];
+  const featured = h.services[0];
+  const specs = h.services.slice(1, 5).map(s => `<div class="spec-card"><div class="icon">${esc(s.name[0])}</div><div><h3>${esc(s.name)}</h3><p>${esc(s.sub)}. ${esc(s.who)}.</p></div></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(featured.name)} — ${esc(b.name)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
+<style>:root{--bg:#f5f5f7;--card:#fff;--ink:#1d1d1f;--ink-2:#6e6e73;--line:#d2d2d7;--brand:#0071e3;--brand-soft:#e6f0fd;--ok:#34c759}*{box-sizing:border-box;-webkit-font-smoothing:antialiased}body{margin:0;background:var(--bg);color:var(--ink);font-family:'Inter',sans-serif;line-height:1.47;font-size:17px}.topnav{position:sticky;top:0;z-index:100;background:rgba(245,245,247,.85);backdrop-filter:blur(20px);border-bottom:1px solid var(--line);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;font-size:13px}.topnav .brand{font-weight:600}.topnav .product-name{color:var(--ink-2)}.topnav .reserve{background:var(--brand);color:#fff;padding:7px 14px;border-radius:999px;text-decoration:none;font-weight:500}.hero{text-align:center;padding:80px 24px 60px}.hero .super{font-size:14px;color:var(--brand);font-weight:500;margin:0 0 12px;text-transform:uppercase;letter-spacing:.06em}.hero h1{font-size:clamp(48px,7vw,80px);line-height:1.05;letter-spacing:-.02em;margin:0 0 16px;font-weight:700}.hero .tagline{font-size:clamp(20px,2.4vw,28px);color:var(--ink-2);margin:0 auto 32px;max-width:700px;line-height:1.3}.hero .meta{display:inline-flex;gap:24px;flex-wrap:wrap;justify-content:center;font-size:15px;color:var(--ink-2);margin-bottom:32px}.hero .meta strong{color:var(--ink);font-weight:500}.ctas{display:flex;justify-content:center;gap:12px;flex-wrap:wrap}.btn-primary,.btn-link{padding:14px 28px;border-radius:999px;text-decoration:none;font-weight:500;font-size:15px;display:inline-block}.btn-primary{background:var(--brand);color:#fff}.btn-link{color:var(--brand)}.btn-link::after{content:' →'}.visual{margin:64px auto 0;max-width:600px;height:380px;background:linear-gradient(180deg,#fff 0%,#f0f0f3 100%);border-radius:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--line)}.visual .blob{width:240px;height:240px;border-radius:50%;background:radial-gradient(circle at center,var(--brand) 0%,#0a4a99 60%,#062b5a 100%);box-shadow:inset 0 0 60px rgba(0,0,0,.4),0 20px 40px rgba(0,0,0,.15)}.spec-section{padding:100px 24px;text-align:center}.spec-section .super{font-size:13px;color:var(--brand);font-weight:500;text-transform:uppercase;letter-spacing:.06em;margin:0 0 12px}.spec-section h2{font-size:clamp(36px,5vw,56px);line-height:1.1;margin:0 0 64px;letter-spacing:-.02em;font-weight:700;max-width:800px;margin-left:auto;margin-right:auto}.spec-grid{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(2,1fr);gap:16px;text-align:left}.spec-card{background:var(--card);border-radius:18px;padding:36px 32px;display:flex;gap:20px}.spec-card .icon{width:44px;height:44px;border-radius:50%;background:var(--brand-soft);color:var(--brand);display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0}.spec-card h3{font-size:20px;margin:0 0 8px;font-weight:600}.spec-card p{margin:0;color:var(--ink-2);font-size:15px;line-height:1.55}.scarcity{background:var(--ink);color:#fff;padding:80px 24px;text-align:center}.scarcity .super{font-size:14px;color:#ff9500;text-transform:uppercase;letter-spacing:.08em;margin:0 0 12px;font-weight:500}.scarcity h2{font-size:clamp(36px,5vw,56px);line-height:1.1;letter-spacing:-.02em;margin:0 0 16px;font-weight:700}.scarcity p{color:rgba(255,255,255,.7);margin:0 auto 32px;max-width:560px;font-size:17px}.scarcity .btn-primary{background:#fff;color:var(--ink)}footer{padding:32px 24px;text-align:center;font-size:13px;color:var(--ink-2)}@media(max-width:720px){.spec-grid{grid-template-columns:1fr}.visual{height:260px}.visual .blob{width:160px;height:160px}}</style></head><body>
+<nav class="topnav"><span class="brand">${esc(b.name)}</span><span class="product-name">${esc(featured.name)}</span><a class="reserve" href="#reserve">Reserve →</a></nav>
+<section class="hero"><p class="super">A ${esc(featured.who.toLowerCase())}, single-practitioner</p><h1>${esc(featured.name)}.</h1><p class="tagline">${esc(featured.sub)}.<br>Designed for ${esc(h.heroLine.toLowerCase().replace(/\.$/, ''))}.</p><div class="meta"><span><strong>${esc(featured.price)}</strong> · published</span><span><strong>${esc(featured.who.match(/\d+\s*\w+/)?.[0] || '60 min')}</strong> · in the chair</span><span><strong>1 visit</strong> · same-day result</span></div><div class="ctas"><a class="btn-primary" href="#reserve">Reserve a slot</a><a class="btn-link" href="#how">See what's included</a></div><div class="visual"><div class="blob"></div></div></section>
+<section class="spec-section" id="how"><p class="super">What's included</p><h2>Four other services in the practice.</h2><div class="spec-grid">${specs}</div></section>
+<section class="scarcity" id="reserve"><p class="super">Limited slots</p><h2>This week.</h2><p>${esc(h.bookCadence)}</p><a class="btn-primary" href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)} to reserve</a></section>
+<footer>${esc(b.name)} · ${esc(b.addr)}, ${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)} · ${esc(b.phone)}</footer>
+</body></html>`;
+}
+
+function tplG(b) {
+  const h = HOOKS[b.specialty];
+  const rows = h.services.map((s, i) => `<div class="row"><span class="n">${String(i+1).padStart(2,'0')}</span><span class="nm">${esc(s.name)}</span><span class="who">${esc(s.who)}</span><span class="pr">${esc(s.price)}</span></div>`).join('');
+  const heroWords = h.heroLine.toUpperCase().split(' ').slice(0, 6);
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name)}!! • ${esc(h.typeLabel.toLowerCase())} • zine</title>
+<link href="https://fonts.googleapis.com/css2?family=Bungee&family=Caveat:wght@400;700&family=DM+Serif+Display&family=Space+Mono&display=swap" rel="stylesheet">
+<style>:root{--paper:#fff8e1;--ink:#1a1a1a;--hot:#ff3e3e;--lime:#bcf65c;--blue:#3232ff;--pink:#ffabd5}*{box-sizing:border-box}html,body{margin:0;padding:0;overflow-x:hidden}body{background:var(--paper);color:var(--ink);font-family:'Space Mono',monospace;font-size:15px;line-height:1.4;background-image:radial-gradient(circle,#1a1a1a08 1px,transparent 1px);background-size:18px 18px}.stickers{padding:14px 20px;background:var(--ink);color:var(--paper);display:flex;justify-content:space-between;align-items:center;border-bottom:4px solid var(--hot);font-family:'Bungee',sans-serif;font-size:11px;text-transform:uppercase;letter-spacing:.15em}.stickers .a{transform:rotate(-2deg)}.stickers .b{background:var(--hot);padding:4px 12px;color:#fff}.stickers .c{transform:rotate(1.5deg)}.hero{position:relative;padding:60px 20px 100px;min-height:80vh;overflow:visible}.hero h1{font-family:'Bungee',sans-serif;font-size:clamp(64px,15vw,200px);line-height:.85;margin:0;letter-spacing:-.04em;text-transform:uppercase;position:relative;z-index:5}.hero h1 span:nth-child(1){display:block;color:var(--hot);transform:rotate(-2deg)}.hero h1 span:nth-child(2){display:block;background:var(--ink);color:var(--lime);padding:0 .12em;transform:rotate(1deg) translateX(8%);width:fit-content;margin:8px 0}.hero h1 span:nth-child(3){display:block;color:var(--blue);transform:rotate(-1deg) translateX(2%);font-style:italic;font-family:'DM Serif Display',serif}.sticker{position:absolute;font-family:'Caveat',cursive;font-weight:700;background:var(--lime);padding:6px 14px;border:2px solid var(--ink);box-shadow:4px 4px 0 var(--ink);transform:rotate(-3deg);z-index:10;font-size:20px}.sticker.s1{top:8%;right:10%;background:var(--pink);transform:rotate(8deg)}.sticker.s2{top:60%;right:6%;background:var(--lime);transform:rotate(-5deg)}.cosmic{position:absolute;font-size:80px;z-index:1}.cosmic.c1{top:5%;left:60%;color:var(--blue);transform:rotate(15deg)}.cosmic.c2{bottom:20%;right:20%;color:var(--hot);transform:rotate(-20deg)}.menu{padding:80px 20px}.menu-h{display:flex;align-items:flex-end;gap:20px;margin-bottom:48px;flex-wrap:wrap}.menu-h h2{font-family:'Bungee',sans-serif;font-size:clamp(60px,10vw,140px);margin:0;line-height:.9}.menu-h h2 em{font-style:normal;color:var(--hot);font-family:'DM Serif Display',serif}.menu-h .sub{font-family:'Caveat',cursive;font-weight:700;font-size:32px;color:var(--blue);transform:rotate(-2deg)}.row{display:grid;grid-template-columns:60px 1fr 200px 100px;gap:20px;padding:24px 0;border-top:3px solid var(--ink);align-items:baseline}.row:last-child{border-bottom:3px solid var(--ink)}.row .n{font-family:'Bungee',sans-serif;font-size:28px;color:var(--hot)}.row .nm{font-family:'DM Serif Display',serif;font-size:24px;font-style:italic}.row .who{font-family:'Space Mono',monospace;font-size:12px;background:var(--lime);padding:4px 8px;display:inline-block;width:fit-content;text-transform:uppercase;letter-spacing:.1em}.row .pr{font-family:'Bungee',sans-serif;font-size:32px;text-align:right}.book-bar{padding:80px 20px;background:var(--lime);border-top:4px solid var(--ink);border-bottom:4px solid var(--ink);text-align:center}.book-bar h3{font-family:'Bungee',sans-serif;font-size:clamp(48px,8vw,96px);line-height:.9;margin:0 0 24px;text-transform:uppercase}.book-bar h3 em{font-style:normal;color:var(--hot)}.book-bar p{font-family:'DM Serif Display',serif;font-size:22px;max-width:520px;margin:0 auto 32px;font-style:italic}.book-bar .phone{display:inline-block;background:var(--ink);color:var(--paper);font-family:'Bungee',sans-serif;font-size:28px;padding:18px 32px;text-decoration:none;border:3px solid var(--ink);box-shadow:8px 8px 0 var(--hot);transform:rotate(-1deg)}footer{padding:30px 20px;background:var(--ink);color:var(--paper);font-family:'Space Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}@media(max-width:720px){.row{grid-template-columns:1fr;gap:8px}.row .pr{text-align:left}.sticker.s1,.sticker.s2{display:none}}</style></head><body>
+<div class="stickers"><span class="a">${esc(b.name.toUpperCase())}!!</span><span class="b">★★★ ${esc(h.keywords[0])}</span><span class="c">${esc(b.city.toUpperCase())} · ZINE NO. 12</span></div>
+<section class="hero"><span class="cosmic c1">✦</span><span class="sticker s1">!! ${esc(h.keywords[1] || 'OG SINCE 2014')} !!</span><h1><span>${esc(heroWords[0] || 'YOU')}</span><span>+ ${esc(heroWords.slice(1, 3).join(' '))}</span><span>= ${esc(heroWords.slice(3).join(' ') || 'BIG MAGIC')}</span></h1><span class="sticker s2">cash, venmo, OR a tip</span><span class="cosmic c2">✸</span></section>
+<section class="menu"><div class="menu-h"><h2>THE <em>MENU</em></h2><span class="sub">→ no packages, ever ←</span></div>${rows}</section>
+<section class="book-bar"><h3>CALL <em>FIRST.</em></h3><p>${esc(h.bookCadence)}</p><a class="phone" href="tel:${phoneTel(b.phone)}">${esc(b.phone)}</a></section>
+<footer><span>© ${esc(b.name.toLowerCase())}</span><span>${esc(b.addr.toLowerCase())}, ${esc(b.city.toLowerCase())}</span><span>zine 12 · 2026</span></footer>
+</body></html>`;
+}
+
+function tplH(b) {
+  const h = HOOKS[b.specialty];
+  const cells = h.services.slice(0, 4).map((s, i) => `<div class="cell c${i+1}"><div class="label"><small>${esc(s.who)}</small>${esc(s.name)}</div></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name)} — A film about ${esc(h.nounSing)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
+<style>:root{--ink:#0a0a0a;--paper:#fafaf7;--quiet:#888;--gold:#d4af37}*{box-sizing:border-box}body{margin:0;background:var(--ink);color:var(--paper);font-family:'Inter',sans-serif;font-weight:300;line-height:1.5;font-size:16px}.nav{position:fixed;top:0;left:0;right:0;padding:24px 32px;display:flex;justify-content:space-between;align-items:center;z-index:100;background:linear-gradient(180deg,rgba(10,10,10,.7),transparent)}.nav .brand{font-family:'Fraunces',serif;font-size:18px}.nav .actions{display:flex;gap:24px;font-size:12px;text-transform:uppercase;letter-spacing:.16em}.nav .actions a{color:var(--paper);text-decoration:none;opacity:.75}.nav .actions a.book{opacity:1;border:1px solid var(--paper);padding:8px 16px}.frame{position:relative;height:100vh;min-height:680px;width:100%;overflow:hidden}.frame .img{position:absolute;inset:0}.frame .grad{position:absolute;inset:0}.frame .meta{position:absolute;left:32px;bottom:32px;right:32px;display:flex;justify-content:space-between;align-items:flex-end;font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:rgba(250,250,247,.7)}.frame .meta .l strong{display:block;font-family:'Fraunces',serif;font-style:italic;font-size:22px;text-transform:none;letter-spacing:0;color:var(--paper);margin-bottom:6px;font-weight:400}.frame .center{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;padding:0 24px}.frame .center h1{font-family:'Fraunces',serif;font-size:clamp(56px,9vw,140px);line-height:1;letter-spacing:-.02em;font-weight:300;margin:0;max-width:1000px;font-style:italic}.frame .center h1 em{font-style:normal;color:var(--gold)}.f1 .img{background:linear-gradient(135deg,#0a0a0a 0%,#1a1f2e 30%,#2c3e50 60%,#3b5878 100%)}.f1 .grad{background:linear-gradient(180deg,rgba(10,10,10,.4) 0%,transparent 30%,transparent 70%,rgba(10,10,10,.6) 100%)}.f2 .img{background:linear-gradient(160deg,#3a2418 0%,#5e3d28 40%,#8a614a 100%)}.f2 .grad{background:linear-gradient(180deg,transparent 50%,rgba(10,10,10,.5) 100%)}.caption{padding:120px 24px;text-align:center;background:var(--paper);color:var(--ink)}.caption h2{font-family:'Fraunces',serif;font-size:clamp(32px,5vw,56px);line-height:1.15;font-weight:300;margin:0 auto 24px;max-width:780px;font-style:italic}.caption .small{font-size:12px;text-transform:uppercase;letter-spacing:.18em;color:var(--quiet)}.strip{padding:120px 0;background:var(--ink);overflow:hidden}.strip .sup{text-align:center;font-size:12px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet);margin:0 0 12px}.strip h2{font-family:'Fraunces',serif;font-size:clamp(36px,6vw,72px);line-height:1.1;text-align:center;font-style:italic;font-weight:300;margin:0 0 80px}.films{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:rgba(250,250,247,.1)}.films .cell{aspect-ratio:3/4;position:relative;display:flex;align-items:flex-end;padding:24px}.films .cell .label{font-family:'Fraunces',serif;font-style:italic;font-size:18px;text-shadow:0 2px 16px rgba(0,0,0,.5);position:relative;z-index:2;line-height:1.2}.films .cell .label small{display:block;font-family:'Inter',sans-serif;font-style:normal;font-size:10px;text-transform:uppercase;letter-spacing:.18em;opacity:.7;margin-bottom:6px}.films .cell::before{content:'';position:absolute;inset:0;background:linear-gradient(180deg,transparent 50%,rgba(0,0,0,.7) 100%);z-index:1}.films .cell.c1{background:linear-gradient(135deg,#1a2438,#3a5278)}.films .cell.c2{background:linear-gradient(160deg,#3a2418,#7a5238)}.films .cell.c3{background:linear-gradient(180deg,#1a2a1a,#3a5238)}.films .cell.c4{background:linear-gradient(220deg,#2a1a3a,#5a3878)}.closing{padding:120px 24px;background:var(--paper);color:var(--ink);text-align:center}.closing .super{font-size:12px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet);margin:0 0 12px}.closing h2{font-family:'Fraunces',serif;font-size:clamp(40px,6vw,80px);line-height:1.1;font-style:italic;font-weight:300;margin:0 0 32px}.closing .reserve{display:inline-block;background:var(--ink);color:var(--paper);padding:18px 36px;text-decoration:none;font-size:13px;text-transform:uppercase;letter-spacing:.18em;border:1px solid var(--ink)}footer{padding:32px 24px;text-align:center;font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--quiet);background:var(--ink)}@media(max-width:720px){.films{grid-template-columns:1fr 1fr}}</style></head><body>
+<nav class="nav"><span class="brand">${esc(b.name)}</span><div class="actions"><a class="book" href="#book">Reserve</a></div></nav>
+<section class="frame f1"><div class="img"></div><div class="grad"></div><div class="center"><h1>${esc(h.heroLine.split(' ').slice(0, 4).join(' '))}<br><em>${esc(h.heroLine.split(' ').slice(4).join(' ') || h.nounSing)}.</em></h1></div><div class="meta"><div class="l"><strong>Frame 01</strong>The open</div><div>${esc(b.city)}, ${esc(b.st)}</div></div></section>
+<section class="caption"><p class="small">A short film in two frames</p><h2>${esc(h.quote)}</h2></section>
+<section class="frame f2"><div class="img"></div><div class="grad"></div><div class="center"><h1><em>${esc(h.nounSing.charAt(0).toUpperCase() + h.nounSing.slice(1))}.</em><br>The way it should be.</h1></div><div class="meta"><div class="l"><strong>Frame 02</strong>The practice</div><div>${esc(h.keywords.slice(0, 2).join(' · '))}</div></div></section>
+<section class="strip"><p class="sup">A film catalog</p><h2>The treatment menu,<br><em>shot in sequence.</em></h2><div class="films">${cells}</div></section>
+<section class="closing" id="book"><p class="super">The closing</p><h2>Reserve a frame.</h2><a class="reserve" href="tel:${phoneTel(b.phone)}">Call ${esc(b.phone)} →</a></section>
+<footer>© ${esc(b.name)} · ${esc(b.addr)}, ${esc(b.city)}, ${esc(b.st)}</footer>
+</body></html>`;
+}
+
+function tplI(b) {
+  const h = HOOKS[b.specialty];
+  const tones = ['t1','t2','t3','t4','t5','t6','t7','t8'];
+  const cards = h.services.map((s, i) => `<div class="card${i === 0 ? ' featured' : ''}"><div class="thumb ${tones[i % tones.length]}">${esc(s.name)}</div><div class="body"><h3 class="name">${esc(s.name)}</h3><p class="meta"><span>${esc(s.who.split('·')[0].trim())}</span><span>${esc(b.city)}</span></p><div class="row-bottom"><span class="price">${esc(s.price)}</span><a class="book" href="tel:${phoneTel(b.phone)}">Book</a></div></div></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name)} — Browse all ${esc(h.nounSing)} services</title>
+<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
+<style>:root{--bg:#fafaf8;--card:#fff;--ink:#0a0a0a;--ink-2:#5a5a5a;--line:#e8e3d8;--brand:#4a6b3a;--brand-soft:#eef3ea;--accent:#c08a64}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:'Inter',sans-serif;font-size:14px;line-height:1.5}.appbar{background:var(--card);border-bottom:1px solid var(--line);padding:12px 20px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:50}.appbar .logo{font-weight:700;font-size:16px}.appbar .logo span{color:var(--brand)}.appbar .search{flex:1;max-width:480px;position:relative}.appbar .search input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:10px 14px 10px 40px;font-size:14px;font-family:inherit}.appbar .search::before{content:'⌕';position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--ink-2);font-size:18px}.appbar .actions{display:flex;gap:8px}.appbar .ghost{padding:9px 14px;border-radius:8px;background:transparent;border:1px solid var(--line);color:var(--ink);text-decoration:none;font-size:13px;font-weight:500}.appbar .primary{padding:9px 14px;border-radius:8px;background:var(--brand);color:#fff;text-decoration:none;font-size:13px;font-weight:600}.subbar{background:var(--bg);padding:14px 20px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;font-size:13px;color:var(--ink-2)}.subbar strong{color:var(--ink);font-weight:600}.app{display:grid;grid-template-columns:240px 1fr;min-height:calc(100vh - 88px)}.sidebar{background:var(--card);border-right:1px solid var(--line);padding:24px 20px}.filter-group{margin-bottom:28px}.filter-group h4{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:var(--ink-2);margin:0 0 12px;font-weight:600}.chip-row{display:flex;flex-wrap:wrap;gap:6px}.chip{display:inline-block;padding:6px 12px;border-radius:999px;background:var(--bg);border:1px solid var(--line);font-size:12px;font-weight:500;color:var(--ink-2);cursor:pointer}.chip.on{background:var(--brand-soft);color:var(--brand);border-color:var(--brand)}.checkbox-row label{display:flex;align-items:center;gap:8px;font-size:13px;padding:4px 0}.content{padding:24px;background:var(--bg)}.toolbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.toolbar .count strong{color:var(--ink);font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.card{background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden;cursor:pointer}.card .thumb{aspect-ratio:5/3;display:flex;align-items:flex-end;padding:18px;color:#fff;font-weight:600;font-size:18px;position:relative}.card .thumb.t1{background:linear-gradient(135deg,#94a896,#4d6155)}.card .thumb.t2{background:linear-gradient(160deg,#f0e0d2,#c08a64)}.card .thumb.t3{background:linear-gradient(220deg,#5a4a4a,#2c2c28)}.card .thumb.t4{background:linear-gradient(180deg,#a06b48,#5e3d28)}.card .thumb.t5{background:linear-gradient(135deg,#c5b8a3,#7a6f5c)}.card .thumb.t6{background:linear-gradient(140deg,#e8d8c8,#a08870)}.card .thumb.t7{background:linear-gradient(200deg,#3a4a3a,#1a2a1a)}.card .thumb.t8{background:linear-gradient(180deg,#d9c2a8,#94785f)}.card .body{padding:16px 18px}.card .name{font-size:15px;font-weight:600;margin:0 0 4px;line-height:1.3}.card .meta{font-size:12px;color:var(--ink-2);margin:0 0 12px}.card .meta span+span::before{content:' · '}.card .row-bottom{display:flex;justify-content:space-between;align-items:center}.card .price{font-size:16px;font-weight:700}.card .book{padding:6px 12px;border-radius:6px;background:var(--brand);color:#fff;text-decoration:none;font-size:12px;font-weight:600}.card.featured .thumb::after{content:'Most booked';position:absolute;top:12px;left:12px;background:var(--accent);color:#fff;padding:4px 8px;border-radius:6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}@media(max-width:900px){.app{grid-template-columns:1fr}.sidebar{display:none}}</style></head><body>
+<div class="appbar"><div class="logo">${esc(b.name.split(' ')[0].toLowerCase())} <span>${esc((b.name.split(' ')[1] || h.typeLabel).toLowerCase())}</span></div><div class="search"><input type="text" placeholder="Search treatments..." value="${esc(h.nounSing)}"></div><div class="actions"><a class="ghost" href="tel:${phoneTel(b.phone)}">Call</a><a class="primary" href="#book">Book →</a></div></div>
+<div class="subbar"><span><strong>${esc(h.typeLabel.toLowerCase())} library</strong> · ${h.services.length} services</span><span>${esc(b.city)}, ${esc(b.st)}</span></div>
+<div class="app"><aside class="sidebar"><div class="filter-group"><h4>Type</h4><div class="chip-row"><span class="chip on">All</span>${h.services.slice(0,3).map(s=>`<span class="chip">${esc(s.name.split(' ')[0])}</span>`).join('')}</div></div><div class="filter-group"><h4>What we are</h4><div class="checkbox-row">${h.keywords.map(k => `<label><input type="checkbox" checked>${esc(k)}</label>`).join('')}</div></div></aside>
+<main class="content"><div class="toolbar"><p class="count"><strong>${h.services.length}</strong> services match</p></div><div class="grid">${cards}</div></main></div>
+</body></html>`;
+}
+
+function tplJ(b) {
+  const h = HOOKS[b.specialty];
+  const sigCards = h.services.slice(0, 3).map((s, i) => `<div class="sig-card"><p class="num">${['i.','ii.','iii.'][i]}</p><h3>${esc(s.name)}</h3><p>${esc(s.sub)}.</p><p class="price">${esc(s.price)}<small>${esc(s.who)}</small></p></div>`).join('');
+  return `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${esc(b.name)} — By Reservation · ${esc(b.city)}</title>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;1,300&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
+<style>:root{--ink:#080806;--paper:#f7f3ea;--gold:#c9a861;--quiet:#a8a094;--rule:rgba(201,168,97,.25)}*{box-sizing:border-box}body{margin:0;background:var(--ink);color:var(--paper);font-family:'Inter',sans-serif;font-weight:300;line-height:1.7;font-size:15px}.topnav{padding:24px 40px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--rule);position:sticky;top:0;z-index:50;background:rgba(8,8,6,.92);backdrop-filter:blur(12px)}.topnav .brand{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:22px;letter-spacing:.18em}.topnav .brand .dot{color:var(--gold);font-style:italic}.topnav nav{display:flex;gap:36px;font-size:11px;text-transform:uppercase;letter-spacing:.22em;color:var(--quiet)}.topnav nav a{color:var(--quiet);text-decoration:none}.lang{font-size:11px;text-transform:uppercase;letter-spacing:.22em;color:var(--quiet)}.lang strong{color:var(--gold);font-weight:300}.hero{position:relative;height:80vh;min-height:600px;overflow:hidden;display:flex;align-items:flex-end;padding:80px 40px 60px}.hero .video{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1a1612 0%,#080806 70%);z-index:1}.hero .video::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at 30% 40%,rgba(201,168,97,.08) 0%,transparent 50%),radial-gradient(circle at 70% 60%,rgba(201,168,97,.04) 0%,transparent 60%)}.hero .inner{position:relative;z-index:2;max-width:1280px;margin:0 auto;width:100%}.hero .super{font-size:11px;text-transform:uppercase;letter-spacing:.3em;color:var(--gold);margin:0 0 24px}.hero h1{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(48px,7vw,108px);line-height:1.05;margin:0 0 32px;max-width:980px}.hero h1 em{font-style:italic;color:var(--gold)}.hero .sub{font-family:'Cormorant Garamond',serif;font-size:22px;line-height:1.5;max-width:560px;font-style:italic;color:rgba(247,243,234,.85);margin:0 0 32px}.hero .reserve{display:inline-block;padding:18px 40px;border:1px solid var(--gold);color:var(--gold);text-decoration:none;font-size:11px;text-transform:uppercase;letter-spacing:.28em}.signature{padding:140px 40px;border-top:1px solid var(--rule)}.signature-i{max-width:1100px;margin:0 auto}.signature .super{font-size:11px;text-transform:uppercase;letter-spacing:.3em;color:var(--gold);margin:0 0 20px;text-align:center}.signature h2{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(40px,5vw,68px);text-align:center;margin:0 0 80px;line-height:1.15;font-style:italic}.sig-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:48px}.sig-card{padding:48px 32px;border:1px solid var(--rule);text-align:center}.sig-card .num{font-family:'Cormorant Garamond',serif;font-size:48px;color:var(--gold);font-style:italic;margin:0 0 24px;line-height:1}.sig-card h3{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:24px;letter-spacing:.04em;margin:0 0 16px;text-transform:uppercase}.sig-card p{color:rgba(247,243,234,.65);font-size:14px;line-height:1.65;margin:0 0 28px}.sig-card .price{font-family:'Cormorant Garamond',serif;font-size:18px;font-style:italic;color:var(--gold)}.sig-card .price small{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.2em;color:var(--quiet);margin-top:4px;font-style:normal}.story{padding:140px 40px;background:#0c0c0a;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}.story-i{max-width:880px;margin:0 auto;text-align:center}.story p{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(22px,3vw,32px);line-height:1.45;font-style:italic;margin:0 0 32px;color:rgba(247,243,234,.92)}.story cite{font-style:normal;font-size:11px;text-transform:uppercase;letter-spacing:.28em;color:var(--gold)}.reservation{padding:140px 40px}.res-i{max-width:680px;margin:0 auto;text-align:center}.res-i .super{font-size:11px;text-transform:uppercase;letter-spacing:.3em;color:var(--gold);margin:0 0 20px}.res-i h2{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(48px,6vw,80px);margin:0 0 32px;font-style:italic;line-height:1.05}.res-i .phone{display:inline-block;padding:18px 48px;border:1px solid var(--gold);color:var(--gold);text-decoration:none;font-size:14px;letter-spacing:.18em;font-family:'Cormorant Garamond',serif;font-style:italic}footer{padding:64px 40px 40px;border-top:1px solid var(--rule)}.foot{max-width:1280px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:48px;font-size:13px;color:var(--quiet)}.foot h4{font-size:11px;text-transform:uppercase;letter-spacing:.22em;color:var(--gold);margin:0 0 16px;font-weight:400}.copyright{max-width:1280px;margin:48px auto 0;padding-top:24px;border-top:1px solid var(--rule);display:flex;justify-content:space-between;font-size:11px;text-transform:uppercase;letter-spacing:.22em;color:var(--quiet)}@media(max-width:720px){.topnav nav{display:none}.sig-grid,.foot{grid-template-columns:1fr}}</style></head><body>
+<header class="topnav"><span class="brand">${esc(b.name)} <span class="dot">·</span></span><nav><a href="#sig">Signature</a><a href="#story">Story</a><a href="#reserve">Reservation</a></nav><span class="lang"><strong>EN</strong> · 中文 · ES</span></header>
+<section class="hero"><div class="video"></div><div class="inner"><p class="super">By appointment · ${esc(b.city)}</p><h1>${esc(h.heroLine.split('.')[0])}.<br><em>${esc(h.heroLine.split('.')[1] || 'By reservation')}.</em></h1><p class="sub">A private ${esc(h.typeLabel.toLowerCase())} in ${esc(b.city)}, California — for those who consider the way they look after themselves worth the time.</p><a class="reserve" href="#reserve">Reserve a consultation →</a></div></section>
+<section class="signature" id="sig"><div class="signature-i"><p class="super">Signature treatments</p><h2>Three forms of care.<br>One philosophy.</h2><div class="sig-grid">${sigCards}</div></div></section>
+<section class="story" id="story"><div class="story-i"><p>${esc(h.quote)}</p><cite>${esc(h.quoteAuthor)}</cite></div></section>
+<section class="reservation" id="reserve"><div class="res-i"><p class="super">Reservation</p><h2>Sit with us.</h2><a class="phone" href="tel:${phoneTel(b.phone)}">${esc(b.phone)}</a></div></section>
+<footer><div class="foot"><div><h4>Visit</h4>${esc(b.addr)}<br>${esc(b.city)}, ${esc(b.st)} ${esc(b.zip)}</div><div><h4>Reservations</h4>${esc(b.phone)}</div><div><h4>Hours</h4>By appointment · Tue-Sat</div></div><div class="copyright"><span>© ${esc(b.name)}</span><span>${esc(b.city)}, ${esc(b.st)}</span></div></footer>
+</body></html>`;
+}
+
+const TEMPLATES = { a: tplA, b: tplB, c: tplC, d: tplD, e: tplE, f: tplF, g: tplG, h: tplH, i: tplI, j: tplJ };
+
+function pickDesigns(idx) {
+  const all = ['a','b','c','d','e','f','g','h','i','j'];
+  // step of 3 cycles all 10 (gcd(3,10)=1) so adjacent businesses don't share trios
+  const offsets = [(idx * 3) % 10, (idx * 3 + 4) % 10, (idx * 3 + 7) % 10];
+  return offsets.map(o => all[o]);
+}
+
+let count = 0;
+const summary = [];
+BUSINESSES.forEach((biz, idx) => {
+  const designs = pickDesigns(idx);
+  designs.forEach(d => {
+    const fn = TEMPLATES[d];
+    if (!fn) { console.error(`no template ${d}`); return; }
+    const html = fn(biz);
+    const out = path.join(ROOT, `${biz.id}-${d}.html`);
+    fs.writeFileSync(out, html);
+    count++;
+  });
+  summary.push(`${biz.id} ${biz.name.padEnd(28)} → ${designs.join(', ')}`);
+});
+
+console.log('Generated mockups:');
+summary.forEach(s => console.log('  ' + s));
+console.log(`\n✓ ${count} files written to ${ROOT}`);

← eff0b9f Initial commit — LA County professional directory (doctors)  ·  back to Professional Directory  ·  tighten .gitignore: add missing standing-rule patterns (buil 81db480 →