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">→</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">—</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'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's worth seeing in Comfort?">What'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 →</u> 1300 </button> 1301 <span class="chat-guide-status" tabindex="0"> 1302 <span class="cbs-dot" aria-hidden="true"></span>June · v11 · live 1303 <span class="cbs-tip" role="tooltip">June is live — 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 →</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 — 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">▾</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> — 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 — local knowledge first, from people who actually live and work in the Hill Country.</p> 1370 <div class="about-us-sig"> 1371 Spencer & 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 — towns, parks, attractions, rivers, events & 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 →</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 — 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">▾</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">▾</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">▾</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">▾</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">▾</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">▾</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">▾</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">▾</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">▾</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">▾</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 · Since 2001</div> 1678 <div class="f-stats-line stars">4.9★ across 389 Google reviews · 4.8★ App Store · 4.7★ 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> · <a href="#" class="ob-mail" data-u="spencer" data-d="backroads.ai" onclick="return false;">spencer@backroads.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> · 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 — 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">×</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 — 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…</option> 1755 <option value="content_idea">Content idea — something we should add or feature</option> 1756 <option value="data_correction">Data correction — June got something wrong</option> 1757 <option value="bug_report">Bug or technical issue — something didn't work</option> 1758 <option value="general_feedback">General feedback — 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 — 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 ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[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+'¤t=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">×</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 →</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.