Smooth scroll, booking refactor, success summary

- Add scroll.js: data-scroll-to attribute drives smooth scrollIntoView;
  scroll-margin-top at md+ accounts for fixed header offset
- Wire Services, FAQ, Book An Appointment, View Our Services nav/hero
  links to on-page anchors; don't close hamburger on scroll-link clicks
- Refactor booking calendar: own the fetch (useEffect + dateRange state)
  instead of handing URL to FullCalendar; removes fetchedRef complexity;
  noSlotsInMonth derived cleanly from fetchLoading + fetchedEvents
- Success state shows appointment summary (name, service, date/time);
  hides calendar/form on success; no "book another" button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Philip Peterson 2026-06-03 22:14:39 -07:00
parent 9e1e6a57b7
commit 8962fc5f0e
10 changed files with 245 additions and 175 deletions

View file

@ -558,6 +558,14 @@ video {
html {
background-color: #86aab6;
scroll-behavior: smooth;
}
@media (min-width: 768px) {
[id] {
scroll-margin-top: 110px;
/* fixed header (78px) + breathing room */
}
}
/* Neutralise any theme container constraints */
@ -639,6 +647,10 @@ html {
pointer-events: none;
}
.visible{
visibility: visible;
}
.static{
position: static;
}
@ -689,6 +701,10 @@ html {
margin-bottom: 0.125rem;
}
.mb-1{
margin-bottom: 0.25rem;
}
.mb-10{
margin-bottom: 2.5rem;
}
@ -713,6 +729,10 @@ html {
margin-bottom: 1.25rem;
}
.mb-6{
margin-bottom: 1.5rem;
}
.mb-\[1vw\]{
margin-bottom: 1vw;
}
@ -733,28 +753,12 @@ html {
margin-top: 0.5rem;
}
.mt-\[2vw\]{
margin-top: 2vw;
}
.mb-1{
margin-bottom: 0.25rem;
}
.mb-6{
margin-bottom: 1.5rem;
}
.mt-8{
margin-top: 2rem;
}
.mt-1{
margin-top: 0.25rem;
}
.mt-3{
margin-top: 0.75rem;
.mt-\[2vw\]{
margin-top: 2vw;
}
.block{
@ -1073,6 +1077,11 @@ html {
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.border-green-200{
--tw-border-opacity: 1;
border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
}
.border-pt-blue-200{
--tw-border-opacity: 1;
border-color: rgb(184 212 220 / var(--tw-border-opacity, 1));
@ -1102,15 +1111,15 @@ html {
border-color: rgb(255 255 255 / 0.6);
}
.border-green-200{
--tw-border-opacity: 1;
border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
}
.bg-current{
background-color: currentColor;
}
.bg-green-50{
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
}
.bg-pt-blue-100{
--tw-bg-opacity: 1;
background-color: rgb(221 232 240 / var(--tw-bg-opacity, 1));
@ -1159,11 +1168,6 @@ html {
background-color: rgb(255 255 255 / 0.9);
}
.bg-green-50{
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
}
.bg-gradient-to-b{
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
}
@ -1227,6 +1231,15 @@ html {
padding: 1.5rem;
}
.p-8{
padding: 2rem;
}
.px-3{
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4{
padding-left: 1rem;
padding-right: 1rem;
@ -1282,11 +1295,6 @@ html {
padding-bottom: 1em;
}
.px-3{
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.pb-1{
padding-bottom: 0.25rem;
}
@ -1303,20 +1311,20 @@ html {
padding-top: 0px;
}
.pt-4{
padding-top: 1rem;
.pt-20{
padding-top: 5rem;
}
.pt-px{
padding-top: 1px;
.pt-4{
padding-top: 1rem;
}
.pt-8{
padding-top: 2rem;
}
.pt-20{
padding-top: 5rem;
.pt-px{
padding-top: 1px;
}
.text-left{
@ -1495,11 +1503,26 @@ html {
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.text-green-700{
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
}
.text-green-800{
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
}
.text-pt-blue-500{
--tw-text-opacity: 1;
color: rgb(48 111 142 / var(--tw-text-opacity, 1));
}
.text-red-500{
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-white{
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@ -1513,25 +1536,6 @@ html {
color: rgb(255 255 255 / 0.8);
}
.text-red-500{
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
}
.text-green-700{
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
}
.text-green-800{
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
}
.underline{
text-decoration-line: underline;
}
.no-underline{
text-decoration-line: none;
}
@ -1637,11 +1641,6 @@ html {
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.hover\:text-green-800:hover{
--tw-text-opacity: 1;
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
}
.focus\:border-pt-blue-500:focus{
--tw-border-opacity: 1;
border-color: rgb(48 111 142 / var(--tw-border-opacity, 1));

View file

@ -7,6 +7,12 @@
@layer base {
html {
background-color: theme('colors.pt-blue.400');
scroll-behavior: smooth;
}
@media (min-width: theme('screens.sm')) {
[id] {
scroll-margin-top: 110px; /* fixed header (78px) + breathing room */
}
}
/* Neutralise any theme container constraints */
.page-wrapper {

View file

@ -18,7 +18,6 @@ const EMPTY_FORM = { firstName: "", lastName: "", phone: "", comments: "" };
function formatPhone(raw) {
let d = String(raw || "").replace(/\D/g, "");
if (d.length === 11 && d[0] === "1") {
// NANP with leading 1: show "1 (xxx) xxx-xxxx"
const rest = d.slice(1);
return "1 (" + rest.slice(0, 3) + ") " + rest.slice(3, 6) + "-" + rest.slice(6);
}
@ -60,19 +59,21 @@ const CX = {
submitBtn: "px-[4em] py-[1em] bg-pt-blue-500 text-white text-sm font-medium transition-colors border-2 border-pt-blue-500 hover:bg-pt-blue-600 hover:border-pt-blue-600 disabled:opacity-50",
// ── Calendar overlay ──────────────────────────────────────────────────
calWrapper: "relative",
noSlotsOverlay: "absolute inset-0 z-10 flex items-center justify-center pt-20 pointer-events-none",
calWrapper: "relative",
noSlotsOverlay: "absolute inset-0 z-10 flex items-center justify-center pt-20 pointer-events-none",
// ── Success state ──────────────────────────────────────────────────────
successSection: "mt-8 pt-8 border-t border-pt-blue-200",
successBox: "p-6 bg-green-50 border border-green-200 text-green-800",
successTitle: "font-medium",
successBody: "text-sm mt-1",
successLink: "mt-3 text-sm text-green-700 underline hover:text-green-800",
successBox: "p-8 bg-green-50 border border-green-200 text-green-800",
successTitle: "text-2xl font-semibold mb-4",
successSummary: "flex flex-col gap-1 text-base mb-4",
successNote: "text-sm text-green-700",
};
// Module-level vars accessible from FullCalendar callbacks without stale closures.
var selectedDate = null;
var selectedDateSlots = [];
var currentEvents = [];
function localDateStr(d) {
return d.getFullYear() + "-" +
@ -87,47 +88,48 @@ function nextBusinessDay() {
return localDateStr(d);
}
function slotLabel(date) {
var h = date.getHours();
function slotLabel(startStr) {
var d = new Date(startStr);
var h = d.getHours();
return (h % 12 || 12) + (h < 12 ? "AM" : "PM") + " PST";
}
function formatAppointmentDate(startStr) {
var parts = startStr.split("T")[0].split("-");
var d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
var date = d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
return date + " at " + slotLabel(startStr);
}
function Booking({ settings }) {
const [service, setService] = useState("diagnostic");
const [dateRange, setDateRange] = useState(null); // "startStr/endStr", set by datesSet
const [fetchedEvents, setFetchedEvents] = useState(null); // null = not yet fetched
const [fetchLoading, setFetchLoading] = useState(false);
const [slots, setSlots] = useState([]);
const [selectedSlotId, setSelectedSlotId] = useState(null);
const [formData, setFormData] = useState(EMPTY_FORM);
const [submitting, setSubmitting] = useState(false);
const [submitError, setSubmitError] = useState(null);
const [success, setSuccess] = useState(false);
const [confirmedAppointment, setConfirmedAppointment] = useState(null);
const [noSlotsInMonth, setNoSlotsInMonth] = useState(false);
const calEl = useRef(null);
const calRef = useRef(null);
const initializedRef = useRef(false);
const prevServiceRef = useRef(null);
const autoAdvanceRef = useRef(0);
const fetchedRef = useRef(false);
const isFirstServiceRender = useRef(true);
const initDate = useMemo(nextBusinessDay, []);
function buildEventsUrl(svc) {
return settings.eventsUrl + "?service=" + svc;
}
// ── Initialize FullCalendar once ───────────────────────────────────────
useEffect(function () {
if (!calEl.current || !window.FullCalendar) return;
function markDays(events) {
calEl.current.querySelectorAll(".fc-daygrid-day.has-availability").forEach(function (d) {
d.classList.remove("has-availability");
});
events.forEach(function (event) {
var dateStr = event.startStr.substring(0, 10);
var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + dateStr + "\"]");
if (dayEl) dayEl.classList.add("has-availability");
});
}
var cal = new FullCalendar.Calendar(calEl.current, {
initialView: "dayGridMonth",
initialDate: initDate,
@ -146,17 +148,13 @@ function Booking({ settings }) {
eventDisplay: "none",
dayMaxEvents: false,
loading: function (isLoading) {
if (!isLoading) fetchedRef.current = true;
},
datesSet: function () {
// Visible range changed. Sets dateRange state → triggers fetch effect.
datesSet: function (info) {
calEl.current.querySelectorAll(".fc-daygrid-day.is-selected").forEach(function (d) {
d.classList.remove("is-selected");
});
setSelectedSlotId(null);
setNoSlotsInMonth(false);
fetchedRef.current = false;
if (selectedDate) {
var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + selectedDate + "\"]");
if (dayEl) {
@ -168,38 +166,20 @@ function Booking({ settings }) {
} else {
setSlots([]);
}
setDateRange(info.startStr + "/" + info.endStr);
},
// Mark which day cells have availability whenever FullCalendar's
// event list changes (fires after addEventSource / removeAllEventSources).
eventsSet: function (events) {
markDays(events);
if (!initializedRef.current && fetchedRef.current) {
fetchedRef.current = false;
var dates = [...new Set(events.map(function (e) { return e.startStr.substring(0, 10); }))].sort();
var firstDate = dates[0];
if (firstDate) {
initializedRef.current = true;
autoAdvanceRef.current = 0;
var firstSlots = events
.filter(function (e) { return e.startStr.startsWith(firstDate); })
.sort(function (a, b) { return a.start - b.start; });
selectedDate = firstDate;
selectedDateSlots = firstSlots;
var targetEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + firstDate + "\"]");
if (targetEl) {
targetEl.classList.add("is-selected");
setSlots(firstSlots);
}
} else if (autoAdvanceRef.current < 12) {
autoAdvanceRef.current++;
cal.next();
}
}
if (initializedRef.current && fetchedRef.current) {
var viewStart = cal.view.currentStart;
var viewEnd = cal.view.currentEnd;
var inView = events.filter(function (e) { return e.start >= viewStart && e.start < viewEnd; });
setNoSlotsInMonth(inView.length === 0);
}
calEl.current.querySelectorAll(".fc-daygrid-day.has-availability").forEach(function (d) {
d.classList.remove("has-availability");
});
events.forEach(function (event) {
var dateStr = event.startStr.substring(0, 10);
var dayEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + dateStr + "\"]");
if (dayEl) dayEl.classList.add("has-availability");
});
},
dayCellClassNames: function (arg) {
@ -213,9 +193,9 @@ function Booking({ settings }) {
d.classList.remove("is-selected");
});
arg.dayEl.classList.add("is-selected");
var daySlots = cal.getEvents()
.filter(function (e) { return e.startStr.startsWith(arg.dateStr); })
.sort(function (a, b) { return a.start - b.start; });
var daySlots = currentEvents
.filter(function (e) { return e.start.startsWith(arg.dateStr); })
.sort(function (a, b) { return a.start < b.start ? -1 : 1; });
selectedDate = arg.dateStr;
selectedDateSlots = daySlots;
setSelectedSlotId(null);
@ -230,29 +210,87 @@ function Booking({ settings }) {
return function () { cal.destroy(); };
}, []);
// ── Fetch events when service or visible range changes ─────────────────
useEffect(function () {
if (!dateRange) return;
var parts = dateRange.split("/");
var url = buildEventsUrl(service) + "&start=" + parts[0] + "&end=" + parts[1];
setFetchLoading(true);
setFetchedEvents(null);
fetch(url)
.then(function (r) { return r.json(); })
.then(function (data) {
currentEvents = data;
setFetchedEvents(data);
setFetchLoading(false);
})
.catch(function () {
currentEvents = [];
setFetchedEvents([]);
setFetchLoading(false);
});
}, [service, dateRange]);
// ── Push fetched events into FullCalendar; auto-advance or auto-select ─
useEffect(function () {
if (fetchedEvents === null) return;
var cal = calRef.current;
if (!cal) return;
cal.removeAllEventSources();
if (fetchedEvents.length > 0) {
cal.addEventSource(fetchedEvents); // fires eventsSet → marks availability
if (!initializedRef.current) {
var dates = [...new Set(fetchedEvents.map(function (e) { return e.start.substring(0, 10); }))].sort();
var firstDate = dates[0];
if (firstDate) {
initializedRef.current = true;
autoAdvanceRef.current = 0;
var firstSlots = fetchedEvents
.filter(function (e) { return e.start.startsWith(firstDate); })
.sort(function (a, b) { return a.start < b.start ? -1 : 1; });
selectedDate = firstDate;
selectedDateSlots = firstSlots;
var targetEl = calEl.current.querySelector(".fc-daygrid-day[data-date=\"" + firstDate + "\"]");
if (targetEl) {
targetEl.classList.add("is-selected");
setSlots(firstSlots);
}
}
}
} else if (!initializedRef.current && autoAdvanceRef.current < 12) {
autoAdvanceRef.current++;
cal.next(); // fires datesSet → setDateRange → fetch effect re-runs
} else if (initializedRef.current) {
setNoSlotsInMonth(true);
}
}, [fetchedEvents]);
// ── Reset and navigate when service changes ────────────────────────────
useEffect(function () {
var cal = calRef.current;
if (!cal) return;
var isInitial = prevServiceRef.current === null;
prevServiceRef.current = service;
if (!isInitial) {
initializedRef.current = false;
autoAdvanceRef.current = 0;
fetchedRef.current = false;
selectedDate = null;
selectedDateSlots = [];
setSlots([]);
setSelectedSlotId(null);
setFormData(EMPTY_FORM);
setSubmitError(null);
setSuccess(false);
setNoSlotsInMonth(false);
cal.gotoDate(initDate);
if (isFirstServiceRender.current) {
isFirstServiceRender.current = false;
return;
}
cal.removeAllEventSources();
cal.addEventSource(buildEventsUrl(service));
selectedDate = null;
selectedDateSlots = [];
currentEvents = [];
initializedRef.current = false;
autoAdvanceRef.current = 0;
setSlots([]);
setSelectedSlotId(null);
setFormData(EMPTY_FORM);
setSubmitError(null);
setSuccess(false);
setNoSlotsInMonth(false);
setFetchedEvents(null);
cal.gotoDate(initDate); // fires datesSet → setDateRange → fetch effect
}, [service]);
function handleSlotClick(slot) {
@ -275,8 +313,8 @@ function Booking({ settings }) {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
start: slot.startStr,
end: slot.endStr,
start: slot.start,
end: slot.end,
service: service,
firstName: formData.firstName,
lastName: formData.lastName,
@ -287,16 +325,20 @@ function Booking({ settings }) {
if (res.ok) {
setSubmitting(false);
setSubmitError(null);
setConfirmedAppointment({
start: slot.start,
service: service,
firstName: formData.firstName,
lastName: formData.lastName,
});
setSuccess(true);
setSelectedSlotId(null);
setFormData(EMPTY_FORM);
} else {
setSubmitting(false);
if (res.status === 422) {
setSubmitError("That slot was just booked. Please choose another time.");
} else {
setSubmitError("Something went wrong. Please try again.");
}
setSubmitError(res.status === 422
? "That slot was just booked. Please choose another time."
: "Something went wrong. Please try again.");
}
}).catch(function () {
setSubmitting(false);
@ -308,6 +350,7 @@ function Booking({ settings }) {
return html`
<div>
${!success ? html`
<p class=${CX.selectorLabel}>Select Appointment Type</p>
<div class=${CX.selectorGrid}>
${TYPES.map(function (t) {
@ -364,29 +407,7 @@ function Booking({ settings }) {
` : null}
</div>
${success ? html`
<div class=${CX.successSection}>
<div class=${CX.successBox}>
<p class=${CX.successTitle}>Request received!</p>
<p class=${CX.successBody}>Thank you. We'll contact you shortly to confirm your appointment.</p>
<button
type="button"
onClick=${function () {
setSuccess(false);
setFormData(EMPTY_FORM);
if (calEl.current) {
calEl.current.querySelectorAll(".fc-daygrid-day.is-selected").forEach(function (d) {
d.classList.remove("is-selected");
});
}
}}
class=${CX.successLink}
>Book another appointment</button>
</div>
</div>
` : null}
${selectedSlot ? html`
${!success && selectedSlot ? html`
<form onSubmit=${handleSubmit} autocomplete="on" class=${CX.formSection}>
<p class=${CX.formHeading}>Your Details</p>
@ -456,6 +477,21 @@ function Booking({ settings }) {
</button>
</form>
` : null}
` : null}
${success && confirmedAppointment ? html`
<div class=${CX.successSection}>
<div class=${CX.successBox}>
<p class=${CX.successTitle}>Request received!</p>
<div class=${CX.successSummary}>
<p>${confirmedAppointment.firstName} ${confirmedAppointment.lastName}</p>
<p>${TYPES.find(function (t) { return t.id === confirmedAppointment.service; }).label}</p>
<p>${formatAppointmentDate(confirmedAppointment.start)}</p>
</div>
<p class=${CX.successNote}>We'll contact you shortly to confirm your appointment.</p>
</div>
</div>
` : null}
</div>
`;
}

View file

@ -13,6 +13,7 @@
nav.querySelectorAll('a').forEach(function (link) {
link.addEventListener('click', function () {
if (link.dataset.scrollTo) return; // page scrolls away — no need to close
nav.classList.remove('is-open');
btn.setAttribute('aria-expanded', 'false');
});

View file

@ -0,0 +1,13 @@
// Add data-scroll-to="#target-id" to any link to smooth-scroll instead of navigate.
(function () {
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('click', function (e) {
var link = e.target.closest('[data-scroll-to]');
if (!link) return;
var target = document.querySelector(link.dataset.scrollTo);
if (!target) return;
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth' });
});
});
})();

View file

@ -4,6 +4,7 @@ app:
css/app.css: {}
js:
js/nav.js: {}
js/scroll.js: {}
js/phone-format.js: {}
js/components/rpt-toggle.js: { attributes: { type: module } }
js/components/rpt-carousel.js: { attributes: { type: module } }

View file

@ -89,6 +89,10 @@ function riverside_pt_mail(string $key, array &$message, array $params): void {
'Slot: ' . $start->format('l, F j, Y') . ', ' . $start->format('g:i A') . '' . $end->format('g:i A'),
];
if (!empty($params['email'])) {
array_splice($lines, 1, 0, 'Email: ' . $params['email']);
}
if (!empty($params['comments'])) {
$lines[] = 'Comments: ' . $params['comments'];
}

View file

@ -42,6 +42,7 @@ class ScheduleController extends ControllerBase {
$firstName = trim($data['firstName'] ?? $data['first_name'] ?? '');
$lastName = trim($data['lastName'] ?? $data['last_name'] ?? '');
$email = trim($data['email'] ?? '');
$phone = trim($data['phone'] ?? '');
$comments = $data['comments'] ?? '';
$service = $data['service'] ?? 'diagnostic';
@ -51,7 +52,7 @@ class ScheduleController extends ControllerBase {
// Full contact info present (new embedded booking flow on homepage):
// validate, send the request email immediately, and return success.
// This replaces the previous /schedule/book form page.
if ($firstName && $lastName && $phone) {
if ($firstName && $lastName && $email && $phone) {
// Prevent double-booking against existing appointment nodes (same logic as before).
$conflict = \Drupal::entityQuery('node')
->condition('type', 'appointment')
@ -71,6 +72,7 @@ class ScheduleController extends ControllerBase {
$sent = $this->mailManager->mail('riverside_pt', 'booking_request', $to, $lang, [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'phone' => $phone,
'comments' => $comments,
'start' => $start,
@ -91,7 +93,9 @@ class ScheduleController extends ControllerBase {
'start' => $start,
'end' => $end,
'service' => $service,
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'phone' => $phone,
'comments' => $comments,
'provider_id' => $providerId,

View file

@ -20,7 +20,10 @@
text-[blue-900] font-medium
{% endif %}
"
href="{{ item.url }}">{{ item.title }}</a>
href="{{ item.url }}"
{% if item.title == 'Services' %}data-scroll-to="#pt-services"{% endif %}
{% if item.title == 'FAQ' %}data-scroll-to="#pt-faq"{% endif %}
>{{ item.title }}</a>
</li>
{% endif %}
{% endfor %}
@ -39,7 +42,9 @@
href="{{ item.url }}">{{ item.title }}</a>
{% else %}
<a class="inline-block px-5 py-2 rounded-none bg-pt-blue-500 text-white text-sm font-medium no-underline rounded border border-pt-navy whitespace-nowrap transition-colors hover:bg-pt-blue-600 hover:border-pt-blue-600 hover:text-white"
href="{{ item.url }}">{{ item.title }}</a>
href="{{ item.url }}"
{% if item.title == 'Book An Appointment' %}data-scroll-to="#book-an-appointment"{% endif %}
>{{ item.title }}</a>
{% endif %}
{% endif %}
{% endfor %}

View file

@ -35,6 +35,7 @@
>Book An Appointment</a>
<a
href="/services"
data-scroll-to="#pt-services"
class="hidden sm:inline-block text-[clamp(0.25rem,1vw,1.25vw)] px-[4em] py-[1em] bg-pt-blue-400 text-white font-medium no-underline transition-colors border-2 border-white hover:bg-pt-sage-500"
>View Our Services</a>
</div>
@ -45,7 +46,7 @@
</section>
</div>
<section class="py-16 px-6 bg-white">
<section id="pt-services" class="py-16 px-6 bg-white">
<div class="max-w-[1040px] mx-auto mb-12">
<p class="text-sm tracking-widest uppercase text-pt-blue-500 font-semibold text-center mb-2">Bringing Relief</p>
<h2 class="text-[2.25rem] font-serif font-normal text-gray-900 leading-tight text-center">Our Wide Range of Physical Therapy Services</h2>
@ -122,6 +123,6 @@
</div>
</section>
<rpt-faq class="block"></rpt-faq>
<rpt-faq id="pt-faq" class="block"></rpt-faq>
<div class="bg-pt-blue-400 h-[240px]"></div>