#ba-explorer, #baModal { --ba-navy: #214883; --ba-navy-dark: #0a1f44; --ba-accent: #4878d4; --ba-heading: #2a2a2a; } #ba-panel .nav.nav-pills {scrollbar-width: thin;scrollbar-color: #214883 #fff0;} .ba-heading-color { color: var(--ba-heading); } .ba-bg-navy { background-color: var(--ba-navy); } .ba-fw-500 { font-weight: 500; } /* BS4 has no font-weight-500 utility */ .ba-fs-base { font-size: 1rem; } /* BS4 has no fs-* utilities */ .ba-dim { opacity: .75; } /* BS4 has no opacity-* utilities */ .ba-eyebrow { letter-spacing: .14em; } /* no tracking utility */ .ba-card { border-radius: 20px; } /* reference cards are 20px; BS4 rounded-lg is .3rem */ .ba-card .embed-responsive:first-child { border-top-left-radius: 20px; border-top-right-radius: 20px; } /* Navy buttons: BS4 .btn-primary is the site's default blue, so brand-colour a scoped variant. */ #ba-explorer .btn-outline-primary, #baModal .btn-outline-primary { color: var(--ba-navy); border-color: var(--ba-navy); } #ba-explorer .btn-outline-primary:hover, #baModal .btn-outline-primary:hover, #ba-explorer .btn-outline-primary:not(:disabled):active, #baModal .btn-outline-primary:not(:disabled):active { color: #fff; background-color: var(--ba-navy); border-color: var(--ba-navy); } #ba-explorer .btn-primary, #baModal .btn-primary { background-color: var(--ba-navy); border-color: var(--ba-navy); } #ba-explorer .btn-primary:hover, #baModal .btn-primary:hover { background-color: var(--ba-navy-dark); border-color: var(--ba-navy-dark); } /* keeps UA button chrome in BS4 — reset it. */ #ba-explorer .nav-pills .nav-link, #baModal .ba-view-pills .nav-link { background: transparent; border: 0; } #ba-explorer .nav-pills .nav-link { color: #5a5a5a; } #ba-explorer .nav-pills .nav-link:hover { color: var(--ba-navy); } #ba-explorer .nav-pills .nav-link.active { background-color: var(--ba-navy); color: #fff; } #ba-explorer .nav-pills .nav-link.active:hover { color: #fff !important; } /* 2:1 before/after composites — BS4 ships 21by9/16by9/4by3/1by1 only. */ .ba-embed-2by1::before { padding-top: 50%; } .ba-cover { object-fit: cover; } /* no object-fit utility in BS4 */ /* Corner labels: BS4 has no top-0/left-0/right-0 utilities. Override .embed-responsive-item sizing for badges. */ .ba-badge { position: absolute; top: .5rem; width: auto; height: auto; font-weight: 500; } .ba-badge--l { left: .5rem; } .ba-badge--r { right: .5rem; } /* Photo tile gradient so the category name reads over any cover image. */ .ba-tile-shade { background: linear-gradient(to top, rgba(10,31,68,.85) 0%, rgba(10,31,68,.15) 55%, rgba(10,31,68,0) 100%); } .ba-tile-btn { width: 100%; } .ba-tile-btn:focus { outline: none; box-shadow: none; } .ba-tile-btn:hover { transform: translateY(-7px)!important; } /* Selected / focused category ring drawn on the card so it follows the 20px radius on every corner. */ .ba-tile { position: relative; } .ba-tile::after { content:""; position:absolute; top:0; right:0; bottom:0; left:0; border-radius: inherit; pointer-events:none; box-shadow: inset 0 0 0 0 var(--ba-accent); transition: box-shadow .15s ease; } .ba-tile.is-active::after, .ba-tile.has-focus::after { box-shadow: inset 0 0 0 4px var(--ba-accent); } /* Hover lift on clickable case cards. */ .ba-case-btn { transition: transform .18s ease, box-shadow .18s ease; } .ba-case-btn:hover, .ba-case-btn:focus { transform: translateY(-3px); box-shadow: 0 14px 40px rgba(11,34,80,.12) !important; outline: none; } /* ---- Modal (Filter Grid style): dark stage + view pills, details rail ---- */ #baModal .modal-content { border-radius: 20px; overflow: hidden; } @media (min-width: 992px) { #baModal .ba-stage { min-height: 360px; } } #baModal .ba-view-pills .nav-link { color: rgba(255,255,255,.75); padding: .25rem .75rem; } #baModal .ba-view-pills .nav-link:hover { color: #fff; } #baModal .ba-view-pills .nav-link.active { background: #fff; color: var(--ba-navy-dark); } #baModal .ba-view-item { color: #5a5a5a; } #baModal .ba-view-item.is-current { color: var(--ba-navy); font-weight: 500; } #baModal .ba-view-item.is-current .ba-view-num { background: var(--ba-navy); color: #fff; border-color: var(--ba-navy); } #baModal .ba-pill-l { border-top-left-radius: 50rem; border-bottom-left-radius: 50rem; } #baModal .ba-pill-r { border-top-right-radius: 50rem; border-bottom-right-radius: 50rem; } #baModal .carousel-item .ba-embed-2by1 { background: #212529; } Real Patient Transformations Before & After Galleries Explore real results by treatment category and see what’s possible for you. Choose a category, then a procedure. Gallery Request Appointment Loading galleries… × Previous Next × Photos are shown with patient consent. Individual results may vary. Request Appointment About this procedure ← Prev Next → /* * Before & After gallery — data comes from the B&A; data page (data-ba-src above). * Data page contract — one per procedure; headings and page order are ignored: * * first row = view labels (Front View, Left Diagonal, …), each following row = one case, * each cell = image URL (plain text, , or ). Empty cells are skipped. * Tables without a "ba-…--…" id are ignored, so the page can hold anything else. * * URL params (all optional, case-insensitive, name or slug): * ?category=face * ?procedure=brow-lift (or "Brow Lift") * ?procedure=brow-lift&case;=1 (opens Brow Lift case 1 in the modal) * The URL stays in sync as visitors browse, so the address bar is always shareable. */ (function () { 'use strict'; const IMG = 'https://storage.googleapis.com/treatspace-prod-media/pracimg/u-2926/'; // Tile config (the only thing the data page doesn't carry). Key = category slug from the table id; // object order = tile order. A category missing here still renders, with a title-cased name and no image. const CATS = { face: { name: 'Face', img: IMG + 'Face.webp', desc: 'Lifts, eyelids, nose, ears, neck & scar revision' }, breast: { name: 'Breast', img: IMG + 'breast-2.webp', desc: 'Augmentation, lift, reduction & implant revision' }, body: { name: 'Body', img: IMG + 'Body.webp', desc: 'Liposuction, tummy tuck, mommy makeover & body lift' }, male: { name: 'Male', img: IMG + 'men.webp', desc: 'Procedures for men — body & face' } }; // ---- helpers ---- const slug = s => String(s).toLowerCase().replace(/&/g, ' ').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's'); const cellUrl = td => { const el = td.querySelector('img, a'); const u = (el ? el.getAttribute(el.tagName === 'IMG' ? 'src' : 'href') || '' : td.textContent).trim(); return /^(https?:\/)?\//.test(u) ? u : ''; }; const titleCase = s => s.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); // ---- parse the data page into [{ name, key, procs: [{ name, label, key, uid, url, cat, cases: [{ id, num, cat, proc, views: [[src, label]] }] }] }] function parse(html) { const doc = new DOMParser().parseFromString(html, 'text/html'); const cats = {}; let id = 0; doc.querySelectorAll('table[id^="ba-"]').forEach(t => { const m = t.id.match(/^ba-([a-z0-9-]+?)--([a-z0-9-]+)$/); if (!m || t.rows.length < 2) return; const [, ck, pk] = m; const cat = cats[ck] || (cats[ck] = { key: ck, name: (CATS[ck] && CATS[ck].name) || titleCase(ck), procs: [] }); const attr = a => (t.getAttribute(a) || '').trim(); const name = attr('aria-label') || titleCase(pk); const proc = { name, key: pk, uid: ck + '--' + pk, cat, cases: [], url: attr('data-target'), label: attr('data-label') || name // optional short tab label, e.g. "Body" for "Male Body" }; const [head, ...rows] = [...t.rows]; const labels = [...head.cells].map(c => c.textContent.trim()); rows.forEach(r => { const views = [...r.cells].map((c, i) => [cellUrl(c), labels[i] || 'View ' + (i + 1)]).filter(v => v[0]); if (views.length) proc.cases.push({ id: id++, num: proc.cases.length + 1, cat, proc, views }); }); if (proc.cases.length) cat.procs.push(proc); }); const order = Object.keys(CATS), rank = c => (order.indexOf(c.key) + 1 || 99); return Object.values(cats).filter(c => c.procs.length).sort((a, b) => rank(a) - rank(b)); } // ---- templates (same markup/classes as the static build) ---- const figure = (src, alt, grid) => `` + `` + 'Before' + 'After'; const tile = c => { const cfg = CATS[c.key] || {}, n = c.procs.reduce((s, p) => s + p.cases.length, 0); return ` ${cfg.img ? `` : ''} ${plural(n, 'case')} ${esc(c.name)} ${cfg.desc ? `${esc(cfg.desc)}` : ''} ${c.procs.map(p => `${esc(p.label)}`).join('')} `; }; const card = k => { const [src, view] = k.views[0]; return ` ${figure(src, `${k.proc.name} case ${k.num} — ${view}, before and after`, true)} Case ${k.num}${esc(view)} ${plural(k.views.length, 'view')} `; }; const grid = (p, j) => ` ${esc(p.label)} · ${plural(p.cases.length, 'patient')} Click any case to see every angle ${p.cases.map(card).join('')} `; const panel = c => ` ${c.procs.map((p, j) => `${esc(p.label)} (${p.cases.length})`).join('')} ${c.procs.map(grid).join('')} `; // ---- behaviour ---- function init($, cats) { if (!cats.length) throw new Error('No galleries found on the data page'); const byId = {}, byUid = {}; cats.forEach(c => c.procs.forEach(p => { byUid[p.uid] = p; p.cases.forEach(k => { byId[k.id] = k; }); })); $('#ba-tiles').html(cats.map(tile).join('')); $('#ba-panels').html(cats.map(panel).join('')); const $root = $('#ba-explorer'), $crumb = $('#ba-crumb'), $tileBtns = $('[data-ba-cat-btn]'); let live = false; // don't rewrite the URL until the initial selection is applied function sync(params) { if (!live || !window.history.replaceState) return; const q = new URLSearchParams(location.search); Object.keys(params).forEach(k => (params[k] == null ? q.delete(k) : q.set(k, params[k]))); const s = q.toString(); history.replaceState(history.state, '', location.pathname + (s ? '?' + s : '') + location.hash); } // ---- Category tiles -> panel ---- function showCat(cat, scroll) { $tileBtns.each(function () { const on = this.getAttribute('data-ba-cat-btn') === cat; this.setAttribute('aria-pressed', String(on)); $(this).closest('.ba-tile').toggleClass('is-active', on); }); $('[data-ba-cat-panel]').each(function () { $(this).toggleClass('d-none', this.getAttribute('data-ba-cat-panel') !== cat); }); $crumb.text($(`[data-ba-cat-btn="${cat}"] .h4`).text()); const p = byUid[$(`[data-ba-cat-panel="${cat}"] [data-ba-proc].active`).attr('data-ba-proc')]; sync({ category: cat, procedure: p ? p.key : null, case: null }); if (scroll) $('html, body').animate({ scrollTop: $('#ba-panel').offset().top - 16 }, 350); } function showProc(uid) { const $panel = $(`[data-ba-proc-grid="${uid}"]`).closest('[data-ba-cat-panel]'); $panel.find('[data-ba-proc]').each(function () { const on = this.getAttribute('data-ba-proc') === uid; $(this).toggleClass('active', on); this.setAttribute('aria-selected', String(on)); }); $panel.find('[data-ba-proc-grid]').each(function () { $(this).toggleClass('d-none', this.getAttribute('data-ba-proc-grid') !== uid); }); const p = byUid[uid]; sync({ category: p.cat.key, procedure: p.key, case: null }); } $root .on('click', '[data-ba-cat-btn]', function () { showCat(this.getAttribute('data-ba-cat-btn'), true); }) // keyboard focus ring on the whole card (BS4 has no :has() polyfill; toggle a class instead) .on('focus', '[data-ba-cat-btn]', function () { $(this).closest('.ba-tile').addClass('has-focus'); }) .on('blur', '[data-ba-cat-btn]', function () { $(this).closest('.ba-tile').removeClass('has-focus'); }) .on('click', '[data-ba-jump]', function () { const p = byUid[this.getAttribute('data-ba-jump')]; showCat(p.cat.key, true); showProc(p.uid); }) .on('click', '[data-ba-proc]', function () { showProc(this.getAttribute('data-ba-proc')); }); // ---- Modal (Filter Grid style) ---- const $modal = $('#baModal'), $inner = $('#baCarouselInner'), $pills = $('#baViewPills'), $carousel = $('#baCarousel'); let currentId = 0; // prev/next walk the procedure grid currently on screen const visibleOrder = () => $('.ba-cat-panel:not(.d-none) .ba-proc-grid:not(.d-none) [data-ba-open]').map(function () { return +this.getAttribute('data-ba-open'); }).get(); function renderCase(id) { const c = byId[id], v = c.views; currentId = id; $('#baModalCrumb').text(c.cat.name + ' · Case ' + c.num); $('#baModalTitle').text(c.proc.name); $inner.html(v.map((x, i) => `${figure(x[0], `${c.proc.name} case ${c.num} — ${x[1]}, before and after`)}`).join('')); $pills.html(v.map((x, i) => `${esc(x[1])}`).join('')); $('#baViewListLabel').text(plural(v.length, 'angle') + ', in order'); $('#baViewList').html(v.map((x, i) => `` + `#${i + 1}${esc(x[1])}${i ? '' : 'Showing'}`).join('')); $('#baCounter').text(v[0][1] + ' · 1 of ' + v.length); const url = c.proc.url; $('#baD-service').attr('href', url || '#').text('About ' + c.proc.name).toggleClass('d-none', !url); const order = visibleOrder(), idx = order.indexOf(c.id); $('#baPrev').prop('disabled', idx <= 0).off('click').on('click', () => renderCase(order[idx - 1])); $('#baNext').prop('disabled', idx === -1 || idx === order.length - 1).off('click').on('click', () => renderCase(order[idx + 1])); $carousel.carousel(0); sync({ category: c.cat.key, procedure: c.proc.key, case: c.num }); } $modal .on('show.bs.modal', e => { if (e.relatedTarget) renderCase(+e.relatedTarget.getAttribute('data-ba-open')); }) .on('hidden.bs.modal', () => sync({ case: null })); $carousel.on('slid.bs.carousel', e => { const c = byId[currentId]; $pills.find('.nav-link').each(function (i) { const on = i === e.to; $(this).toggleClass('active', on); this.setAttribute('aria-selected', String(on)); }); $('#baViewList .ba-view-item').each(function (i) { const on = i === e.to; $(this).toggleClass('is-current', on); this.setAttribute('aria-current', String(on)); $(this).find('.ba-view-now').text(on ? 'Showing' : ''); }); $('#baCounter').text(c.views[e.to][1] + ' · ' + (e.to + 1) + ' of ' + c.views.length); }); // ---- Initial selection from URL params ---- const q = new URLSearchParams(location.search); const want = k => slug(q.get(k) || ''); const caseNum = +((q.get('case') || '').match(/\d+/) || [0])[0]; let cat = cats.find(c => c.key === want('category')), proc = null; if (want('procedure')) { (cat ? [cat] : cats).some(c => (proc = c.procs.find(p => [p.key, p.uid, slug(p.label)].includes(want('procedure')))) && (cat = c)); } const deep = !!(cat || proc); cat = cat || cats[0]; showCat(cat.key, deep); if (proc) showProc(proc.uid); const kase = caseNum && (proc || cat.procs[0]).cases[caseNum - 1]; if (kase) { renderCase(kase.id); $modal.modal('show'); } live = true; } // ---- boot: start the fetch now, render once jQuery/Bootstrap (end of ) are ready ---- const root = document.getElementById('ba-explorer'); const domReady = new Promise(r => (document.readyState !== 'loading' ? r() : document.addEventListener('DOMContentLoaded', r))); Promise.all([ fetch(root.getAttribute('data-ba-src'), { credentials: 'same-origin' }).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }), domReady ]) .then(([html]) => init(window.jQuery, parse(html))) .catch(err => { console.error('[ba-gallery]', err); const s = document.getElementById('ba-status'); if (s) s.innerHTML = 'The gallery didn’t load.Refresh the page, or request an appointment to see results in person.'; }); })();