PageSourceSearch

https://boerne.ai/

html boerne.ai collected 2026-10-05 14:49:03 UTC 189,497 bytes, 3,544 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<!-- BUILD 2026-07-12b-boerne -->
5<!-- BOERNE CARD GATE v9 2026-06-15 (broadened no-card scrubber: strips card refs + tap-through CTAs) (gate + card-reference backstop: orphaned tap-the-card lines stripped when no card renders): free directory businesses are mention-only; cards reserved for paid/anchor/parks/landmarks/galleries -->
6<!-- EVENTS-CARD 2026-06-12 — events handoff card added (front-end + guide prompt) -->
7<meta charset="UTF-8">
8<meta name="viewport" content="width=device-width, initial-scale=1.0">
9
10<title>Boerne, Texas — Main Street, Antiques & German Heritage — HillCountry.ai</title>
11<meta name="description" content="Boerne, Texas — the Kendall County seat 30 miles northwest of San Antonio. South Main Street shopping, the best antique corridor in the Hill Country, five craft breweries, Cave Without a Name, and where to stay. Part of HillCountry.ai, an AI-powered discovery network for the Texas Hill Country.">
12<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
13<meta name="googlebot" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
14<meta name="bingbot" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
15<meta name="theme-color" content="#0e2e1f">
16<meta name="author" content="Backroads Hill Country">
17
18<link rel="canonical" href="https://boerne.ai/">
19<link rel="icon" href="/favicon.png" sizes="any" type="image/png">
20<link rel="icon" href="/favicon.svg" type="image/svg+xml">
21<link rel="apple-touch-icon" href="/favicon.png">
22<!-- Network hreflang cluster — live HillCountry.ai sites -->
23<link rel="alternate" hreflang="en" href="https://hillcountry.ai/">
24<link rel="alternate" hreflang="en" href="https://bandera.ai/">
25<link rel="alternate" hreflang="en" href="/">
26<link rel="alternate" hreflang="en" href="https://concan.ai/">
27<link rel="alternate" hreflang="en" href="https://fredericksburg.ai/">
28<link rel="alternate" hreflang="en" href="https://kerrville.ai/">
29<link rel="alternate" hreflang="en" href="https://leakey.ai/">
30<link rel="alternate" hreflang="x-default" href="https://hillcountry.ai/">
31
32<!-- Favicon - HC logo (shared across all network sites) -->
33<!-- Geo -->
34<meta name="geo.region" content="US-TX">
35<meta name="geo.placename" content="Boerne, Texas">
36<meta name="geo.position" content="29.7947;-98.7320">
37<meta name="ICBM" content="29.7947, -98.7320">
38
39
40<link rel="dns-prefetch" href="//fonts.googleapis.com">
41<link rel="dns-prefetch" href="//fonts.gstatic.com">
42<link rel="preconnect" href="https://fonts.googleapis.com">
43<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
44<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
45
46<!-- Open Graph -->
47<meta property="og:type" content="website">
48<meta property="og:title" content="Boerne, Texas — Main Street, Antiques & German Heritage — HillCountry.ai">
49<meta property="og:description" content="Boerne, Texas — South Main Street, antiques, German heritage, craft breweries, and Cave Without a Name, just up I-10 from San Antonio. From HillCountry.ai, an AI-powered discovery network for the Texas Hill Country.">
50<meta property="og:url" content="https://boerne.ai/">
51<meta property="og:image" content="https://boerne.ai/og-facebook.png">
52<meta property="og:image:type" content="image/jpeg">
53<meta property="og:image:width" content="1200">
54<meta property="og:image:height" content="630">
55<meta property="og:image:alt" content="Boerne, Texas — Hill Country bluebonnets and live oak">
56<meta property="og:site_name" content="HillCountry.ai · Boerne">
57<meta property="og:locale" content="en_US">
58
59<!-- Twitter -->
60<meta name="twitter:card" content="summary_large_image">
61<meta name="twitter:image" content="https://boerne.ai/og.png">
62<meta name="twitter:title" content="Boerne, Texas — Main Street, Antiques & German Heritage">
63<meta name="twitter:description" content="Boerne, Texas — South Main Street, antiques, German heritage, craft breweries, and Cave Without a Name, just up I-10 from San Antonio. From HillCountry.ai, an AI-powered discovery network for the Texas Hill Country.">
64
65<!-- Counter.dev (network-shared) + GA4 (town-specific) -->
66<script src="https://cdn.counter.dev/script.js" data-id="48d2fa28-6e4b-4ddb-a539-746997b03f5c" data-utcoffset="-5" defer></script>
66
67<!-- NETWORK GA 2026-06-15 — single network tag G-LJGW1567S1; per-town split by hostname -->
vendor: 64 bytes, lines 67-68
67
68<script async src="https://www.googletagmanager.com/gtag/js?id=
68G-LJGW1567S1
vendor: 12 bytes, line 68
68"></script>
69<script>
vendor: 118 bytes, line 69
69window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','
69G-LJGW1567S1
vendor: 3 bytes, line 69
69');
69</script>
69
70
71<style>
72
73  :root{
74    --green-deep:#082118;
75    --green:#0e2e1f;
76    --green-mid:#15402b;
77    --green-soft:#1d5238;
78    --gold:#d9a441;
79    --gold-light:#f0c56c;
80    --cream:#fffaf0;
81    --sand:#d8d1c0;
82    --line:rgba(255,250,240,.14);
83    --serif:'Inter',system-ui,sans-serif;
84    --sans:'Inter',system-ui,sans-serif;
85  }
86
87/* NETWORK STATUS BANNER — thin strip at very top showing this town is live
88   on the HillCountry.ai network. Persistent, not dismissible. Links to hub. */
89.network-banner{display:block;text-align:center;background:#0e2e1f;
90  color:var(--gold-light);font-size:12px;letter-spacing:.18em;text-transform:uppercase;
91  font-weight:600;padding:7px 14px;text-decoration:none;
92  border-bottom:1px solid rgba(217,164,65,.18)}
93.network-banner:hover{background:#14362f;color:var(--cream)}
94.network-banner .nb-dot{display:inline-block;width:7px;height:7px;border-radius:50%;
95  background:#5fb87a;margin-right:8px;vertical-align:1px;
96  box-shadow:0 0 6px rgba(95,184,122,.55)}
97.network-banner .nb-arrow{margin-left:8px;opacity:.7}
98
99  *{box-sizing:border-box;margin:0;padding:0}
100  html{scroll-behavior:smooth;scroll-padding-top:74px;overflow-y:scroll;scrollbar-gutter:stable}
101  body{font-family:var(--sans);background:var(--green);color:var(--cream);-webkit-font-smoothing:antialiased}
102  ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:var(--green-deep)}::-webkit-scrollbar-thumb{background:var(--green-soft);border-radius:3px}
103  ::selection{background:var(--gold);color:var(--green-deep)}
104  a{color:inherit;text-decoration:none}
105
106  @keyframes rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
107  @keyframes fade{from{opacity:0}to{opacity:1}}
108  @keyframes pulse{0%,100%{opacity:.25}50%{opacity:1}}
109  @keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(7px)}}
110
111  /* NAV */
112  nav{position:sticky;top:0;z-index:50;display:flex;flex-wrap:wrap;row-gap:8px;align-items:center;justify-content:space-between;
113    padding:20px 7vw;background:rgba(8,33,24,.55);backdrop-filter:blur(14px);border-bottom:1px solid var(--line)}
114  .wordmark{font-family:var(--serif);font-size:26px;letter-spacing:-.01em}
115  .wordmark span{color:var(--gold)}
116  .nav-links{display:flex;gap:22px;align-items:center;margin-left:auto}
117  .nav-links a{font-family:var(--sans);font-size:16px;color:var(--cream);
118    letter-spacing:.02em;transition:color .18s ease;padding:6px 0;
119    border-bottom:1px solid transparent}
120  .nav-links a:hover{color:var(--gold)}
121  /* "Businesses" nav item — plain gold text (no box), bold so it still reads
122     as the primary action without a pill outline. */
123  .nav-links a.nav-cta{color:var(--gold-light);font-weight:600}
124  .nav-links a.nav-cta:hover{color:var(--cream)}
125  @media(max-width:720px){
126    .nav-links{gap:14px}
127    .nav-links a{font-size:14px}
128    .nav-links a.nav-cta{font-size:14px}
129  .wordmark{font-size:20px}
130  }
131  @media(max-width:520px){
132    .nav-links a{font-size:13px}
133  }
134  .more-section{border-top:1px solid var(--line)}
135  .more-toggle{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:9px;
136    padding:26px 7vw;font-family:var(--sans);font-size:12px;font-weight:600;letter-spacing:.14em;
137    text-transform:uppercase;color:var(--gold-light);transition:color .15s ease;-webkit-user-select:none;user-select:none}
138  .more-toggle::-webkit-details-marker{display:none}
139  .more-toggle::marker{content:""}
140  .more-toggle:hover{color:var(--cream)}
141  .more-toggle-chev{font-size:11px;transition:transform .2s ease}
142  details[open] > .more-toggle .more-toggle-chev{transform:rotate(180deg)}
143  .more-toggle .lbl-open{display:none}
144  details[open] > .more-toggle .lbl-closed{display:none}
145  details[open] > .more-toggle .lbl-open{display:inline}
146  .more-section > section:first-of-type{border-top:0}
147
148  /* HERO */
149  .hero{position:relative;min-height:clamp(560px,75vh,800px);display:flex;flex-direction:column;align-items:center;justify-content:center;
150    text-align:center;padding:42px 7vw 104px;overflow:hidden;
151    background:linear-gradient(157deg,var(--green-deep) 0%,var(--green) 46%,var(--green-soft) 100%)}
152  .topo{position:absolute;inset:0;opacity:.5;pointer-events:none}
153  .grain{position:absolute;inset:0;opacity:.05;pointer-events:none;mix-blend-mode:overlay;
154    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E")}
155  .hero-inner{position:relative;max-width:780px;animation:rise .9s ease both;width:100%}
156  .eyebrow{font-size:14px;letter-spacing:.28em;text-transform:uppercase;color:var(--gold-light);margin-bottom:14px;font-weight:600;animation:fade 1.1s ease both}
157  h1{font-family:var(--serif);font-weight:600;letter-spacing:-.015em;font-size:clamp(44px,7vw,72px);line-height:1.08;margin-bottom:12px}
158  .hero-towns{font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--gold-light);opacity:.85;margin-top:-22px;margin-bottom:32px;font-weight:600;animation:fade 1.3s ease both}
159  .subtitle{font-size:clamp(15px,1.6vw,18px);color:var(--sand);letter-spacing:.02em;margin-bottom:32px}
160  .subtitle b{color:var(--gold-light);font-weight:500}
161
162  /* COMING-SOON TILE */
163  .scout-wrap{max-width:600px;margin:0 auto;background:rgba(255,250,240,.05);border:1px solid var(--line);
164    border-radius:18px;padding:34px 28px;backdrop-filter:blur(10px);box-shadow:0 24px 60px -28px rgba(0,0,0,.6);
165    animation:rise 1s .15s ease both;text-align:center}
166  .scout-label{font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:var(--gold-light);font-weight:600;margin-bottom:10px}
167  .scout-name{font-family:var(--serif);font-weight:600;font-size:26px;color:var(--cream);margin-bottom:8px;letter-spacing:-.01em}
168  .scout-status{font-size:14px;color:var(--sand);line-height:1.55;max-width:440px;margin:0 auto}
169  .scout-status b{color:var(--gold-light);font-weight:500}
170
171  .scroll-hint{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);color:var(--sand);
172    font-size:11px;letter-spacing:.2em;text-transform:uppercase;display:flex;flex-direction:column;
173    align-items:center;gap:8px;opacity:.55;animation:fade 1.4s 1s ease both;pointer-events:none}
174  .scroll-hint span{animation:bob 2s ease-in-out infinite;font-size:16px}
175
176  /* LIST YOUR BUSINESS CTA CARD */
177  .lyb-section{padding:50px 7vw;background:var(--green-deep);border-top:1px solid var(--line)}
178  .lyb-card{max-width:780px;margin:0 auto;background:var(--green-mid);border:1px solid var(--line);
179    border-radius:14px;padding:30px 34px;display:flex;align-items:center;gap:28px;
180    transition:border-color .25s}
181  .lyb-card:hover{border-color:rgba(217,164,65,.5)}
182  .lyb-text{flex:1;min-width:0}
183  .lyb-eyebrow{font-size:11px;letter-spacing:.24em;text-transform:uppercase;color:var(--gold-light);font-weight:600;margin-bottom:8px}
184  .lyb-title{font-family:var(--serif);font-weight:600;font-size:20px;color:var(--cream);margin-bottom:6px;letter-spacing:-.01em}
185  .lyb-sub{font-size:14px;color:var(--sand);line-height:1.55}
186  .lyb-cta{flex-shrink:0;background:var(--gold);color:var(--green-deep);font-family:var(--sans);font-weight:600;
187    font-size:14px;padding:13px 24px;border-radius:9px;letter-spacing:.01em;transition:background .2s;white-space:nowrap}
188  .lyb-cta:hover{background:var(--gold-light)}
189  @media(max-width:640px){
190    .lyb-section{padding:36px 6vw}
191    .lyb-card{flex-direction:column;align-items:stretch;text-align:center;padding:26px 22px}
192    .lyb-cta{align-self:center}
193  }
194
195  @media(max-width:860px){
196    nav{padding:16px 6vw}
197    .hero{padding:34px 6vw 50px;min-height:clamp(500px,72vh,720px)}
198  }
199
200  /* ─────────────────────────────────────────
201     SECTION 2 — About the Hill Country + Network map
202     ───────────────────────────────────────── */
203
204  /* About the Hill Country — facts grid + activities */
205  .about-hc{padding:78px 7vw;background:var(--green-deep);border-top:1px solid var(--line)}
206  .about-hc-inner{max-width:880px;margin:0 auto}
207  .about-hc-label{font-family:var(--serif);font-style:italic;
208    color:var(--gold-light);font-size:14px;text-align:center;margin-bottom:10px}
209  .about-hc-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
210    font-size:clamp(28px,4vw,40px);text-align:center;margin-bottom:30px;color:var(--cream)}
211  .about-hc-lede{font-size:16px;line-height:1.7;color:var(--sand);
212    text-align:justify;max-width:680px;margin:0 auto 38px;letter-spacing:.005em}
213  .about-hc-lede strong{color:var(--cream);font-weight:600}
214  .facts-grid{display:grid;grid-template-columns:repeat(2,1fr);
215    gap:22px 30px;background:var(--green-mid);border:1px solid var(--line);
216    border-radius:14px;padding:26px 30px;margin:0 auto 30px;max-width:600px}
217  .fact{display:flex;flex-direction:column;gap:4px}
218  .fact-label{font-size:11px;letter-spacing:.18em;text-transform:uppercase;
219    color:var(--gold-light);font-weight:600}
220  .fact-value{font-size:15px;color:var(--cream);font-weight:500;line-height:1.4}
221  .fact-value small{display:block;font-size:12px;color:var(--sand);
222    font-weight:400;margin-top:2px}
223
224  /* Category breadcrumb — compact line at the bottom of the facts grid box.
225     Reads as a discovery-layer summary: pick an area, pick a town, find what to
226     do, meet people, find a place to stay. Tracked letter-spacing reduced so
227     all 6 categories fit on one line within the 600px content box. */
228  .facts-cats{grid-column:1/-1;margin-top:6px;padding-top:18px;border-top:1px solid var(--line);
229    text-align:center;font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
230    color:var(--gold-light);font-weight:600;line-height:1.8;white-space:nowrap;
231    overflow-x:auto}
232  .facts-cats span{opacity:.55;margin:0 5px}
233
234  @media(max-width:640px){
235    .about-hc{padding:54px 6vw}
236    .facts-grid{padding:22px 24px;gap:18px 22px;grid-template-columns:1fr}
237    .facts-cats{font-size:10px;letter-spacing:.1em;white-space:normal}
238    .facts-cats span{margin:0 4px}
239  }
240
241  /* Network map */
242  .network-section{padding:74px 7vw;background:var(--green);
243    border-top:1px solid var(--line)}
244  .network-inner{max-width:1100px;margin:0 auto}
245  .network-label{font-family:var(--serif);font-style:italic;color:var(--gold-light);
246    font-size:14px;text-align:center;margin-bottom:10px}
247  .network-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
248    font-size:clamp(28px,4vw,40px);text-align:center;margin-bottom:36px;color:var(--cream);max-width:780px;margin-left:auto;margin-right:auto;line-height:1.2}
249  .network-sub{font-size:15px;line-height:1.65;color:var(--sand);text-align:center;
250    max-width:600px;margin:0 auto 36px}
251  .network-map-wrap{background:var(--green);border:1px solid var(--line);
252    border-radius:14px;overflow:hidden;box-shadow:0 18px 40px -20px rgba(0,0,0,.5)}
253  @media(max-width:640px){
254    .network-section{padding:54px 6vw}
255  }
256
257  /* ─────────────────────────────────────────
258     SECTION 3 — About Us + Businesses (Stand behind your listing)
259     ───────────────────────────────────────── */
260
261  /* About Us — Spencer & Jess + Backroads HQ note */
262  .about-us{padding:78px 7vw;border-top:1px solid var(--line);background:var(--green-deep)}
263  .about-us-inner{max-width:780px;margin:0 auto;text-align:center}
264  .about-us-badge{display:inline-block;font-family:var(--sans);font-size:11px;
265    letter-spacing:.2em;text-transform:uppercase;color:var(--gold-light);
266    font-weight:600;background:rgba(217,164,65,.08);border:1px solid rgba(217,164,65,.3);
267    padding:7px 14px;border-radius:999px;margin-bottom:18px}
268  .about-us-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
269    font-size:clamp(26px,3.6vw,34px);margin-bottom:24px;color:var(--cream);line-height:1.2}
270  .about-us-text{font-size:15.5px;line-height:1.75;color:var(--sand);margin-bottom:20px;
271    text-align:justify;hyphens:auto}
272  .about-us-text strong{color:var(--cream);font-weight:600}
273  .about-us-hq{margin-top:34px;padding-top:22px;border-top:1px solid var(--line);
274    font-family:var(--serif);font-style:italic;color:var(--gold-light);
275    font-size:15px;line-height:1.5;text-align:center}
276  .about-us-hq small{display:block;font-size:12px;color:var(--sand);font-style:normal;
277    letter-spacing:.18em;text-transform:uppercase;margin-top:6px;font-weight:600}
278  .about-us-sig{margin-top:30px;font-family:var(--serif);font-style:italic;
279    color:var(--gold-light);font-size:15.5px;line-height:1.5}
280  .about-us-sig small{display:block;font-size:13px;color:var(--sand);
281    font-style:normal;letter-spacing:.04em;margin-top:4px;text-transform:uppercase}
282  @media(max-width:640px){
283    .about-us{padding:54px 6vw}
284  }
285
286  /* Businesses section — "Stand behind your listing" with pull-quote */
287  .biz-section{padding:78px 7vw;border-top:1px solid var(--line);background:var(--green)}
288  .biz-inner{max-width:720px;margin:0 auto;text-align:center}
289  .biz-eyebrow{font-size:11px;letter-spacing:.28em;text-transform:uppercase;
290    color:var(--gold-light);font-weight:600;margin-bottom:14px}
291  .biz-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
292    font-size:clamp(28px,4vw,40px);margin-bottom:32px;color:var(--cream);line-height:1.2}
293  .biz-text{font-size:15.5px;line-height:1.75;color:var(--sand);margin:0 0 28px;
294    text-align:justify;hyphens:auto}
295  .biz-text strong{color:var(--cream);font-weight:600}
296
297  /* Pull-quote — italic with hairline rules above/below. Three-line break for
298     spoken-cadence rhythm. "recommended" no longer styled differently — reads
299     as plain text within the italic line. */
300  .biz-pullquote{margin:36px 0;padding:26px 0;
301    border-top:1px solid rgba(217,164,65,.35);border-bottom:1px solid rgba(217,164,65,.35);
302    text-align:center;max-width:600px;margin-left:auto;margin-right:auto}
303  .biz-pullquote p{font-family:var(--serif);font-size:clamp(20px,2.4vw,24px);
304    font-style:italic;font-weight:400;color:var(--cream);margin:0;
305    line-height:1.5;letter-spacing:-.005em}
306
307  .biz-cta{display:inline-block;background:var(--gold);color:var(--green-deep);
308    font-family:var(--sans);font-weight:600;font-size:14px;padding:14px 28px;
309    border-radius:9px;letter-spacing:.01em;margin-top:14px;transition:background .2s}
310  .biz-cta:hover{background:var(--gold-light)}
311
312  @media(max-width:640px){
313    .biz-section{padding:54px 6vw}
314    .biz-pullquote{margin:28px 0;padding:22px 0}
315  }
316
317  /* ─────────────────────────────────────────
318     SECTION 4 — Early Access + FAQ + Footer
319     ───────────────────────────────────────── */
320
321  /* Early Access */
322  .ea-section{padding:74px 7vw;background:var(--green-deep);border-top:1px solid var(--line)}
323  .ea-inner{max-width:620px;margin:0 auto;text-align:center}
324  .ea-label{font-family:var(--serif);font-style:italic;color:var(--gold-light);
325    font-size:14px;margin-bottom:10px}
326  .ea-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
327    font-size:clamp(26px,3.6vw,34px);margin-bottom:14px;color:var(--cream);line-height:1.2}
328  .ea-sub{font-size:15px;line-height:1.65;color:var(--sand);margin:0 auto 28px;max-width:520px}
329  .ea-form{display:flex;gap:10px;background:rgba(255,250,240,.05);border:1px solid var(--line);
330    border-radius:12px;padding:6px;max-width:480px;margin:0 auto}
331  .ea-form input{flex:1;background:transparent;border:none;outline:none;color:var(--cream);
332    font-family:var(--sans);font-size:15px;padding:12px 14px}
333  .ea-form input::placeholder{color:var(--sand);opacity:.55}
334  .ea-form button{background:var(--gold);color:var(--green-deep);border:none;border-radius:8px;
335    font-family:var(--sans);font-weight:600;font-size:14px;padding:0 22px;cursor:pointer;
336    letter-spacing:.01em;transition:background .2s;white-space:nowrap}
337  .ea-form button:hover{background:var(--gold-light)}
338  .ea-note{font-size:12px;color:var(--sand);opacity:.7;margin-top:14px;letter-spacing:.02em}
339  @media(max-width:640px){
340    .ea-section{padding:54px 6vw}
341    .ea-form{flex-direction:column;padding:8px;gap:6px}
342    .ea-form button{padding:14px}
343  }
344
345  /* FAQ */
346  .faq-sec{background:var(--green);padding:74px 7vw;border-top:1px solid var(--line)}
347  .faq-inner{max-width:880px;margin:0 auto}
348  .faq-label{font-family:var(--serif);font-style:italic;color:var(--gold-light);
349    font-size:14px;text-align:center;margin-bottom:10px}
350  .faq-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
351    font-size:clamp(28px,4vw,40px);text-align:center;margin-bottom:42px;color:var(--cream)}
352  .faq-item{background:var(--green-mid);border:1px solid var(--line);border-radius:12px;
353    padding:0;margin-bottom:14px;transition:border-color .25s;overflow:hidden}
354  .faq-item:hover{border-color:rgba(217,164,65,.35)}
355  .faq-q-btn{width:100%;background:transparent;border:none;color:var(--cream);
356    font-family:var(--serif);font-weight:700;letter-spacing:-.02em;font-size:18px;
357    text-align:left;padding:22px 26px;cursor:pointer;display:flex;justify-content:space-between;
358    align-items:center;gap:16px;line-height:1.35}
359  .faq-caret{flex-shrink:0;width:18px;height:18px;display:flex;align-items:center;
360    justify-content:center;color:var(--gold);font-size:14px;transition:transform .3s ease;
361    transform:rotate(0deg)}
362  .faq-item.open .faq-caret{transform:rotate(180deg)}
363  /* Animate via grid-template-rows — animates to natural content height (smoother than max-height).
364     Wrapper goes from 0fr to 1fr; inner div is overflow:hidden so the row collapses cleanly.
365     Padding transitions in sync to avoid the jump at the end of the animation. */
366  .faq-a-wrap{display:grid;grid-template-rows:0fr;transition:grid-template-rows .32s ease}
367  .faq-item.open .faq-a-wrap{grid-template-rows:1fr}
368  .faq-a{overflow:hidden;min-height:0;font-size:14.5px;line-height:1.7;color:var(--sand);
369    text-align:justify;hyphens:auto;padding:0 26px;
370    transition:padding-bottom .32s ease}
371  .faq-item.open .faq-a{padding-bottom:22px}
372  .faq-a strong{color:var(--cream);font-weight:600}
373  /* Subtle inline links in FAQ answers — own properties only.
374     Same color as body text, gold underline on hover only. */
375  .faq-a a{color:inherit;text-decoration:none;border-bottom:1px solid transparent;
376    transition:border-color .2s,color .2s}
377  .faq-a a:hover{color:var(--gold-light);border-bottom-color:var(--gold)}
378  @media(max-width:640px){
379    .faq-sec{padding:54px 6vw}
380    .faq-q-btn{padding:20px 22px;font-size:17px}
381    .faq-a{padding:0 22px;text-align:left;transition:padding-bottom .32s ease}
382    .faq-item.open .faq-a{padding-bottom:20px}
383  }
384
385  /* Footer — hub-specific footer structure: wordmark uppercased and bumped, no town suffix,
386     locked two-line stats treatment, no self-referential network blurb. */
387  footer{background:var(--green-deep);border-top:1px solid var(--line);text-align:center;
388    padding:44px 7vw 46px}
389  .f-mark{font-family:var(--serif);font-size:18px;margin-bottom:10px;letter-spacing:-.01em}
390  .f-mark span{color:var(--gold)}
391  .f-tagline{font-family:var(--serif);font-style:italic;color:var(--gold-light);
392    font-size:15px;margin-bottom:8px}
393  .f-stats-line{font-size:13px;color:var(--sand);letter-spacing:.03em;margin-bottom:4px}
394  .f-stats-line.stars{color:var(--gold-light);letter-spacing:.04em}
395  .f-social-row{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;
396    gap:12px;margin:18px 0 14px;padding-bottom:14px;border-bottom:1px dashed rgba(255,250,240,.10);
397    font-size:12.5px}
398  .f-social-row a{color:var(--gold-light);font-size:13px;font-weight:600;
399    letter-spacing:.01em;border-bottom:1px solid rgba(217,164,65,.4);padding-bottom:1px;
400    transition:color .15s ease}
401  .f-social-row a:hover{color:var(--cream)}
402  .f-social-row .f-divider{display:inline-block;width:1px;height:13px;background:rgba(217,164,65,0.45);
403    vertical-align:middle;margin:0 4px}
404  .f-row{margin-top:6px;font-size:12.5px;color:var(--sand);line-height:1.45}
405  .f-row a{color:var(--gold-light);font-weight:600;border-bottom:1px solid rgba(217,164,65,.4)}
406  .f-row a:hover{color:var(--cream)}
407  .f-network-blurb{margin-top:6px;padding-top:6px;border-top:1px dashed rgba(255,250,240,.10);
408    font-size:12.5px;color:var(--sand);max-width:640px;margin-left:auto;margin-right:auto;
409    line-height:1.6;opacity:.85}
410  .f-network-blurb a{color:var(--gold-light);font-weight:600;border-bottom:1px solid rgba(217,164,65,.4)}
411  .f-network-blurb a:hover{color:var(--cream)}
412  @media(max-width:640px){
413    footer{padding:36px 6vw 40px}
414    .f-social-row{gap:12px;font-size:12.5px}
415    .f-social-row .f-divider{display:none}
416  }
417
418  /* Custom select chevron — white (matches cream), replaces OS-default dark arrow.
419     Locked May 23, 2026 — applies to all <select> across the network. */
420  select, .field select, .signup-field select, .pmgmt-field select {
421    -webkit-appearance: none;
422    -moz-appearance: none;
423    appearance: none;
424    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%23fffaf0' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='3 5 6 8 9 5'/></svg>");
425    background-repeat: no-repeat;
426    background-position: right 14px center;
427    background-size: 12px 12px;
428    padding-right: 38px !important;
429  }
430
431
432/* Early Access success badge — green bg with gold border/text, shown inline after AJAX submit. */
433.ea-success{display:none;max-width:520px;margin:14px auto 0;background:rgba(217,164,65,.08);
434  border:1px solid var(--gold);border-radius:12px;padding:14px 20px;
435  color:var(--gold-light);font-size:15px;line-height:1.5;text-align:center;
436  font-family:var(--serif);font-style:italic;font-weight:500;
437  animation:rise .45s ease both}
438.ea-success.show{display:block}
439.ea-note{max-width:520px;margin:12px auto 0;color:var(--sand);font-size:12.5px;
440  text-align:center;opacity:.75;letter-spacing:.01em}
441
442
443/* Custom select chevron — white (matches cream), replaces OS-default dark arrow.
444   Locked May 23, 2026 — applies to all <select> across the network. */
445select, .field select, .signup-field select, .pmgmt-field select {
446  -webkit-appearance: none;
447  -moz-appearance: none;
448  appearance: none;
449  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%23fffaf0' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><polyline points='3 5 6 8 9 5'/></svg>");
450  background-repeat: no-repeat;
451  background-position: right 14px center;
452  background-size: 12px 12px;
453  padding-right: 38px !important;
454}
455/* NETWORK STATUS BANNER — thin strip at very top showing this town is live
456   on the HillCountry.ai network. Persistent, not dismissible. Links to hub. */
457.network-banner{display:block;text-align:center;background:#0e2e1f;
458  color:var(--gold-light);font-size:12px;letter-spacing:.18em;text-transform:uppercase;
459  font-weight:600;padding:7px 14px;text-decoration:none;
460  border-bottom:1px solid rgba(217,164,65,.18)}
461  .chat-wrap{max-width:680px;margin:0 auto;width:100%;background:rgba(255,250,240,.04);
462    scroll-margin-top:180px;
463    border:1px solid var(--line);border-radius:18px;overflow:visible;backdrop-filter:blur(10px);
464    box-shadow:0 24px 60px -28px rgba(0,0,0,.6);position:relative;text-align:left}
465.chat-thread{padding:22px 22px 8px;min-height:120px;max-height:min(60vh,520px);overflow-y:auto;
466    display:flex;flex-direction:column;gap:14px}
467.chat-thread::-webkit-scrollbar{width:6px}
468.chat-thread::-webkit-scrollbar-thumb{background:rgba(255,250,240,.18);border-radius:3px}
469.chat-msg{display:flex;align-items:flex-start;gap:10px;animation:fade .3s ease both}
470.chat-msg.user{justify-content:flex-end}
471.avatar{flex-shrink:0;width:28px;height:28px;border-radius:50%;
472    background:var(--gold);color:var(--green-deep);display:flex;align-items:center;justify-content:center;
473    font-weight:700;font-size:13px;letter-spacing:.02em}
474.chat-msg .bubble{background:rgba(255,250,240,.06);border:1px solid var(--line);border-radius:14px;
475    padding:13px 16px;max-width:85%;font-size:14.5px;line-height:1.55;color:var(--cream);text-align:justify}
476.chat-msg.user .bubble{background:var(--gold);color:var(--green-deep);border-color:var(--gold)}
477/* Typing indicator — animated dots while the guide composes a reply. */
478.typing-indicator{display:flex;gap:4px;padding:2px 2px;align-items:center}
479.typing-indicator span{display:block;width:6px;height:6px;border-radius:50%;background:var(--sand);
480    opacity:.5;animation:typing-bounce 1.2s infinite}
481.typing-indicator span:nth-child(2){animation-delay:.18s}
482.typing-indicator span:nth-child(3){animation-delay:.36s}
483@keyframes typing-bounce{0%,80%,100%{transform:translateY(0);opacity:.3}40%{transform:translateY(-6px);opacity:1}}
484/* Contextual status line shown if the reply takes a moment (live data / cold start). */
485.typing-status{font-size:16px;color:var(--cream);font-style:italic;opacity:.85}
486.bubble p{margin:0}
487.bubble p + p{margin-top:10px}
488.bubble a{color:var(--gold-light);text-decoration:underline;text-decoration-color:rgba(240,197,108,.4)}
489.bubble a:hover{text-decoration-color:var(--gold-light)}
490.bubble strong{color:inherit;font-weight:inherit}
491  .start-pills{display:flex;flex-wrap:wrap;gap:8px;padding:2px 22px 14px}
492  .start-pills .start-pill{background:transparent;border:1px solid var(--line);color:var(--sand);font-family:var(--sans);font-size:13px;line-height:1.25;padding:8px 13px;border-radius:999px;cursor:pointer;text-align:left;transition:border-color .15s,color .15s,background .15s}
493  .start-pills .start-pill:hover,.start-pills .start-pill:focus-visible{border-color:var(--gold-light);color:var(--cream);background:rgba(217,164,65,.08);outline:none}
494  .start-pills.hidden{display:none}
495  .start-pills .start-pill-lodging{text-decoration:none;display:inline-block}
496  @media (max-width:600px){.start-pills{padding:2px 14px 12px;gap:6px}.start-pills .start-pill{font-size:12px;padding:7px 11px}}
497.chat-input-row{display:flex;gap:10px;padding:14px 14px 16px;border-top:1px solid var(--line);
498    background:rgba(8,33,24,.4);border-radius:0 0 18px 18px}
499.chat-input-row textarea{flex:1;background:rgba(8,33,24,.5);border:1px solid var(--gold);
500    border-radius:11px;resize:none;outline:none;padding:11px 14px;font-family:inherit;font-size:15px;
501    color:var(--cream);line-height:1.4;min-height:42px;max-height:120px;transition:border-color .15s ease}
502.chat-input-row textarea::placeholder{color:var(--sand);opacity:.6}
503.chat-input-row textarea:focus{border-color:var(--gold)}
504.chat-input-row .send{background:var(--gold);color:var(--green-deep);border:none;border-radius:11px;
505    width:48px;height:42px;font-size:20px;cursor:pointer;font-weight:700;transition:background .15s ease}
506.chat-input-row .send:hover{background:var(--gold-light)}
507.chat-input-row .send:disabled{opacity:.4;cursor:not-allowed}
508.chat-status{display:flex;justify-content:flex-end;align-items:center;max-width:680px;
509    margin:10px auto 0;padding:0 4px}
510/* When the share-feedback link is present, lay it out left, status right */
511  .chat-guide-status-wrap{justify-content:space-between;gap:12px;flex-wrap:wrap}
512.chat-report-link{background:transparent;border:none;padding:5px 4px;
513    font-family:var(--sans);font-size:12.5px;color:var(--gold-light);
514    cursor:pointer;text-align:left;line-height:1.4;
515    transition:color .15s ease;font-weight:500;text-decoration:none}
516.chat-report-link:hover{color:var(--gold)}
517.chat-report-link:hover u{text-decoration-color:var(--gold)}
518.chat-report-link u{text-decoration:underline;text-decoration-color:var(--gold-light);
519    text-underline-offset:2px}
520.chat-guide-status{display:inline-flex;align-items:center;gap:7px;
521    font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
522    font-weight:600;color:var(--sand);opacity:.85;
523    padding:5px 12px;border:1px solid rgba(217,164,65,.22);border-radius:999px;
524    background:rgba(8,33,24,.45);cursor:help;position:relative}
525.chat-guide-status .cbs-dot{display:inline-block;width:6px;height:6px;border-radius:50%;
526    background:#3fae6e;animation:cbsPulse 1.6s ease-in-out infinite}
527.cbs-tip{position:absolute;top:calc(100% + 9px);right:0;width:250px;text-align:left;
528    background:#0c2519;border:1px solid rgba(217,164,65,.3);border-radius:9px;padding:9px 12px;
529    font-size:11.5px;line-height:1.5;letter-spacing:.01em;text-transform:none;font-weight:500;
530    color:var(--cream);opacity:0;visibility:hidden;pointer-events:none;z-index:60;
531    box-shadow:0 10px 28px -12px rgba(0,0,0,.65);transition:opacity .08s ease}
532  @media(max-width:560px){.cbs-tip{right:auto;left:0;width:min(78vw,250px);max-width:none}}
533.chat-guide-status:hover .cbs-tip,.chat-guide-status:focus .cbs-tip{opacity:1;visibility:visible}
534/* GUIDE CARDS — inline below the message bubble */
535  .guide-cards{margin-top:12px;padding-top:12px;border-top:1px solid var(--line);
536    display:flex;flex-direction:column;gap:8px}
537.guide-cards-label{font-size:10px;letter-spacing:.22em;text-transform:uppercase;
538    color:var(--gold-light);font-weight:600;opacity:.85}
539.guide-card{display:flex;align-items:center;gap:12px;padding:11px 13px;
540    background:rgba(8,33,24,.45);border:1px solid var(--line);border-radius:12px;
541    text-decoration:none;color:var(--cream);transition:border-color .15s ease,background .15s ease}
542.guide-card:hover{border-color:rgba(217,164,65,.4);background:rgba(8,33,24,.65)}
543.guide-card .card-photo{flex-shrink:0;width:52px;height:52px;border-radius:8px;
544    background-size:cover;background-position:center;background-color:rgba(255,250,240,.04)}
545.guide-card .card-photo.contain{background-size:contain;background-repeat:no-repeat;background-position:center}
546.guide-card .card-photo.cream-bg{background-color:var(--cream)}
547/* Authority placeholder — used until Spencer provides a clean <a href="https://www.backroadstexas.net/" target="_blank" rel="noopener" style="color:var(--gold-light,#d6b26e);font-weight:600;text-decoration:none">Backroads</a> logo */
548  .guide-card .card-photo.authority-placeholder{
549    background:linear-gradient(135deg,var(--gold) 0%,var(--gold-light) 100%);
550    display:flex;align-items:center;justify-content:center;
551    font-family:var(--serif);font-weight:700;font-size:18px;color:var(--green-deep);
552    letter-spacing:.06em}
553.guide-card .card-photo.authority-placeholder::after{content:"BHC"}
554.guide-card .card-text{flex:1;min-width:0}
555.guide-card .card-name{font-weight:600;font-size:14.5px;color:var(--cream);line-height:1.3;margin-bottom:3px}
556.guide-card .card-meta{font-size:12px;color:var(--sand);opacity:.85;line-height:1.4;
557    overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
558.guide-card .card-tier{flex-shrink:0;font-size:9px;letter-spacing:.16em;text-transform:uppercase;
559    font-weight:700;padding:3px 7px;border-radius:4px;align-self:flex-start;margin-top:2px}
560.guide-card .card-tier.authority{background:var(--gold);color:var(--green-deep)}
561.guide-card .card-tier.partner{background:rgba(217,164,65,.3);color:var(--gold-light)}
562.guide-card .card-tier.member{background:rgba(217,164,65,.15);color:var(--gold-light)}
563/* GUIDE ERROR STATE — bubble when API fails */
564  .chat-msg.error .bubble{background:rgba(180,60,60,.15);border-color:rgba(180,60,60,.35);color:#f0d8d8}
565.chat-msg.error .avatar{background:#b44040;color:var(--cream)}
566.chat-wrap{border-radius:14px}
567.chat-thread{padding:16px 14px 6px;max-height:min(60dvh,440px);gap:12px}
568.chat-msg .bubble{font-size:14px;padding:10px 13px;max-width:88%}
569.avatar{width:24px;height:24px;font-size:12px}
570.chat-input-row{padding:10px 10px 12px;gap:8px}
571.chat-input-row textarea{font-size:16px;padding:10px 12px;min-height:40px}
572.chat-input-row .send{width:44px;height:40px;font-size:18px}
573.guide-card{padding:10px 11px;gap:10px}
574.guide-card .card-photo{width:46px;height:46px}
575.guide-card .card-name{font-size:14px}
576.guide-card .card-meta{font-size:11.5px}
577/* Guide chips — compact business cards rendered inline in the guide's bubble when
578     surfacing businesses. Same visual model as Brody on Bandera.
579     Each chip = photo + name + meta + tier tag, clickable. */
580  .guide-chips{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
581.guide-chips-label{font-size:10px;letter-spacing:.18em;text-transform:uppercase;
582    color:var(--gold-light);font-weight:700;margin-bottom:8px}
583.guide-chips-row{display:flex;gap:5px;flex-wrap:nowrap;justify-content:flex-start}
584/* Flexible chips: all chips share the row width evenly and never wrap.
585     Up to 6 chips fit on one line by shrinking to fit. min-width:0 lets them
586     compress below content width; max-width keeps a single chip from ballooning. */
587  .guide-chip{flex:1 1 0;min-width:0;max-width:110px;background:var(--green-mid);
588    border:1px solid var(--line);border-radius:8px;overflow:hidden;
589    display:flex;flex-direction:column;animation:rise .5s ease both;
590    cursor:pointer;transition:transform .15s,border-color .15s;
591    position:relative;text-decoration:none;color:inherit}
592.guide-chip:hover{transform:translateY(-2px);border-color:rgba(217,164,65,.45)}
593.guide-chip,.guide-chip:hover,.guide-chip:focus,.guide-chip:active,
594  .guide-chip *,.guide-chip *:hover,.guide-chip *:focus,.guide-chip *:active{
595    text-decoration:none !important}
596.guide-chip:nth-child(1){animation-delay:.05s}
597.guide-chip:nth-child(2){animation-delay:.18s}
598.guide-chip:nth-child(3){animation-delay:.31s}
599.guide-chip:nth-child(4){animation-delay:.44s}
600.guide-chip .chip-photo{height:54px;background-size:cover;background-position:center;
601    background-color:#0a1d14;position:relative}
602.chip-photo-initial{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
603    font-family:var(--sans);font-weight:700;font-size:26px;color:var(--gold-light);
604    letter-spacing:-.02em;text-shadow:0 1px 2px rgba(0,0,0,.4);opacity:.92}
605.guide-chip.tier-authority{border-color:var(--gold);
606    box-shadow:0 0 0 1px rgba(217,164,65,.25)}
607.guide-chip.tier-partner{border-color:rgba(217,164,65,.4)}
608.guide-chip.tier-member,.guide-chip.tier-anchor{border-color:rgba(217,164,65,.28)}
609/* Tier badge top-left on photo */
610  .chip-tier-tag{position:absolute;top:3px;left:3px;font-size:8.5px;font-weight:800;
611    letter-spacing:.09em;text-transform:uppercase;padding:2px 5px;border-radius:3px;
612    backdrop-filter:blur(4px);line-height:1.05;
613    box-shadow:0 1px 2px rgba(0,0,0,.28)}
614.chip-tier-tag.authority{background:var(--gold);color:var(--green-deep)}
615.chip-tier-tag.partner{background:rgba(217,164,65,.92);color:var(--green-deep)}
616.chip-tier-tag.member{background:rgba(255,250,240,.22);color:var(--cream);
617    border:1px solid rgba(255,250,240,.42)}
618.chip-body{padding:6px 8px 8px;display:flex;flex-direction:column;gap:1px;flex:1}
619.chip-name{font-family:var(--sans);font-weight:700;font-size:11px;
620    line-height:1.18;letter-spacing:-.005em;color:var(--cream);
621    overflow:hidden;text-overflow:ellipsis;display:-webkit-box;
622    -webkit-line-clamp:2;-webkit-box-orient:vertical}
623.chip-meta{font-size:9px;color:var(--sand);opacity:.8;line-height:1.3;margin-top:1px}
624@media(max-width:640px){
625    .guide-chips-row{gap:4px}
626    .guide-chips-row .guide-chip .chip-photo{height:46px}
627    .guide-chips-row .chip-name{font-size:9.5px}
628    .guide-chips-row .chip-meta{font-size:8px}
629    .guide-chips-row .chip-tier-tag{font-size:6px;padding:1px 3px}
630    .guide-chips-row .chip-photo-initial{font-size:20px}
631    .guide-chips-row .chip-body{padding:5px 5px 6px}
632  }
633/* ─── SURFACE 2 — Big cards in the page below the chat (ported from Brody) ─── */
634  .guide-surface{margin-top:32px;margin-bottom:48px;padding:0 24px;
635    margin-left:auto;margin-right:auto;animation:rise .5s ease both;
636    display:flex;flex-direction:column;align-items:flex-start;
637    width:fit-content;max-width:100%}
638/* The surface shrinks to the width of the 4-column grid and is centered on
639     the page by the auto margins. The head fills that same width (width:100%),
640     so the title's left edge lines up with the first card. */
641  .guide-surface-head{display:flex;align-items:baseline;justify-content:space-between;
642    margin-bottom:18px;gap:14px;flex-wrap:wrap;width:100%}
643.guide-surface-title{font-family:var(--serif);font-weight:700;
644    font-size:clamp(22px,3vw,28px);letter-spacing:-.015em;color:var(--cream)}
645.guide-surface-sub{font-size:13px;color:var(--sand);opacity:.85}
646/* 4 cards across (8 cards = 2 rows). Fixed 4-column track so cards form
647     proper rows instead of stacking. */
648  .guide-surface-grid{display:grid;
649    grid-template-columns:repeat(4,220px);
650    gap:12px;justify-content:center;width:100%}
651.guide-surface-load-more{background:transparent;border:1px dashed rgba(217,164,65,.32);
652    color:var(--gold-light);font-family:var(--sans);font-size:13px;
653    padding:10px 18px;border-radius:8px;
653cursor:pointer;letter-spacing:.01em;font-weight:500;
654    transition:border-color .15s ease,background .15s ease,color .15s ease}
655.guide-surface-load-more:hover{border-color:var(--gold-light);
656    background:rgba(217,164,65,.05);color:var(--gold);border-style:solid}
657.big-card{background:var(--green-mid);border:1px solid var(--line);
658    border-radius:12px;overflow:hidden;display:flex;flex-direction:column;
659    text-decoration:none;color:inherit;
660    transition:transform .25s,border-color .25s,box-shadow .25s;
661    animation:rise .5s ease both;cursor:pointer}
662.big-card:hover{transform:translateY(-3px);border-color:rgba(217,164,65,.5);
663    box-shadow:0 18px 40px -22px rgba(0,0,0,.5)}
664/* Pulse highlight when a chip is tapped — temporary glow on its big-card destination */
665  .big-card.bc-pulse{box-shadow:0 0 0 3px var(--gold),0 18px 40px -22px rgba(0,0,0,.5);
666    border-color:var(--gold);transition:box-shadow .3s,border-color .3s}
667.big-card.tier-authority{border:1.5px solid var(--gold);
668    box-shadow:0 0 0 1px rgba(217,164,65,.18)}
669.big-card.tier-partner{border-color:rgba(217,164,65,.5)}
670.big-card.tier-member,.big-card.tier-anchor{border-color:rgba(217,164,65,.32);
671    background:linear-gradient(180deg,var(--green-mid) 0%,rgba(21,64,43,.92) 100%)}
672.big-card-photo{height:120px;background-size:cover;background-position:center;
673    background-color:#0a1d14;position:relative}
674.big-card-body{padding:11px 13px 13px;display:flex;flex-direction:column;flex:1;gap:5px;text-align:left}
675/* Cross-site standard: card text left-aligned */
676.chip-body,.chip-name,.big-card,.big-card-body,.bc-name,.thread-card,.card-name,.guide-card .card-name{text-align:left}
677/* Surface-card letter fallback — large, centered (network-standard). */
678.bc-photo-initial{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
679    font-family:var(--serif);font-weight:700;font-size:64px;color:var(--gold-light);
680    letter-spacing:-.02em;text-shadow:0 2px 6px rgba(0,0,0,.4);opacity:.9}
681.bc-name{font-family:var(--serif);font-weight:700;font-size:15px;line-height:1.22;
682    letter-spacing:-.01em;color:var(--cream)}
683.bc-summary{font-size:12px;color:var(--sand);line-height:1.45;
684    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
685.bc-meta-row{display:flex;flex-wrap:wrap;gap:7px;font-size:11.5px;color:var(--sand)}
686.bc-meta-row span{display:inline-flex;align-items:center;gap:3px}
687/* Claim CTA — small + subtle gold (network-standard). */
688.bc-claim-cta{display:block;margin-top:8px;padding-top:7px;
689    border-top:1px dashed rgba(217,164,65,.18);
690    font-size:11px;color:rgba(217,164,65,.7);
691    text-decoration:none;line-height:1.3;letter-spacing:.01em;
692    transition:color .15s ease}
693.bc-claim-cta:hover{color:var(--gold-light)}
694@media(max-width:640px){
695    .guide-surface{margin-top:24px;padding:0 14px;width:auto;align-items:stretch}
696    .guide-surface-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
697    .big-card-photo{height:110px}
698    .guide-surface-head{margin-bottom:14px;padding:0 4px}
699    .bc-name{font-size:13.5px}
700    .bc-summary{-webkit-line-clamp:2;font-size:11.5px}
701    .bc-meta-row{font-size:11px;gap:5px}
702  }
703@media(min-width:641px) and (max-width:900px){
704    .guide-surface-grid{grid-template-columns:repeat(3,210px)}
705  }
706@media(max-width:380px){
707    .big-card-photo{height:100px}
708    .bc-name{font-size:13px}
709  }
710@media(max-width:640px){
711    .lyb-section{padding:36px 6vw}
712    .lyb-card{flex-direction:column;align-items:stretch;text-align:center;padding:26px 22px}
713    .lyb-cta{align-self:center}
714  }
715/* ─── REPORT / FEEDBACK MODAL (ported from Brody v29) ─────────────── */
716  .report-modal-overlay{position:fixed;inset:0;background:rgba(8,33,24,.85);
717    backdrop-filter:blur(8px);z-index:1000;display:none;align-items:flex-start;
718    justify-content:center;padding:40px 20px;overflow-y:auto}
719.report-modal-overlay.show{display:flex;animation:fade .25s ease both}
720.report-modal{background:var(--green);border:1px solid var(--line);border-radius:18px;
721    max-width:520px;width:100%;padding:40px 44px 36px;position:relative;
722    box-shadow:0 30px 80px -20px rgba(0,0,0,.7);animation:rise .4s ease both;margin:auto 0}
723.report-modal-close{position:absolute;top:18px;right:20px;background:transparent;
724    border:none;color:var(--sand);font-size:24px;
724cursor:pointer;width:36px;height:36px;
725    border-radius:50%;display:flex;align-items:center;justify-content:center;
726    transition:background .15s ease,color .15s ease}
727.report-modal-close:hover{background:rgba(255,250,240,.08);color:var(--cream)}
728.report-modal-label{font-family:var(--serif);font-style:italic;color:var(--gold-light);
729    font-size:13px;margin-bottom:8px}
730.report-modal-h2{font-family:var(--serif);font-weight:700;letter-spacing:-.02em;
731    font-size:clamp(22px,3.4vw,28px);margin-bottom:10px;color:var(--cream);line-height:1.2}
732.report-modal-intro{font-size:14.5px;line-height:1.6;color:var(--sand);margin-bottom:28px}
733.report-field{margin-bottom:18px}
734.report-field label{display:block;font-size:12.5px;letter-spacing:.05em;
735    text-transform:uppercase;font-weight:600;color:var(--gold-light);margin-bottom:6px}
736.report-field label .req{color:var(--gold);margin-left:2px}
737.report-field input[type=email],
738  .report-field select,
739  .report-field textarea{width:100%;background:rgba(8,33,24,.6);border:1px solid var(--line);
740    border-radius:8px;padding:11px 14px;color:var(--cream);font-family:var(--sans);
741    font-size:14.5px;outline:none;transition:border-color .15s ease}
742.report-field input:focus,.report-field select:focus,.report-field textarea:focus{
743    border-color:var(--gold)}
744.report-field textarea{min-height:80px;resize:vertical;font-family:var(--sans);line-height:1.5}
745.report-field .hint{font-size:12px;color:var(--sand);opacity:.7;margin-top:6px;line-height:1.5}
746.report-actions{display:flex;gap:12px;margin-top:6px;align-items:center}
747.report-submit{background:var(--gold);color:var(--green-deep);border:none;border-radius:10px;
748    padding:13px 26px;font-family:var(--sans);font-weight:700;font-size:14.5px;letter-spacing:.01em;
749    cursor:pointer;transition:background .15s ease}
750.report-submit:hover{background:var(--gold-light)}
751.report-submit:disabled{opacity:.55;cursor:wait}
752.report-cancel{color:var(--sand);font-size:13.5px;cursor:pointer;font-family:var(--sans);
753    background:transparent;border:none;padding:13px 14px}
754.report-cancel:hover{color:var(--cream)}
755.report-success{display:none;padding:20px 22px;background:rgba(217,164,65,.1);
756    border:1px solid var(--gold);border-radius:10px;color:var(--gold-light);
757    font-size:14.5px;line-height:1.6}
758.report-success.show{display:block;animation:rise .5s ease both}
759@media(max-width:600px){
760    .report-modal{padding:30px 24px 28px}
761    .report-modal-overlay{padding:20px 12px}
762  }
763/* ─── LIVE RIVER CONDITIONS WIDGET (river towns only) ───────────────── */
764.river-cond{position:relative;z-index:2;clear:both;background:rgba(8,33,24,.45);
765    border-top:1px solid var(--line);border-bottom:1px solid var(--line);
766    padding:40px 24px;margin:0}
767.river-cond-inner{max-width:760px;margin:0 auto;background:rgba(8,33,24,.55);
768    border:1px solid var(--line);border-radius:16px;padding:22px 24px;backdrop-filter:blur(8px)}
769.river-cond-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:16px}
770.river-cond-label{font-family:var(--serif);font-style:italic;color:var(--gold-light);font-size:14px}
771.river-cond-sub{font-size:12px;color:var(--sand);letter-spacing:.02em;margin-top:2px}
772.river-cond-chart{font-size:12px;color:var(--gold-light);text-decoration:none;white-space:nowrap;
773    border-bottom:1px solid rgba(217,164,65,.4);padding-bottom:1px}
774.river-cond-chart:hover{color:var(--cream)}
775.river-cond-readout{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:14px}
776.rc-stat{background:rgba(255,250,240,.04);border:1px solid var(--line);border-radius:10px;padding:12px 10px;text-align:center}
777.rc-val{font-family:var(--serif);font-weight:700;font-size:30px;line-height:1;color:var(--cream);letter-spacing:-.02em}
778.rc-unit{font-size:11px;color:var(--sand);margin-top:6px;letter-spacing:.02em}
779.river-cond-verdict{display:flex;align-items:center;gap:9px;font-size:14px;line-height:1.45;
780    color:var(--cream);background:rgba(255,250,240,.04);border:1px solid var(--line);
781    border-radius:10px;padding:11px 14px}
782.rc-dot{flex-shrink:0;width:9px;height:9px;border-radius:50%;background:var(--sand);
783    box-shadow:0 0 0 3px rgba(255,250,240,.08)}
784.river-cond-verdict.rc-good .rc-dot{background:#3fae6e;box-shadow:0 0 0 3px rgba(63,174,110,.18)}
785.river-cond-verdict.rc-low .rc-dot{background:var(--gold);box-shadow:0 0 0 3px rgba(217,164,65,.2)}
786.river-cond-verdict.rc-high .rc-dot{background:#d98441;box-shadow:0 0 0 3px rgba(217,132,65,.2)}
787.river-cond-verdict.rc-unknown .rc-dot{background:var(--sand);opacity:.6}
788.river-cond-foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;
789    margin-top:12px;font-size:11px;color:var(--sand);opacity:.85}
790.river-cond-foot a{color:var(--gold-light);text-decoration:none;border-bottom:1px solid rgba(217,164,65,.3)}
791@media(max-width:640px){
792    .river-cond{padding:28px 14px}
793    .river-cond-inner{padding:16px 16px}
794    .rc-val{font-size:26px}
795  }
796
797/* Blinking gold status dot in the guide training-status line. */
798@keyframes cbsPulse{
799    0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(217,164,65,.6)}
800    50%{opacity:.55;box-shadow:0 0 0 5px rgba(217,164,65,0)}
801  }
802
803  .f-explore{display:flex;flex-wrap:wrap;justify-content:center;gap:7px 20px;margin:16px auto 4px;max-width:640px}
804  .f-explore a{color:var(--gold-light,#f0c56c);font-size:13.5px;text-decoration:none;letter-spacing:.02em;border-bottom:1px solid rgba(217,164,65,.4)}
805  .f-explore a:hover{color:var(--cream,#fffaf0)}
806</style>
807
808<!-- JSON-LD structured data — generated from town data -->
809<script type="application/ld+json">
810{
811  "@context": "https://schema.org",
812  "@graph": [
813    {
814      "@type": "Organization",
815      "@id": "https://backroadstexas.net/#org",
816      "name": "Backroads Hill Country",
817      "alternateName": "Backroads",
818      "url": "https://www.backroadstexas.net/",
819      "logo": "https://www.backroadstexas.net/images/backroads-logo.png",
820      "description": "Texas Hill Country vacation rental management — locally operated since 2001. Manages 104 properties across 13 Hill Country towns.",
821      "foundingDate": "2001",
822      "areaServed": {
823        "@type": "AdministrativeArea",
824        "name": "Texas Hill Country"
825      },
826      "address": {
827        "@type": "PostalAddress",
828        "addressLocality": "Bandera",
829        "addressRegion": "TX",
830        "addressCountry": "US"
831      },
832      "email": "[email protected]",
833      "telephone": "+1-830-522-4661",
834      "aggregateRating": {
835        "@type": "AggregateRating",
836        "ratingValue": "4.9",
837        "reviewCount": "389",
838        "bestRating": "5"
839      },
840      "sameAs": [
841        "https://www.facebook.com/backroadshillcountry",
842        "https://www.facebook.com/groups/hillcountrytravelapp",
843        "https://www.instagram.com/backroadsreservations/",
844        "https://www.google.com/maps/place/Backroads+Reservations",
845        "https://www.backroadstexas.net/",
846        "https://hillcountry.app/",
847        "https://hillcountry.ai/"
848      ],
849      "disambiguatingDescription": "HillCountry.ai is the Texas Hill Country travel and vacation-rental discovery network operated by Backroads Hill Country, a locally owned travel company based in Bandera, Texas, in business since 2001. It is a travel resource for planning Hill Country trips — towns, rivers, parks, wineries, and lodging — and is not an artificial-intelligence consulting, softw
849are-development, or data-center company. The “.ai” refers to the web address, not to AI services.",
850      "knowsAbout": [
851        "Texas Hill Country travel",
852        "Vacation rentals",
853        "Hill Country towns",
854        "Texas Hill Country rivers and swimming holes",
855        "Texas state parks",
856        "Hill Country wineries",
857        "Live music and dance halls",
858        "Local travel guides",
859        "Hill Country trip planning"
860      ]
861    },
862    {
863      "@type": "WebSite",
864      "@id": "https://boerne.ai/#website",
865      "url": "https://boerne.ai/",
866      "name": "Boerne, Texas — HillCountry.ai",
867      "alternateName": "boerne.ai",
868      "description": "Boerne, Texas — Main Street, Antiques & German Heritage. Part of HillCountry.ai, an AI-powered discovery network for the Texas Hill Country.",
869      "inLanguage": "en-US",
870      "publisher": {
871        "@id": "https://backroadstexas.net/#org"
872      },
873      "isPartOf": {
874        "@id": "https://hillcountry.ai/#network"
875      },
876      "potentialAction": {
877        "@type": "SearchAction",
878        "target": {
879          "@type": "EntryPoint",
880          "urlTemplate": "https://boerne.ai/?q={search_term_string}"
881        },
882        "query-input": "required name=search_term_string"
883      }
884    },
885    {
886      "@type": "WebSite",
887      "@id": "https://hillcountry.ai/#network",
888      "url": "https://hillcountry.ai/",
889      "name": "HillCountry.ai",
890      "description": "HillCountry.ai is an AI-powered discovery network for the Texas Hill Country."
891    },
892    {
893      "@type": "WebPage",
894      "@id": "https://boerne.ai/#webpage",
895      "url": "https://boerne.ai/",
896      "name": "Boerne, Texas — Main Street, Antiques & German Heritage — HillCountry.ai",
897      "description": "Boerne, Texas — the Kendall County seat 30 miles northwest of San Antonio. South Main Street shopping, the best antique corridor in the Hill Country, five craft breweries, Cave Without a Name, and where to stay. Part of HillCountry.ai, an AI-powered discovery network for the Texas Hill Country.",
898      "inLanguage": "en-US",
899      "isPartOf": {
900        "@id": "https://boerne.ai/#website"
901      },
902      "about": {
903        "@id": "https://boerne.ai/#place"
904      },
905      "breadcrumb": {
906        "@id": "https://boerne.ai/#breadcrumb"
907      },
908      "publisher": {
909        "@id": "https://backroadstexas.net/#org"
910      },
911      "datePublished": "2026-05-28",
912      "dateModified": "2026-05-28",
913      "speakable": {
914        "@type": "SpeakableSpecification",
915        "cssSelector": [
916          ".hero h1",
917          ".about-hc-lede",
918          ".faq-a"
919        ]
920      }
921    },
922    {
923      "@type": "BreadcrumbList",
924      "@id": "https://boerne.ai/#breadcrumb",
925      "itemListElement": [
926        {
927          "@type": "ListItem",
928          "position": 1,
929          "name": "HillCountry.ai",
930          "item": "https://hillcountry.ai/"
931        },
932        {
933          "@type": "ListItem",
934          "position": 2,
935          "name": "Boerne, Texas",
936          "item": "https://boerne.ai/"
937        }
938      ]
939    },
940    {
941      "@type": [
942        "TouristDestination",
943        "Place"
944      ],
945      "@id": "https://boerne.ai/#place",
946      "name": "Boerne",
947      "alternateName": [
948        "Boerne, Texas"
949      ],
950      "description": "Boerne (say it BURN-ee) is the county seat of Kendall County, about 30 miles northwest of San Antonio on I-10 — close enough for a day trip from the city, far enough that it still feels like somewhere. It was founded in 1849 by German immigrants and freethinkers and named for Ludwig Börne, a German political writer. The heritage is real: limestone architecture, German family names, and a culture of craftsmanship that still shows up on the street. People come for the shopping, the food, the breweries, and what may be the best antique corridor in the Hill Country. What they don't expect is how much there is to do.",
951      "geo": {
952        "@type": "GeoCoordinates",
953        "latitude": "29.7947",
954        "longitude": "-98.7320"
955      },
956      "address": {
957        "@type": "PostalAddress",
958        "addressLocality": "Boerne",
959        "addressRegion": "TX",
960        "postalCode": "78006",
961        "addressCountry": "US"
962      },
963      "containedInPlace": {
964        "@type": "AdministrativeArea",
965        "name": "Kendall County",
966        "containedInPlace": {
967          "@type": "AdministrativeArea",
968          "name": "Texas",
969          "addressCountry": "US"
970        }
971      },
972      "touristType": [
973        "Day-trippers from San Antonio looking for a Main Street town",
974        "Antique hunters working the South Main corridor",
975        "Craft-beer travelers doing the walkable brewery cluster",
976        "Families visiting Cave Without a Name and the Cibolo Nature Center",
977        "Shoppers after boutiques, gifts, and home goods",
978        "History travelers drawn to the German heritage and limestone architecture"
979      ],
980      "includesAttraction": [
981        {
982          "@type": "TouristAttraction",
983          "name": "South Main Street — walkable shopping, antiques, and breweries; the commercial heart of Boerne"
984        },
985        {
986          "@type": "TouristAttraction",
987          "name": "Cave Without a Name — show cave with 45-minute tours, a constant 66°F inside"
988        },
989        {
990          "@type": "TouristAttraction",
991          "name": "Cibolo Nature Center — free trails along Cibolo Creek, 100 acres, 200+ bird species"
992        },
993        {
994          "@type": "TouristAttraction",
995          "name": "The antique corridor — Carousel Antiques, Johns Road Antique Mall, Corner Cartel and more"
996        },
997        {
998          "@type": "TouristAttraction",
999          "name": "Craft breweries — five within walking distance, led by the Dodging Duck Brewhaus on the creek"
1000        },
1001        {
1002          "@type": "TouristAttraction",
1003          "name": "Old Jail Museum — the original 1884 county jail, small but well-curated"
1004        },
1005        {
1006          "@type": "TouristAttraction",
1007          "name": "Enchanted Rock State Natural Area (45 min W) — granite dome hiking near Fredericksburg"
1008        },
1009        {
1010          "@type": "TouristAttraction",
1011          "name": "Hill Country State Natural Area (30 min W) — hiking and equestrian trails near Bandera"
1012        }
1013      ]
1014    },
1015    {
1016      "@type": "ItemList",
1017      "@id": "https://boerne.ai/#attractions-list",
1018      "name": "Boerne attractions and nearby sights",
1019      "itemListElement": [
1020        {
1021          "@type": "ListItem",
1022          "position": 1,
1023          "name": "South Main Street — walkable shopping, antiques, and breweries; the commercial heart of Boerne"
1024        },
1025        {
1026          "@type": "ListItem",
1027          "position": 2,
1028          "name": "Cave Without a Name — show cave with 45-minute tours, a constant 66°F inside"
1029        },
1030        {
1031          "@type": "ListItem",
1032          "position": 3,
1033          "name": "Cibolo Nature Center — free trails along Cibolo Creek, 100 acres, 200+ bird species"
1034        },
1035        {
1036          "@type": "ListItem",
1037          "position": 4,
1038          "name": "The antique corridor — Carousel Antiques, Johns Road Antique Mall, Corner Cartel and more"
1039        },
1040        {
1041          "@type": "ListItem",
1042          "position": 5,
1043          "name": "Craft breweries — five within walking distance, led by the Dodging Duck Brewhaus on the creek"
1044        },
1045        {
1046          "@type": "ListItem",
1047          "position": 6,
1048          "name": "Old Jail Museum — the original 1884 county jail, small but well-curated"
1049        },
1050        {
1051          "@type": "ListItem",
1052          "position": 7,
1053          "name": "Enchanted Rock State Natural Area (45 min W) — granite dome hiking near Fredericksburg"
1054        },
1055        {
1056          "@type": "ListItem",
1057          "position": 8,
1058          "name": "Hill Country State Natural Area (30 min W) — hiking and equestrian trails near Bandera"
1059        }
1060      ]
1061    },
1062    {
1063      "@type": "FAQPage",
1064      "@id": "https://boerne.ai/#faqpage",
1065      "mainEntity": [
1066        {
1067          "@type": "Question",
1068          "name": "How do you pronounce Boerne?",
1069          "acceptedAnswer": {
1070            "@type": "Answer",
1071            "text": "BURN-ee. Two syllables. It's named for Ludwig Börne, a German political writer the founding settlers admired. Say it wrong and a local will gently correct you."
1072          }
1073        },
1074        {
1075          "@type": "Question",
1076          "name": "What is there to do in Boerne for a day?",
1077          "acceptedAnswer": {
1078            "@type": "Answer",
1079            "text": "Start on South Main Street — park near the Dienger Trading Co. and walk south through the shops and antique dealers. Have breakfast at Bear Moon Bakery (get there before 9 on weekends). In the afternoon, drive 10 minutes out to Cave Without a Name on Kreutzberg Road, or walk the free trails at the Cibolo Nature Center. End the day on the patio at the Dodging Duck Brewhaus, right on Cibolo Creek."
1080          }
1081        },
1082        {
1083          "@type": "Question",
1084          "name": "Is Boerne good for antiques?",
1085          "acceptedAnswer": {
1086            "@type": "Answer",
1087            "text": "Very. It's arguably the best antique corridor in the Hill Country — better than Fredericksburg for furniture and vintage housewares. The strongest stretch runs along South Main Street: Carousel Antiques, Corner Cartel, Laurie Saunders, Two Trick Pony, Mainly Vintage. For one big space under one roof, the Johns Road Antique Mall is the largest single dealer in town. Give yourself two hours if you're serious."
1088          }
1089        },
1090        {
1091          "@type": "Question",
1092          "name": "Where should I eat in Boerne?",
1093          "acceptedAnswer": {
1094            "@type": "Answer",
1095            "text": "Depends on the meal. Breakfast: Bear Moon Bakery or Mary's Tacos. Lunch: Richter Tavern or the Dienger Trading Co. Dinner: Botero Tapas & Wine Bar for something a little nicer, Richter Tavern for something reliable. If you'll drive 12 miles north, Po Po Family Restaurant in Welfare has been open since 1929."
1096          }
1097        },
1098        {
1099          "@type": "Question",
1100          "name": "Does Boerne have craft breweries?",
1101          "acceptedAnswer": {
1102            "@type": "Answer",
1103            "text": "Five within walking distance of each other, more or less. The Dodging Duck Brewhaus on River Road has the best patio in town, right on the creek. Also Free Roam Brewing, Tusculum Brewing, and Cibolo Creek Brewing along the Main corridor, plus Richter Cork & Keg for wine. Easy to do on foot."
1104          }
1105        },
1106        {
1107          "@type": "Question",
1108          "name": "Is Boerne good with kids?",
1109          "acceptedAnswer": {
1110            "@type": "Answer",
1111            "text": "Cave Without a Name is the best option — the tour is just long enough to hold their attention, and it's a constant 66°F inside. Boerne City Park has good playgrounds and creek access, and the Cibolo Nature Center has easy trails and lots of wildlife. For a half-day adventure, Enchanted Rock is about 45 minutes west."
1112          }
1113        },
1114        {
1115          "@type": "Question",
1116          "name": "Can you swim or tube on Cibolo Creek?",
1117          "acceptedAnswer": {
1118            "@type": "Answer",
1119            "text": "Honestly, no — the Cibolo Creek is beautiful but not deep enough for tubing. For real river swimming you'll want to drive: the Guadalupe near Kerrville is about 45 minutes, and the Frio at Concan is about 90 minutes. Cibolo Creek is for walking the trails and admiring it, not floating it."
1120          }
1121        },
1122        {
1123          "@type": "Question",
1124          "name": "Where can I stay in Boerne?",
1125          "acceptedAnswer": {
1126            "@type": "Answer",
1127            "text": "Backroads Hill Country manages vacation rentals in the Boerne area — including Boerne Mountain Retreat and a property on the Guadalupe River. Use the lodging page on this site to see what's open for your dates, or just ask me and I'll point you to the right fit."
1128          }
1129        },
1130        {
1131          "@type": "Question",
1132          "name": "When is the best time to visit Boerne?",
1133          "acceptedAnswer": {
1134            "@type": "Answer",
1135            "text": "Spring and fall are ideal — mild weather for walking Main Street and the trails. The town gets busy on weekends year-round and around the holidays, when the Christmas lighting and Main Street décor draw crowds. Weekday mornings are the quietest time to have the antique shops mostly to yourself."
1136          }
1137        },
1138        {
1139          "@type": "Question",
1140          "name": "Anything I should know before I come?",
1141          "acceptedAnswer": {
1142            "@type": "Answer",
1143            "text": "Boerne is a fast-growing town and businesses open and close faster than anyone can keep up with — if you've got your heart set on a specific spot, call ahead before you make the drive. Most of what you'll want is walkable on and around South Main Street, so park once and explore on foot."
1144          }
1145        }
1146      ]
1147    },
1148    {
1149      "@type": "LodgingBusiness",
1150      "@id": "https://boerne.ai/#guadalupe-river-retreat",
1151      "name": "Guadalupe River Retreat",
1152      "description": "A Backroads Hill Country managed property on the Guadalupe River near Boerne.",
1153      "address": {
1154        "@type": "PostalAddress",
1155        "addressLocality": "Boerne",
1156        "addressRegion": "TX",
1157        "addressCountry": "US"
1158      },
1159      "containedInPlace": {
1160        "@id": "https://boerne.ai/#place"
1161      },
1162      "url": "https://www.backroadstexas.net/property/guadalupe-river-retreat/",
1163      "potentialAction": {
1164        "@type": "ReserveAction",
1165        "target": {
1166          "@type": "EntryPoint",
1167          "urlTemplate": "https://www.backroadstexas.net/property/guadalupe-river-retreat/",
1168          "actionPlatform": [
1169            "http://schema.org/DesktopWebPlatform",
1170            "http://schema.org/MobileWebPlatform"
1171          ]
1172        },
1173        "result": {
1174          "@type": "LodgingReservation",
1175          "name": "Reserve Guadalupe River Retreat"
1176        }
1177      }
1178    },
1179    {
1180      "@type": "LodgingBusiness",
1181      "@id": "https://boerne.ai/#boerne-mountain-retreat",
1182      "name": "Boerne Mountain Retreat",
1183      "description": "A Backroads Hill Country managed property in the Boerne area.",
1184      "address": {
1185        "@type": "PostalAddress",
1186        "addressLocality": "Boerne",
1187        "addressRegion": "TX",
1188        "addressCountry": "US"
1189      },
1190      "containedInPlace": {
1191        "@id": "https://boerne.ai/#place"
1192      },
1193      "url": "https://www.backroadstexas.net/property/boerne-mountain-retreat/",
1194      "potentialAction": {
1195        "@type": "ReserveAction",
1196        "target": {
1197          "@type": "EntryPoint",
1198          "urlTemplate": "https://www.backroadstexas.net/property/boerne-mountain-retreat/",
1199          "actionPlatform": [
1200            "http://schema.org/DesktopWebPlatform",
1201            "http://schema.org/MobileWebPlatform"
1202          ]
1203        },
1204        "result": {
1205          "@type": "LodgingReservation",
1206          "name": "Reserve Boerne Mountain Retreat"
1207        }
1208      }
1209    },
1210    {
1211      "@type": "Claim",
1212      "@id": "https://boerne.ai/#claim-since-2001",
1213      "name": "Backroads Hill Country has managed Texas Hill Country vacation rentals since 2001.",
1214      "appearance": "https://www.backroadstexas.net/",
1215      "author": {
1216        "@id": "https://backroadstexas.net/#org"
1217      }
1218    },
1219    {
1220      "@type": "Claim",
1221      "@id": "https://boerne.ai/#claim-rating",
1222      "name": "Backroads Hill Country has a 4.9-star rating across 389 Google reviews.",
1223      "author": {
1224        "@id": "https://backroadstexas.net/#org"
1225      }
1226    }
1227  ]
1228}
1229</script>
1229
1230<!-- GA-FIX 2026-06-09 -->
1231<!-- HCJ-THREAD 2026-06-10 -->
1232<style>
1233/* SMALL WEATHER CHIP (nav, glanceable — current temp + condition) */
1234.wx-chip{display:inline-flex;align-items:center;gap:7px;font-family:var(--sans);font-size:14px;color:var(--cream);white-space:nowrap;padding-left:18px;border-left:1px solid var(--line)}
1235.wx-chip-icon{width:19px;height:19px;color:var(--gold-light);display:inline-flex;flex-shrink:0}
1236.wx-chip-icon svg{width:100%;height:100%}
1237.wx-chip-temp{font-weight:600;min-width:30px;text-align:center}
1238.wx-chip-cond{display:none}
1239@media(max-width:600px){.wx-chip{padding-left:11px;gap:5px}.wx-chip-cond{display:none}}
1240</style>
1241<style id="wx-normalise">.wx-chip-icon{color:#f0c56c !important}.wx-chip-icon svg{stroke:currentColor}.wx-chip-temp{color:#fffaf0 !important}@media(max-width:600px){.ev-nav,.nav-in{flex-wrap:nowrap !important;gap:8px}.ev-navlinks,.nav-links{flex-wrap:nowrap !important;gap:13px !important;min-width:0}.ev-navlinks a,.nav-links a{font-size:14px !important;white-space:nowrap}.wx-chip{padding-left:9px !important;gap:4px !important;font-size:13px !important;flex-shrink:0}.wx-chip-icon{width:16px !important;height:16px !important}}@media(max-width:380px){.ev-navlinks,.nav-links{gap:10px !important}.ev-navlinks a,.nav-links a{font-size:13px !important}}</style>
1242</head>
1243<body data-town="boerne">
1244
1245<!-- Network status banner - full-width strip at top -->
1246<a class="network-banner" href="https://hillcountry.ai" target="_blank" rel="noopener" aria-label="Visit the HillCountry.ai network hub">
1247  <span class="nb-dot" aria-hidden="true"></span>HillCountry.ai network<span class="nb-arrow" aria-hidden="true">&rarr;</span>
1248</a>
1249
1250<nav>
1251  <div class="wordmark"><a href="/" class="wm-hub" aria-label="Boerne home">hillcountry<span>.ai</span> / boerne</a></div>
1252  <div class="nav-links">
1253    <a href="/things-to-do">Things to Do</a>
1254    <a href="/vacation-rentals">Vacation Rentals</a>
1255    <a href="/events">Events</a>
1256      <span class="wx-chip" id="wx-chip" aria-label="Current weather in Boerne" title="Current weather in Boerne">
1257        <span class="wx-chip-icon" id="wx-chip-icon"></span>
1258        <span class="wx-chip-temp" id="wx-chip-temp">&mdash;</span>
1259        <span class="wx-chip-cond" id="wx-chip-cond"></span>
1260      </span>
1261  </div>
1262</nav>
1263
1264<header class="hero">
1265  <svg class="topo" viewBox="0 0 1440 800" preserveAspectRatio="none" aria-hidden="true">
1266    <path d="M0 200 Q360 120 720 200 T1440 200" fill="none" stroke="#1d5238" stroke-width="1" opacity=".4"/>
1267    <path d="M0 320 Q360 240 720 320 T1440 320" fill="none" stroke="#1d5238" stroke-width="1" opacity=".4"/>
1268    <path d="M0 440 Q360 360 720 440 T1440 440" fill="none" stroke="#1d5238" stroke-width="1" opacity=".4"/>
1269    <path d="M0 560 Q360 480 720 560 T1440 560" fill="none" stroke="#1d5238" stroke-width="1" opacity=".4"/>
1270  </svg>
1271  <div class="grain" aria-hidden="true"></div>
1272  <div class="hero-inner">
1273    <div class="eyebrow">Main Street, Antiques & German Heritage</div>
1274    <h1>Boerne, Texas</h1>
1275    <div class="subtitle">Lodging · South Main Street · Antiques · Local Guide</div>
1276    <div class="hero-towns">Boerne · Comfort</div>
1277
1278    <!-- Coming-soon guide tile. No chat scaffolding. Replaced when guide ships per-town. -->
1279        <!-- June chat widget — Boerne's AI local guide (v0.1) -->
1280    <div class="chat-wrap" id="ask" role="region" aria-label="Chat with June, your Boerne local guide">
1281      <div class="chat-thread" id="chat-thread" aria-live="polite">
1282        <!-- June's opening message rendered by JS on load -->
1283      </div>
1284            <div class="start-pills" id="start-pills" role="group" aria-label="Suggested questions">
1285        <button type="button" class="start-pill" data-prompt="What&#x27;s happening on Main Street this weekend?">Main Street this weekend?</button>
1286        <a class="start-pill start-pill-lodging" href="/vacation-rentals">Vacation Rentals</a>
1287        <button type="button" class="start-pill" data-prompt="Best restaurant for date night in Boerne?">Best restaurant for date night in Boerne?</button>
1288        <button type="button" class="start-pill" data-prompt="What can we do with kids today?">What can we do with kids today?</button>
1289        <button type="button" class="start-pill" data-prompt="Is Cave Without a Name worth it?">Is Cave Without a Name worth it?</button>
1290        <button type="button" class="start-pill" data-prompt="What&#x27;s worth seeing in Comfort?">What&#x27;s worth seeing in Comfort?</button>
1291      </div>
1292      <div class="chat-input-row">
1293        <textarea id="chat-input" placeholder="Ask June about Boerne..." rows="1" aria-label="Ask June about Boerne"></textarea>
1294        <button class="send" id="send-btn" aria-label="Send">→</button>
1295      </div>
1296    </div>
1297    <div class="chat-status chat-guide-status-wrap">
1298      <button type="button" class="chat-report-link" id="report-problem-trigger" aria-haspopup="dialog">
1299        Got a content idea, see something off, or want to help us improve? <u>Share it &rarr;</u>
1300      </button>
1301      <span class="chat-guide-status" tabindex="0">
1302        <span class="cbs-dot" aria-hidden="true"></span>June &middot; v11 &middot; live
1303        <span class="cbs-tip" role="tooltip">June is live &mdash; let us know if something looks wrong. Your conversations are not used to train our AI model.</span>
1304      </span>
1305    </div>
1306  </div>
1307  <div class="scroll-hint">scroll for more<span>↓</span></div>
1308</header>
1309
1310<!-- LIST YOUR BUSINESS CTA -->
1311<section class="lyb-section">
1312  <a href="/founder" class="lyb-card" aria-label="List your business in the HillCountry.ai network">
1313    <div class="lyb-text">
1314      <div class="lyb-eyebrow">For Hill Country Businesses</div>
1315      <div class="lyb-title">Want to be the answer?</div>
1316      <div class="lyb-sub">If you run a Hill Country business, here's where to start. One page that explains how the network works and what listing involves.</div>
1317    </div>
1318    <div class="lyb-cta">List your business &rarr;</div>
1319  </a>
1320</section>
1321
1322<!-- ABOUT THIS TOWN -->
1323<section class="about-hc" id="about">
1324  <div class="about-hc-inner">
1325    <div class="about-hc-label">About Boerne</div>
1326    <h2 class="about-hc-h2">Boerne, Texas &mdash; Main Street, Antiques & German Heritage</h2>
1327    <p class="about-hc-lede">Boerne (say it <strong>BURN-ee</strong>) is the county seat of Kendall County, about 30 miles northwest of San Antonio on I-10 — close enough for a day trip from the city, far enough that it still feels like somewhere. It was founded in <strong>1849</strong> by German immigrants and freethinkers and named for Ludwig Börne, a German political writer. The heritage is real: limestone architecture, German family names, and a culture of craftsmanship that still shows up on the street. People come for the shopping, the food, the breweries, and what may be the best antique corridor in the Hill Country. What they don't expect is how much there is to do.</p>
1328
1329    <div class="facts-grid">
1330            <div class="fact">
1331        <div class="fact-label">Founded</div>
1332        <div class="fact-value">1849<small>German immigrants · named for Ludwig Börne</small></div>
1333      </div>
1334      <div class="fact">
1335        <div class="fact-label">County</div>
1336        <div class="fact-value">Kendall County<small>county seat</small></div>
1337      </div>
1338      <div class="fact">
1339        <div class="fact-label">Population</div>
1340        <div class="fact-value">~18,000<small>city · ~45,000 greater area</small></div>
1341      </div>
1342      <div class="fact">
1343        <div class="fact-label">Known for</div>
1344        <div class="fact-value">South Main Street<small>antiques · craft breweries · German heritage</small></div>
1345      </div>
1346      <div class="fact">
1347        <div class="fact-label">Nearby</div>
1348        <div class="fact-value">Cave Without a Name<small>Cibolo Nature Center · Enchanted Rock</small></div>
1349      </div>
1350      <div class="fact">
1351        <div class="fact-label">From San Antonio</div>
1352        <div class="fact-value">~30 miles NW<small>about 30 minutes on I-10</small></div>
1353      </div>
1354    </div>
1355  </div>
1356</section>
1357
1358<!-- ABOUT US -->
1359<details class="more-section" id="more">
1360  <summary class="more-toggle">
1361    <span class="more-toggle-label"><span class="lbl-closed">Read more about Boerne.ai</span><span class="lbl-open">Show less</span></span>
1362    <span class="more-toggle-chev" aria-hidden="true">&#9662;</span>
1363  </summary>
1364<section class="about-us" id="about-us">
1365  <div class="about-us-inner">
1366    <div class="about-us-badge">About Us</div>
1367    <h2 class="about-us-h2">Built by a locally operated Hill Country travel company.</h2>
1368    <p class="about-us-text">boerne.ai is built by <strong>Spencer and Jess Forrest</strong>, owners of <strong>Backroads Hill Country</strong> &mdash; a locally operated Texas Hill Country travel company that has represented Hill Country vacation rentals since 2001, with thousands of guest stays coordinated across the region.</p>
1369    <p class="about-us-text">Most travel platforms flatten a place like Boerne into generic top-10 lists. This is built the other way around &mdash; local knowledge first, from people who actually live and work in the Hill Country.</p>
1370    <div class="about-us-sig">
1371      Spencer &amp; Jess Forrest
1372      <small>Backroads Hill Country</small>
1373    </div>
1374  </div>
1375</section>
1376
1377<!-- The HillCountry.ai network map — every town site shows this with their
1378     own pin highlighted. The current town pin is automatically enlarged and
1379     turned gold by the script below, based on either body data-town or the
1380     domain's first hostname segment. -->
1381<section class="network-section">
1382  <div class="network-inner">
1383    <div class="network-label">The Network</div>
1384    <h2 class="network-h2">Part of HillCountry.ai</h2>
1385    <p class="network-sub"><strong>The Digital Front Door to the Texas Hill Country.</strong> An AI-powered discovery network connecting towns, rivers, parks, events, and the places worth knowing across the region.</p>
1386    <div class="network-map-wrap">
1387      <svg id="hc-network-map" viewBox="0 0 1000 600" width="100%" role="img" aria-labelledby="nmT nmD" style="display:block;font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;">
1388        <title id="nmT">The HillCountry.ai network across the Texas Hill Country</title>
1389        <desc id="nmD">A network spanning towns, parks, attractions, rivers, events and activities across the Texas Hill Country, with a central hub connected to named town sites.</desc>
1390        <rect x="0" y="0" width="1000" height="600" fill="#082118"/>
1391        <rect x="0" y="0" width="1000" height="600" fill="#0e2e1f"/>
1392        <g transform="translate(0,-30)">
1393        <g opacity="0.5" stroke="#14402c" stroke-width="1" fill="none">
1394          <path d="M0,200 C220,160 360,250 540,210 C720,170 860,260 1000,220"/>
1395          <path d="M0,310 C200,280 380,360 560,320 C760,275 880,370 1000,335"/>
1396          <path d="M0,430 C240,400 400,480 600,435 C780,395 900,475 1000,450"/>
1397          <path d="M0,520 C220,495 420,555 620,520 C800,485 900,540 1000,520"/>
1398        </g>
1399        <g opacity="0.28" fill="none" stroke="#d9a441" stroke-width="1" stroke-linecap="round">
1400          <path d="M500,340 C420,295 360,255 300,215"/>
1401          <path d="M500,340 C470,280 450,235 430,185"/>
1402          <path d="M500,340 C560,290 600,250 660,210"/>
1403          <path d="M500,340 C600,315 690,285 760,255"/>
1404          <path d="M500,340 C470,395 440,435 400,475"/>
1405          <path d="M500,340 C400,385 270,440 140,475"/>
1406          <path d="M500,340 C560,395 610,430 670,465"/>
1407          <path d="M500,340 C500,260 530,180 545,130"/>
1408          <path d="M500,340 C540,280 575,225 605,175"/>
1409          <path d="M500,340 C620,300 740,210 830,165"/>
1410          <path d="M500,340 C400,360 280,400 190,430"/>
1411          <path d="M500,340 C690,350 800,370 880,385"/>
1412          <path d="M500,340 C600,365 720,450 790,495"/>
1413          <path d="M500,340 C520,415 510,475 500,515"/>
1414        </g>
1415        <g fill="#d9a441">
1416          <circle cx="850" cy="305" r="4" opacity="0.6"/>
1417          <circle cx="280" cy="495" r="4" opacity="0.6"/>
1418          <circle cx="790" cy="495" r="4" opacity="0.6"/>
1419          <circle cx="210" cy="260" r="3.5" opacity="0.5"/>
1420          <circle cx="345" cy="175" r="3.5" opacity="0.5"/>
1421          <circle cx="880" cy="455" r="3.5" opacity="0.5"/>
1422          <circle cx="180" cy="345" r="3.5" opacity="0.5"/>
1423          <circle cx="330" cy="420" r="3.5" opacity="0.5"/>
1424          <circle cx="760" cy="380" r="3.5" opacity="0.5"/>
1425          <circle cx="250" cy="350" r="3" opacity="0.42"/>
1426          <circle cx="900" cy="240" r="3" opacity="0.42"/>
1427          <circle cx="370" cy="535" r="3" opacity="0.42"/>
1428        </g>
1429        <g>
1430          <circle cx="500" cy="340" r="96" fill="#0e2e1f" stroke="#d9a441" stroke-width="3"/>
1431          <circle cx="500" cy="340" r="96" fill="none" stroke="#f0c56c" stroke-width="1" opacity="0.5"/>
1432          <circle cx="500" cy="340" r="110" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.25"/>
1433          <text x="500" y="335" text-anchor="middle" fill="#f0c56c" font-size="24" font-weight="700" letter-spacing="-0.3">hillcountry.ai</text>
1434          <text x="500" y="365" text-anchor="middle" fill="#fffaf0" font-size="12" font-weight="600" letter-spacing="2" opacity="0.7">THE NETWORK HUB</text>
1435        </g>
1436        <g id="hc-pins">
1437          <g data-town="bandera"><circle cx="300" cy="215" r="6.5" fill="#d9a441"/><circle cx="300" cy="215" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1438            <text x="300" y="189" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Bandera</text></g>
1439          <g data-town="fredericksburg"><circle cx="430" cy="185" r="6.5" fill="#d9a441"/><circle cx="430" cy="185" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1440            <text x="430" y="159" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Fredericksburg</text></g>
1441          <g data-town="kerrville"><circle cx="660" cy="210" r="6.5" fill="#d9a441"/><circle cx="660" cy="210" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1442            <text x="660" y="184" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Kerrville</text></g>
1443          <g data-town="boerne"><circle cx="760" cy="255" r="6.5" fill="#d9a441"/><circle cx="760" cy="255" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1444            <text x="782" y="261" text-anchor="start" fill="#fffaf0" font-size="19" font-weight="600">Boerne</text></g>
1445          <g data-town="concan"><circle cx="400" cy="475" r="6.5" fill="#d9a441"/><circle cx="400" cy="475" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1446            <text x="400" y="511" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Concan</text></g>
1447          <g data-town="wimberley"><circle cx="670" cy="465" r="6.5" fill="#d9a441"/><circle cx="670" cy="465" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1448            <text x="670" y="501" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Wimberley</text></g>
1449          <g data-town="llano"><circle cx="545" cy="130" r="6.5" fill="#d9a441"/><circle cx="545" cy="130" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1450            <text x="545" y="104" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Llano</text></g>
1451          <g data-town="marblefalls"><circle cx="830" cy="165" r="6.5" fill="#d9a441"/><circle cx="830" cy="165" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1452            <text x="830" y="139" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Marble Falls</text></g>
1453          <g data-town="leakey"><circle cx="190" cy="430" r="6.5" fill="#d9a441"/><circle cx="190" cy="430" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1454            <text x="190" y="466" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Leakey</text></g>
1455          <g data-town="gruene"><circle cx="800" cy="492" r="6.5" fill="#d9a441"/><circle cx="800" cy="492" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1456            <text x="800" y="526" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Gruene</text></g>
1457          <g data-town="campwood"><circle cx="140" cy="475" r="6.5" fill="#d9a441"/><circle cx="140" cy="475" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1458            <text x="140" y="509" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Camp Wood</text></g>
1459          <g data-town="drippingsprings"><circle cx="880" cy="385" r="6.5" fill="#d9a441"/><circle cx="880" cy="385" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1460            <text x="880" y="359" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Dripping Springs</text></g>
1461          <g data-town="luckenbach"><circle cx="625" cy="155" r="6.5" fill="#d9a441"/><circle cx="625" cy="155" r="12" fill="none" stroke="#d9a441" stroke-width="1" opacity="0.4"/>
1462            <text x="625" y="129" text-anchor="middle" fill="#fffaf0" font-size="19" font-weight="600">Luckenbach</text></g>
1463        </g>
1464        </g>
1465        <text x="40" y="46" fill="#fffaf0" font-size="20" font-weight="700" letter-spacing="2">THE HILLCOUNTRY.AI NETWORK</text>
1466        <text x="500" y="556" text-anchor="middle" fill="#fffaf0" font-size="21" font-weight="600" letter-spacing="0.3">One AI discovery layer for every place worth knowing in the Texas Hill Country</text>
1467        <text x="500" y="584" text-anchor="middle" fill="#f0c56c" font-size="15.5" font-weight="600" letter-spacing="0.8">100+ AI domains &mdash; towns, parks, attractions, rivers, events &amp; activities</text>
1468      </svg>
1469    </div>
1470  </div>
1471</section>
1472
1473<script>
1474/* Network map town-highlight: enlarges the pin for the town this page is on.
1475   Detects from current hostname (e.g. fredericksburg.ai -> highlights fredericksburg). */
1476(function() {
1477  try {
1478    // First check explicit data-town on body (works in any preview environment).
1479    // Then fall back to hostname detection (works on deployed *.ai domain).
1480    var slug = (document.body && document.body.getAttribute('data-town')) || '';
1481    if (!slug) {
1482      var host = (location.hostname || '').replace(/^www\./, '').toLowerCase();
1483      slug = host.split('.')[0];
1484    }
1485    slug = (slug || '').toLowerCase();
1486    if (!slug || slug === 'localhost' || slug === 'hillcountry') return;
1487
1488    var pin = document.querySelector('#hc-pins g[data-town="' + slug + '"]');
1489    if (!pin) return;
1490    var circle = pin.querySelector('circle:first-child');
1491    var ring = pin.querySelector('circle:nth-child(2)');
1492    var label = pin.querySelector('text');
1493    // Enlarge the dot
1494    if (circle) { circle.setAttribute('r', '10'); circle.setAttribute('fill', '#f0c56c'); }
1495    // Enlarge the ring with more visibility
1496    if (ring) { ring.setAttribute('r', '18'); ring.setAttribute('opacity', '0.85'); ring.setAttribute('stroke-width', '2'); ring.setAttribute('stroke', '#f0c56c'); }
1497    // Enlarge the town name AND push it further from the pin (so larger
1498    // text doesn't touch the enlarged ring)
1499    if (label) {
1500      label.setAttribute('font-size', '26');
1501      label.setAttribute('font-weight', '700');
1502      label.setAttribute('fill', '#f0c56c');
1503      var currentY = parseFloat(label.getAttribute('y'));
1504      var pinCy = parseFloat(circle.getAttribute('cy'));
1505      // If label is above pin (y < pinCy), move it up. If below, move it down.
1506      var labelIsAbove = currentY < pinCy;
1507      label.setAttribute('y', labelIsAbove ? (currentY - 8) : (currentY + 8));
1508    }
1509  } catch (e) { console.warn('Network map highlight failed:', e); }
1510})();
1511</script>
1511
1512
1513<!-- EARLY ACCESS -->
1514<section class="ea-section" id="early-access">
1515  <div class="ea-inner">
1516    <div class="ea-label">Early Access</div>
1517    <h2 class="ea-h2">More towns are on the way.</h2>
1518    <p class="ea-sub">We're adding towns across the Hill Country. Leave your email and we'll let you know when new towns join the network.</p>
1519    <form id="ea-form"
1520          action="https://api.web3forms.com/submit"
1521          method="POST"
1522          novalidate
1523          class="ea-form">
1524      <input type="hidden" name="access_key" value="79ea7c00-4e49-4861-9194-baa9d28b0a6a">
1525      <input type="hidden" name="subject" value="Early Access Signup - boerne.ai (Backroads Hill Country)">
1526      <input type="hidden" name="from_name" value="boerne.ai Early Access (Backroads Hill Country)">
1527      <input type="hidden" name="replyto" id="ea-replyto" value="">
1528      <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">
1529      <input type="email" name="email" id="ea-email" placeholder="[email protected]" required autocomplete="email" aria-label="Email address">
1530      <button type="submit" id="ea-submit" class="ea-submit">Notify me &rarr;</button>
1531    </form>
1532    <div class="ea-note">No spam. One email when new towns join the network.</div>
1533    <div class="ea-success" id="ea-success">Thanks &mdash; we'll be in touch when new towns go live on the network.</div>
1534  </div>
1535</section>
1536
1537<script>
1538/* Early Access form handler - Web3Forms AJAX, inline thank-you, no redirect */
1539(function() {
1540  var form = document.getElementById('ea-form');
1541  if (!form) return;
1542  form.addEventListener('submit', async function(e) {
1543    e.preventDefault();
1544    var btn = document.getElementById('ea-submit');
1545    var success = document.getElementById('ea-success');
1546    btn.disabled = true;
1547    btn.textContent = 'Sending...';
1548
1549    var emailField = document.getElementById('ea-email');
1550    var replyto = document.getElementById('ea-replyto');
1551    if (emailField && replyto) {
1552      replyto.value = emailField.value;
1553    }
1554
1555    try {
1556      var formData = new FormData(form);
1557      var res = await fetch('https://api.web3forms.com/submit', {
1558        method: 'POST',
1559        body: formData,
1560        headers: { 'Accept': 'application/json' }
1561      });
1562      var data = await res.json();
1563      if (res.ok && data.success) {
1564        form.style.display = 'none';
1565        if (success) success.classList.add('show');
1566        // Clear the #early-access hash so a future refresh doesn't re-scroll to and
1567        // re-trigger this state. replaceState avoids any scroll jump or reload.
1568        if (window.location.hash) {
1569          history.replaceState(null, '', window.location.pathname + window.location.search);
1570        }
1571      } else {
1572        btn.disabled = false;
1573        btn.innerHTML = 'Try again';
1574        alert('Something went wrong. Try again, or email [email protected].');
1575      }
1576    } catch (err) {
1577      btn.disabled = false;
1578      btn.innerHTML = 'Try again';
1579      alert('Lost the signal. Check your connection and try again.');
1580    }
1581  });
1582})();
1583</script>
1583
1584
1585<!-- FAQ -->
1586<section class="faq-sec" id="faq">
1587  <div class="faq-inner">
1588    <div class="faq-label">Local Knowledge</div>
1589    <h2 class="faq-h2">Frequently asked about Boerne</h2>
1590    
1591    <div class="faq-item" data-tag="pronounce">
1592      <button class="faq-q-btn" aria-expanded="false">
1593        <span>How do you pronounce Boerne?</span>
1594        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1595      </button>
1596      <div class="faq-a-wrap"><div class="faq-a"><strong>BURN-ee</strong>. Two syllables. It's named for <strong>Ludwig Börne</strong>, a German political writer the founding settlers admired. Say it wrong and a local will gently correct you.</div></div>
1597    </div>
1598
1599    <div class="faq-item" data-tag="things-to-do">
1600      <button class="faq-q-btn" aria-expanded="false">
1601        <span>What is there to do in Boerne for a day?</span>
1602        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1603      </button>
1604      <div class="faq-a-wrap"><div class="faq-a">Start on <strong>South Main Street</strong> — park near the Dienger Trading Co. and walk south through the shops and antique dealers. Have breakfast at <strong>Bear Moon Bakery</strong> (get there before 9 on weekends). In the afternoon, drive 10 minutes out to <strong>Cave Without a Name</strong> on Kreutzberg Road, or walk the free trails at the <strong>Cibolo Nature Center</strong>. End the day on the patio at the <strong>Dodging Duck Brewhaus</strong>, right on Cibolo Creek.</div></div>
1605    </div>
1606
1607    <div class="faq-item" data-tag="antiques">
1608      <button class="faq-q-btn" aria-expanded="false">
1609        <span>Is Boerne good for antiques?</span>
1610        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1611      </button>
1612      <div class="faq-a-wrap"><div class="faq-a">Very. It's arguably the best antique corridor in the Hill Country — better than Fredericksburg for furniture and vintage housewares. The strongest stretch runs along <strong>South Main Street</strong>: Carousel Antiques, Corner Cartel, Laurie Saunders, Two Trick Pony, Mainly Vintage. For one big space under one roof, the <strong>Johns Road Antique Mall</strong> is the largest single dealer in town. Give yourself two hours if you're serious.</div></div>
1613    </div>
1614
1615    <div class="faq-item" data-tag="where-to-eat">
1616      <button class="faq-q-btn" aria-expanded="false">
1617        <span>Where should I eat in Boerne?</span>
1618        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1619      </button>
1620      <div class="faq-a-wrap"><div class="faq-a">Depends on the meal. <strong>Breakfast:</strong> Bear Moon Bakery or Mary's Tacos. <strong>Lunch:</strong> Richter Tavern or the Dienger Trading Co. <strong>Dinner:</strong> Botero Tapas & Wine Bar for something a little nicer, Richter Tavern for something reliable. If you'll drive 12 miles north, <strong>Po Po Family Restaurant</strong> in Welfare has been open since 1929.</div></div>
1621    </div>
1622
1623    <div class="faq-item" data-tag="breweries">
1624      <button class="faq-q-btn" aria-expanded="false">
1625        <span>Does Boerne have craft breweries?</span>
1626        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1627      </button>
1628      <div class="faq-a-wrap"><div class="faq-a">Five within walking distance of each other, more or less. The <strong>Dodging Duck Brewhaus</strong> on River Road has the best patio in town, right on the creek. Also Free Roam Brewing, Tusculum Brewing, and Cibolo Creek Brewing along the Main corridor, plus Richter Cork & Keg for wine. Easy to do on foot.</div></div>
1629    </div>
1630
1631    <div class="faq-item" data-tag="kids">
1632      <button class="faq-q-btn" aria-expanded="false">
1633        <span>Is Boerne good with kids?</span>
1634        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1635      </button>
1636      <div class="faq-a-wrap"><div class="faq-a"><strong>Cave Without a Name</strong> is the best option — the tour is just long enough to hold their attention, and it's a constant 66°F inside. <strong>Boerne City Park</strong> has good playgrounds and creek access, and the <strong>Cibolo Nature Center</strong> has easy trails and lots of wildlife. For a half-day adventure, Enchanted Rock is about 45 minutes west.</div></div>
1637    </div>
1638
1639    <div class="faq-item" data-tag="swimming">
1640      <button class="faq-q-btn" aria-expanded="false">
1641        <span>Can you swim or tube on Cibolo Creek?</span>
1642        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1643      </button>
1644      <div class="faq-a-wrap"><div class="faq-a">Honestly, no — the <strong>Cibolo Creek</strong> is beautiful but not deep enough for tubing. For real river swimming you'll want to drive: the <strong>Guadalupe</strong> near Kerrville is about 45 minutes, and the <strong>Frio</strong> at Concan is about 90 minutes. Cibolo Creek is for walking the trails and admiring it, not floating it.</div></div>
1645    </div>
1646
1647    <div class="faq-item" data-tag="where-to-stay">
1648      <button class="faq-q-btn" aria-expanded="false">
1649        <span>Where can I stay in Boerne?</span>
1650        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1651      </button>
1652      <div class="faq-a-wrap"><div class="faq-a"><a href="https://www.backroadstexas.net/" target="_blank" rel="noopener"><strong>Backroads Hill Country</strong></a> manages vacation rentals in the Boerne area — including Boerne Mountain Retreat and a property on the Guadalupe River. Use the lodging page on this site to see what's open for your dates, or just ask me and I'll point you to the right fit.</div></div>
1653    </div>
1654
1655    <div class="faq-item" data-tag="when-to-visit">
1656      <button class="faq-q-btn" aria-expanded="false">
1657        <span>When is the best time to visit Boerne?</span>
1658        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1659      </button>
1660      <div class="faq-a-wrap"><div class="faq-a"><strong>Spring and fall</strong> are ideal — mild weather for walking Main Street and the trails. The town gets busy on weekends year-round and around the holidays, when the Christmas lighting and Main Street décor draw crowds. Weekday mornings are the quietest time to have the antique shops mostly to yourself.</div></div>
1661    </div>
1662
1663    <div class="faq-item" data-tag="before-you-come">
1664      <button class="faq-q-btn" aria-expanded="false">
1665        <span>Anything I should know before I come?</span>
1666        <span class="faq-caret" aria-hidden="true">&#9662;</span>
1667      </button>
1668      <div class="faq-a-wrap"><div class="faq-a">Boerne is a fast-growing town and businesses open and close faster than anyone can keep up with — if you've got your heart set on a specific spot, call ahead before you make the drive. Most of what you'll want is walkable on and around <strong>South Main Street</strong>, so park once and explore on foot.</div></div>
1669    </div>
1670  </div>
1671</section>
1672
1673</details>
1674<footer>
1675  <div class="f-mark">hillcountry<span>.ai</span> / boerne</div>
1676  <div class="f-tagline">The Digital Front Door to the Texas Hill Country</div>
1677  <div class="f-stats-line">Powered by Backroads Hill Country &middot; Since 2001</div>
1678  <div class="f-stats-line stars">4.9&#9733; across 389 Google reviews &middot; 4.8&#9733; App Store &middot; 4.7&#9733; Google Play</div>
1679
1680  <div class="f-social-row" style="margin-top:18px">
1681    <a href="https://www.facebook.com/groups/hillcountrytravelapp" target="_blank" rel="noopener">Facebook Group</a>
1682    <a href="https://www.facebook.com/backroadshillcountry" target="_blank" rel="noopener">Facebook Page</a>
1683    <a href="https://www.instagram.com/backroadsreservations/" target="_blank" rel="noopener">Instagram</a>
1684    <a href="https://www.youtube.com/@hillcountryapp" target="_blank" rel="noopener">YouTube</a>
1685    <span class="f-divider" aria-hidden="true"></span>
1686    <a href="https://www.hillcountry.app" target="_blank" rel="noopener">App Store</a>
1687    <a href="https://www.hillcountry.app" target="_blank" rel="noopener">Google Play</a>
1688  </div>
1689
1690  <div class="f-row">
1691    Built on the Hill Country track record of <a href="https://backroadstexas.net" target="_blank" rel="noopener"><strong>Backroads Hill Country</strong></a>, representing Hill Country vacation rentals since 2001.
1692  </div>
1693
1694  <div class="f-row">
1695    Business inquiries: <a href="/founder">For businesses</a> &middot; <a href="#" class="ob-mail" data-u="spencer" data-d="backroads.ai" onclick="return false;">spencer&#64;backroads&#46;ai</a>
1696  </div>
1697
1698  <div class="f-row">
1699    Own a Hill Country property? <a href="/founder">Talk to us about representing your property</a>
1700  </div>
1701
1702  <div class="f-row">
1703    <a href="/privacy">Privacy Policy</a> &middot; Updated May 2026
1704  </div>
1705
1706    <div class="f-explore">
1707    <a href="https://hillcountry.ai/rivers/">Rivers</a>
1708    <a href="https://hillcountry.ai/parks/">Parks</a>
1709    <a href="/what-is-boerne">About Boerne</a>
1710  </div>
1711  <div class="f-network-blurb">
1712    <a href="https://hillcountry.ai" target="_blank" rel="noopener"><strong>
1712HillCountry.ai</strong></a> is a network of 100+ .ai domains organized by town, river, park, stay, event, and experience &mdash; all feeding into one Hill Country discovery layer.
1713  </div>
1714</footer>
1715
1716
1717<!-- FAQ accordion handler -->
1718<script>
1719(function() {
1720  var btns = document.querySelectorAll('.faq-q-btn');
1721  btns.forEach(function(btn) {
1722    btn.addEventListener('click', function() {
1723      var item = btn.closest('.faq-item');
1724      var isOpen = item.classList.toggle('open');
1725      btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
1726    });
1727  });
1728})();
1729</script>
1729
1730
1731<!-- ═══════════════════════════════════════════════════════════
1732     REPORT / FEEDBACK MODAL — routes to [email protected] via web3forms.
1733     Triggered by #report-problem-trigger button below the chat box.
1734     Ported from Brody v29. Subject line is Boerne-specific.
1735     ═══════════════════════════════════════════════════════════ -->
1736<div class="report-modal-overlay" id="report-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="report-modal-h2">
1737  <div class="report-modal">
1738    <button class="report-modal-close" id="report-modal-close" aria-label="Close form" type="button">&times;</button>
1739    <div class="report-modal-label">Help us improve</div>
1740    <h2 class="report-modal-h2" id="report-modal-h2">Good, bad, or ugly &mdash; we want to hear it.</h2>
1741    <p class="report-modal-intro">June's still learning Boerne. Share a content idea, flag something that looks off, or just tell us how we could make this better. Every submission goes straight to our team.</p>
1742
1743    <form id="report-form" action="https://api.web3forms.com/submit" method="POST" novalidate>
1744      <input type="hidden" name="access_key" value="79ea7c00-4e49-4861-9194-baa9d28b0a6a">
1745      <input type="hidden" name="subject" value="Boerne.ai · Feedback from a traveler">
1746      <input type="hidden" name="from_name" value="Boerne.ai Feedback">
1747      <input type="hidden" name="recipient_intent" value="[email protected]">
1748      <input type="hidden" name="replyto" id="report-replyto" value="">
1749      <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off">
1750
1751      <div class="report-field">
1752        <label for="report-type">What is this?<span class="req">*</span></label>
1753        <select id="report-type" name="submission_type" required>
1754          <option value="">Pick one&hellip;</option>
1755          <option value="content_idea">Content idea &mdash; something we should add or feature</option>
1756          <option value="data_correction">Data correction &mdash; June got something wrong</option>
1757          <option value="bug_report">Bug or technical issue &mdash; something didn't work</option>
1758          <option value="general_feedback">General feedback &mdash; good, bad, or ugly</option>
1759        </select>
1760      </div>
1761
1762      <div class="report-field">
1763        <label for="report-issue">Tell us more<span class="req">*</span></label>
1764        <textarea id="report-issue" name="submission_detail" required rows="5" placeholder="Examples: 'You should feature the river festival in July.' / 'June listed hours that look wrong for a place.' / 'The map didn't load on my phone.' / 'Love the site, would be great if...'"></textarea>
1765        <div class="hint">Detail helps. What you noticed, what you'd suggest, what you'd want to see.</div>
1766      </div>
1767
1768      <div class="report-field">
1769        <label for="report-email">Your email (optional)</label>
1770        <input type="email" id="report-email" name="reporter_email" placeholder="[email protected]">
1771        <div class="hint">Only if you want us to follow up. We won't add you to any list.</div>
1772      </div>
1773
1774      <div class="report-actions">
1775        <button type="submit" class="report-submit" id="report-submit">Send it</button>
1776        <button type="button" class="report-cancel" id="report-cancel-btn">Cancel</button>
1777      </div>
1778    </form>
1779
1780    <div class="report-success" id="report-success">
1781      <strong style="color:var(--cream);display:block;margin-bottom:8px">Thank you.</strong>
1782      We got it &mdash; this goes straight to our team. If you left an email, we'll follow up.
1783    </div>
1784  </div>
1785</div>
1786<script>
1787(function(){
1788  var GUIDE_CONFIG = {
1789    endpoint: '/api/june',
1790    timeoutMs: 30000,
1791    maxPairs: 20
1792  };
1793
1794  // ─── DATA LOAD: POI + rentals for card lookup ────────────────────
1795  window.BOERNE_DATA = { poi: {}, rentals: {}, loaded: false };
1796
1797  window.BOERNE_DATA.ready = (async function loadData() {
1798    try {
1799      var poiRes = await fetch('/data/poi-boerne.json');
1800      if (poiRes.ok) {
1801        var poiJson = await poiRes.json();
1802        (poiJson.businesses || []).forEach(function(b) {
1803          var existing = window.BOERNE_DATA.poi[b.id];
1804          if (existing) {
1805            // Same business appears in multiple categories — merge category memberships
1806            // into an array on `categories`, keep the first occurrence as the canonical entry.
1807            if (!existing.categories) existing.categories = [existing.category];
1808            if (existing.categories.indexOf(b.category) === -1) {
1809              existing.categories.push(b.category);
1810            }
1811          } else {
1812            b.categories = [b.category];
1813            window.BOERNE_DATA.poi[b.id] = b;
1814          }
1815        });
1816      }
1817      var rentalRes = await fetch('/data/rentals-boerne.json');
1818      if (rentalRes.ok) {
1819        var rentalJson = await rentalRes.json();
1820        (rentalJson.properties || []).forEach(function(p) {
1821          window.BOERNE_DATA.rentals[p.id] = p;
1822        });
1823      }
1824      window.BOERNE_DATA.loaded = true;
1825      console.log('[June] Loaded',
1826        Object.keys(window.BOERNE_DATA.poi).length, 'POIs and',
1827        Object.keys(window.BOERNE_DATA.rentals).length, 'rentals');
1828    } catch (e) {
1829      console.warn('[June] Data load failed:', e.message);
1830    }
1831  })();
1832
1833  // ─── CARD LOOKUP: maps an ID to a renderable card object ─────────
1834  function getCardData(id) {
1835    if (!id) return null;
1836
1837    // Synthetic Backroads Authority card (not in JSON)
1838    if (id === 'backroads-hill-country-boerne' || id === 'backroads-hill-country') {
1839      return {
1840        kind: 'rental',
1841        id: 'backroads-hill-country-boerne',
1842        name: 'Backroads Hill Country',
1843        tier: 'authority',
1844        photo: null,
1845        summary: 'Hill Country rentals since 2001.',
1846        website: '/vacation-rentals'
1847      };
1848    }
1849
1850    // Hill Country Travel App
1851    if (id === 'hill-country-travel-app' || id === 'hillcountry-app') {
1852      return {
1853        kind: 'tool',
1854        id: 'hill-country-travel-app',
1855        name: 'Hill Country Travel App',
1856        tier: 'baseline',
1857        photo: '/images/hill-country-travel-app.jpg',
1858        summary: 'Forty Hill Country towns in one app. Businesses, attractions, parks, vacation rentals, events.',
1859        website: 'https://hillcountry.app',
1860        category: 'Travel Tools'
1861      };
1862    }
1863
1864    // HillCountry.ai network — the broader discovery network. Surfaced on "What is HillCountry.ai?" questions.
1865    if (id === 'hillcountry-ai-network' || id === 'hillcountry-ai' || id === 'hill-country-ai-network') {
1866      return {
1867        kind: 'tool',
1868        id: 'hillcountry-ai-network',
1869        name: 'HillCountry.ai',
1870        tier: 'baseline',
1871        photo: '/images/hc-favicon.svg',
1872        photo_fit: 'contain',
1873        photo_bg: 'dark',
1874        photo_initial: 'H',
1875        summary: 'The Digital Front Door to the Texas Hill Country — an AI-powered discovery network of town guides.',
1876        website: 'https://hillcountry.ai',
1877        category: 'Network'
1878      };
1879    }
1880
1881    // Brody — Bandera's AI guide. Surfaced when user asks about Bandera.
1882    // Hill Country Events card — surfaced when a visitor asks what's on / upcoming events / live music / rodeos / markets / festivals. Routes to the events calendar.
1883    if (id === 'events' || id === 'hill-country-events' || id === 'whats-on') {
1884      return {
1885        kind: 'tool',
1886        id: 'events',
1887        name: 'Hill Country Events',
1888        tier: 'baseline',
1889        photo: 'https://boerne.ai/images/events-card.jpg',
1890        photo_fit: 'cover',
1891        photo_bg: 'dark',
1892        summary: 'Live music, rodeos, markets & festivals across the Hill Country — browse the full calendar by date and town.',
1893        website: '/events',
1894        category: 'Events',
1895      };
1896    }
1897
1898    if (id === 'brody' || id === 'brody-bandera' || id === 'bandera-guide') {
1899      return {
1900        kind: 'tool',
1901        id: 'brody',
1902        name: 'Brody the Bandera Guide',
1903        tier: 'baseline',
1904        photo: '/images/guide-mark.png',
1905        photo_fit: 'contain',
1906        photo_bg: 'white',
1907        photo_initial: 'B',
1908        summary: 'Bandera local guide. Cowboy capital, rallies, river, ranches.',
1909        website: 'https://bandera.ai',
1910        category: 'Network'
1911      };
1912    }
1913
1914    // Cooper — Fredericksburg's AI guide. Surfaced when user asks about Fredericksburg.
1915    if (id === 'cooper' || id === 'cooper-fredericksburg' || id === 'fredericksburg-guide') {
1916      return {
1917        kind: 'tool',
1918        id: 'cooper',
1919        name: 'Cooper the Fredericksburg Guide',
1920        tier: 'baseline',
1921        photo: '/images/guide-mark.png',
1922        photo_fit: 'contain',
1923        photo_bg: 'white',
1924        photo_initial: 'C',
1925        summary: 'Fredericksburg local guide. Wine country, Main Street, German heritage.',
1926        website: 'https://fredericksburg.ai',
1927        category: 'Network'
1928      };
1929    }
1930
1931    // Wade — Concan's AI guide. Surfaced when user asks about Concan / the Frio.
1932    if (id === 'bella' || id === 'bella-wimberley' || id === 'wimberley-guide') {
1933      return {
1934        kind: 'tool',
1935        id: 'bella',
1936        name: 'Bella the Wimberley Guide',
1937        tier: 'baseline',
1938        photo: '/images/guide-mark.png',
1939        photo_fit: 'contain',
1940        photo_bg: 'white',
1941        photo_initial: 'B',
1942        summary: 'Wimberley guide. The swimming holes, the Square, and Market Days.',
1943        website: 'https://wimberley.ai',
1944        category: 'Network'
1945      };
1946    }
1947
1948    if (id === 'dale' || id === 'dale-camp-wood' || id === 'camp-wood-guide') {
1949      return {
1950        kind: 'tool',
1951        id: 'dale',
1952        name: 'Dale the Camp Wood Guide',
1953        tier: 'baseline',
1954        photo: '/images/guide-mark.png',
1955        photo_fit: 'contain',
1956        photo_bg: 'white',
1957        photo_initial: 'D',
1958        summary: 'Camp Wood & Nueces Canyon guide. The Nueces River, the Twisted Sisters, quiet river country.',
1959        website: 'https://campwood.ai',
1960        category: 'Network'
1961      };
1962    }
1963
1964    if (id === 'wade' || id === 'wade-concan' || id === 'concan-guide') {
1965      return {
1966        kind: 'tool',
1967        id: 'wade',
1968        name: 'Wade the Concan Guide',
1969        tier: 'baseline',
1970        photo: '/images/guide-mark.png',
1971        photo_fit: 'contain',
1972        photo_bg: 'white',
1973        photo_initial: 'W',
1974        summary: 'Concan & Frio River guide. Tubing, Garner State Park, river cabins.',
1975        website: 'https://concan.ai',
1976        category: 'Network'
1977      };
1978    }
1979
1980    // Ray — Leakey's AI guide. Surfaced when user asks about Leakey / the upper Frio / Twisted Sisters.
1981    if (id === 'ray' || id === 'ray-leakey' || id === 'leakey-guide') {
1982      return {
1983        kind: 'tool',
1984        id: 'ray',
1985        name: 'Ray the Leakey Guide',
1986        tier: 'baseline',
1987        photo: '/images/guide-mark.png',
1988        photo_fit: 'contain',
1989        photo_bg: 'white',
1990        photo_initial: 'R',
1991        summary: 'Leakey & upper Frio guide. Twisted Sisters, the river, hunting country.',
1992        website: 'https://leakey.ai',
1993        category: 'Network'
1994      };
1995    }
1996
1997    if (id === 'june' || id === 'june-boerne' || id === 'boerne-guide') {
1998      return {
1999        kind: 'tool',
2000        id: 'june',
2001        name: 'June the Boerne Guide',
2002        tier: 'baseline',
2003        photo: '/images/guide-mark.png',
2004        photo_fit: 'contain',
2005        photo_bg: 'white',
2006        photo_initial: 'J',
2007        summary: 'Boerne guide. South Main Street, antiques, German heritage, the breweries.',
2008        website: 'https://boerne.ai',
2009        category: 'Network'
2010      };
2011    }
2012
2013    if (id === 'hazel' || id === 'hazel-kerrville' || id === 'kerrville-guide') {
2014      return {
2015        kind: 'tool',
2016        id: 'hazel',
2017        name: 'Hazel the Kerrville Guide',
2018        tier: 'baseline',
2019        photo: '/images/guide-mark.png',
2020        photo_fit: 'contain',
2021        photo_bg: 'white',
2022        photo_initial: 'H',
2023        summary: 'Kerrville guide. The Guadalupe, the arts, the Folk Festival, where to stay.',
2024        website: 'https://kerrville.ai',
2025        category: 'Network'
2026      };
2027    }
2028
2029    // Get Listed — synthetic card for business inquiries (matches /founder page title)
2030    if (id === 'list-your-business' || id === 'list_your_business' || id === 'founder') {
2031      return {
2032        kind: 'tool',
2033        id: 'list-your-business',
2034        name: 'Get Listed',
2035        tier: 'baseline',
2036        photo: null,
2037        summary: 'List your business. Member, Partner, Authority tiers. Founding rates locked.',
2038        website: '/founder',
2039        category: 'Network'
2040      };
2041    }
2042
2043    // POI lookup
2044    if (window.BOERNE_DATA.poi[id]) {
2045      var b = window.BOERNE_DATA.poi[id];
2046
2047      // ─── CARD GATE (2026-06-15) ───────────────────────────────────────
2048      // Free directory businesses are MENTION-ONLY: June names a few in prose,
2049      // but they never render a card. Cards are reserved for paid tiers
2050      // (member/partner/authority), anchors, and parks/landmarks/museums
2051      // (re-tiered to 'anchor' in the POI data), plus any baseline flagged
2052      // keep_card (galleries held for a paid pitch). Returning null here removes
2053      // an ineligible business from EVERY render path — chat chips, the section
2054      // below, and the Surface-2 category auto-fill all build through this f
2054n.
2055      if ((b.tier || 'baseline') === 'baseline' && b.keep_card !== true) return null;
2056      if (!b.photo && b.tier === 'anchor') return null; // no-photo guard: suppress placeholder cards
2057
2058      // Internal URL routing — Partner/Authority businesses with dedicated pages
2059      // on boerne.ai override their external website so the chip routes to
2060      // our page. Driven by the POI data's `internal_url` field (set per business
2061      // when its Partner/Authority page ships) — never hardcode a business here.
2062      var internalUrl = b.internal_url || null;
2063
2064      return {
2065        kind: 'poi',
2066        id: b.id,
2067        name: b.name,
2068        tier: b.tier || 'baseline',
2069        category: b.category,
2070        photo: b.photo || null,
2071        summary: b.description || b.address || '',
2072        website: internalUrl || b.website,
2073        phone: b.phone
2074      };
2075    }
2076
2077    // Rental lookup
2078    if (window.BOERNE_DATA.rentals[id]) {
2079      var p = window.BOERNE_DATA.rentals[id];
2080      return {
2081        kind: 'rental',
2082        id: p.id,
2083        name: p.name,
2084        tier: 'baseline',
2085        photo: p.photo,
2086        summary: (p.bedrooms + 'br/' + p.bathrooms + 'ba, sleeps ' + p.sleeps_max + ' · ' + (p.rate_range || '')).trim(),
2087        website: p.website_url
2088      };
2089    }
2090
2091    return null;
2092  }
2093
2094  // ─── RENDER: build a chip element (matches Brody's pattern exactly) ────
2095  // Each chip = mini-card with photo, name, tier badge, meta.
2096  // Click logic (same as Brody on Bandera):
2097  //   rental + tool + paid POI → clickable
2098  //   baseline POI → static (no href, cursor:default)
2099  // Category icons for cards without a photo (ported network-wide).
2100  var CAT_ICONS = {
2101  'Restaurant':['M8 3V8M10 3V8M12 3V8M8 8h4M10 8V21M16 3c2.4 2 2.4 7 0 9v9',''],
2102  'Store':['M6 8h12l-1 12H7zM9 8V6a3 3 0 0 1 6 0v2',''],
2103  'Bar':['M6 9h8v9a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2zM14 11h2.4a1.8 1.8 0 0 1 0 4.4H14M6 9a1.5 1.5 0 0 1 2.7 0a1.5 1.5 0 0 1 2.7 0a1.5 1.5 0 0 1 2.6 0',''],
2104  'Park':['M12 3 8.5 10h2.2L6.5 16h11L13.3 10h2.2zM12 16v5',''],
2105  'Museum':['M3.5 9 12 4l8.5 5zM6 10v8M10 10v8M14 10v8M18 10v8M4 18h16M3.5 20.5h17',''],
2106  'Horseback Riding':['M7 20V11a5 5 0 0 1 10 0v9M5.5 20H8.5M15.5 20H18.5','<circle cx="9" cy="9" r=".6"/><circle cx="15" cy="9" r=".6"/>'],
2107  'Winery':['M8 3h8c0 6-3.2 8-4 8s-4-2-4-8zM12 11v8M8.5 20h7',''],
2108  'Attraction':['M12 3.5 14.6 9 20.5 9.6 16 13.7 17.4 19.5 12 16.4 6.6 19.5 8 13.7 3.5 9.6 9.4 9z',''],
2109  'Event':['M5 6h14v14H5zM5 10h14M9 4v4M15 4v4',''],
2110  'Scenic Drives':['M3 19 9 8l3.5 6.5L16 9l5 10z',''],
2111  'Swimming Holes':['M3 9c2-2 4-2 6 0s4 2 6 0 4-2 6 0M3 14c2-2 4-2 6 0s4 2 6 0 4-2 6 0M3 19c2-2 4-2 6 0s4 2 6 0 4-2 6 0',''],
2112  'Church':['M12 2V5M10.5 3.4H13.5M6.5 11 12 6.5 17.5 11M8 11v9M16 11v9M6 20h12M10.5 20v-4.2a1.5 1.5 0 0 1 3 0V20',''],
2113  '_default':['M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11z','<circle cx="12" cy="10" r="2.3"/>']
2114  };
2115  function catIcon(category, px){
2116    var k=(category||'').toLowerCase(), key='_default';
2117  function H(){for(var i=0;i<arguments.length;i++){if(k.indexOf(arguments[i])>-1)return true;}return false;}
2118  if(H('restaurant','dining','cafe','coffee','breakfast','bbq','smokehouse','eatery','food','grill','pizza','taco','barbecue','steak')) key='Restaurant';
2119  else if(H('winery','wineries','vineyard','wine','tasting')) key='Winery';
2120  else if(H('brewery','breweries','beer','bar','pub','saloon','liquor','spirits','distillery')) key='Bar';
2121  else if(H('museum','culture','gallery','heritage','historic','history','art')) key='Museum';
2122  else if(H('swimming','tubing','float','outfitter','kayak','canoe','river','water')) key='Swimming Holes';
2123  else if(H('scenic','drive')) key='Scenic Drives';
2124  else if(H('horseback','horse','riding','stable')) key='Horseback Riding';
2125  else if(H('park','nature','natural','outdoors','trail','garden')) key='Park';
2126  else if(H('church','chapel','worship','cathedral')) key='Church';
2127  else if(H('event','rally','rallies','festival','fair')) key='Event';
2128  else if(H('antique','shop','gift','store','retail','grocery','hardware','supplies','pharmacy','market','boutique','goods','mercantile','convenience')) key='Store';
2129  else if(H('attraction','family','kid','kids','entertainment','adventure','tour','fun')) key='Attraction';
2130    var ic=CAT_ICONS[key]||CAT_ICONS['_default'];
2131    return '<svg viewBox="0 0 24 24" width="'+px+'" height="'+px+'" fill="none" stroke="#d9a441" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:.92" aria-hidden="true"><path d="'+ic[0]+'"/>'+ic[1]+'</svg>';
2132  }
2133
2134  function normWebsite(u){
2135    if(!u) return u;
2136    u = String(u).trim();
2137    if(/^https?:\/\//i.test(u)) return u;        // already absolute
2138    if(u.charAt(0) === '/') return u;             // internal root-relative path
2139    if(/^[a-z0-9-]+(\.[a-z0-9-]+)+(\/[^\s]*)?$/i.test(u)) return 'https://' + u;  // bare domain -> add scheme
2140    return u;                                      // not URL-like ("None listed") -> leave alone
2141  }
2142  function trackCardClick(el, cardId){
2143    if(!el || !cardId) return;
2144    el.setAttribute('data-card-id', cardId);
2145    el.addEventListener('click', function(){
2146      try {
2147        var sid = window.__hcSession || (window.__hcSession = ((new URLSearchParams(location.search).get('hcj'))||(window.crypto&&crypto.randomUUID?crypto.randomUUID():'s-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2))));
2148        if(!sid) return;
2149        var pl = JSON.stringify({ site: location.hostname.replace(/^www\./,''), session: sid, click: cardId });
2150        var u = 'https://hillcountry.ai/.netlify/functions/log';
2151        if (navigator.sendBeacon) { navigator.sendBeacon(u, new Blob([pl],{type:'text/plain'})); }
2152        else { fetch(u,{method:'POST',headers:{'Content-Type':'text/plain'},body:pl,keepalive:true}).catch(function(){}); }
2153      } catch(e){}
2154    });
2155  }
2156  function renderCard(card) {
2157    var tier = card.tier || 'baseline';
2158    var kind = card.kind || 'poi';
2159    var clickable = (kind === 'rental') ||
2160                    (kind === 'tool') ||
2161                    (kind === 'poi' && tier !== 'baseline');
2162
2163    var el = document.createElement('a');
2164    el.className = 'guide-chip';
2165    trackCardClick(el, card.id);
2166
2167    // Tier styling
2168    if (card.tier === 'authority') el.className += ' tier-authority';
2169    else if (card.tier === 'partner') el.className += ' tier-partner';
2170    else if (card.tier === 'member') el.className += ' tier-member';
2171    else if (card.tier === 'anchor') el.className += ' tier-anchor';
2172
2173    if (clickable && card.website) {
2174      el.href = (function(u){ try{ var _n=['hillcountry.ai','bandera.ai','boerne.ai','concan.ai','fredericksburg.ai','kerrville.ai','leakey.ai','wimberley.ai']; var _x=new URL(u,location.href); var _h=_x.hostname.replace(/^www\./,''); if(_h!==location.hostname.replace(/^www\./,'') && _n.indexOf(_h)>-1){ var _s=(window.__hcSession||(window.__hcSession=((new URLSearchParams(location.search).get('hcj'))||(window.crypto&&crypto.randomUUID?crypto.randomUUID():'s-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2))))); if(_s){ _x.searchParams.set('hcj',_s); return _x.toString(); } } }catch(e){} return u; })(normWebsite(card.website));
2175      var isInternal = card.website.charAt(0) === '/' || card.website.indexOf('boerne.ai') > -1;
2176      if (!isInternal) {
2177        el.target = '_blank';
2178        el.rel = 'noopener';
2179      }
2180    } else {
2181      // Baseline POI chip: not a navigation link, but clicking scrolls to the
2182      // matching big card in Surface 2 below and pulses it briefly.
2183      el.removeAttribute('href');
2184      el.style.cursor = 'pointer';
2185      el.addEventListener('click', function(ev) {
2186        ev.preventDefault();
2187        ev.stopPropagation();
2188        var target = document.getElementById('bc-' + card.id);
2189        console.log('[June chip click] looking for bc-' + card.id, 'found:', !!target);
2190        if (target) {
2191          target.scrollIntoView({ behavior: 'smooth', block: 'center' });
2192          target.classList.add('bc-pulse');
2193          setTimeout(function() { target.classList.remove('bc-pulse'); }, 1800);
2194        } else {
2195          console.warn('[June chip click] No matching big card for', card.id);
2196        }
2197      });
2198    }
2199
2200    // Photo block (with tier badge overlay if Authority/Partner/Member)
2201    var photo = document.createElement('div');
2202    photo.className = 'chip-photo';
2203
2204    // Special: Backroads logo with cream backdrop
2205    if (card.id === 'backroads-hill-country-boerne') {
2206      photo.style.backgroundImage = "url('/images/backroads-logo-shield-transparent.png')";
2207      photo.style.backgroundSize = 'contain';
2208      photo.style.backgroundRepeat = 'no-repeat';
2209      photo.style.backgroundColor = 'var(--cream)';
2210    } else if (card.photo) {
2211      photo.style.backgroundImage = "url('" + card.photo + "')";
2212      if (card.photo_fit === 'contain') {
2213        photo.style.backgroundSize = 'contain';
2214        photo.style.backgroundRepeat = 'no-repeat';
2215        photo.style.backgroundPosition = 'center';
2216        photo.style.backgroundColor = (card.photo_bg === 'white') ? '#ffffff' : (card.photo_bg === 'cream') ? 'var(--cream)' : '#15402b';
2217      }
2218    } else {
2219      // No-photo: show initial as fallback (custom initial via photo_initial, else first letter of name)
2220      var initial = document.createElement('div');
2221      initial.className = 'chip-photo-initial';
2222      initial.innerHTML = catIcon(card.icon || card.category, 26);
2223      photo.appendChild(initial);
2224    }
2225
2226    // Tier badge in top-left corner of photo
2227    // ONLY show badge for actual paid tiers — baseline NEVER gets a badge
2228    var paidTiers = ['authority', 'partner', 'member'];
2229    if (card.tier && paidTiers.indexOf(card.tier) !== -1) {
2230      var tierTag = document.createElement('div');
2231      tierTag.className = 'chip-tier-tag ' + card.tier;
2232      tierTag.textContent = card.tier === 'authority' ? '★ Authority'
2233                          : card.tier === 'partner' ? 'Partner'
2234                          : 'Member';
2235      photo.appendChild(tierTag);
2236    }
2237
2238    el.appendChild(photo);
2239
2240    // Body: name + meta
2241    var body = document.createElement('div');
2242    body.className = 'chip-body';
2243
2244    var name = document.createElement('div');
2245    name.className = 'chip-name';
2246    name.textContent = card.name;
2247    body.appendChild(name);
2248
2249    // Meta: short context — category or summary, kept brief for chip space
2250    var metaText = card.category || card.summary || '';
2251    if (metaText) {
2252      var meta = document.createElement('div');
2253      meta.className = 'chip-meta';
2254      // Trim long meta — chips are small
2255      meta.textContent = metaText.length > 32 ? metaText.substring(0, 30) + '…' : metaText;
2256      body.appendChild(meta);
2257    }
2258
2259    el.appendChild(body);
2260    return el;
2261  }
2262
2263  // Tier rank for sorting (matches Brody — Authority always leftmost)
2264  function tierRank(tier) {
2265    var rank = { authority: 0, partner: 1, member: 2, anchor: 2.5, baseline: 3 }[tier];
2266    return rank === undefined ? 4 : rank;
2267  }
2268
2269  // Pretty category label for the chip-stack header
2270  function prettyCategoryLabel(cat) {
2271    if (!cat) return 'Recommendations';
2272    var map = {
2273      'vacation-rentals': 'Vacation Rentals',
2274      'lodging': 'Where to Stay',
2275      'wineries': 'Wineries',
2276      'wine': 'Wineries',
2277      'breweries': 'Breweries',
2278      'restaurants': 'Restaurants',
2279      'german-food': 'German Food',
2280      'bbq': 'BBQ',
2281      'coffee': 'Coffee & Breakfast',
2282      'bakeries': 'Bakeries',
2283      'bars': 'Bars & Nightlife',
2284      'live-music': 'Live Music',
2285      'shopping': 'Shopping',
2286      'stores': 'Shops',
2287      'galleries': 'Galleries',
2288      'museums': 'Museums',
2289      'parks': 'Parks & Outdoors',
2290      'things-to-do': 'Things to Do',
2291      'attractions': 'Attractions',
2292      'services': 'Services',
2293      'events': 'Events',
2294      'travel-tools': 'Travel Tools'
2295    };
2296    if (map[cat]) return map[cat];
2297    // Fallback: title-case the raw category
2298    return cat.replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
2299  }
2300
2301  function renderCards(cardIds, category, container) {
2302    if (!cardIds || !cardIds.length) return;
2303
2304    // Look up all cards and filter nulls
2305    var cards = [];
2306    cardIds.forEach(function(id) {
2307      var data = getCardData(id);
2308      if (data) cards.push(data);
2309      else console.warn('[June] Unknown card ID:', id);
2310    });
2311    if (!cards.length) return;
2312
2313    // Sort by tier — Authority leftmost (v29 ordering rule)
2314    cards.sort(function(a, b) { return tierRank(a.tier) - tierRank(b.tier); });
2315
2316    // ═══════════════════════════════════════════════════════════════════
2317    // SESSION-BASED ROTATION (ported from Brody v15)
2318    // Paid tiers (Authority/Partner/Member) stay in tier-sorted position.
2319    // For baseline cards, prefer ones the user hasn't been shown yet
2320    // in this session. Tracked in sessionStorage by ID + timestamp.
2321    // ═══════════════════════════════════════════════════════════════════
2322    try {
2323      var SHOWN_KEY = 'guide_cards_shown_v1';
2324      var shown = JSON.parse(sessionStorage.getItem(SHOWN_KEY) || '{}');
2325      var paidCards = [];
2326      var baselineCards = [];
2327      cards.forEach(function(c) {
2328        var tier = c.tier || 'baseline';
2329        if (tier === 'baseline') baselineCards.push(c);
2330        else paidCards.push(c);
2331      });
2332      // Sort baseline by lastShownAt ascending — oldest first (preferred to show again)
2333      baselineCards.sort(function(a, b) {
2334        return (shown[a.id] || 0) - (shown[b.id] || 0);
2335      });
2336      // Re-assemble: paid (tier-sorted) first, then baseline (rotation-sorted)
2337      cards = paidCards.concat(baselineCards);
2338    } catch (e) {
2339      console.warn('[June] Card rotation skipped:', e.message);
2340    }
2341
2342    // Cap chips so they fit on one row: 4 on mobile, 6 on desktop.
2343    // Use a generous mobile threshold — many phones report CSS widths in the
2344    // 540-640 range (larger phones, landscape), and those should still get 4.
2345    var chipCap = (typeof window !== 'undefined' && window.innerWidth <= 640) ? 4 : 6;
2346    cards = cards.slice(0, chipCap);
2347
2348    // Mark these cards as shown (timestamp for rotation)
2349    try {
2350      var SHOWN_KEY_MARK = 'guide_cards_shown_v1';
2351      var shownMark = JSON.parse(sessionStorage.getItem(SHOWN_KEY_MARK) || '{}');
2352      var now = Date.now();
2353      cards.forEach(function(c) {
2354        if (c && c.id) shownMark[c.id] = now;
2355      });
2356      sessionStorage.setItem(SHOWN_KEY_MARK, JSON.stringify(shownMark));
2357    } catch (e) { /* skip */ }
2358
2359    // Build chip stack
2360    var stack = document.createElement('div');
2361    stack.className = 'guide-chips';
2362
2363    var row = document.createElement('div');
2364    row.className = 'guide-chips-row';
2365    cards.forEach(function(card) {
2366      row.appendChild(renderCard(card));
2367    });
2368    stack.appendChild(row);
2369
2370    container.appendChild(stack);
2371  }
2372
2373  // ─── SURFACE 2: big cards in a page section below the chat ──────────
2374  // Ported from Brody — renders dynamic primary section with bigger cards
2375  // for fuller detail viewing. Replaces previous Surface 2 each turn.
2376  function renderBigCard(card) {
2377    var tier = card.tier || 'baseline';
2378    var kind = card.kind || 'poi';
2379    var clickable = (kind === 'rental') ||
2380                    (kind === 'tool') ||
2381                    (kind === 'poi' && tier !== 'baseline');
2382
2383    var el = document.createElement('a');
2384    el.className = 'big-card tier-' + tier;
2385    el.id = 'bc-' + card.id;
2386    trackCardClick(el, card.id);
2387
2388    if (clickable && card.website) {
2389      el.href = (function(u){ try{ var _n=['hillcountry.ai','bandera.ai','boerne.ai','concan.ai','fredericksburg.ai','kerrville.ai','leakey.ai','wimberley.ai']; var _x=new URL(u,location.href); var _h=_x.hostname.replace(/^www\./,''); if(_h!==location.hostname.replace(/^www\./,'') && _n.indexOf(_h)>-1){ var _s=(window.__hcSession||(window.__hcSession=((new URLSearchParams(location.search).get('hcj'))||(window.crypto&&crypto.randomUUID?crypto.randomUUID():'s-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2))))); if(_s){ _x.searchParams.set('hcj',_s); return _x.toString(); } } }catch(e){} return u; })(normWebsite(card.website));
2390      var isInternal = card.website.charAt(0) === '/' ||
2391                       card.website.indexOf('boerne.ai') > -1;
2392      if (!isInternal) {
2393        el.target = '_blank';
2394        el.rel = 'noopener';
2395      }
2396    } else {
2397      el.removeAttribute('href');
2398      el.style.cursor = 'default';
2399    }
2400
2401    // Tier badge — POI only
2402    var tierBadge = '';
2403    if (kind === 'poi') {
2404      if (tier === 'authority') {
2405        tierBadge = '<div class="bc-tier-badge authority">★ Authority</div>';
2406      } else if (tier === 'partner') {
2407        tierBadge = '<div class="bc-tier-badge partner">Partner</div>';
2408      } else if (tier === 'member') {
2409        tierBadge = '<div class="bc-tier-badge member">Member</div>';
2410      }
2411    }
2412
2413    // Photo
2414    var photoStyle = '';
2415    var photoFallback = '';
2416    if (card.id === 'backroads-hill-country-boerne') {
2417      photoStyle = "background-image:url('/images/backroads-logo-shield-transparent.png') !important;" +
2418        "background-size:contain !important;background-repeat:no-repeat !important;" +
2419        "background-position:center !important;background-color:#f5ead5 !important;";
2420    } else if (card.photo) {
2421      if (card.photo_fit === 'contain') {
2422        var pbg = (card.photo_bg === 'white') ? '#ffffff' : (card.photo_bg === 'cream') ? '#f5ead5' : '#15402b';
2423        photoStyle = "background-image:url('" + card.photo + "') !important;background-size:contain !important;background-repeat:no-repeat !important;background-position:center !important;background-color:" + pbg + " !important;";
2424      } else {
2425        photoStyle = "background-image:url('" + card.photo + "')";
2426      }
2427    } else {
2428      var initial = (card.name || '?').trim().charAt(0).toUpperCase();
2429      var fallbackBg = (tier === 'authority' || tier === 'partner')
2430        ? 'linear-gradient(135deg,#1d5238 0%,#15402b 100%)'
2431        : 'linear-gradient(135deg,#15402b 0%,#0a1d14 100%)';
2432      photoStyle = 'background:' + fallbackBg;
2433      photoFallback = '<div class="bc-photo-initial">' + catIcon(card.icon || card.category, 30) + '</div>';
2434    }
2435
2436    // Meta row
2437    var metaParts = [];
2438    if (card.kind === 'rental') {
2439      if (card.bedrooms) metaParts.push('<span>🛏 ' + card.bedrooms + 'br</span>');
2440      if (card.sleeps_max) metaParts.push('<span>sleeps ' + card.sleeps_max + '</span>');
2441    } else {
2442      if (card.phone) metaParts.push('<span>📞 ' + escapeHTML(card.phone) + '</span>');
2443      if (card.address) {
2444        var addr = String(card.address).split(',')[0].trim();
2445        if (addr.length > 30) addr = addr.slice(0, 28) + '…';
2446        metaParts.push('<span>📍 ' + escapeHTML(addr) + '</span>');
2447      }
2448      if (card.category && kind === 'poi') metaParts.push('<span>' + escapeHTML(card.category) + '</span>');
2449    }
2450
2451    // Claim CTA for free baseline POI
2452    var claimCta = '';
2453    if (kind === 'poi' && tier === 'baseline') {
2454      var claimCategory = encodeURIComponent(card.category || '');
2455      claimCta = '<a href="/founder?claim=' + encodeURIComponent(card.id) + '&category=' + claimCategory +
2456                 '#claim-form" class="bc-claim-cta" onclick="event.stopPropagation()">Is this your business? Claim free listing →</a>';
2457    }
2458
2459    el.innerHTML =
2460      '<div class="big-card-photo" style="' + photoStyle + '">' + tierBadge + photoFallback + '</div>' +
2461      '<div class="big-card-body">' +
2462        '<div class="bc-name">' + escapeHTML(card.name) + '</div>' +
2463        (metaParts.length ? '<div class="bc-meta-row">' + metaParts.join('') + '</div>' : '') +
2464        (card.summary ? '<div class="bc-summary">' + escapeHTML(String(card.summary).slice(0, 180)) + '</div>' : '') +
2465        claimCta +
2466      '</div>';
2467    return el;
2468  }
2469
2470  // Orchestrator: render the Surface 2 section below the chat thread
2471  function renderGuideSurface(cardIds, category) {
2472    console.log('[June Surface 2]', { cardIds: cardIds, category: category });
2473    // Remove any existing surface from a prior turn
2474    var existing = document.getElementById('guide-surface-current');
2475    if (existing) existing.remove();
2476
2477    if (!cardIds || !cardIds.length) return;
2478
2479    // Look up cards June emitted
2480    var cards = [];
2481    var emittedIds = {};
2482    cardIds.forEach(function(id) {
2483      var c = getCardData(id);
2484      if (c) {
2485        cards.push(c);
2486        emittedIds[c.id] = true;
2487      }
2488    });
2489    if (!cards.length) return;
2490
2491    // ─── INFER CATEGORY from emitted cards if not provided ─────────────
2492    // If June didn't pass a category string (or passed something we don't
2493    // recognize), look at what kinds of cards he emitted. If all/most are
2494    // POI businesses in the same category, use that.
2495    if (!category || category === 'recommendations') {
2496      var catCounts = {};
2497      cards.forEach(function(c) {
2498        if (c.kind === 'poi' && c.category) {
2499          catCounts[c.category] = (catCounts[c.category] || 0) + 1;
2500        }
2501      });
2502      var topCat = null, topN = 0;
2503      Object.keys(catCounts).forEach(function(k) {
2504        if (catCounts[k] > topN) { topCat = k; topN = catCounts[k]; }
2505      });
2506      if (topCat) {
2507        category = topCat;
2508      }
2509    }
2510
2511    // ─── AUTO-FILL: pad Surface 2 to 8 cards from the full POI directory ────
2512    // If June emitted only 3 wineries, Surface 2 should still feel full —
2513    // pull additional businesses from the same category, prefer least-recently-shown
2514    // (session rotation), skip ones already in the emitted list.
2515    // Pull a generous set from the category (up to 24) so the "Load more" link
2516    // below has the rest of the directory to reveal. We display the first 8 and
2517    // tuck the remainder behind "Load more" to keep the page tight.
2518    var TARGET_COUNT = 24;
2519    if (cards.length < TARGET_COUNT && category && window.BOERNE_DATA && window.BOERNE_DATA.poi) {
2520      // Map June's category string → this town's POI category(ies).
2521      // Generated per-town by build.py from the town's actual POI categories.
2522      var categoryMap = {"antiques": ["Antiques"], "breweries & bars": ["Breweries & Bars"], "breweries": ["Breweries & Bars"], "bars": ["Breweries & Bars"], "family & kids": ["Family & Kids"], "family": ["Family & Kids"], "kids": ["Family & Kids"], "horseback riding": ["Horseback Riding"], "horseback": ["Horseback Riding"], "riding": ["Horseback Riding"], "museums & culture": ["Museums & Culture"], "museums": ["Museums & Culture"], "culture": ["Museums & Culture"], "restaurants": ["Restaurants"], "shopping": ["Shopping"], "shops & gifts": ["Shops & Gifts"], "shops": ["Shops & Gifts"], "gifts": ["Shops & Gifts"], "wineries": ["Wineries"], "food": ["Restaurants"], "dining": ["Restaurants"], "eat": ["Restaurants"]};
2523
2524      var normCat = String(category).toLowerCase();
2525      var poiCats = categoryMap[normCat] || [];
2526      // Fallback 1: treat the emitted category as an exact POI category.
2527      if (!poiCats.length) poiCats = [category];
2528      // Fallback 2: fuzzy match — find any real POI category whose words overlap
2529      // the emitted category (e.g. "outfitters" → "Outfitters & Tubing",
2530      // "coffee" → "Coffee & Cafes"). This catches anything the map missed so
2531      // cards never silently go missing.
2532      (function(){
2533        var realCats = {};
2534        Object.keys(window.BOERNE_DATA.poi).forEach(function(id){
2535          var b = window.BOERNE_DATA.poi[id];
2536          (b.categories || [b.category]).forEach(function(c){ if (c) realCats[c] = true; });
2537        });
2538        var matched = poiCats.filter(function(c){ return realCats[c]; });
2539        if (!matched.length) {
2540          var token = normCat.replace(/[^a-z]+/g,' ').trim().split(' ')[0]; // first word
2541          Object.keys(realCats).forEach(function(c){
2542            if (token && c.toLowerCase().indexOf(token) !== -1) matched.push(c);
2543          });
2544        }
2545        if (matched.length) poiCats = matched;
2546      })();
2547
2548      // Refinement: for some queries the POI category is too broad.
2549      // E.g. "breweries" maps to "Bars" (all 32 bars), but we only want
2550      // actual breweries. Filter by name keywords for these sub-queries.
2551      var nameFilter = null;
2552      var nameFilterLabel = null;
2553      if (normCat === 'breweries' || normCat === 'beer') {
2554        nameFilterLabel = 'brewery (name contains brew/biergarten/beer)';
2555        nameFilter = function(name) {
2556          var n = name.toLowerCase();
2557          return n.indexOf('brew') !== -1 || n.indexOf('biergarten') !== -1 || n.indexOf('beer') !== -1;
2558        };
2559      }
2560
2561      if (poiCats.length) {
2562        // Get session rotation tracker
2563        var SHOWN_KEY = 'guide_cards_shown_v1';
2564        var shown = {};
2565        try { shown = JSON.parse(sessionStorage.getItem(SHOWN_KEY) || '{}'); } catch (e) {}
2566
2567        // Pull candidates from POI matching the category, excluding already-emitted
2568        var candidates = [];
2569        var rejectedByFilter = [];
2570        Object.keys(window.BOERNE_DATA.poi).forEach(function(id) {
2571          if (emittedIds[id]) return;
2572          var b = window.BOERNE_DATA.poi[id];
2573          var bizCats = b.categories || [b.category];
2574          var matches = false;
2575          for (var i = 0; i < bizCats.length; i++) {
2576            if (poiCats.indexOf(bizCats[i]) !== -1) { matches = true; break; }
2577          }
2578          if (matches) {
2579            if (nameFilter && !nameFilter(b.name)) {
2580              rejectedByFilter.push(b.name);
2581              return;
2582            }
2583            var data = getCardData(id);
2584            if (data) candidates.push(data);
2585          }
2586        });
2587
2588        console.log('[June Surface 2] Auto-fill: category=', category, 'poiCats=', poiCats,
2589                    'nameFilter=', nameFilterLabel || 'none',
2590                    'candidates found=', candidates.length,
2591                    'rejected by name filter=', rejectedByFilter.length,
2592                    'cards before fill=', cards.length);
2593        if (rejectedByFilter.length > 0 && rejectedByFilter.length < 15) {
2594          console.log('[June Surface 2] Rejected names:', rejectedByFilter);
2595        }
2596
2597        // Sort candidates: paid tier first, then by least-recently-shown
2598        candidates.sort(function(a, b) {
2599          var aTier = tierRank(a.tier);
2600          var bTier = tierRank(b.tier);
2601          if (aTier !== bTier) return aTier - bTier;
2602          return (shown[a.id] || 0) - (shown[b.id] || 0);
2603        });
2604
2605        // Fill up to TARGET_COUNT
2606        var needed = TARGET_COUNT - cards.length;
2607        var added = candidates.slice(0, needed);
2608        cards = cards.concat(added);
2609        // Mark these as emitted too so dedup works
2610        added.forEach(function(c) { emittedIds[c.id] = true; });
2611      }
2612    }
2613
2614    // Sort all cards by tier — Authority leftmost
2615    cards.sort(function(a, b) { return tierRank(a.tier) - tierRank(b.tier); });
2616
2617    // Keep up to 24 available so "Load more" has something to reveal; we show
2618    // the first 8 and tuck the rest behind a "Load more" link to keep the page
2619    // from getting very long on deep directories.
2620    cards = cards.slice(0, 24);
2621    var SURFACE_INITIAL = 8;
2622    var surfaceDisplay = cards.slice(0, SURFACE_INITIAL);
2623    var surfaceHidden = cards.slice(SURFACE_INITIAL);
2624
2625    // Mark these cards as shown (for rotation tracking)
2626    try {
2627      var SHOWN_KEY_MARK = 'guide_cards_shown_v1';
2628      var shownMark = JSON.parse(sessionStorage.getItem(SHOWN_KEY_MARK) || '{}');
2629      var now = Date.now();
2630      surfaceDisplay.forEach(function(c) {
2631        if (c && c.id) shownMark[c.id] = now;
2632      });
2633      sessionStorage.setItem(SHOWN_KEY_MARK, JSON.stringify(shownMark));
2634    } catch (e) { /* skip */ }
2635
2636    // Build outer wrapper
2637    var wrap = document.createElement('section');
2638    wrap.id = 'guide-surface-current';
2639    wrap.className = 'guide-surface';
2640
2641    var grid = document.createElement('div');
2642    grid.className = 'guide-surface-grid';
2643    surfaceDisplay.forEach(function(c, idx) {
2644      var cardEl = renderBigCard(c);
2645      cardEl.style.animationDelay = (idx * 60) + 'ms';
2646      grid.appendChild(cardEl);
2647    });
2648    wrap.appendChild(grid);
2649
2650    // "Load more" — reveal the hidden cards inline when clicked. Keeps the
2651    // initial page tight (8 cards) while still giving access to the full set.
2652    if (surfaceHidden.length > 0) {
2653      var loadMoreWrap = document.createElement('div');
2654      loadMoreWrap.style.cssText = 'width:100%;display:flex;justify-content:center;margin-top:14px';
2655      var loadMore = document.createElement('button');
2656      loadMore.type = 'button';
2657      loadMore.className = 'guide-surface-load-more';
2658      loadMore.textContent = 'View more ' + prettyCategoryLabel(category).toLowerCase() + ' →';
2659      loadMore.addEventListener('click', function() {
2660        try {
2661          var sk = 'guide_cards_shown_v1';
2662          var sm = JSON.parse(sessionStorage.getItem(sk) || '{}');
2663          var t = Date.now();
2664          surfaceHidden.forEach(function(c) { if (c && c.id) sm[c.id] = t; });
2665          sessionStorage.setItem(sk, JSON.stringify(sm));
2666        } catch (e) { /* skip */ }
2667        surfaceHidden.forEach(function(c, idx) {
2668          var cardEl = renderBigCard(c);
2669          cardEl.style.animationDelay = (idx * 50) + 'ms';
2670          grid.appendChild(cardEl);
2671        });
2672        loadMoreWrap.remove();
2673      });
2674      loadMoreWrap.appendChild(loadMore);
2675      wrap.appendChild(loadMoreWrap);
2676    }
2677
2678    // Insert Surface 2 right under the header (which contains the chat). If a
2679    // live river-conditions widget is present (it sits just after the header),
2680    // insert Surface 2 BEFORE it so the order reads: chat → surfaced results →
2681    // live river conditions. Otherwise before the first content section.
2682    var riverWidget = document.getElementById('river-conditions');
2683    if (riverWidget && riverWidget.parentNode) {
2684      riverWidget.parentNode.insertBefore(wrap, riverWidget);
2685    } else {
2686      var firstSection = document.querySelector('body > section');
2687      if (firstSection) {
2688        firstSection.parentNode.insertBefore(wrap, firstSection);
2689      } else {
2690        document.body.appendChild(wrap);
2691      }
2692    }
2693  }
2694
2695  // ─── MESSAGE RENDERING ────────────────────────────────────────────
2696  function escapeHTML(s) {
2697    return String(s).replace(/[&<>"']/g, function(m) {
2698      return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m];
2699    });
2700  }
2701
2702  function renderGuideText(text) {
2703    // Minimal markdown: **bold**, [text](url), bare URLs, phone numbers
2704    var html = escapeHTML(text);
2705    html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
2706    html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
2707    html = html.replace(/(https?:\/\/[^\s<]+)/g, function(m) {
2708      // Skip if already inside an anchor tag
2709      return '<a href="' + m + '" target="_blank" rel="noopener">' + m + '</a>';
2710    });
2711    html = html.replace(/(\b\d{3}-\d{3}-\d{4}\b|\(\d{3}\)\s*\d{3}-\d{4})/g,
2712      '<a href="tel:$1">$1</a>');
2713    // Treat any newline (single or double) as a paragraph break — June
2714    // sometimes returns single \n between paragraphs and we want them spaced.
2715    var paragraphs = html.split(/\n+/).map(function(p) { return p.trim(); }).filter(Boolean);
2716    return paragraphs.map(function(p) { return '<p>' + p + '</p>'; }).join('');
2717  }
2718
2719  // ─── CARD-REFERENCE BACKSTOP (2026-06-15) ─────────────────────────────
2720  // June is told never to point at a card on a no-card turn, but if he slips
2721  // ("tap the cards below") and nothing actually rendered, strip that orphaned
2722  // line so the visitor never sees a pointer to cards that aren't there. Only
2723  // runs when zero cards rendered, so it can't touch legitimate handoffs.
2724  function stripCardHandoff(text){
2725    if(!text) return text;
2726    var isPointer = function(s){
2727      var t=(s||'').trim();
2728      if(!t) return false;
2729      if(/(?:\bno\b|\bnot\b|n't|never|do(?:es)?\s+not|don't|doesn't|without)[^.!?]*\bcards?\b/i.test(t)) return false;
2730      if(/\bcards?\b/i.test(t)) return true;
2731      if(/\btap\s+(?:for|to|the|through|here|one|on|them|any|a\b|that|each)/i.test(t)) return true;
2732      if(/\bclick\s+(?:for|to|the|through|here|on|any|that|each)/i.test(t)) return true;
2733      if(/catches your eye/i.test(t)) return true;
2734      if(/\b(?:pick|choose)\s+(?:from|one|any|what)\b/i.test(t)) return true;
2735      if(/\b(?:surface|pull\s+up)\s+(?:some\s+|a\s+|a\s+few\s+|you\s+|the\s+)?(?:options?|spots?|places?|cards?|recommendations?|picks?|listings?)\b/i.test(t)) return true;
2736      if(/(?:put|placed|added|listed|dropped|see|all|both|three|them|these|those|options?|info|details|more|each)[^.!?]*\bbelow\b/i.test(t)) return true;
2737      if(/\bbelow\b[\s.!?]*$/i.test(t)) return true;
2738      return false;
2739    };
2740    var sentences = text.match(/[^.!?]*[.!?]+|[^.!?]+$/g) || [text];
2741    var kept=[];
2742    sentences.forEach(function(sen){
2743      var work = sen;
2744      var di = Math.max(work.lastIndexOf('\u2014'), work.lastIndexOf('\u2013'));
2745      if(di > -1){
2746        var tail = work.slice(di+1);
2747        if(isPointer(tail)){
2748          var term = (work.match(/[.!?]+\s*$/)||[''])[0];
2749          work = work.slice(0,di).replace(/[\s\u2014\u2013\-,;:]+$/,'') + term;
2750        }
2751      }
2752      var core = work.replace(/[.!?\s]+$/,'').trim();
2753      if(!core) return;
2754      if(isPointer(core)) return;
2755      kept.push(work);
2756    });
2757    var out=kept.join(' ').replace(/\s+/g,' ').replace(/\s+([.!?,;:])/g,'$1').trim();
2758    return out||text;
2759  }
2760
2761  function appendMessage(role, text, cardIds, category) {
2762    var thread = document.getElementById('chat-thread');
2763    var msg = document.createElement('div');
2764    msg.className = 'chat-msg ' + role;
2765
2766    if (role === 'june') {
2767      var avatar = document.createElement('div');
2768      avatar.className = 'avatar';
2769      avatar.textContent = 'J';
2770      msg.appendChild(avatar);
2771    }
2772
2773    var bubble = document.createElement('div');
2774    bubble.className = 'bubble';
2775    if (role === 'june') {
2776      bubble.innerHTML = renderGuideText(text);
2777    } else {
2778      bubble.textContent = text;
2779    }
2780    msg.appendChild(bubble);
2781
2782    thread.appendChild(msg);
2783
2784    // Render cards inline if June emitted them, THEN run the card-reference
2785    // backstop: if no card actually rendered, strip any orphaned "tap the
2786    // cards" line from the bubble (CARD-REFERENCE RULE, enforced client-side).
2787    if (role === 'june') {
2788      var runRender = function() {
2789        // Rendering must NEVER throw out of appendMessage: the send handler
2790        // calls this inside the same try that shows the "didn't go through"
2791        // error, so a render hiccup would masquerade as an API failure. Guard it.
2792        try {
2793          if (cardIds && cardIds.length) {
2794            renderCards(cardIds, category, bubble);
2795            renderGuideSurface(cardIds, category);
2796          }
2797          var anyCard = bubble.querySelectorAll('.guide-chip').length > 0 ||
2798                        !!document.getElementById('guide-surface-current');
2799          if (!anyCard) {
2800            var cleaned = stripCardHandoff(text);
2801            if (cleaned && cleaned !== text) bubble.innerHTML = renderGuideText(cleaned);
2802          }
2803        } catch (e) {
2804          console.warn('[June] render/backstop skipped:', e && e.message);
2805        }
2806        thread.scrollTop = thread.scrollHeight;
2807      };
2808      if (cardIds && cardIds.length && !window.BOERNE_DATA.loaded) {
2809        window.BOERNE_DATA.ready.then(runRender);
2810      } else {
2811        runRender();
2812      }
2813    }
2814
2815    thread.scrollTop = thread.scrollHeight;
2816    return msg;
2817  }
2818
2819  function showTyping() {
2820    var thread = document.getElementById('chat-thread');
2821    var msg = document.createElement('div');
2822    msg.className = 'chat-msg june typing-msg';
2823    msg.id = 'typing-indicator';
2824    var avatar = document.createElement('div');
2825    avatar.className = 'avatar';
2826    avatar.textContent = 'J';
2827    msg.appendChild(avatar);
2828    var bubble = document.createElement('div');
2829    bubble.className = 'bubble';
2830    bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
2831    msg.appendChild(bubble);
2832    thread.appendChild(msg);
2833    thread.scrollTop = thread.scrollHeight;
2834  }
2835
2836  function hideTyping() {
2837    var t = document.getElementById('typing-indicator');
2838    if (t) t.remove();
2839  }
2840
2841  // ─── API CALL ─────────────────────────────────────────────────────
2842  var conversation = [];
2843
2844  async function sendToGuide(userMessage) {
2845    conversation.push({ role: 'user', content: userMessage });
2846
2847    // Keep the conversation from growing unbounded. The backend caps at 20
2848    // user messages and returns an error past that; trim here so a long
2849    // session keeps working instead of hitting that wall. We keep the most
2850    // recent turns (enough for context) and drop the oldest. Pairs are
2851    // user+assistant, so keep the last ~16 messages (~8 exchanges).
2852    var MAX_KEPT_MESSAGES = 16;
2853    if (conversation.length > MAX_KEPT_MESSAGES) {
2854      conversation = conversation.slice(conversation.length - MAX_KEPT_MESSAGES);
2855      // Ensure the trimmed history starts with a user message (the API
2856      // requires the first message to be from the user).
2857      while (conversation.length && conversation[0].role !== 'user') {
2858        conversation.shift();
2859      }
2860    }
2861
2862    var ctrl = new AbortController();
2863    var timeout = setTimeout(function() { ctrl.abort(); }, GUIDE_CONFIG.timeoutMs);
2864
2865    try {
2866      var res = await fetch(GUIDE_CONFIG.endpoint, {
2867        method: 'POST',
2868        headers: { 'Content-Type': 'application/json', 'x-session-id': (window.__hcSession || (window.__hcSession = ((new URLSearchParams(location.search).get('hcj')) || (window.crypto && crypto.randomUUID ? crypto.randomUUID() : 's-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2))))) },
2869        body: JSON.stringify({ messages: conversation }),
2870        signal: ctrl.signal
2871      });
2872      clearTimeout(timeout);
2873
2874      if (!res.ok) {
2875        var errBody = await res.json().catch(function() { return {}; });
2876        // Prefer the backend's friendly message. Fall back to a friendly
2877        // message keyed off the error code, then to a generic line — never
2878        // surface a bare "HTTP 400" to a traveler.
2879        var friendly = errBody.message;
2880        if (!friendly) {
2881          if (errBody.error === 'conversation_cap') {
2882            friendly = "We've covered a lot of ground — start a fresh chat and I'll keep up.";
2883          } else if (errBody.error === 'message_too_long') {
2884            friendly = "That message is a bit long for me — try shortening it.";
2885          } else if (res.status >= 500) {
2886            friendly = "Something hiccuped on my end. Give it another try in a moment.";
2887          } else {
2888            friendly = "Something didn't go through. Try asking again.";
2889          }
2890        }
2891        // SELF-REPORTING (temporary diagnostic, v9): announce the real cause
2892        var __reason = 'HTTP ' + res.status + (errBody && errBody.error ? ' · ' + errBody.error : '');
2893        try { console.error('[JUNE ERR]', res.status, errBody); } catch (e) {}
2894        var __err = new Error(friendly + '  (' + __reason + ')');
2895        __err.__reason = __reason;
2896        throw __err;
2897      }
2898
2899      var data = await res.json();
2900
2901      // Add June's reply to conversation history
2902      conversation.push({ role: 'assistant', content: data.reply || '', cards: data.cards || [], category: data.category || null });
2903
2904      return {
2905        reply: data.reply || '',
2906        cards: data.cards || [],
2907        category: data.category || null
2908      };
2909    } catch (err) {
2910      clearTimeout(timeout);
2911      if (err.name === 'AbortError') {
2912        try { console.error('[JUNE ERR] client-abort/timeout', GUIDE_CONFIG.timeoutMs + 'ms'); } catch (e) {}
2913        throw new Error('June took too long — try again  (timeout)');
2914      }
2915      if (err && err.__reason) { throw err; }   // already-annotated server error
2916      // fetch threw before/without a response — request blocked, CORS, DNS, or offline
2917      try { console.error('[JUNE ERR] network/no-response', err && err.name, err && err.message); } catch (e) {}
2918      throw new Error("Something didn't go through  (no-response · " + ((err && err.name) || 'err') + ")");
2919    }
2920  }
2921
2922  // ─── INPUT HANDLING ───────────────────────────────────────────────
2923  var input = document.getElementById('chat-input');
2924  var sendBtn = document.getElementById('send-btn');
2925
2926  function autoResizeTextarea() {
2927    input.style.height = 'auto';
2928    input.style.height = Math.min(input.scrollHeight, 120) + 'px';
2929  }
2930  input.addEventListener('input', autoResizeTextarea);
2931
2932  // ─── MOBILE KEYBOARD BEHAVIOR (ported from Brody v29) ────────────
2933  // Detect touch-device / mobile so keyboard behavior only fires there.
2934  var __isTouchDevice = (typeof window !== 'undefined') &&
2935    ('ontouchstart' in window || (navigator.maxTouchPoints || 0) > 0);
2936
2937  // When the user submits on mobile, blur the input so iOS dismisses the
2938  // keyboard, then scroll the input row back into view at the bottom of the
2939  // viewport so the user can ask another question without scrolling.
2940  function __submitCleanupForMobile() {
2941    if (!__isTouchDevice) return;
2942    try { input.blur(); } catch (e) {}
2943    requestAnimationFrame(function() {
2944      setTimeout(function() {
2945        try {
2946          var row = document.querySelector('.chat-input-row');
2947          if (row) row.scrollIntoView({ behavior: 'smooth', block: 'end' });
2948        } catch (e) {}
2949      }, 250);
2950    });
2951  }
2952
2953  // When the input gains focus on mobile, make sure it stays visible above
2954  // the keyboard. iOS usually handles this, but if the page has scrolled
2955  // (e.g., user was reading Surface 2 cards then tapped to ask again), we
2956  // nudge the input row into view first.
2957  input.addEventListener('focus', function() {
2958    if (!__isTouchDevice) return;
2959    setTimeout(function() {
2960      try {
2961        input.scrollIntoView({ behavior: 'smooth', block: 'center' });
2962      } catch (e) {}
2963    }, 100);
2964  });
2965
2966  async function handleSend() {
2967    var text = input.value.trim();
2968    if (!text) return;
2969    try { if (typeof gtag === 'function') { window.__guideMsgCount = (window.__guideMsgCount || 0) + 1; gtag('event', 'guide_message', { town: 'boerne', guide: 'June', message_number: window.__guideMsgCount }); } } catch (e) {}
2970    if (sendBtn.disabled) return;
2971
2972    sendBtn.disabled = true;
2973    input.value = '';
2974    autoResizeTextarea();
2975
2976    appendMessage('user', text);
2977    showTyping();
2978
2979    try {
2980      var result = await sendToGuide(text);
2981      // Events-page handoff ("Tell me about local events") must deterministically surface the
2982      // events calendar card + Events answer — never parks/attractions — regardless of model output.
2983      if (window.__eventsHandoff || /local events/i.test(text)) { result.cards = ['events']; result.category = 'Events'; window.__eventsHandoff = false; }
2984      hideTyping();
2985      appendMessage('june', result.reply, result.cards, result.category);
2986      // The reply just landed and may be long — the thread has grown, so the
2987      // earlier submit-scroll is now stale. On mobile, re-anchor the input row
2988      // to the bottom of the viewport so the box to type into stays visible.
2989      if (__isTouchDevice) {
2990        requestAnimationFrame(function() {
2991          setTimeout(function() {
2992            try {
2993              var row = document.querySelector('.chat-input-row');
2994              if (row) row.scrollIntoView({ behavior: 'smooth', block: 'end' });
2995            } catch (e) {}
2996          }, 120);
2997        });
2998      }
2999    } catch (err) {
3000      hideTyping();
3001      var msg = document.createElement('div');
3002      msg.className = 'chat-msg error';
3003      var avatar = document.createElement('div');
3004      avatar.className = 'avatar';
3005      avatar.textContent = '!';
3006      msg.appendChild(avatar);
3007      var bubble = document.createElement('div');
3008      bubble.className = 'bubble';
3009      // The friendly messages from sendToGuide are already complete sentences.
3010      // Only append the contact fallback for generic/technical errors, not for
3011      // the self-explanatory cap or length messages.
3012      var selfExplanatory = /fresh chat|bit long|hiccuped/.test(err.message || '');
3013      bubble.textContent = selfExplanatory
3014        ? err.message
3015        : (/backroads\.ai/.test(err.message || '')
3016            ? err.message
3017            : err.message + ' You can also reach Backroads Hill Country at [email protected].');
3018      msg.appendChild(bubble);
3019      document.getElementById('chat-thread').appendChild(msg);
3020      console.error('[June] Error:', err);
3021    } finally {
3022      sendBtn.disabled = false;
3023      // Only auto-focus on desktop. On mobile, refocusing reopens the keyboard
3024      // which contradicts the __submitCleanupForMobile blur behavior.
3025      if (!__isTouchDevice) input.focus();
3026    }
3027  }
3028
3029  sendBtn.addEventListener('click', function(e) {
3030    e.preventDefault();
3031    handleSend();
3032    __submitCleanupForMobile();
3033  });
3034  sendBtn.addEventListener('touchend', function(e) {
3035    if (!e.cancelable) return;
3036    e.preventDefault();
3037    handleSend();
3038    __submitCleanupForMobile();
3039  }, { passive: false });
3040
3041  input.addEventListener('keydown', function(e) {
3042    if (e.key === 'Enter' && !e.shiftKey) {
3043      e.preventDefault();
3044      handleSend();
3045      __submitCleanupForMobile();
3046    }
3047  });
3048
3049  // ─── GUIDE'S OPENING MESSAGE ─────────────────────────────────────
3050  appendMessage('june',
3051    "I'm June — 20 years in Boerne. Ask me about South Main Street, the antiques, where to eat, or where to stay. I'll give it to you straight."
3052  );
3053
3054})();
3055</script>
vendor: 1 bytes, line 3055
3055
3056<script>
3057/* Report-a-problem modal — open/close/submit via web3forms. Routes to
3058   [email protected] through the subject routing on the inbox side. */
3059(function() {
3060  var overlay = document.getElementById('report-modal-overlay');
3061  var trigger = document.getElementById('report-problem-trigger');
3062  var closeBtn = document.getElementById('report-modal-close');
3063  var cancelBtn = document.getElementById('report-cancel-btn');
3064  var form = document.getElementById('report-form');
3065  var submitBtn = document.getElementById('report-submit');
3066  var success = document.getElementById('report-success');
3067
3068  if (!overlay || !trigger) return;
3069
3070  function openModal() {
3071    overlay.classList.add('show');
3072    document.body.style.overflow = 'hidden';
3073    setTimeout(function() {
3074      var first = document.getElementById('report-issue');
3075      if (first) first.focus();
3076    }, 100);
3077  }
3078  function closeModal() {
3079    overlay.classList.remove('show');
3080    document.body.style.overflow = '';
3081  }
3082
3083  trigger.addEventListener('click', function(e) { e.preventDefault(); openModal(); });
3084  if (closeBtn) closeBtn.addEventListener('click', closeModal);
3085  if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
3086  overlay.addEventListener('click', function(e) { if (e.target === overlay) closeModal(); });
3087  document.addEventListener('keydown', function(e) {
3088    if (e.key === 'Escape' && overlay.classList.contains('show')) closeModal();
3089  });
3090
3091  if (form) {
3092    form.addEventListener('submit', async function(e) {
3093      e.preventDefault();
3094      submitBtn.disabled = true;
3095      submitBtn.textContent = 'Sending...';
3096      var emailField = document.getElementById('report-email');
3097      var replyto = document.getElementById('report-replyto');
3098      if (emailField && replyto && emailField.value) replyto.value = emailField.value;
3099      try {
3100        var formData = new FormData(form);
3101        var res = await fetch('https://api.web3forms.com/submit', {
3102          method: 'POST', body: formData, headers: { 'Accept': 'application/json' }
3103        });
3104        var data = await res.json();
3105        if (res.ok && data.success) {
3106          form.style.display = 'none';
3107          success.classList.add('show');
3108        } else {
3109          submitBtn.disabled = false;
3110          submitBtn.textContent = 'Try again';
3111          alert('Something went wrong. Try again, or email [email protected] directly.');
3112        }
3113      } catch (err) {
3114        submitBtn.disabled = false;
3115        submitBtn.textContent = 'Try again';
3116        alert('Lost the signal. Check your connection and try again.');
3117      }
3118    });
3119  }
3120})();
3121</script>
vendor: 1 bytes, line 3121
3121
3122<script>
3123/* Email obfuscation — wires .ob-mail links to construct mailto: at click time.
3124   Scrapers reading raw HTML don't see a mailto: or [email protected] string. */
3125(function() {
3126  function getEmail(el) {
3127    var u = el.getAttribute('data-u') || '';
3128    var d = el.getAttribute('data-d') || '';
3129    return u + String.fromCharCode(64) + d;
3130  }
3131  document.addEventListener('click', function(e) {
3132    var t = e.target;
3133    while (t && t !== document.body && !(t.classList && t.classList.contains('ob-mail'))) { t = t.parentNode; }
3134    if (t && t.classList && t.classList.contains('ob-mail')) {
3135      e.preventDefault();
3136      window.location.href = 'mailto:' + getEmail(t);
3137    }
3138  });
3139  window.__getObEmail = function(el) { return getEmail(el); };
3140})();
3141</script>
vendor: 1 bytes, line 3141
3141
3142<script>
3143/* ?ask= prefill handoff from the events page — fills the guide box, scrolls, focuses; auto-sends when src=events */
3144(function(){
3145  function prefillFromAsk(){
3146    var params; try{ params=new URLSearchParams(window.location.search); }catch(e){ return; }
3147    var q=params.get("ask"); if(!q) return;
3148    if(params.get("src")==="events"){ window.__eventsHandoff=true; }
3149    var input=document.getElementById("chat-input");
3150    if(!input) return;
3151    input.value=q;
3152    try{ input.dispatchEvent(new Event("input",{bubbles:true})); }catch(e){}
3153    try{ window.scrollTo({top:0,behavior:"smooth"}); }catch(e){ try{ window.scrollTo(0,0); }catch(_){} }
3154    /* Was: auto-click Send when src=events. That made every event card fire the
3155       guide API on page LOAD -- and crawlers load every URL, so Google's
3156       Pryor OK data centre was walking 2,843 ask_url links and billing us,
3157       ~359 hits/day. Now events prefill and focus like every other ?ask=;
3158       a human still just presses Enter. One keystroke for people, zero API
3159       calls for bots. */
3160    if(window.__eventsHandoff){
3161      var lbl=document.getElementById("events-prefill-note");
3162      if(lbl) lbl.hidden=false;   /* optional: "Press enter to ask about this event" */
3163    }
3164    setTimeout(function(){ try{ input.focus({preventScroll:true}); }catch(e){ try{ input.focus(); }catch(_){} } }, 500);
3165  }
3166  if(document.readyState==="complete") prefillFromAsk();
3167  else window.addEventListener("load", prefillFromAsk);
3168})();
3169</script>
3169
3170<!-- ask-prefill + events-intent v2 + card-align 2026-06-12 -->
3171  
3171<script>
3172  // Suggested-question chips: one tap fills + sends, then hides. No follow-up set.
3173  (function(){
3174    var pillWrap = document.getElementById('start-pills');
3175    var input = document.getElementById('chat-input');
3176    var sendBtn = document.getElementById('send-btn');
3177    if (!pillWrap || !input || !sendBtn) return;
3178    function hidePills(){ pillWrap.classList.add('hidden'); }
3179    Array.prototype.forEach.call(pillWrap.querySelectorAll('.start-pill'), function(btn){
3180      btn.addEventListener('click', function(){
3181        if (btn.classList.contains('start-pill-lodging')) {
3182          try { if (typeof gtag === 'function') gtag('event', 'lodging_pill_click', { town: 'boerne' }); } catch(e){}
3183          return; // anchor navigates to /vacation-rentals
3184        }
3185        var q = btn.getAttribute('data-prompt') || btn.textContent.trim();
3186        if(!q) return;
3187        input.value = q;
3188        try { input.dispatchEvent(new Event('input', { bubbles: true })); } catch(e){}
3189        hidePills();
3190        sendBtn.click();
3191      });
3192    });
3193    sendBtn.addEventListener('click', hidePills);
3194    input.addEventListener('keydown', function(e){ if(e.key === 'Enter' && !e.shiftKey) hidePills(); });
3195  })();
3196  </script>
3196
3197
3198<script>
3199(function(){
3200  var LAT=29.7947, LON=-98.732; // Boerne, TX
3201  var I={
3202    sun:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.5V5M12 19v2.5M2.5 12H5M19 12h2.5M5.1 5.1l1.7 1.7M17.2 17.2l1.7 1.7M18.9 5.1l-1.7 1.7M6.8 17.2l-1.7 1.7"/></svg>',
3203    partly:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><circle cx="8.5" cy="8" r="3"/><path d="M8.5 2.6V4M3.4 8H2M4.7 4.2l.9.9M13.3 8h-1"/><path d="M7.5 19h8.2a3.4 3.4 0 0 0 .3-6.78A4.7 4.7 0 0 0 7 11a3.4 3.4 0 0 0 .5 8z"/></svg>',
3204    cloud:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M7 18h9a3.8 3.8 0 0 0 .4-7.58A5.2 5.2 0 0 0 6.3 9 3.8 3.8 0 0 0 7 18z"/></svg>',
3205    fog:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M7 12h8.5a3.4 3.4 0 0 0 .3-6.78A4.8 4.8 0 0 0 6.4 4"/><path d="M4 16h13M6 19.5h11"/></svg>',
3206    rain:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M7 14h8.5a3.6 3.6 0 0 0 .3-7.18A5 5 0 0 0 6.3 6"/><path d="M8.5 17.5l-1 2.4M12 17.5l-1 2.4M15.5 17.5l-1 2.4"/></svg>',
3207    snow:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M7 13h8.5a3.6 3.6 0 0 0 .3-7.18A5 5 0 0 0 6.3 5"/><path d="M8.5 17.5h.01M12 19h.01M15.5 17.5h.01M10.2 20.5h.01M13.8 20.5h.01"/></svg>',
3208    storm:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7 13h8.5a3.6 3.6 0 0 0 .3-7.18A5 5 0 0 0 6.3 5"/><path d="M12.5 14.5l-2.2 3.6h2.8l-2.2 3.6"/></svg>'
3209  };
3210  function wmo(c){
3211    if(c===0) return {l:'Clear',i:'sun'};
3212    if(c===1||c===2) return {l:'Partly cloudy',i:'partly'};
3213    if(c===3) return {l:'Overcast',i:'cloud'};
3214    if(c===45||c===48) return {l:'Fog',i:'fog'};
3215    if(c>=51&&c<=57) return {l:'Drizzle',i:'rain'};
3216    if(c>=61&&c<=67) return {l:'Rain',i:'rain'};
3217    if(c>=71&&c<=77) return {l:'Snow',i:'snow'};
3218    if(c>=80&&c<=82) return {l:'Showers',i:'rain'};
3219    if(c===85||c===86) return {l:'Snow showers',i:'snow'};
3220    if(c>=95) return {l:'Thunderstorm',i:'storm'};
3221    return {l:'',i:'cloud'};
3222  }
3223  function load(){
3224    var url='https://api.open-meteo.com/v1/forecast?latitude='+LAT+'&longitude='+LON+'&current=temperature_2m,weather_code&temperature_unit=fahrenheit&timezone=America%2FChicago';
3225    var ctrl=new AbortController(); var to=setTimeout(function(){ctrl.abort();},7000);
3226    fetch(url,{signal:ctrl.signal})
3227      .then(function(r){clearTimeout(to); if(!r.ok) throw 0; return r.json();})
3228      .then(function(d){
3229        var cur=wmo(d.current.weather_code);
3230        var ic=document.getElementById('wx-chip-icon'); if(ic) ic.innerHTML=I[cur.i];
3231        var tp=document.getElementById('wx-chip-temp'); if(tp) tp.textContent=Math.round(d.current.temperature_2m)+'\u00b0';
3232        var cd=document.getElementById('wx-chip-cond'); if(cd) cd.textContent=cur.l;
3233        var chip=document.getElementById('wx-chip'); /* chip stays in layout from load */
3234      })
3235      .catch(function(){clearTimeout(to);});
3236  }
3237  if(document.readyState!=='loading') load(); else document.addEventListener('DOMContentLoaded', load);
3238})();
3239</script>
3239
3240
3241<!-- ===== POI SHEET PATCH (additive — appended to index.html, nothing above is modified) =====
3242     Turns the guide's answers into plain text with tappable POI names, ChatGPT-style,
3243     and retires the inline card strip + the second card surface below the chat.
3244     To roll back: delete this block. Nothing else in the page depends on it. -->
3245<style>
3246  /* weather chip: identical colours on every page */
3247  .wx-chip-icon{color:#f0c56c !important}
3248  .wx-chip-icon svg{stroke:currentColor}
3249  .wx-chip-temp{color:#fffaf0 !important}
3250  /* nav: keep links + weather chip on one line on phones */
3251  @media(max-width:600px){
3252    .ev-nav,.nav-in{flex-wrap:nowrap !important;gap:8px}
3253    .ev-navlinks,.nav-links{flex-wrap:nowrap !important;gap:13px !important;min-width:0}
3254    .ev-navlinks a,.nav-links a{font-size:14px !important;white-space:nowrap}
3255    .wx-chip{padding-left:9px !important;gap:4px !important;font-size:13px !important;flex-shrink:0}
3256    .wx-chip-icon{width:16px !important;height:16px !important}
3257  }
3258  @media(max-width:380px){
3259    .ev-navlinks,.nav-links{gap:10px !important}
3260    .ev-navlinks a,.nav-links a{font-size:13px !important}
3261  }
3262  /* the "scroll for more" hint under the hero */
3263  .scroll-hint{display:none !important}
3264  /* retire every card surface in the chat (the guide still builds them; they simply never render) */
3265  /* legacy label/heading furniture for the card surfaces */
3266  .guide-cards-label, .guide-surface-head, .guide-surface-title, .guide-surface-sub,
3267  .guide-surface-load-more { display:none !important; }
3268
3269  /* tappable POI name inside an answer */
3270  .hcs-more{display:block;margin-top:9px;font-size:12.5px;font-weight:600;color:var(--gold-light);
3271    cursor:pointer;text-decoration:underline;text-underline-offset:2px}
3272  .hcs-more:hover{color:var(--gold)}
3273  .hcs-link{color:var(--gold-light);text-decoration:underline;text-underline-offset:2px;
3274    text-decoration-thickness:1px;cursor:pointer}
3275  .hcs-link:hover{color:var(--gold)}
3276
3277  /* the sheet */
3278  .hcs-veil{position:fixed;inset:0;z-index:9000;background:rgba(4,14,9,.62);opacity:0;
3279    pointer-events:none;transition:opacity .22s ease}
3280  .hcs-veil.on{opacity:1;pointer-events:auto}
3281  .hcs{position:fixed;z-index:9001;overscroll-behavior:contain;touch-action:pan-y;left:18px;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));top:auto;visibility:hidden;pointer-events:none;max-height:min(88vh,88dvh);overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#5fb87a transparent;
3282    background:radial-gradient(120% 90% at 90% 0%,#26694a,#1a4e33 55%,#123a26);
3283    border:1.5px solid rgba(217,164,65,.7);border-radius:18px;
3284    transform:translateY(102%);transition:transform .26s cubic-bezier(.22,1,.36,1);
3285    box-shadow:0 -18px 44px -18px rgba(0,0,0,.7);font-family:var(--sans,Inter,system-ui,sans-serif)}
3286  .hcs.on{transform:translateY(0);visibility:visible;pointer-events:auto}
3287  @media(min-width:821px){
3288    .hcs{left:50%;right:auto;bottom:auto;top:50%;width:min(430px,86vw);max-height:82vh;border-radius:16px;
3289      border:2px solid rgba(217,164,65,.9);box-shadow:0 30px 70px -20px rgba(0,0,0,.75);
3290      transform:translate(-50%,-42%) scale(.97);opacity:0;
3291      transition:transform .2s ease,opacity .2s ease}
3292    .hcs.on{transform:translate(-50%,-50%) scale(1);opacity:1}
3293  }
3294  .hcs-band{position:relative;z-index:3;height:34px;background:#123a26;border-radius:17px 17px 0 0;border-bottom:1px solid rgba(217,164,65,.35);overflow:hidden}
3295  @media(min-width:821px){.hcs-band{height:30px;border-radius:14px 14px 0 0}}
3296  .hcs-x{position:absolute;top:7px;right:12px;z-index:5;width:32px;height:32px;border:0;border-radius:50%;cursor:pointer;background:#d9a441;color:#0e2e1f;font-size:19px;font-weight:700;line-height:1;display:flex;align-items:center;justify-content:center}
3297  .hcs-x:hover{background:#f0c56c}@media(min-width:821px){.hcs-x{top:12px;background:#0e2e1f;color:#d9a441;border:1.5px solid rgba(217,164,65,.6)}.hcs-x:hover{background:#153f2a;color:#f0c56c}}
3298  .hcs-photo{height:clamp(112px,21vh,178px);background-size:cover;background-position:center;background-color:#123a26}
3299  @media(min-width:821px){.hcs-photo{height:186px}}
3300  .hcs-body{padding:13px 18px 20px;position:relative;overflow:hidden;text-align:left}.hcs-body,.hcs-body *{text-align:left}.hcs-lede,.hcs-fact,.hcs-chips{text-align:left}
3301  .hcs-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:7px}
3302  .hcs-chip{font-size:9.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#f0c56c;
3303    border:1px solid rgba(217,164,65,.5);border-radius:20px;padding:3px 9px}
3304  .hcs-chip.solid{background:#d9a441;color:#082118;border-color:#d9a441}
3305  .hcs h3{font-size:19px;color:#fffaf0;margin:0 0 6px;font-weight:700;line-height:1.2;position:relative;padding-bottom:7px}
3306  .hcs p.hcs-lede{font-size:13px;color:#dbe4d6;line-height:1.5;margin:0 0 11px;font-weight:500;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
3307  .hcs-facts{display:flex;flex-direction:column;gap:6px;margin-bottom:13px;border-top:1px solid rgba(217,164,65,.28);padding-top:10px}
3308  .hcs-fact{display:flex;gap:10px;font-size:13px}
3309  .hcs-fact b{flex:0 0 78px;color:#f0c56c;font-weight:700;font-size:10px;letter-spacing:.09em;
3310    text-transform:uppercase;padding-top:2px}
3311  .hcs-fact span{color:#eef3e6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
3312  .hcs-fact a{color:#fff;text-decoration:none}
3313  .hcs-fact a:hover{color:#f0c56c}
3314  .hcs-open{display:inline-flex;align-items:center;gap:7px;background:#d9a441;color:#082118;
3315    font-size:13px;font-weight:600;text-decoration:none;padding:10px 17px;border-radius:9px}
3316  .hcs-open:hover{background:#f0c56c}
3317  /* band gets a topo line, like the cards on the pages */
3318  .hcs-band{overflow:hidden}
3319  .hcs-wave{position:absolute;left:0;right:0;bottom:-1px;width:100%;height:34px;pointer-events:none}
3320  /* faint contour watermark behind the body */
3321  .hcs-topo{position:absolute;right:-20px;bottom:-10px;width:330px;height:110px;opacity:.11;color:#d9a441;pointer-events:none;z-index:0}
3322  .hcs-body{position:relative}
3323  .hcs-body > *:not(.hcs-topo){position:relative;z-index:1}
3324  /* gold hairline above the facts */
3325  .hcs-facts{border-top:1px solid rgba(217,164,65,.28);padding-top:13px}
3326  /* name gets a small gold rule under it */
3327  .hcs h3{position:relative;padding-bottom:9px}
3328  .hcs h3:after{content:"";position:absolute;left:0;bottom:0;width:34px;height:2px;background:#d9a441;border-radius:2px}
3329
3330  .hcs::-webkit-scrollbar{width:5px}
3331  .hcs::-webkit-scrollbar-thumb{background:#5fb87a;border-radius:3px}
3332  .hcs::-webkit-scrollbar-track{background:transparent}
3333  body.hcs-lock{position:fixed;left:0;right:0;width:100%;overflow:hidden;overscroll-behavior:none}
3334  html.hcs-lock-h{overscroll-behavior:none}
3335</style>
3336
3337<div class="hcs-veil" id="hcs-veil"></div>
3338<div class="hcs" id="hcs" role="dialog" aria-modal="true" aria-label="Place details">
3339  <div class="hcs-band"><svg class="hcs-wave" viewBox="0 0 1000 34" preserveAspectRatio="none" aria-hidden="true"><path d="M0 17 Q62 7 125 16 T250 14 T375 18 T500 12 T625 17 T750 13 T875 18 T1000 14" fill="none" stroke="#d9a441" stroke-width="1.4" opacity=".5" vector-effect="non-scaling-stroke"/><path d="M0 26 Q62 17 125 25 T250 22 T375 26 T500 21 T625 25 T750 22 T875 26 T1000 23" fill="none" stroke="#d9a441" stroke-width="1.2" opacity=".25" vector-effect="non-scaling-stroke"/></svg></div>
3340  <button class="hcs-x" id="hcs-x" type="button" aria-label="Close">&times;</button>
3341  <div class="hcs-photo" id="hcs-photo"></div>
3342  <div class="hcs-body">
3343    <svg class="hcs-topo" viewBox="0 0 460 150" preserveAspectRatio="none" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true"><path d="M0 104 Q70 70 140 98 T280 82 T460 96M0 128 Q85 92 170 120 T340 104 T460 118M0 150 Q95 116 190 142 T380 126 T460 140"/></svg>
3344    <div class="hcs-chips" id="hcs-chips"></div>
3345    <h3 id="hcs-name"></h3>
3346    <p class="hcs-lede" id="hcs-lede"></p>
3347    <div class="hcs-facts" id="hcs-facts"></div>
3348    <a class="hcs-open" id="hcs-open" href="#">Open full page &rarr;</a>
3349  </div>
3350</div>
3351
3352<script>
3353(function(){
3354  var IDX=[], veil=document.getElementById('hcs-veil'), sheet=document.getElementById('hcs');
3355  if(!veil||!sheet) return;
3356  fetch('/data/poi-index.json').then(function(r){return r.json();})
3357    .then(function(j){ IDX=(j||[]).slice().sort(function(a,b){return b.name.length-a.name.length;}); sweep(); })
3358    .catch(function(){});
3359
3360  function rx(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); }
3361
3362  function open(rec){
3363    document.getElementById('hcs-photo').style.backgroundImage = rec.img ? ("url('"+rec.img+"')") : '';
3364    var chips=document.getElementById('hcs-chips'); chips.innerHTML='';
3365    [[rec.cat,0],[rec.town,0],[/^free/i.test(rec.admission||'')?'Free':'',1]].forEach(function(c){
3366      if(!c[0]) return; var s=document.createElement('span');
3367      s.className='hcs-chip'+(c[1]?' solid':''); s.textContent=c[0]; chips.appendChild(s);
3368    });
3369    document.getElementById('hcs-name').textContent=rec.name;
3370    document.getElementById('hcs-lede').textContent=rec.lede||'';
3371    var f=document.getElementById('hcs-facts'); f.innerHTML='';
3372    function row(k,v,href){
3373      if(!v) return;
3374      var d=document.createElement('div'); d.className='hcs-fact';
3375      var b=document.createElement('b'); b.textContent=k; d.appendChild(b);
3376      var s=document.createElement('span');
3377      if(href){ var a=document.createElement('a'); a.href=href; a.textContent=v;
3378        if(href.indexOf('http')===0){a.target='_blank'; a.rel='noopener';} s.appendChild(a); }
3379      else { s.textContent=v; }
3380      d.appendChild(s); f.appendChild(d);
3381    }
3382    row('Hours',rec.hours); row('Admission',rec.admission); row('Best time',rec.best);
3383    row('Phone',rec.phone, rec.phone?('tel:+1'+rec.phone.replace(/\D/g,'')):'');
3384    document.getElementById('hcs-open').href=rec.url;
3385    veil.className='hcs-veil on'; sheet.className='hcs on';
3386    __y = window.pageYOffset || document.documentElement.scrollTop || 0;
3387    document.body.style.top = (-__y) + 'px';
3388    document.body.classList.add('hcs-lock');
3389    document.documentElement.classList.add('hcs-lock-h');
3390    sheet.scrollTop = 0;
3391    if(window.gtag) gtag('event','poi_sheet_open',{poi:rec.slug});
3392  }
3393  var __y = 0;
3394  function close(){
3395    veil.className='hcs-veil'; sheet.className='hcs';
3396    document.body.classList.remove('hcs-lock');
3397    document.documentElement.classList.remove('hcs-lock-h');
3398    document.body.style.top='';
3399    window.scrollTo(0, __y);
3400  }
3401  veil.addEventListener('click',close);
3402  veil.addEventListener('touchmove',function(e){ e.preventDefault(); },{passive:false});
3403  // a drag inside a non-scrolling sheet must not bleed through to the page
3404  sheet.addEventListener('touchmove',function(e){
3405    if(sheet.scrollHeight <= sheet.clientHeight + 1) e.preventDefault();
3406  },{passive:false});
3407  document.getElementById('hcs-x').addEventListener('click',close);
3408  document.addEventListener('keydown',function(e){ if(e.key==='Escape') close(); });
3409
3410  // leaving the page (tapping "Open full page") closes the sheet, and it stays closed if the
3411  // visitor comes back — browsers restore a cached page exactly as it was left.
3412  window.addEventListener('pagehide', close);
3413  window.addEventListener('pageshow', function(e){ if(e.persisted) close(); });
3414  window.addEventListener('popstate', close);
3415
3416
3417  // the prompt still says "tap the card"; there are no cards now, so soften it in the rendered text
3418  function detune(html){
3419    return html
3420      .replace(/\s*[—–-]?\s*tap the card below[^.?!]*/gi,'')
3421      .replace(/\s*[—–-]?\s*tap the card[^.?!]*/gi,'')
3422      .replace(/\s*[—–-]?\s*see the card below[^.?!]*/gi,'')
3423      .replace(/\s*the card below\b/gi,' the link')
3424      .replace(/\s*\(card below\)/gi,'');
3425  }
3426
3427  function linkify(node){
3428    if(!IDX.length || !node) return;
3429    // re-run while a reply streams; settle only when the text stops changing
3430    var sig=(node.textContent||'').length+':'+(node.innerHTML||'').length;
3431    if(node.getAttribute('data-hcs')===sig) return;
3432    node.setAttribute('data-hcs',sig);
3433
3434    // strip "tap the card" phrasing first (the prompt still says it; there are no cards)
3435    var cleaned=detune(node.innerHTML);
3436    if(cleaned!==node.innerHTML){ node.innerHTML=cleaned; }
3437
3438    var linked={};
3439    node.querySelectorAll('.hcs-link').forEach(function(el){ linked[el.getAttribute('data-slug')]=1; });
3440
3441    // walk text nodes only — never touches attributes or existing links
3442    for(var k=0;k<IDX.length;k++){
3443      var rec=IDX[k];
3444      if(linked[rec.slug]) continue;
3445      var names=[rec.name].concat(rec.alias||[]);
3446      var done=false;
3447      for(var n=0;n<names.length && !done;n++){
3448        var re=new RegExp('(^|[^\\w])('+rx(names[n])+')(?![\\w])','i');
3449        var walker=document.createTreeWalker(node,NodeFilter.SHOW_TEXT,null,fal
3449se), tn, batch=[];
3450        while((tn=walker.nextNode())){ batch.push(tn); }
3451        for(var b=0;b<batch.length && !done;b++){
3452          var t=batch[b];
3453          if(t.parentNode && t.parentNode.closest && t.parentNode.closest('.hcs-link')) continue;
3454          var m=re.exec(t.nodeValue);
3455          if(!m) continue;
3456          var start=m.index+m[1].length, end=start+m[2].length;
3457          var after=t.splitText(start); after.splitText(m[2].length);
3458          var span=document.createElement('span');
3459          span.className='hcs-link'; span.setAttribute('data-slug',rec.slug);
3460          span.textContent=after.nodeValue;
3461          after.parentNode.replaceChild(span,after);
3462          linked[rec.slug]=1; done=true;
3463        }
3464      }
3465    }
3466  }
3467
3468  // one delegated listener — survives innerHTML being rewritten as replies stream
3469  document.addEventListener('click',function(e){
3470    var el=e.target && e.target.closest ? e.target.closest('.hcs-link,.hcs-more') : null;
3471    if(!el) return;
3472    e.preventDefault();
3473    var r=IDX.filter(function(x){return x.slug===el.getAttribute('data-slug');})[0];
3474    if(r) open(r);
3475  });
3476
3477  // Cards are retired for POIs, but rentals still use them, so this removes per card
3478  // rather than by class — the two share the same markup.
3479  function isRental(el){
3480    var h=(el.getAttribute && el.getAttribute('href')) || '';
3481    if(/backroadstexas|\/vacation-rentals|\/cabins|\/cottages|\/river-cabins/i.test(h)) return true;
3482    var t=(el.textContent||'').toLowerCase();
3483    return /night|sleeps|bedroom|rental/.test(t);
3484  }
3485  function stripCards(){
3486    document.querySelectorAll('.guide-chip, .big-card, .brody-chip, .thread-card, .dealt-card').forEach(function(el){
3487      if(!isRental(el) && el.parentNode) el.parentNode.removeChild(el);
3488    });
3489    // drop now-empty containers
3490    document.querySelectorAll('.guide-chips-row, .guide-chips, .guide-surface-grid, .guide-surface, .thread-cards, .thread-cards-label, .thread-cards-load-more, .dealt-cards, .dealt-stack, .dealt-cat-label, .chips-below-hint, .pulse-arrow, .brody-chips, .thread-cards-wrap')
3491      .forEach(function(c){
3492        if(c.parentNode && !c.querySelector('.guide-chip, .big-card')) c.parentNode.removeChild(c);
3493      });
3494  }
3495
3496  // If the visitor asked about a specific place and the answer used "it" instead of the
3497  // name, there is nothing to underline — so offer the card explicitly.
3498  function ensureLink(node){
3499    if(!IDX.length || !node || node.querySelector('.hcs-link, .hcs-more')) return;
3500    var msg=node.closest ? node.closest('.chat-msg') : null;
3501    if(!msg) return;
3502    var prev=msg.previousElementSibling, asked='';
3503    while(prev && !asked){
3504      if(prev.classList && prev.classList.contains('user')){
3505        var bb=prev.querySelector('.bubble'); asked=bb?(bb.textContent||''):'';
3506      }
3507      prev=prev.previousElementSibling;
3508    }
3509    if(!asked) return;
3510    for(var i=0;i<IDX.length;i++){
3511      var rec=IDX[i], names=[rec.name].concat(rec.alias||[]);
3512      for(var n=0;n<names.length;n++){
3513        var re=new RegExp('(^|[^\\w])'+rx(names[n])+'(?![\\w])','i');
3514        if(re.test(asked)){
3515          var a=document.createElement('span');
3516          a.className='hcs-more'; a.setAttribute('data-slug',rec.slug);
3517          a.textContent='View '+rec.name+' \u2192';
3518          node.appendChild(a);
3519          return;
3520        }
3521      }
3522    }
3523  }
3524
3525  function sweep(){
3526    stripCards();
3527    var _bubbles = document.querySelectorAll('.chat-msg:not(.user) .bubble, .msg-brody .bubble');
3528/* the seeded opening greeting is always the first guide message — never linkify it,
3529   it is a welcome, not an answer to a search */
3530_bubbles.forEach(function(b,i){ if(i===0) return; linkify(b); ensureLink(b); });
3531  }
3532  var thread=document.getElementById('chat-thread');
3533  if(thread && window.MutationObserver){
3534    new MutationObserver(function(){ setTimeout(sweep,60); })
3535      .observe(thread,{childList:true,subtree:true});
3536  }
3537  setInterval(sweep,1500);   // catches streamed replies that finish after a mutation
3538})();
3539</script>
3539
3540<!-- ===== END POI SHEET PATCH ===== -->
3541
3542</body>
3543
3544</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.