← back to Professional Directory

scripts/apply-templates.js

388 lines

#!/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' },
  { id: '73303', name: 'Wynn Nail Spa',           city: 'Los Angeles',      st: 'CA', zip: '90004', addr: '4321 Beverly Blvd',      phone: '(323) 660-1133', specialty: 'nail' },
  { id: '74987', name: "Angel's Touch",           city: 'Los Angeles',      st: 'CA', zip: '90042', addr: '6014 N Figueroa St',     phone: '(323) 254-7700', specialty: 'hair' },
  { id: '75922', name: 'Fashion Optical',         city: 'San Diego',        st: 'CA', zip: '92103', addr: '3785 5th Ave',           phone: '(619) 296-2222', specialty: 'optometry' },
  { id: '75972', name: 'Ocean & Land Corp',       city: 'Los Angeles',      st: 'CA', zip: '90015', addr: '1330 S Figueroa St',     phone: '(213) 765-4400', specialty: 'aesthetics' },
  { id: '76473', name: 'Fitstop',                 city: 'Woodland Hills',   st: 'CA', zip: '91367', addr: '21900 Burbank Blvd',     phone: '(818) 992-6500', specialty: 'fitness' },
  { id: '76523', name: 'REAXN Performance Training', city: 'Burbank',       st: 'CA', zip: '91504', addr: '2735 N Hollywood Way',   phone: '(818) 845-2255', specialty: 'fitness' },
  { id: '77031', name: 'Dog Life Whittier',       city: 'Whittier',         st: 'CA', zip: '90601', addr: '6780 Greenleaf Ave',     phone: '(562) 698-8855', specialty: 'petcare' },
];

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',
  },
  optometry: {
    nounSing: 'eye exam', kicker: 'A neighborhood optometry practice',
    heroLine: 'Frames you actually want to wear.',
    storyTitle: 'Why we curate frames the way we do',
    storyP1: 'Most optical shops carry whatever the buying group puts in their catalog. Two big distributors control 80% of the frames in the United States, and the rest is private-label markup at 8x cost. The result is a wall of frames that all look slightly the same.',
    storyP2: 'We buy from designers we visit ourselves once a year at trade shows in Milan, Paris, and New York. Lindberg, Cutler & Gross, Garrett Leight, vintage Persol. The choice costs us about 20% off our gross margin per frame. It buys us a frame wall that is actually worth choosing from.',
    services: [
      { name: 'Routine eye exam', sub: '30-min vision + pressure check', price: '$95', who: '30 min · Doctor' },
      { name: 'Comprehensive exam', sub: 'OCT imaging + 75-min exam', price: '$165', who: '75 min · Doctor' },
      { name: 'Pediatric exam', sub: 'Children 6 and up, EPSDT-covered', price: '$95', who: '30 min · Doctor' },
      { name: 'Contact-lens fitting', sub: 'Trial pair + 1-week follow-up', price: '$140', who: '60 min · Doctor' },
      { name: 'Frames & lenses', sub: 'Curated houses, in-house lab', price: 'From $290', who: '60-min fitting' },
    ],
    quote: '"Every other shop tried to sell me Ray-Bans I would have hated in two months. They put a $340 Lindberg titanium on my face and I haven\'t wanted to switch in four years."',
    quoteAuthor: '— Daniel T., 4-year patient',
    bookCadence: 'Same-week routine slots. Comprehensive exams 3-5 days out.',
    keywords: ['CURATED FRAMES', 'IN-HOUSE LAB', 'INSURANCE ACCEPTED', 'NEIGHBORHOOD'],
    typeLabel: 'OPTOMETRY',
  },
  fitness: {
    nounSing: 'session', kicker: 'A small-group performance training studio',
    heroLine: 'Stronger than you were six weeks ago.',
    storyTitle: 'Why we cap classes at six',
    storyP1: 'The big-box gyms run group classes at 30 people. The trainer cannot watch your form, cannot adjust your loading, cannot call out the eight people doing the squat with their knees collapsing inward. That is how three-quarters of the people who join a gym in January wreck a knee by March.',
    storyP2: 'We cap our small-group classes at six. Every session has a coach who knows your name, your movement history, and your loading from last week. Programming is written by us, not pulled from a franchise app. We charge what that costs ($35 a class, $280/month unlimited), and we keep the room small.',
    services: [
      { name: 'Drop-in class', sub: 'One small-group session, any time', price: '$35', who: '60 min · max 6' },
      { name: 'Unlimited monthly', sub: 'All small-group classes', price: '$280/mo', who: 'No commitment' },
      { name: '1:1 personal training', sub: 'Custom programming, written for you', price: '$120', who: '60 min · Coach' },
      { name: 'New-member assessment', sub: 'Movement screen + intro session', price: 'Free', who: '60 min · once' },
      { name: 'Sports-performance block', sub: '8-week season prep, 3x/week', price: '$895', who: '8 weeks' },
    ],
    quote: '"I joined three big-box gyms in two years and quit each one. Six weeks here and I haven\'t missed a session."',
    quoteAuthor: '— Marisa K., member since 2024',
    bookCadence: 'Walk-ins welcome for assessment. Class slots open 7 days ahead.',
    keywords: ['CAPPED AT SIX', 'WRITTEN PROGRAMMING', 'COACH-LED', 'NO FRANCHISE'],
    typeLabel: 'PERFORMANCE TRAINING',
  },
  petcare: {
    nounSing: 'visit', kicker: 'A neighborhood dog daycare',
    heroLine: 'Where your dog wants to be while you are at work.',
    storyTitle: 'Why we limit headcount to twenty',
    storyP1: 'The big chain doggy daycares run forty to sixty dogs a day per facility. The math works: high turnover, $40 a day, automated check-in. The dogs are stressed by the noise, the staff cannot tell who is bullying whom, and the floor never gets disinfected to the standard a healthy adult immune system needs.',
    storyP2: 'We limit headcount to twenty dogs at a time, by appointment, with a temperament assessment before any new dog joins the pack. We charge what that costs ($55 a day, $250 a week), and our staff knows every dog by name and what their pulling triggers are.',
    services: [
      { name: 'Daycare day', sub: 'Drop-off 7-9am, pickup by 6pm', price: '$55', who: 'Full day · max 20' },
      { name: 'Weekly bundle', sub: '5 days, prepaid', price: '$250', who: '$50/day' },
      { name: 'Boarding overnight', sub: 'Dogs assessed pre-stay', price: '$85', who: 'Per night' },
      { name: 'Group walk', sub: '60-min neighborhood walk, max 4 dogs', price: '$30', who: '60 min · 1:4' },
      { name: 'Temperament assessment', sub: 'Required before first daycare day', price: 'Free', who: '90 min · once' },
    ],
    quote: '"My anxious rescue would not look at the staff at the chain. Three weeks here and he runs to the door when we pull up."',
    quoteAuthor: '— Anika P., owner of Marvel',
    bookCadence: 'Daycare books 2-3 days ahead. Boarding 2 weeks for weekends.',
    keywords: ['CAPPED AT TWENTY', 'PRE-ASSESSED PACK', 'BY APPOINTMENT', 'NO CHAIN'],
    typeLabel: 'DOG DAYCARE',
  },
  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}`);