diff --git a/.env.example b/.env.example index 46e4495..cac390b 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ APP_NAME="Danks & Strydom Physiotherapy" APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=https://danksandstrydom.co.za +APP_URL=http://localhost DEPLOY_TOKEN= @@ -63,11 +63,22 @@ AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" -# Contact form notification recipient(s). Comma-separate for multiple. -CONTACT_MAIL_TO=info@danksandstrydom.co.za - -# Public-facing practice details (shown on the site). -CONTACT_PHONE="+27 00 000 0000" -CONTACT_EMAIL="hello@danksandstrydom.co.za" -CONTACT_ADDRESS="123 Wellness Avenue, Suite 4, Cape Town, 8001" -CONTACT_MAP_EMBED_URL="'https://www.google.com/maps/embed?pb=!3m2!1sen!2sza!4v1781261744374!5m2!1sen!2sza!6m8!1m7!1sxjKimELv0brVyDOlsBgq_g!2m2!1d-26.07852308781387!2d28.25012390275597!3f175.0676123469271!4f-11.99162658894005!5f0.7820865974627469' width='600' height='450' style='border:0;' allowfullscreen='' loading='lazy' referrerpolicy='no-referrer-when-downgrade' +# Contact details supplied by the practice; verify entrance/map before release. No sample recipient or map. +CONTACT_MAIL_TO="admin@danksandstrydom.co.za" +CONTACT_PHONE="011 391 3126" +CONTACT_EMAIL="admin@danksandstrydom.co.za" +CONTACT_ADDRESS="Suite 102, Surgiklin Studios, Unit 12, Glen Eagle Office Park, Koorsboom Avenue, Glen Marais, Kempton Park, 1619, Gauteng, South Africa" +CONTACT_LOCATION_VERIFIED=true +CONTACT_STREET="Suite 102, Surgiklin Studios, Unit 12, Glen Eagle Office Park, Koorsboom Avenue, Glen Marais" +CONTACT_LOCALITY="Kempton Park" +CONTACT_REGION="Gauteng" +CONTACT_POSTCODE="1619" +CONTACT_HOURS="Monday–Friday: 07:30–17:30. Saturday: By appointment. Sunday and public holidays: Closed." +CONTACT_MAP_EMBED_URL="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3583.7425911415435!2d28.2610527!3d-26.074667599999998!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x1e9515b5389c59e3%3A0xc9913cffe2473c26!2sDanks%20And%20Strydom!5e0!3m2!1sen!2sza!4v1789387905460!5m2!1sen!2sza" +CONTACT_DIRECTIONS_URL="https://maps.app.goo.gl/WoxWSjSzNbtEkTr66" + +# Enable on production only after release review; APP_URL is this environment's origin. +SITE_INDEXABLE=false +SITE_CANONICAL_REDIRECTS=false +# Opt-in event adapter; no analytics provider is loaded automatically. +SITE_ANALYTICS_ENABLED=false diff --git a/CONTENT-TODO.md b/CONTENT-TODO.md new file mode 100644 index 0000000..a39e419 --- /dev/null +++ b/CONTENT-TODO.md @@ -0,0 +1,85 @@ +# Optional content to complete + +All eight service pages, About and Patient Information are enabled for public release. Contact's booking information is public content. The user waived Cheryl's approval step; none of the optional items below blocks publication. This branch has not been merged or deployed. + +Unknown values are `null` or empty arrays in `config/site.php`. Keep them empty until real information is supplied. Every environment omits incomplete fields without website notices. Optional profile fields are not used in metadata or structured data. Photographs require both an approved asset path (relative to `public/`) and descriptive alt text. Do not use stock people as practitioner portraits. + +## Testimonials — three quotations confirmed and approved + +The user has now explicitly confirmed that the supplied screenshot quotations are genuine and that the practice has permission to publish their quotes and display names. The top-row quotations from Sarah M., Priya N. and Lerato K. are recorded as approved, in that order. This records the user’s confirmation, not independent source verification. Public source URLs were not supplied, so the cards have no source links or ratings. + +Edit **`config/site.php`**, under **`site.testimonials.entries`**, to add genuine testimonials. The separate **`site.testimonials.samples`** list contains three neutral design examples and is restricted to local/staging; changing a sample's approval flag cannot publish it. When no approved entries exist, production omits the whole section, while local/staging show labelled samples. Once approved entries exist, those replace the sample set. + +| Done | Field | What to supply / where it appears | +|---|---|---| +| [x] | `site.testimonials.entries..quote` | Exact genuine quotation with permission for website use; homepage card below the treatment images. | +| [x] | `site.testimonials.entries..display_name` | The reviewer's permitted public display name; card attribution. Do not infer an identity. | +| [ ] | `site.testimonials.entries..source_url` | Optional original review's HTTP(S) URL; use `null` when unavailable. Only valid supplied links are displayed. A link does not establish permission by itself. | +| [x] | `site.testimonials.entries..approved` | Set the boolean `true` only after verifying provenance and permission for that exact quote and display name. Unapproved or incomplete entries never display. | + +Entry structure (fill the empty values with genuine content first): + +```php +'entries' => [ + ['quote' => null, 'display_name' => null, 'source_url' => null, 'approved' => false], +], +``` + +Keep permission evidence privately with the practice; do not commit consent documents or private patient details. Do not copy neutral sample text into the production entries. No ratings, Review schema or aggregate ratings are supported. These publication controls are specific to testimonials; they do not reinstate page approval or password access. Rebuild the Laravel configuration cache through the normal release process after content changes. + +## Practitioner information + +All keys below are in **`config/site.php`**. `site.pages.about.practitioners.0` is **Cheryl Myburgh**; `site.pages.about.practitioners.1` is **Elize Strydom**. Keep this order when editing the numeric entries. + +Edit the visible draft biographies directly in this file: + +- **Cheryl:** `site.pages.about.practitioners.0.biography` — About, below her name/title. Uses only her physiotherapy degree and extensive human/equine experience. Equine experience is biographical, not an offering at this practice. +- **Elize:** `site.pages.about.practitioners.1.biography` — About, below her name/title. Uses only her physiotherapy degree and human physiotherapy experience. + +These are editable drafts based on confirmed facts, not missing biography notices. Additional detail is optional. Keep unknown values empty; no registration numbers, years, employment history or specialist credentials have been supplied. After editing configuration on a deployed site, the operator must rebuild Laravel's configuration cache through the normal release process. + +| Done | Person / missing item | Configuration key | What to supply / where it appears | +|---|---|---|---| +| [ ] | Cheryl: exact qualifications | `site.pages.about.practitioners.0.qualifications` | List of exact degree/qualification titles, without inferred specialist credentials; About profile below the short biography. | +| [ ] | Cheryl: universities | `site.pages.about.practitioners.0.universities` | List of awarding universities; About profile. | +| [ ] | Cheryl: longer biography | `site.pages.about.practitioners.0.expanded_biography` | Optional factual expansion of the existing short biography; About profile. Do not invent years, employment history or clinical interests. | +| [ ] | Cheryl: languages | `site.pages.about.practitioners.0.languages` | List of languages offered in consultations; About profile. | +| [ ] | Cheryl: approved photograph | `site.pages.about.practitioners.0.photo.path` and `.photo.alt` | Approved image asset and accurate alternative text; About profile. Both must be filled before an image appears. | +| [ ] | Elize: exact qualifications | `site.pages.about.practitioners.1.qualifications` | List of exact degree/qualification titles; About profile below the short biography. | +| [ ] | Elize: universities | `site.pages.about.practitioners.1.universities` | List of awarding universities; About profile. | +| [ ] | Elize: longer biography | `site.pages.about.practitioners.1.expanded_biography` | Optional factual expansion; About profile. Existing short biography is already recorded. | +| [ ] | Elize: languages | `site.pages.about.practitioners.1.languages` | List of languages offered in consultations; About profile. | +| [ ] | Elize: approved photograph | `site.pages.about.practitioners.1.photo.path` and `.photo.alt` | Approved image asset and accurate alternative text; About profile. Both must be filled before an image appears. | + +## Other optional practice information + +All keys below are also in **`config/site.php`**. Populating a `body` adds its labelled section to Patient Information. Keep the existing confirmed sections consistent if adding more detail. + +| Done | Missing item | Configuration key | What to supply / where it appears | +|---|---|---|---| +| [ ] | Consultation fee amounts | `site.pages.patient-information.optional_sections.consultation_fees.body` | Current amounts and currency, only if you want prices displayed. Patient Information: Current consultation fees. The existing “contact the practice” wording is usable without prices. | +| [ ] | Payment timing | `site.pages.patient-information.optional_sections.payment_timing.body` | When payment is due. Patient Information: When payment is due. Cash/card acceptance is already confirmed. | +| [ ] | Medical-aid submission process | `site.pages.patient-information.optional_sections.medical_aid_claims.body` | Whether the practice submits claims, whether patients pay first, and the actual process; no assumptions about scheme coverage. Patient Information: Submitting medical-aid claims. | +| [ ] | Declined claims | `site.pages.patient-information.optional_sections.declined_claims.body` | Who is responsible and what happens if a claim is declined. Patient Information: Declined medical-aid claims. | +| [ ] | Separate missed-appointment policy | `site.pages.patient-information.optional_sections.missed_appointments.body` | A no-show policy, if there is one. Patient Information: Missed appointments. Do not infer this from the confirmed cancellation policy. | + +## Confirmed information already recorded + +Do not treat these as missing or replace them with placeholders: + +- **Names, professional title and short biographies:** `config/site.php`, `site.pages.about.practitioners.{0,1}.name`, `.title`, `.biography`. Both hold physiotherapy degrees; exact degree titles/universities remain unknown. Cheryl's supplied human/equine experience is biographical and does not create an equine service offering at these rooms. +- **Eight service offerings and patient-facing descriptions:** `config/site.php`, `site.pages.`. All eight are now published in configuration. +- **One-hour appointments, patient-information form, assessment-first approach, nothing to bring, booking/confirmation, referrals and follow-up:** `config/site.php`, `site.pages.patient-information.sections` and `site.pages.contact.sections`. +- **Cash/card acceptance and cancellation wording:** `site.pages.patient-information.sections`. At least 24 hours' notice; with less notice the patient may be liable for the full appointment fee. Do not turn “may” into an automatic charge. +- **Address, arrival, parking/access, hours, telephone, public email and verified map/directions:** `config/contact.php`, `contact.practice.*`. Suite 102, Surgiklin Studios, Unit 12, Glen Eagle Office Park, Koorsboom Avenue, Glen Marais, Kempton Park, 1619; phone 011 391 3126; email admin@danksandstrydom.co.za. +- **Intended enquiry inbox:** admin@danksandstrydom.co.za, recorded in `.env.example`. The actual recipient list is read by `config/contact.php`, `contact.recipients`, from the server's `CONTACT_MAIL_TO`; verifying it is an operational task, not an unknown business fact. + +## External release tasks, not missing website content + +- Valourite must verify existing production environment overrides, mail delivery configuration, recipients, location fields and effective cached configuration. Production preflight must pass before deployment; safe indexing defaults remain false. +- Merge and deployment still require separate authorisation. Publishing configuration here does not change the live site. +- Establish Google Business Profile/Search Console ownership and access through Valourite, reconcile listing details and submit the indexable sitemap after release. No Google-account changes have been made. + +## Image editing + +Illustrative image inventory, alt text and responsive paths: `config/imagery.php`, `imagery.images.`. Internal-page selection: `imagery.pages` keyed by URL. Homepage selections: `resources/views/sections/{hero,services,about,benefits,visit}.blade.php`. See `IMAGE-ASSETS.md` for the full generated-asset map and prompt record. These files must not be used for Cheryl/Elize portrait fields; approved real practitioner photographs remain outstanding as listed above. diff --git a/IMAGE-ASSETS.md b/IMAGE-ASSETS.md new file mode 100644 index 0000000..a750507 --- /dev/null +++ b/IMAGE-ASSETS.md @@ -0,0 +1,79 @@ +# Website image collection + +Generated on 18 September 2026 with the built-in OpenAI image-generation tool (`image_gen.imagegen`). These fictional scenes illustrate physiotherapy; they are not photographs of Cheryl, Elize, actual patients or the practice. They do not confirm particular equipment or techniques at the practice. Practitioner portrait fields in `config/site.php` remain separate and unchanged. + +## Audit and selection + +Before this change, `back_strapping.webp` appeared in the homepage hero, image strip, back/neck hero and every page's social metadata. `knee_strapping.webp` appeared in the first-visit panel, strip, sports and post-operative heroes. `valf_physio.webp` appeared in the About arch, questions card, strip and the remaining nine internal-page heroes. The same three files therefore covered seven homepage slots and twelve internal pages. Dormant human/equine gallery files were not displayed. + +The original images remain in Git/public/images for review, but are no longer used by the public layouts. Close-up taping/manual-treatment imagery was replaced with a coherent assessment/exercise collection matching the requested style and avoiding assumptions about specific practice techniques. No stock downloads, portraits or brand assets were substituted. + +## Final files and usage + +All variants are under `public/images/illustrations/`. Each scene has `-480.webp` (480×360), `-768.webp` (768×576) and `-1200.webp` (1200×900). The filenames below are the large/social variants; smaller variants use the same stem. + +| File | Placement | Selected generated source | +| --- | --- | --- | +| `consultation-1200.webp` | Homepage hero; Contact; Chronic Pain Management | `exec-2030c678-a6d9-4cbf-86ed-232686d549ec.png` | +| `neck-1200.webp` | Homepage first-visit panel; Back & Neck Pain; Patient Information | `exec-ce68b720-5c03-4910-9be6-5fd30464608f.png` | +| `sports-1200.webp` | Homepage image strip; Sports Injury Rehabilitation; Injury Prevention | `exec-63a87707-c7ad-4747-ac88-6a5d4eb68624.png` | +| `knee-1200.webp` | Homepage image strip; Post-Operative Rehabilitation | `exec-4183676f-2e6a-4e3b-815f-aea7fd487d3a.png` | +| `shoulder-1200.webp` | Homepage About illustration; About; Joint & Muscle Pain | `exec-d1809fd2-7b35-42ef-84c6-fd0963b32bf2.png` | +| `mobility-1200.webp` | Homepage image strip; Services hub; Mobility & Movement Assessment | `exec-ff63dd98-073f-498b-b72a-ece22e8310df.png` | +| `exercise-1200.webp` | Homepage questions card; Rehabilitation Exercise Programmes | `exec-3dfd177b-c8ad-4c98-ad7b-95dd36b4b7fb.png` | + +## Delivery and editing + +- Edit asset paths, alt text, dimensions and width variants in `config/imagery.php` (`imagery.images.`); page selection is `imagery.pages`, keyed by existing URL. Homepage selections live in the existing section templates through ``. +- The shared image component emits native `srcset`, `sizes`, intrinsic dimensions and lazy loading. Only the main hero on each page is eager/high priority. The homepage responsive preload matches its hero exactly. Below-fold images retain their existing containers/reveal effects; the three-image strip remains hidden on mobile. +- WebP quality 78, method 6, Lanczos resizing. Original generations are 1448×1086, also 4:3. No content was retouched in the optimisation step. The 21 variants total 727,736 bytes; each large file is 43–66 KB. All seven 480px variants total 111,106 bytes, compared with 224,826 bytes for the previous three source files. Higher-density screens can request larger variants; this is an asset-size comparison, not a measured whole-page performance score. +- Social metadata uses each page's selected 1200×900 image and accurate illustrative alt text. Removed the generated photo from `MedicalClinic.image` to avoid identifying fictional people/premises as the real clinic; the clinic identity, contact/address data and other SEO controls remain intact. +- Captions and a short footer disclosure identify the images as illustrations. Do not populate practitioner portraits from this collection. +- Existing deployment recursively copies `public/images`, including this directory. The deployment test now verifies nested assets survive rejected preflight and are copied after accepted preflight. No deployment was run. + +## Generation prompt record + +Each initial image was generated separately. The consultation prompt was: + +> Use case: photorealistic-natural. Create one editorial photograph for a physiotherapy website, landscape 4:3. A welcoming consultation: a Black South African adult woman patient in modest casual clothing and a female physiotherapist in plain muted blue scrubs sit facing each other at a slight angle, calmly listening, relaxed hands resting separately on their own laps. Natural daylight, warm off-white walls, light oak, a simple uncluttered neutral clinical interior with subtle blue accents. Waist-up with both faces near the central area, ample headroom and space at the sides so an arched container can crop edges safely. Candid professional photography, realistic skin texture and proportions, gentle warm colour grading, no dramatic expressions. Fictional illustrative people and setting, not actual practitioners or a real clinic. No text, logos, badges, watermarks, additional people or specialised equipment. Anatomically correct hands, limbs and faces; simple pose, no touching or overlapping hands. + +The other six prompts were: + +### neck + +> Use case: photorealistic-natural. One editorial physiotherapy photograph, landscape 4:3. A seated adult South African man in a plain T-shirt slowly turns his head comfortably to his left for a neck movement assessment. A female physiotherapist in muted blue scrubs observes from a separate chair. Hands rest independently on laps. Waist-up, no manual manipulation. Natural daylight, warm off-white walls, light oak, neutral welcoming clinical interior and subtle blue accents. Realistic adult anatomy, hands, clothing and proportions. Calm candid professional photography, realistic skin texture and gentle warm colour grading. Keep all subjects within the central two-thirds with ample headroom and side room for arched and rounded image crops. Fictional illustrative people and setting, not actual practitioners, patients or clinic premises. No text, logos, badges, watermarks, clutter, dramatic pain, wounds or specialised equipment. Straightforward plausible poses. + +### sports + +> Use case: photorealistic-natural. One editorial physiotherapy photograph, landscape 4:3. A South African adult woman in modest exercise clothing performs a shallow bodyweight squat, both feet flat and parallel, knees slightly bent, arms comfortably forward. A physiotherapist in muted blue scrubs stands separately to one side observing. Full bodies including feet, no equipment. Natural daylight, warm off-white walls, light oak, neutral welcoming clinical interior and subtle blue accents. Realistic adult anatomy, hands, clothing and proportions. Calm candid professional photography, realistic skin texture and gentle warm colour grading. Keep all subjects within the central two-thirds with ample headroom and side room for arched and rounded image crops. Fictional illustrative people and setting, not actual practitioners, patients or clinic premises. No text, logos, badges, watermarks, clutter, dramatic pain, wounds or specialised equipment. Straightforward plausible poses. + +### knee + +> Use case: photorealistic-natural. One editorial physiotherapy photograph, landscape 4:3. An older South African adult woman sits upright on a simple firm chair, one knee bent with foot on floor and the other leg gently extended forward with heel on floor. A physiotherapist in muted blue scrubs sits on a separate chair observing knee movement. Full bodies and feet, hands resting on own laps, no touching. Natural daylight, warm off-white walls, light oak, neutral welcoming clinical interior and subtle blue accents. Realistic adult anatomy, hands, clothing and proportions. Calm candid professional photography, realistic skin texture and gentle warm colour grading. Keep all subjects within the central two-thirds with ample headroom and side room for arched and rounded image crops. Fictional illustrative people and setting, not actual practitioners, patients or clinic premises. No text, logos, badges, watermarks, clutter, dramatic pain, wounds or specialised equipment. Straightforward plausible poses. + +### shoulder + +> Use case: photorealistic-natural. One editorial physiotherapy photograph, landscape 4:3. A South African adult woman in a plain modest short-sleeved shirt stands raising one arm sideways to shoulder height with a gentle bend in the elbow, during a shoulder movement assessment. A physiotherapist in muted blue scrubs observes from the other side at a respectful distance, hands relaxed. Frame from thighs upward, both entire arms visible. Natural daylight, warm off-white walls, light oak, neutral welcoming clinical interior and subtle blue accents. Realistic adult anatomy, hands, clothing and proportions. Calm candid professional photography, realistic skin texture and gentle warm colour grading. Keep all subjects within the central two-thirds with ample headroom and side room for arched and rounded image crops. Fictional illustrative people and setting, not actual practitioners, patients or clinic premises. No text, logos, badges, watermarks, clutter, dramatic pain, wounds or specialised equipment. Straightforward plausible poses. + +### mobility + +> Use case: photorealistic-natural. One editorial physiotherapy photograph, landscape 4:3. An older South African adult man in modest casual clothing and trainers walks naturally across a spacious uncluttered room. A physiotherapist in muted blue scrubs observes walking alongside at a respectful distance. Full bodies and both feet visible, normal walking stride, no equipment. Natural daylight, warm off-white walls, light oak, neutral welcoming clinical interior and subtle blue accents. Realistic adult anatomy, hands, clothing and proportions. Calm candid professional photography, realistic skin texture and gentle warm colour grading. Keep all subjects within the central two-thirds with ample headroom and side room for arched and rounded image crops. Fictional illustrative people and setting, not actual practitioners, patients or clinic premises. No text, logos, badges, watermarks, clutter, dramatic pain, wounds or specialised equipment. Straightforward plausible poses. + +### exercise + +> Use case: photorealistic-natural. One editorial physiotherapy photograph, landscape 4:3. A South African adult man sits upright on a simple chair practising a gentle exercise with a single continuous flat blue resistance band held between his two hands at waist height, elbows bent beside torso, hands separated slightly with band gently stretched. A physiotherapist in muted blue scrubs sits separately observing. Both people from knees upward, clear simple band with two ends held in hands, no anchors or loops. Natural daylight, warm off-white walls, light oak, neutral welcoming clinical interior and subtle blue accents. Realistic adult anatomy, hands, clothing and proportions. Calm candid professional photography, realistic skin texture and gentle warm colour grading. Keep all subjects within the central two-thirds with ample headroom and side room for arched and rounded image crops. Fictional illustrative people and setting, not actual practitioners, patients or clinic premises. No text, logos, badges, watermarks, clutter, dramatic pain, wounds or specialised equipment. Straightforward plausible poses. + +## Crop review and revisions + +All originals were inspected for anatomy, hands, simple exercise positions and equipment. Initial desktop arch checks exposed tight head/arm crops. Six scenes were regenerated using their own original as the reference; mobility needed no revision. Final source images and browser crops were inspected again. No visible anatomy/equipment defects were identified at review size; these remain generated illustrations, not clinical instructions. + +Sports, knee and shoulder revision prompt: + +> Edit this physiotherapy illustration for an arched website image container. Preserve its people, clinical subject, realistic pose, clothing, warm daylight, neutral room and restrained blue accents. Recompose as a noticeably wider camera view with the people SMALLER and clustered within the middle 55% of the frame. Leave the TOP 28% of the image as uncluttered neutral wall above every head. Leave generous empty space on both left and right. Every head and all hands must be clearly inside the central arch-safe area, not close to top corners. Keep bodies and exercise clearly visible, with realistic proportions, hands, equipment and a physically plausible scene. Landscape 4:3. No words, badges, logos or watermarks. This is fictional illustrative imagery, not actual practitioners or premises. + +Consultation, neck and exercise revision prompt: + +> Recompose this editorial physiotherapy photograph for a rounded arch crop. Preserve the same realistic people, natural hands and simple pose, subject, modest clothes, natural daylight, off-white clinical interior and blue accents. Pull the camera back modestly: leave the upper 25 percent as neutral wall above both heads, and center both people inside the middle 65 percent of the image width. Keep faces, hair, hands and any exercise band well away from top corners and side edges. Landscape 4:3. Anatomically correct. No text, logos, watermarks or specialised equipment. Fictional illustration, not actual practice people or rooms. + +Technical reference: [MDN image element, responsive sources and loading attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img). + diff --git a/PR-DRAFT.md b/PR-DRAFT.md new file mode 100644 index 0000000..33391fa --- /dev/null +++ b/PR-DRAFT.md @@ -0,0 +1,51 @@ +# Local SEO, eight public services and preserved blue website design + +Adds an eight-service patient journey with consistent metadata, canonical URLs, clinic structured data, sitemap/indexing controls and protected synchronous enquiry delivery. Preserves the restored original layouts, typography, image compositions and current blue palette. + +## Physiotherapy imagery + +Seven coordinated generated illustrations replace the three repeatedly used treatment photos, with distinct homepage scenes and relevant per-page selections. Existing blue styling, arched/rounded containers and animations are preserved. Captions/alt text identify illustrations; generated people and rooms are not used as practitioner portraits or clinic identity images. + +Responsive WebP variants (480/768/1200px), intrinsic dimensions, lazy below-fold loading, a matched responsive hero preload and per-page social metadata are configuration-driven. All seven 480px files total 111 KB; all 21 variants total 728 KB. Existing recursive image deployment is covered by an expanded preflight/copy test. IMAGE-ASSETS.md documents the audit, prompts, regeneration decisions and usage map. + +Current validation: **111 PHP tests / 1365 assertions**, **4 JavaScript tests**, Pint, PHPStan, production build, shell syntax and diff checks pass. All 13 public pages checked at desktop/mobile widths; final source anatomy and rendered crops inspected, homepage lazy loading/reveals verified, no broken visible images or horizontal overflow observed. Production assets rebuilt with identical CSS/JS output. Motion-preference behaviour is automated-test covered; actual OS preference/high-DPR hardware emulation was unavailable. No merge or deployment. + +## Testimonials + +Adds “What our patients say” after the treatment images and before About: three desktop cards, stacked mobile cards, existing blue/serif/rounded styling and gentle scroll-reveal stagger. The user subsequently confirmed the screenshot quotations are genuine and permitted for publication. The three top-row quotes from Sarah M., Priya N. and Lerato K. now populate the cards verbatim; no star ratings are displayed. + +The supplied quotes now replace samples in every environment. Three labelled neutral samples remain only as a local/staging fallback when there are no approved entries. Production renders only complete, explicitly approved genuine entries and omits the entire section when empty. Samples live in a separate configuration list and cannot be published by toggling their approval flag. Optional source links require a valid supplied HTTP(S) URL. No Review/aggregate-rating schema or external widget. CONTENT-TODO.md documents where to enter quotes, permitted display names, source URLs and approval. + +Testimonial-specific validation before the imagery update: **109 PHP tests / 1049 assertions**, **4 JavaScript tests**, Pint, PHPStan, production build, shell syntax and diff checks pass. Desktop/mobile browser verified three/one columns, spacing, no overflow and scroll-triggered 0/90/180ms entrances. Production sample exclusion, confirmed quote rendering/order, escaping and source-link checks are automated. No source URLs were supplied; no links are invented. Existing reduced-motion tests/CSS apply; actual browser motion-preference emulation remains unavailable. Assets rebuilt. No merge or deployment. + +## Animation and content refinement + +Verified reference `42880b2` and compared its JS/CSS/Blade. Restored disconnected 30px/850ms scroll entrances and 0/90/180/270ms service-card staggers; applied consistent entrances to internal content. Content stays visible before observation or if JavaScript fails. Existing hover, drift, header and mobile-navigation interactions remain; parallax now responds to reduced-motion and mobile changes. The approved static illustration strip remains unchanged instead of restoring the removed carousel. + +Rewrote all eight services around distinct patient questions, assessment focus, care and follow-up. Removed repeated service-page administration in favour of Patient Information, added relevant related links to the original three services, and separated homepage practitioner/first-visit/location/appointment content. Clinical background uses NHS/CSP sources; references and their limits are recorded in SEO-REVIEW.md. Practice-specific facts remain those supplied by the user. + +## Public content and optional information + +- All eight service pages, About and Patient Information are published in configuration; Contact’s booking information is now normal public content. Home and the hub expose eight ordered crawlable cards, and indexable production discovery contains all 13 URLs. +- The user waived Cheryl’s content approval step. Missing optional facts do not prevent publication. No live site changes occur until a separately authorised merge/deployment. +- Names, titles and editable draft biographies based only on supplied degree/experience facts are public. Cheryl’s equine experience is biographical, not a practice service. Exact qualifications, universities, languages, longer biographies and approved portraits have empty/null fields with supply instructions in `config/site.php`. +- Additional optional fee/payment/medical-aid/missed-appointment facts are similarly stored without invented policies. `CONTENT-TODO.md` lists every key, what to supply, its eventual location, confirmed facts and external release tasks. +- Public pages hide incomplete optional fields and portraits cleanly. No missing-information notices appear in any environment; outstanding facts stay in comments and CONTENT-TODO.md. + +## Preserved safeguards + +The website Basic Auth gate and unused credentials/configuration are removed. Local and staging pages open anonymously while staying noindex, with crawling disallowed and an empty sitemap. Staging is accessible to anyone with its URL; noindex is not access protection. Deployment-token authentication is unchanged. Publication gating remains available if a page is disabled later. The verified location gate, canonical configuration, synchronous mail, replay protection, generic analytics deduplication and production deployment preflight remain intact. + +The original design was restored from Git reference `42880b2`; backup branch `backup/seo-before-layout-restoration-2026-09-14` preserves the earlier SEO work. The central blue theme retains accessible text and focus states. Unsupported testimonials, credentials and unconfirmed promotional claims were not restored. + +## Validation + +95 PHP tests / 953 assertions and 4 JavaScript tests pass, plus Pint, PHPStan, production asset build, shell syntax and diff checks. Browser scrolled all 13 pages at 390px and 1440px: no overflow, one H1 each, eligible reveals triggered. Verified card hover/lift, header/parallax, service navigation and mobile toggle/Escape/focus/link-close. With scripts blocked in the temporary loopback QA transport, all pages remained readable at both widths. Assets rebuilt; fake/mocked mail only. + +Reduced-motion and missing/throwing observer paths have automated coverage, with reduced-motion CSS retained. The browser tool cannot emulate OS motion preference, so an actual reduced-motion browser session remains a manual verification limitation. The Livewire form still requires JavaScript; no-JS checks cover content and links. No hosted staging/production testing, merge or deployment. + +## Remaining release work + +Optional content is tracked in `CONTENT-TODO.md`; it is not a release blocker. Valourite still needs to verify server environment overrides, effective cached production indexing/canonical settings, recipients and contact/location values, and obtain Google-account access for external listing/Search Console work. Existing preflight checks an in-place deployment; it does not provide an atomic release or automatic rollback. Cache-based deduplication does not guarantee exactly-once email delivery. Existing dependency-audit follow-up remains open. + +No merge, deployment, live enquiry, production-setting change or Google-account modification. diff --git a/SEO-REVIEW.md b/SEO-REVIEW.md new file mode 100644 index 0000000..20b83f9 --- /dev/null +++ b/SEO-REVIEW.md @@ -0,0 +1,285 @@ +# Local SEO and enquiry implementation review + +## Physiotherapy imagery — 18 September 2026 + +Replaced the three repeatedly displayed treatment photos with seven coordinated, explicitly illustrative AI-generated scenes. The homepage uses all seven without repetition; internal pages use subject-specific images, with limited reuse for related services. Original assets and optional real practitioner portrait fields remain untouched. The current blue palette, containers, typography, spacing and animation code are preserved. Image captions/footer distinguish fictional people and rooms from the actual practice. + +`config/imagery.php` contains the inventory, responsive variants, alt text, dimensions and URL-to-image mapping; the shared Blade image component handles native responsive loading. Each scene has 480×360, 768×576 and 1200×900 WebP files. Homepage preload matches the responsive hero, and only each page's main hero has high priority. Other images load lazily. Per-page social images have accurate dimensions and illustrative alt text; generated imagery is deliberately omitted from the clinic identity's image property. All existing clinic/contact/address structured data, URLs, canonicals, indexing, sitemap and enquiry protections remain. + +All 21 variants total 727,736 bytes. The seven small variants total 111,106 bytes, versus 224,826 bytes for the previous three originals; density-dependent variants change the actual download size. `IMAGE-ASSETS.md` records the original repetition audit, selected files, placements, prompts and crop revisions. Six originals were regenerated with safer framing after arch inspection; each final source was checked for anatomy, hands, clothing and equipment. + +Validation: **111 PHP tests / 1365 assertions**, **4 JavaScript tests**, Pint, PHPStan, production build, shell syntax and diff checks pass. The rebuild produced identical committed CSS/JS assets. New tests cover every public page's image/social metadata, intrinsic dimensions, width descriptors, file budgets, hero/preload agreement and below-fold loading. The deployment fixture now verifies nested images survive a rejected preflight and are copied after a passed preflight; no deployment was performed. + +Browser: checked all 13 public pages at 1440px desktop and 390px mobile, including final revised crops. Scrolled the homepage to inspect the arch, three-image strip, About, dark first-visit panel and questions card. Visible images loaded, no horizontal overflow or console errors were observed, and reveals completed. The mobile-hidden strip remains hidden. Staging remained noindex. Reduced-motion/failed-observer behaviour is covered by unchanged JS tests; no actual OS motion-preference or high-DPR-device emulation was available. Native responsive sources selected 480px files in this browser. No live messages, merge or deployment. + + + +## Confirmed testimonial publication — 18 September 2026 + +The user explicitly confirmed that the supplied screenshot testimonials are genuine and that the practice has permission to publish the quotations and display names. Activated the three top-row cards: Sarah M., Priya N., Lerato K., in that order, using the screenshot wording verbatim. This is user-confirmed provenance/permission, not independent verification. No source URLs were supplied, so they remain null. No star ratings, clinical category labels or Review/aggregate-rating schema were added. + +Approved entries now render in local, staging and production mode; sample labels disappear. Existing three-card layout, typography, colours, reveal effects and approval filtering are unchanged. The empty/unapproved fallback remains tested with isolated fixtures. CONTENT-TODO.md now records the supplied information rather than listing quotes/approval as outstanding. + +Validation: 109 PHP tests / 1049 assertions, Pint, PHPStan and production build pass; rebuilt production assets are included. Desktop (1440px) and mobile (390px) show the supplied quotations, correct names/order and no sample labels or horizontal overflow. No JavaScript or styling changes. No merge, deployment or live enquiries. + +### Initial sample implementation (superseded by confirmation above) + +## Testimonials — 18 September 2026 + +Added “What our patients say” immediately after the services section's three treatment illustrations and before About. Three rounded white cards share the blue palette, serif text, decorative quotation mark and existing 0/90/180ms reveal animation. Desktop uses three equal columns; mobile stacks cards. Section spacing uses the existing services/About spacing. No carousel, widget, library, rating or review schema. + +Repository templates/history contained five older quotes with names and default five-star ratings, but no sources or website-reuse permission; earlier review notes explicitly withheld them. Replaced that dormant content with config-driven rendering. `site.testimonials.entries` is empty until genuine content is supplied; only complete entries with boolean `approved=true` render. `site.testimonials.samples` is a separate, explicitly local/staging-only fallback, even if a sample's approval flag is changed. Samples use neutral design instructions and “Reviewer display name”, never fabricated experiences. Every sample carries “Sample testimonial — replace before publishing”. Production with no approved entries emits no section wrapper or spacing. Invalid/non-HTTP(S) source URLs are omitted; quote/name/source output is escaped. CONTENT-TODO.md documents each editing key and permission requirement. + +Validation: 106 PHP tests / 1016 assertions, 4 JavaScript tests, Pint, PHPStan, production build, shell syntax and diff checks pass. New tests cover local/staging-only samples, unsupported environments, production HTML/head/schema exclusion including an accidentally approved sample, section order/empty state, genuine-entry filtering, escaping and optional/invalid links. Existing SEO, mail/replay and deployment tests pass. Production assets rebuilt. + +Browser: 1440px desktop showed three equal cards with surrounding image/practitioner spacing; 390px mobile showed one column with no horizontal overflow. Scrolling triggered all three card entrances with computed 0/90/180ms delays. Reused reduced-motion bypass is covered by the existing JavaScript tests and CSS; actual OS/browser reduced-motion emulation remains unavailable in this browser tool, as documented previously. No live form submissions, merge or deployment. At that point, genuine approved quotations and permitted display names were still needed; the confirmation above resolves this. + +## Animation and content refinement — 15 September 2026 + +Verified `42880b2` exists as a commit before comparing its JavaScript, CSS and Blade together. The approved blue palette, fonts, page compositions, image assets and seven-section homepage order remain intact. + +### Animation findings and changes + +- Original reveal CSS used a 30px rise, opacity transition, 850ms duration and the existing easing curve. Current JavaScript still observed `.reveal`, but the SEO CSS forced opacity 1/transform none, disconnecting the effect. Restored the entrance with a finite CSS animation triggered on intersection. Unlike the original hidden waiting state, unobserved content stays visible if JavaScript or IntersectionObserver fails. No forwards fill interferes with completed card hover transforms; keyboard focus cancels an entrance on interactive content. +- Original services markup supplied 0/90/180/270ms delays per four-card row. Those attributes had been lost; restored them on the eight-card grid. Existing hero and benefits delays remain. Extended the same entrance to internal content rows and practitioner profiles, with the original 150ms image offset on page headers. +- Original card lift/colour/shadow, link underlines, button interactions, floating drift, parallax and scroll-header/hamburger transitions were still connected and are retained. Parallax now clears when reduced motion is requested or the viewport becomes mobile, including preference changes after load. +- The original mobile panel used `hidden` toggling, not a sliding panel animation. No invented slide transition added. Navigation keeps the published routes rather than reverting to homepage anchors or testimonials. +- Original desktop carousel/lightbox markup was intentionally replaced during the approved layout restoration by three static illustrations. It remains static to preserve the approved composition; withheld images/testimonials are not restored. Existing unused carousel support is unchanged. No animation library added. + +### Editorial changes + +Home now introduces who the practice helps, the eight services, the two practitioners, a first-visit overview, practical questions, the location/map and appointment request. Full address/entrance/parking information is removed from the earlier practical cards and retained in the location section (and standard footer). Practitioner facts come from the editable biographies. Each service has a distinct introduction, four focused sections and a short card description; the three original services now also have two relevant related links. Sports activity demands, spinal discomfort, surgical restrictions, local joint/muscle symptoms, everyday mobility, persistent-pain function, preparation for activity and exercise progression each have a different purpose. + +Service pages no longer duplicate appointment duration, forms, fees, referrals or cancellation rules. Their existing Patient Information link provides those details, while appointment actions remain in the header and footer. Contact keeps availability/confirmation information concise and links to Patient Information. No degree titles, equipment, named treatment techniques, outcomes, affiliations or practice policies were invented. No placeholder or approval workflow added. + +### Clinical reference notes + +Consulted 15 September 2026. These sources inform general explanations of physiotherapy, assessment and rehabilitation; they are **not evidence that this practice offers a particular technique, specialist programme or NHS service**. The eight offerings, practitioner background and practice arrangements come from the user's confirmed information. Copy uses conditional examples of assessment/exercise guidance, not prescriptions or promised outcomes. No UK registration, referral, funding or service-delivery rules are imported. + +- [NHS: Physiotherapy](https://www.nhs.uk/tests-and-treatments/physiotherapy/) — symptom/history assessment, relevant movement/strength/balance checks and individual exercise guidance. +- [CSP: Rehabilitation](https://www.csp.org.uk/conditions/rehabilitation) — goals based on meaningful activity and assessing functional difficulties (search-retrieved text; direct page subsequently returned 403). +- [NHS: Sprains and strains](https://www.nhs.uk/conditions/sprains-and-strains/) — injury-related movement difficulties and variable recovery; no timelines or self-treatment protocols copied. +- [NHS: Running injuries](https://www.nhs.uk/live-well/exercise/knee-pain-and-other-running-injuries/) — activity demands and gradual build-up; general context only, no running plan prescribed. Page's listed review date is older, so no numerical recommendations are adopted. +- [CSP: Treatment for neck pain](https://www.csp.org.uk/conditions/neck-pain/treatment-neck-pain) — activity and exercise in ongoing neck symptoms; no specific manual techniques claimed. +- [Guy's and St Thomas': Back pain physiotherapy](https://www.guysandstthomas.nhs.uk/health-information/low-back-pain/physiotherapy-and-exercises) — individual activity guidance for back pain. +- [NHS: Getting back to normal after surgery](https://www.nhs.uk/tests-and-treatments/having-surgery/recovery/) — recovery depends on the operation and surgical instructions. +- [CSP: Chronic pain](https://www.csp.org.uk/conditions/chronic-pain) — activity, function and individual goals. No specialist multidisciplinary pain programme is represented as available here. + +### Actual verification and limits + +95 PHP tests / 953 assertions and 4 JavaScript tests pass. Pint, PHPStan (serial debug mode), production build, shell syntax and diff checks pass. Production assets rebuilt. Existing SEO/publication, contact mail/replay/cache-failure and deployment preflight/protection checks remain green. Mail tests use fakes/mocks; no live submissions. + +Browser scrolled all 13 public pages at 1440px and 390px: one H1 each and no horizontal overflow. All eligible reveals triggered (mobile deliberately hides the three desktop illustrations). Observed header scroll state, changing parallax transforms and restored grid delays; card hover retained navy colour and -4px lift after the entrance. Checked linked service navigation and mobile open/Escape/focus restoration/link-close. Inspected homepage, service grid and service content layouts. With scripts blocked by a temporary loopback-only CSP response, every page remained readable at both widths, with no opacity-hidden reveals or overflow. That QA transport is outside the repository and adds no public route or setting. + +Reduced-motion observer bypass and unavailable/throwing observer fallback are covered by JavaScript tests; CSS disables reveal entrances under reduced motion, and existing global reduced-motion rules remain. **The available browser exposes viewport controls but no motion-preference emulation, so an actual OS/browser reduced-motion session and preference-toggle interaction were not verified.** No-JS verification covers readable content and ordinary links; the existing Livewire enquiry form still needs JavaScript. Hosted staging and production were not accessed. No merge or deployment. + +## Current website access and content — 15 September 2026 + +All eight service pages, About, Patient Information and Contact are public in every environment. The website Basic Auth middleware, request-attribute bypass and review credentials/configuration have been removed. Deployment-token authentication is unchanged. Local/staging responses remain noindex/nofollow, robots disallows crawling and the sitemap is empty even when indexing flags are accidentally enabled. Staging is accessible to anyone with its URL: noindex is not access protection. + +The 13-URL production sitemap, publication flags, metadata/canonicals, structured data, blue design and enquiry/deployment safeguards are preserved. Disabled pages remain 404 in every environment, including preview queries. Missing-information notices are removed everywhere. About uses editable draft biographies based only on supplied facts; Cheryl's equine experience remains biographical. Unknown optional fields and incomplete photos are omitted. `CONTENT-TODO.md` documents exact editing keys and outstanding optional information. No content approval workflow is required. + +95 PHP tests / 950 assertions and 2 JavaScript tests pass. Pint, PHPStan (serial debug mode), production asset build, shell syntax and diff checks pass. Tests cover anonymous HTTP/HTTPS local/staging access across every page, noindex/robots/empty sitemap even with indexing enabled, production metadata/sitemap/card destinations, unpublished-page gating, optional-content omission/escaping and deployment-token rejection. Existing synchronous-mail failure, successful-send/cache-failure, replay and preflight tests pass using fake/mocked mail. Browser verification opened staging anonymously and inspected About profiles at 390px and 1440px, plus mobile Patient Information: no overflow or missing-information notices. Design and colours are unchanged; production assets rebuilt. No merge or deployment. + +## Blue colour theme + +Applies the requested blue palette through central Tailwind theme tokens: deep-blue buttons, navy sections/overlays, soft-white and pale-blue surfaces, readable blue-grey text, darker teal-blue links and decorative cyan. Preserves the existing wordmark, imagery, typography, layout, content, navigation, publication flags and all application/SEO behaviour. Updates shadow colours, gradients, selection, placeholders, hover and keyboard focus; semantic red errors and green success remain separate. + +Validation: **87 PHP tests / 483 assertions**, **2 JavaScript tests**, Pint, PHPStan, production asset build, shell syntax and diff checks passed. Built with the bundled supported Node runtime (the system Node 18 is too old for Vite 8). Desktop homepage geometry/fonts matched the pre-theme capture. Browser checks at **390px and 1440px** covered all eight service pages, About, patient information and Contact, with no overflow or broken loaded images. Screenshots inspected homepage, service grid/hover, internal page, dark sections/footer, mobile navigation and form validation/focus. Public service gating remains intact. Success was checked using synthetic input and local log-only mail; no live enquiries. + +Contrast ratios: white primary-button text **9.34:1** (hover **11.44:1**), light-background links **5.37:1**, secondary text **5.60:1**, light cyan on navy **8.96:1**, and muted footer text **6.13:1**. Keyboard focus has a dark outline on light surfaces and a white edge for navy surfaces. Content approval was subsequently waived; operational release requirements remain unchanged. No merge or deployment. + +Prepared 14 September 2026 from the supplied Danks & Strydom SEO strategy. This is a review branch, not a deployed release. No Google account, directory, production server or live mailbox was changed. + +## Eight-service inventory update + +Five requested offerings were added as distinct pages: + +- `/services/joint-muscle-pain` — joint discomfort, muscular aches and movement difficulties. +- `/services/mobility-movement-assessment` — limitations in movement and everyday activities. +- `/services/chronic-pain-management` — persistent pain and everyday function, without a cure promise. +- `/services/injury-prevention` — movement/activity demands and reducing risk, without guaranteed prevention. +- `/services/rehabilitation-exercise-programmes` — individual exercise guidance, progression and review. + +All eight service pages are now `published=true` following the user’s publication instruction. Public hubs show eight linked cards. The empty-services fallback remains available if all services are later unpublished. Sitemap entries continue to follow publication flags; staging remains noindex with an empty sitemap. + +The supplied screenshot defines the card order/icons: sports/pulse, back-neck/spine, post-operative/recovery, joint/joint, mobility/mobility, chronic/chronic, prevention/shield, exercise/program. The restored card styles use four columns on desktop, two on tablet and one on mobile. Labels/icons live in the inventory; clinical wording was not copied blindly from screenshot captions. Existing typography, colour and image treatments are preserved. + +Each new page has distinct metadata/H1, assessment-first wording, one-hour first appointment and patient-information form, individual guidance, follow-up and an enquiry action. Shared fees/payment/cancellation policies stay on patient information. Each new page has only two relevant related services, filtered through the same publication gate. Main navigation is unchanged. + +Validation: **87 PHP tests / 483 assertions**, **2 JavaScript tests**, Pint, PHPStan (512 MB), production build, shell syntax and diff checks passed. Tests cover every new route, public 404/preview exclusion, later publication and sitemap inclusion, canonical/meta output, unique eight-page titles/descriptions, ordered card destinations/icons and filtered related links. Browser checks at **390px and 1440px** followed all eight card destinations, verified the five new layouts, one H1 each, no overflow or broken loaded images, two related links per new page, and one/four hub columns respectively. The desktop card design was compared with the supplied screenshot. A temporary loopback QA transport mismatch was corrected before destination checks; the then-current HTTPS review authentication was unchanged (that gate has since been removed). Frontend assets rebuilt. No merge, deployment or live enquiries. + +## Original visual design restoration + +Backup branch: `backup/seo-before-layout-restoration-2026-09-14`, at `739e4dabc2f0a7ca891162ad3f867efb6fd1051c`. The active SEO branch remains `feature/local-seo-patient-journey` and this updates PR #4. + +Visual reference: pre-SEO commit `42880b2`. Its section diffs were inspected and its homepage rendered in an isolated local checkout. The supplied original screenshot files were not available among this session’s attachments; this pass compares against the Git-rendered reference, not those specific screenshots. + +- Restored the original hero architecture: Fraunces/Instrument Sans typography, original pine/sea/sand/bone colours, image crop, arch/backdrop, floating cards, chips and three-column fact strip. Confirmed appointment length/practitioner/service counts replace unsupported statistics. +- Restored the arched About illustration, offset sand card, split copy, dark numbered section and photo composition. All replacement copy uses confirmed facts, with no old experience, technique, outcome or quality guarantees. +- Reinstated the original seven-part homepage flow: hero → services → About → dark visit details → staggered practical cards → location → contact. The staggered composition replaces testimonials with practical facts; no reviews, stars or patient quotations are restored. +- Services retain the original card styling and gradient section. Published services become cards automatically; public empty services keep a direct enquiry action. A desktop illustration strip replaces the withheld mixed human/equine gallery using existing treatment illustrations, not claimed practitioner portraits. +- Restored the split dark-details/map location panel and the original contact-section washes, spacing and numbered introduction. Current configured address, verified directions/map gate, hours and enquiry component remain in use. +- Internal pages extend the original arch imagery, split introductions, dark surfaces and numbered content layouts. Removed the unrelated repeated link list; kept only a relevant service-to-patient-information link and a concise gated footer navigation. +- No colour rebranding was applied. Source colour/font definitions, JavaScript, routes, metadata/layout schema, canonical/discovery controls, authentication, enquiry mail/cache protections and deployment preflight were not replaced. + +Validation: **80 PHP tests / 378 assertions**, **2 JavaScript tests**, Pint, PHPStan (512 MB), production asset build, shell syntax and diff checks passed. Production assets rebuilt. Browser checks at 390px and 1440px covered the homepage and all seven internal pages in authenticated review: one H1 each, no horizontal overflow, no broken loaded images and no redundant related-link lists. Public services fallback and mobile menu open/Escape close were also checked. Original/restored hero compositions were visually compared at desktop/mobile widths; the internal service layout was inspected on mobile. Existing tests continue to verify production draft exclusion and location gating. No live mail, production changes, merge or deployment. + +## Current content review — supersedes earlier placeholder versions + +The latest user-supplied answers replace earlier conflicting address and policy information. The user has waived Cheryl’s review and authorised publication of the completed service descriptions, short biographies and patient policies. Optional missing facts remain empty and hidden publicly; there are no public approval badges. + +### Implemented content + +- Full address: **Suite 102, Surgiklin Studios, Unit 12, Glen Eagle Office Park, Koorsboom Avenue, Glen Marais, Kempton Park, 1619, Gauteng, South Africa**. Display and split schema fields share this information; no active Monument Road references remain. +- Phone **011 391 3126**, public email/intended enquiry inbox **admin@danksandstrydom.co.za**. Actual mail recipients remain explicitly environment-configured; no live mail was sent. +- Gate access button, left passage to the end, parking/covered parking, ramp and elevator access use the supplied facts. No gate code or blanket wheelchair-accessibility claim was added. +- Hours: Monday–Friday 07:30–17:30; Saturday by appointment; Sundays/public holidays closed. Hours are displayed as text; no fixed Saturday hours are invented in schema. +- One-hour appointments, new-patient form, assessment before appropriate treatment and nothing to bring. Post-operative copy preserves the discussion of surgical instructions/restrictions. +- Completed booking/confirmation, direct-booking/referral, follow-up, fees/payment, interim medical-aid and cancellation wording uses the supplied text. The cancellation wording retains “may”; direct claim handling is not asserted. +- Text-only profiles use “Physiotherapist” and the supplied biographies. Only the already confirmed generic physiotherapy degrees are mentioned. Cheryl’s equine experience is biographical; no equine service or booking option was created. + +### Map inspection + +The approved directions link https://maps.app.goo.gl/WoxWSjSzNbtEkTr66 was resolved and inspected in Google Maps. It opens **Danks And Strydom**, with the matching public phone and Surgiklin Studios address. The embed source was obtained from that listing’s **Share → Embed a map** field; the short share URL is used only for directions. The supplied pin’s feature ID is `0x1e9515b5389c59e3:0xc9913cffe2473c26`. No coordinates were guessed or old map reused. + +The location verification gate remains available and now defaults true for the supplied address and inspected map. Existing environment overrides still win. Google’s listing currently uses an older address format and showed a 17:00 closing time; the website uses the user’s confirmed address and 17:30 weekday closing time. Valourite should reconcile the listing once account access is established. No Google-account edits were made. + +### Publication and staging access + +- Home and all 12 inventory pages are available without website authentication. Only published pages enter navigation and the indexable production sitemap. +- Local and staging remain noindex with crawling disallowed and an empty sitemap. Keep their indexing/canonical flags false and mail on a safe local/log transport. +- There are no review credentials to configure. Existing unused review variables may be removed by the server operator during a separately authorised release; this change does not edit server environments automatically. Rebuild configuration cache during release. +- Staging is public to anyone with its URL. Noindex does not protect confidential information. Deployment and other administrative authentication remain separate. +- Optional missing facts are hidden everywhere; `CONTENT-TODO.md` lists editing keys. No Cheryl approval step is required. + +### Genuinely unresolved items + +1. **Optional profile and policy details:** exact qualifications, universities, languages, longer biographies, portraits and further payment/medical-aid details are tracked in `CONTENT-TODO.md`. No Cheryl approval is required. +2. **Exact qualifications:** degree titles/universities remain unknown and are cleanly omitted. Do not infer registrations or McKenzie credentials. +3. **Medical-aid claim handling:** confirm direct submission and responsibility for rejected claims privately; the website currently directs patients to reception and their scheme. +4. **Photographs, when available:** obtain approved practitioner photos; the finished text layout works without them. +5. **Google-account access:** Valourite coordinates Business Profile/Search Console after ownership/access is established, including address/hours reconciliation. + +The non-www HTTPS origin is confirmed as `https://danksandstrydom.co.za`. Valourite owns hosting and production configuration; these are remaining operational steps, not unanswered business-fact questions. Existing `.env` values (including blanks/stale contact fields) override new defaults; `.env.example` never changes production automatically. + +### Historical validation (superseded by current results above) + +- **80 PHP tests / 378 assertions passed**, including prior deployment/email/replay checks and new authenticated staging, invalid credentials/hash, HTTPS requirement, rate limiting, production 404/navigation/sitemap gating, and draft wording checks. +- **2 JavaScript tests passed**; Pint, PHPStan (512 MB), production Vite build, shell syntax and diff checks passed. Rebuilt production assets are committed. +- Browser checks at **390px and 1440px** covered About, services, three service detail pages, patient information and Contact in authenticated loopback staging: all correct headings, no horizontal overflow or placeholders, and three preview cards. About’s text layout was visually inspected at desktop and mobile widths. Initial QA asset URL configuration was corrected before checking layouts. +- Tests use fake/mocked mail; local review uses log mail with a synthetic recipient. No merge, deployment, production configuration change, live enquiry or Google-account change occurred. + +### Retained technical fixes + +Production preflight validates a private candidate using the existing server environment before maintenance/reset/assets, then validates the rebuilt cached configuration. Mail success is recorded before cache bookkeeping; bookkeeping failures cannot falsely report sending failure. Both fixes and their meaningful tests remain intact. + +### Preflight operation and limits + +`php preflight.php /absolute/path/to/.env` validates fresh candidate configuration without booting application providers or contacting mail/database services. Existing inherited process environment values retain Laravel precedence. The fresh mode deliberately bypasses even an inherited `APP_CONFIG_CACHE` path; a stale valid active cache cannot conceal invalid release settings, and a stale invalid cache cannot reject corrected fresh settings. An explicitly present `.env.production` causes the deployment script to stop for operator review, rather than silently validate one file and later load another. No configuration is overwritten automatically. + +The script checks that `.env` did not change during candidate validation, activates the exact validated SHA, then runs the established cache rebuild. `php preflight.php /absolute/path/to/.env --cached` checks the rebuilt effective configuration before bringing the site online. If anything after maintenance starts fails, maintenance remains enabled for operator recovery; the script no longer exposes a partially completed release via an unconditional `artisan up` trap. The deployment tests use temporary fixtures and stub external executables, not the production server. + +This remains an **in-place deployment**, not an atomic release switch. Code reset, active dependency installation, assets and migrations can still fail after preflight; there is no automatic code/data rollback. Keep backups and restore matching code/assets/configuration before manually bringing the site up. Preflight does not prove SMTP inbox delivery, database readiness, clinical approval, Google indexing or Apache configuration. The host must ensure CLI and PHP-FPM use the same environment/cache paths and avoid concurrent configuration edits. Hard process termination can leave a private candidate directory requiring cleanup; ordinary success/error/signal exits remove it. + +For the first upgrade, an operator must invoke the reviewed version of `deploy.sh` from a trusted separate path: the old script already on the server cannot acquire these safeguards until updated. Do not first reset the live checkout just to obtain the guard. Environment overrides prefixed `DANKS_DEPLOY_` exist for isolated tests/host paths; they do not bypass validation. Candidate preparation needs temporary disk space and Composer network/cache access. No production execution occurred in this task. + +### Cache-based enquiry limits + +Success means the synchronous mail transport returned successfully, not that the recipient read or even received the message. The component retains `sent=true` and clears personal fields even when bookkeeping fails. The normal accepted-ID cache lasts 24 hours and the lock 120 seconds; browser deduplication is per page lifetime. Cache loss/write failure, expired entries/locks, a process crash or lost response after transport acceptance, concurrent retries and a new component/session can still permit duplicate mail. This is not guaranteed exactly-once delivery. Do not encourage a patient to resubmit solely because the post-send cache write failed; monitor the generic bookkeeping warning separately from transport errors. + +## What is ready + +Laravel 13.15.0, Livewire 4.3.1, Tailwind 4.3.0 and Vite 8.0.16 remain in place. Dependencies and architecture were retained. Production assets are committed because the current deployment copies built assets and does not run npm. + +- Homepage local metadata/H1, navigation to about/contact/services, existing hero/services/about/benefits/location/contact anchors, reusable page and service-card templates. +- Per-page metadata, canonical and social URL consistency based on this environment's `APP_URL`. Stable MedicalClinic entity with Physiotherapy as its medical specialty; no reviews, FAQ markup, guessed coordinates or fixed opening hours. +- Environment-aware robots and sitemap endpoints. Only published pages appear in production discovery; development/staging are noindex. Unknown and unpublished paths return 404, including with `?preview=true`. +- Synchronous form delivery retained, with validation, honeypot, five attempts per IP per ten minutes, a short-lived atomic submission lock and a 24-hour accepted-ID cache to prevent replay. Error logs omit transport exception messages that could contain patient data. UI distinguishes requests from confirmed appointments. +- Disabled-by-default analytics adapter emits only generic accepted-enquiry, phone-click and directions-click events. No vendor, tracking ID, page URL, referrer, patient details or clinical selections are transmitted. Acceptance IDs remain in the local adapter for deduplication and are not passed to a provider. +- Unsupported testimonials, experience/quality counters, referral promises, response-time promises and ambiguous mixed human/equine gallery are omitted. Gallery image assets are retained. + +## Page inventory and release checklist + +`config/site.php` contains complete page text and publication flags; `config/contact.php` contains centrally managed contact/location defaults. Draft routes/templates have no public preview query bypass. The current publication status and staging access rules above supersede earlier versions of this PR that displayed placeholders publicly. + +Before release, Valourite should: + +- Confirm the intended published inventory and complete operational release checks. Optional content and Cheryl approval do not block publication. +- Review effective display/split address, verified map/entrance, phone/email and explicit enquiry recipient configuration against the supplied facts. +- Verify the existing server environment/cache has `APP_ENV=production`, `APP_DEBUG=false`, `APP_URL=https://danksandstrydom.co.za`, `SITE_INDEXABLE=true`, `SITE_CANONICAL_REDIRECTS=true`, with staging/local indexing disabled. Safe staging/local indexing defaults stay false. +- Use the candidate preflight and cached postflight, backups and host checks described below. No deployment is authorised by this PR update. +- Coordinate Google listing/Search Console setup through Valourite after ownership and access are established. Do not invent reviews, extra locations, treatment credentials or recovery promises. + +## Material differences from the public audit + +The repository contains configurable contact values rather than an authoritative business record. Its original defaults were placeholders, and its original example map environment line was malformed. Confirmed contact defaults and the inspected map now replace them. The deployed values are not available here. The no-queue synchronous mail implementation already had validation, failure handling and success-state behaviour: these were preserved. Spam controls and analytics were absent in inspected source. The deploy script copied static discovery files into `public_html`; merely deleting repo files would have broken deployment and left stale hosting files, so the script now removes those two obsolete public files. Missing Request/Process imports in the existing deploy route were corrected; that endpoint was never invoked. + +The old whole-address schema, Physiotherapy node type, relative-only menu, non-link service CTA, counters and unsupported policy claims matched the reported concerns. The old desktop carousel includes human and equine images. No historical URL rankings, selected Google canonical, exact Google rank, real mail delivery, consent records or practitioner verification can be inferred from code. The old testimonials anchor is intentionally no longer promoted because its section is withheld. + +Schema vocabulary was checked against https://schema.org/MedicalClinic and https://schema.org/Physiotherapy. Automated tests parse JSON-LD and check type/identity/fields; this is not a claim that Google's Rich Results Test or Schema.org's hosted validator was run. Those release checks remain external. + +## Verification + +See the accompanying test results, browser-check JSON and screenshots. Tests use fake mail; the browser uses local `MAIL_MAILER=log` with synthetic `.test` data. No production delivery is claimed. Existing tests were updated where the intended public behaviour changed (gallery withholding, new metadata and booking wording), while asset checks remain. The initial test setup needed a local app key; after configuration the existing suite passed. Default PHPStan hit this machine's 128 MB limit; rerunning with `--memory-limit=512M` passed. + +`npm ci` initially reported four dependency audit findings (two high, two critical). No lockfile/package versions were changed; follow up with a supported-Node `npm audit` and a separately reviewed dependency update. Do not run `npm audit fix --force` as part of this content release. + +## Hosting and release instructions (prepared, not applied) + +1. The user has authorised the current published inventory and waived Cheryl’s approval. Obtain separate merge/deployment authorisation; the preferred non-www HTTPS hostname is confirmed. +2. Back up the current code revision, `public_html` (including `.htaccess`, index.php and discovery files), `.env`, database and any uploaded media. Record the old SHA and keep a restoration copy off the public document root. This branch has no database migrations. +3. Review on a public staging origin with `APP_ENV=staging`, `APP_URL` set to that staging origin, `SITE_INDEXABLE=false`, `SITE_CANONICAL_REDIRECTS=false`, analytics disabled and safe mail. Staging pages open without a login; robots/noindex are not access protection. +4. Use the locked dependencies with PHP compatible with this Laravel 13 lockfile and Node supported by Vite 8 (local build used bundled Node). Run `composer install`, `npm ci`, `npm run build`, `vendor/bin/pint --dirty --format agent`, `vendor/bin/phpstan analyse --memory-limit=512M`, `php artisan test --compact`, `node --test tests/analytics.test.js` and `bash -n deploy.sh`. Confirm tracked `public/build` matches source. +5. Populate the approved `CONTACT_*` values in the environment. Supply full display address AND split fields from the same approved fact sheet. Review the supplied verified map/address and preserve the verification gate; set the effective flag to true for the approved configuration. Map values must be URLs, never pasted iframe HTML. Confirm SMTP configuration/timeout, actual recipients, writable shared cache and sessions. Synchronous mail is intentionally retained because the host has no queue worker. Monitor generic mail failures and practice reception outcomes; acceptance does not prove inbox receipt. +6. For approved production, set `APP_ENV=production`, `APP_DEBUG=false`, `APP_URL=https://danksandstrydom.co.za`, `SITE_INDEXABLE=true` and `SITE_CANONICAL_REDIRECTS=true`. **These indexing flags default to false: omitting this step will keep production noindex.** Leave analytics false until its provider and privacy handling are settled. +7. The prepared `deploy.sh` still targets `main` and resets its checkout. Do not execute it from this feature branch expecting a preview. After separate merge/deployment authorisation, use the established release process. Its new removal of `public_html/robots.txt` and `public_html/sitemap.xml` allows the Laravel routes to answer through the existing front controller. Verify cPanel document-root/index.php paths; they are outside this checkout. The script validates a private candidate first and does not automatically roll back code on failure; see the review follow-up above for the first-upgrade procedure. +8. `APP_URL` drives SEO output. Application redirects cover known production aliases on GET/HEAD only; they do not redirect local/staging hosts or form POSTs. For direct Apache TLS termination, add the following **production virtual-host-only** rules before the existing Laravel rewrite rules to cover static files too. Keep staging in a different vhost. If TLS terminates at a proxy, have the host configure trusted proxy handling first; do not blindly trust arbitrary forwarded headers. + +```apache +# Only install in the production virtual host after confirming the preferred host. +RewriteEngine On +# Collapse a non-directory trailing slash and canonical host in one response. +RewriteCond %{REQUEST_METHOD} ^(GET|HEAD)$ +RewriteCond %{HTTP_HOST} ^(www\.)?danksandstrydom\.co\.za$ [NC] +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.+)/$ https://danksandstrydom.co.za/$1 [R=301,L,NE] + +RewriteCond %{REQUEST_METHOD} ^(GET|HEAD)$ +RewriteCond %{HTTP_HOST} ^(www\.)?danksandstrydom\.co\.za$ [NC] +RewriteCond %{HTTPS} !=on [OR] +RewriteCond %{HTTP_HOST} ^www\.danksandstrydom\.co\.za$ [NC] +RewriteRule ^ https://danksandstrydom.co.za%{REQUEST_URI} [R=301,L,NE] +``` + +These rules preserve query strings implicitly. Test direct HTTPS, HTTP, www, nested paths, trailing slashes, encoded paths and query strings with `curl -I` and verify one-hop destinations/no loops. Server-specific virtual-host placement should be verified by the host; application tests do not execute Apache. + +9. Clear/rebuild config, routes and views after environment changes. Test homepage/about/contact/services, all eight service URLs and Patient Information, robots, sitemap, nonexistent URL 404, map/address agreement, phone link and mobile menu. Check Schema.org Validator and Google's applicable structured-data tools. Perform one explicitly authorised live delivery test and verify reception receipt. No such test was performed here. +10. Analytics setup: after provider/consent approval, connect a reviewed listener to `site:analytics`. Consume only `event.detail.event`; never enrich it with form state, URLs, referrers, query strings or clinical selections. `SITE_ANALYTICS_ENABLED=true` enables local event dispatch only; it does not install GA/GTM or make network requests. Disable automatic form capture/enhanced measurement where it could collect data. Test one accepted enquiry event and contact clicks with the provider's debugger. + +## Rollback + +Restore the recorded prior code SHA and matching built assets, public files and environment backup. Restore old discovery files if returning to the old static implementation. Clear/rebuild Laravel caches and confirm site availability, forms and canonical/robots behaviour. Do not run migration rollback blindly; this change adds no migrations. If deployment fails after maintenance starts, the revised shell trap keeps maintenance enabled: it does not restore the previous release. Restore or complete the release and explicitly bring it up only after verification. Keep the previous deployment available until post-release checks pass. + +## Separate Google and directory checklist + +- [ ] Practice owns Search Console and Google Business Profile; grant appropriate management access, no duplicate profiles. +- [ ] Export available historical Search Console/profile data; record launch date. If absent, establish a prospective baseline. +- [ ] Verify domain property, indexing/security/manual-action status, submit approved sitemap and inspect homepage/new URLs and selected canonicals. +- [ ] Confirm profile name/categories, address/pin, phone, hours/holidays, services and appointment-request URL; approve real photos and any practitioner profiles. +- [ ] Confirm whether Serengeti or another real location exists before creating a page/profile. +- [ ] Correct Snupit location/category, Think Local former-location details, Medpages address, SAVET scope and McKenzie directory entries only against approved facts. Keep URL/field/replacement/requester/date/result register. +- [ ] Set an appropriate healthcare review/publication policy before any review requests; no automated review campaign has been added. +- [ ] Record local search observations by query, date, device and location; separate organic and Maps. No rank/volume claim is made here. +- [ ] Review comparable 28-day and rolling 90-day Search Console periods, non-brand queries, landing pages and device performance. Reception tracks qualified enquiries and confirmed appointments in its own system; report aggregates, not clinical data to analytics. +- [ ] Obtain PageSpeed Insights/mobile/desktop and field data where available. Local synthetic timings are not real-user Core Web Vitals or ranking evidence. + +## Original implementation results (before this review follow-up) + +- 36 PHP tests passed, 161 assertions; 2 JavaScript tests passed. +- Pint passed; PHPStan passed with 512 MB limit; production Vite build passed; route/config cache and `bash -n deploy.sh` passed. +- Chromium checked homepage, about, services and contact at 390px and 1440px: HTTP 200, one H1, no horizontal overflow, no uncaught JavaScript errors. Mobile menu open/Escape close and form validation checked. Local log-only accepted enquiry rendered successfully. Repeated browser QA initially reached the intended rate limit; the local cache was cleared before the final run. +- One unthrottled local Chromium run: mobile LCP 536 ms, CLS 0.000451; desktop LCP 636 ms, CLS 0.001085. Navigation DOMContentLoaded was 483 ms / 571 ms; resource transfers about 978 KB / 1,000 KB. Ten font preloads remain. These are local engineering observations, not Lighthouse scores, field Core Web Vitals, comparative gains or ranking forecasts. INP was not measured. + +Base revision: `42880b2892d4c22944b64dc0e5ba9e7b75b356bb` on `main`. +Branch: `feature/local-seo-patient-journey`. diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php new file mode 100644 index 0000000..089d925 --- /dev/null +++ b/app/Http/Controllers/SiteController.php @@ -0,0 +1,34 @@ +route()->getName()] ?? null; + abort_if($page === null, 404); + + return view('page', ['page' => $page]); + } + + public function sitemap(): Response + { + return response()->view('sitemap', ['pages' => Site::indexable() ? Site::pages() : []], 200) + ->header('Content-Type', 'application/xml'); + } + + public function robots(): Response + { + $text = Site::indexable() + ? "User-agent: *\nAllow: /\nSitemap: ".Site::url('/sitemap.xml')."\n" + : "User-agent: *\nDisallow: /\n"; + + return response($text)->header('Content-Type', 'text/plain'); + } +} diff --git a/app/Http/Middleware/CanonicalSite.php b/app/Http/Middleware/CanonicalSite.php new file mode 100644 index 0000000..29d77fc --- /dev/null +++ b/app/Http/Middleware/CanonicalSite.php @@ -0,0 +1,31 @@ +isProduction() && config('site.canonical_redirects') + && in_array($host, config('site.alternate_hosts'), true) + && in_array($request->getHost(), config('site.alternate_hosts'), true) + && in_array($request->method(), ['GET', 'HEAD'], true) + && $request->getSchemeAndHttpHost() !== $origin) { + return redirect($origin.$request->getRequestUri(), 301); + } + + $response = $next($request); + if (! Site::indexable()) { + $response->headers->set('X-Robots-Tag', 'noindex, nofollow'); + } + + return $response; + } +} diff --git a/app/Support/ProductionPreflight.php b/app/Support/ProductionPreflight.php new file mode 100644 index 0000000..e1895f0 --- /dev/null +++ b/app/Support/ProductionPreflight.php @@ -0,0 +1,59 @@ + Failed configuration keys, never their values. */ + public function failures(Repository $config): array + { + $failures = []; + foreach (['app.env' => 'production', 'app.debug' => false, 'site.indexable' => true, 'site.canonical_redirects' => true, 'contact.practice.location_verified' => true] as $key => $expected) { + if ($config->get($key) !== $expected) { + $failures[] = $key; + } + } + + if (! in_array($config->get('app.url'), ['https://danksandstrydom.co.za', 'https://danksandstrydom.co.za/'], true)) { + $failures[] = 'app.url'; + } + + $recipients = $config->get('contact.recipients'); + if (! is_array($recipients) || $recipients === [] || array_filter($recipients, fn (mixed $email): bool => ! $this->validEmail($email)) !== []) { + $failures[] = 'contact.recipients'; + } + if (! $this->validEmail($config->get('contact.practice.email'))) { + $failures[] = 'contact.practice.email'; + } + + $phone = $config->get('contact.practice.phone'); + $digits = is_string($phone) ? (preg_replace('/\D/', '', $phone) ?? '') : ''; + if (! is_string($phone) || ! preg_match('/^\+?[0-9 ()-]+$/', $phone) || strlen($digits) < 7 || strlen($digits) > 15 || trim($digits, '0') === '') { + $failures[] = 'contact.practice.phone'; + } + + foreach (['name', 'address', 'street', 'locality', 'region', 'postcode', 'country'] as $field) { + $key = 'contact.practice.'.$field; + $value = $config->get($key); + if (! is_string($value) || trim($value) === '') { + $failures[] = $key; + } + } + foreach (['map_embed_url', 'directions_url'] as $field) { + $key = 'contact.practice.'.$field; + $value = $config->get($key); + if (! is_string($value) || ! filter_var($value, FILTER_VALIDATE_URL) || parse_url($value, PHP_URL_SCHEME) !== 'https' || parse_url($value, PHP_URL_USER) !== null || parse_url($value, PHP_URL_PASS) !== null) { + $failures[] = $key; + } + } + + return $failures; + } + + private function validEmail(mixed $email): bool + { + return is_string($email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } +} diff --git a/app/Support/Site.php b/app/Support/Site.php new file mode 100644 index 0000000..be8fef2 --- /dev/null +++ b/app/Support/Site.php @@ -0,0 +1,53 @@ +isProduction() && (bool) config('site.indexable'); + } + + /** @return list> */ + public static function testimonials(): array + { + $entries = array_values(array_filter(config('site.testimonials.entries', []), fn (array $entry): bool => ($entry['approved'] ?? false) === true && filled($entry['quote'] ?? null) && filled($entry['display_name'] ?? null) + )); + $samples = $entries === [] && app()->environment('local', 'staging'); + if ($samples) { + $entries = config('site.testimonials.samples', []); + } + + return array_values(array_map(function (array $entry) use ($samples): array { + $source = $entry['source_url'] ?? null; + $entry['source_url'] = is_string($source) && filter_var($source, FILTER_VALIDATE_URL) + && in_array(parse_url($source, PHP_URL_SCHEME), ['https', 'http'], true) ? $source : null; + $entry['sample'] = $samples; + + return $entry; + }, $entries)); + } + + /** @return array> */ + public static function services(): array + { + return array_filter(self::pages(), fn (array $page): bool => str_starts_with($page['path'], '/services/')); + } + + /** @return array> */ + public static function pages(): array + { + return array_filter(config('site.pages'), fn (array $page): bool => $page['published']); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 7a2848f..4620278 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware): void { - // + $middleware->append(CanonicalSite::class); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/config/contact.php b/config/contact.php index 17ee9d2..f68ea8c 100644 --- a/config/contact.php +++ b/config/contact.php @@ -30,16 +30,19 @@ 'practice' => [ 'name' => 'Danks & Strydom Physiotherapy', - 'phone' => env('CONTACT_PHONE', '+27 00 000 0000'), - 'email' => env('CONTACT_EMAIL', 'hello@danksandstrydom.co.za'), - 'address' => env('CONTACT_ADDRESS', '123 Wellness Avenue, Suite 4, Cape Town, 8001'), - 'hours' => [ - 'Monday - Friday' => '07:30 - 17:30', - 'Saturday' => 'Closed', - 'Sunday & Public Holidays' => 'Closed', - ], - // Replace with the real Google Maps embed URL when available. - 'map_embed_url' => env('CONTACT_MAP_EMBED_URL', "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d7167.225550505287!2d28.24767237619273!3d-26.078908558884297!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x1e9514d786f1b4bf%3A0x32e2cb459495985a!2s196%20Monument%20Rd%2C%20Glen%20marais%2C%20Johannesburg%2C%201619!5e0!3m2!1sen!2sza!4v1781261967043!5m2!1sen!2sza"), + 'phone' => env('CONTACT_PHONE', '011 391 3126'), + 'email' => env('CONTACT_EMAIL', 'admin@danksandstrydom.co.za'), + 'address' => env('CONTACT_ADDRESS', 'Suite 102, Surgiklin Studios, Unit 12, Glen Eagle Office Park, Koorsboom Avenue, Glen Marais, Kempton Park, 1619, Gauteng, South Africa'), + 'location_verified' => env('CONTACT_LOCATION_VERIFIED', true), + 'street' => env('CONTACT_STREET', 'Suite 102, Surgiklin Studios, Unit 12, Glen Eagle Office Park, Koorsboom Avenue, Glen Marais'), + 'locality' => env('CONTACT_LOCALITY', 'Kempton Park'), + 'region' => env('CONTACT_REGION', 'Gauteng'), + 'postcode' => env('CONTACT_POSTCODE', '1619'), + 'country' => 'ZA', + 'arrival' => 'At the entrance gate, press the access button and wait to be let in. Once inside, park and enter the building. Follow the passage on your left to the end, where you will find Danks & Strydom Physiotherapy.', + 'access' => 'Parking and covered parking are available. The building has ramp access and an elevator.', + 'hours' => env('CONTACT_HOURS', 'Monday–Friday: 07:30–17:30. Saturday: By appointment. Sunday and public holidays: Closed.'), + 'map_embed_url' => env('CONTACT_MAP_EMBED_URL', 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3583.7425911415435!2d28.2610527!3d-26.074667599999998!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x1e9515b5389c59e3%3A0xc9913cffe2473c26!2sDanks%20And%20Strydom!5e0!3m2!1sen!2sza!4v1789387905460!5m2!1sen!2sza'), + 'directions_url' => env('CONTACT_DIRECTIONS_URL', 'https://maps.app.goo.gl/WoxWSjSzNbtEkTr66'), ], - ]; diff --git a/config/imagery.php b/config/imagery.php new file mode 100644 index 0000000..7601f72 --- /dev/null +++ b/config/imagery.php @@ -0,0 +1,71 @@ + [ + 'consultation' => [ + 'path' => 'images/illustrations/consultation-1200.webp', + 'alt' => 'Illustrative consultation between an adult patient and a physiotherapist', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/consultation-480.webp', 768 => 'images/illustrations/consultation-768.webp', 1200 => 'images/illustrations/consultation-1200.webp'], + ], + 'neck' => [ + 'path' => 'images/illustrations/neck-1200.webp', + 'alt' => 'Illustrative seated neck movement assessment', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/neck-480.webp', 768 => 'images/illustrations/neck-768.webp', 1200 => 'images/illustrations/neck-1200.webp'], + ], + 'sports' => [ + 'path' => 'images/illustrations/sports-1200.webp', + 'alt' => 'Illustrative supervised bodyweight exercise', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/sports-480.webp', 768 => 'images/illustrations/sports-768.webp', 1200 => 'images/illustrations/sports-1200.webp'], + ], + 'knee' => [ + 'path' => 'images/illustrations/knee-1200.webp', + 'alt' => 'Illustrative gentle seated knee rehabilitation', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/knee-480.webp', 768 => 'images/illustrations/knee-768.webp', 1200 => 'images/illustrations/knee-1200.webp'], + ], + 'shoulder' => [ + 'path' => 'images/illustrations/shoulder-1200.webp', + 'alt' => 'Illustrative shoulder movement assessment', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/shoulder-480.webp', 768 => 'images/illustrations/shoulder-768.webp', 1200 => 'images/illustrations/shoulder-1200.webp'], + ], + 'mobility' => [ + 'path' => 'images/illustrations/mobility-1200.webp', + 'alt' => 'Illustrative walking and mobility assessment', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/mobility-480.webp', 768 => 'images/illustrations/mobility-768.webp', 1200 => 'images/illustrations/mobility-1200.webp'], + ], + 'exercise' => [ + 'path' => 'images/illustrations/exercise-1200.webp', + 'alt' => 'Illustrative guided resistance-band exercise', + 'width' => 1200, + 'height' => 900, + 'variants' => [480 => 'images/illustrations/exercise-480.webp', 768 => 'images/illustrations/exercise-768.webp', 1200 => 'images/illustrations/exercise-1200.webp'], + ], + ], + 'pages' => [ + '/' => 'consultation', + '/about' => 'shoulder', + '/contact' => 'consultation', + '/services' => 'mobility', + '/services/sports-injury-rehabilitation' => 'sports', + '/services/back-neck-pain' => 'neck', + '/services/post-operative-rehabilitation' => 'knee', + '/services/joint-muscle-pain' => 'shoulder', + '/services/mobility-movement-assessment' => 'mobility', + '/services/chronic-pain-management' => 'consultation', + '/services/injury-prevention' => 'sports', + '/services/rehabilitation-exercise-programmes' => 'exercise', + '/patient-information' => 'neck', + ], +]; diff --git a/config/site.php b/config/site.php new file mode 100644 index 0000000..ee7ffe1 --- /dev/null +++ b/config/site.php @@ -0,0 +1,287 @@ + env('SITE_INDEXABLE', false), + 'canonical_redirects' => env('SITE_CANONICAL_REDIRECTS', false), + 'alternate_hosts' => ['danksandstrydom.co.za', 'www.danksandstrydom.co.za'], + 'analytics_enabled' => env('SITE_ANALYTICS_ENABLED', false), + 'testimonials' => [ + // Add genuine quotes only after confirming provenance and permission for website reuse. + // Each entry: quote, display_name (as permitted), source_url (null if none), approved (boolean). + // Approval applies to the exact quote and display name; keep consent evidence privately, not in Git. + // User confirmed these screenshot quotations are genuine and permitted for website use. + // Source: supplied screenshot, top row; no public source URLs were supplied. + 'entries' => [ + [ + 'quote' => 'After months of lower back pain, I finally feel like myself again. They took real time to understand my situation, and the exercises actually made a difference.', + 'display_name' => 'Sarah M.', + 'source_url' => null, + 'approved' => true, + ], + [ + 'quote' => 'The guidance after my surgery was clear and reassuring at every step. I always knew what to do next and felt fully supported throughout.', + 'display_name' => 'Priya N.', + 'source_url' => null, + 'approved' => true, + ], + [ + 'quote' => 'What stood out was how much they listened. The calm, friendly approach made a real difference — I finally have practical tools to manage my pain.', + 'display_name' => 'Lerato K.', + 'source_url' => null, + 'approved' => true, + ], + ], + // Design samples are isolated from publication entries and can NEVER render in production. + 'samples' => [ + ['quote' => 'This space shows how a short approved quotation will sit within the card.', 'display_name' => 'Reviewer display name', 'source_url' => null, 'approved' => false], + ['quote' => 'Replace this sample copy with an exact quotation you have permission to publish. This longer example helps preview the spacing and line wrapping.', 'display_name' => 'Reviewer display name', 'source_url' => null, 'approved' => false], + ['quote' => 'An approved quotation and the permitted display name will appear here.', 'display_name' => 'Reviewer display name', 'source_url' => null, 'approved' => false], + ], + ], + 'pages' => [ + 'about' => [ + 'path' => '/about', + 'published' => true, + 'title' => 'About Danks & Strydom', + 'description' => 'Meet Elize Strydom and Cheryl Myburgh at Danks & Strydom Physiotherapy in Glen Marais, Kempton Park.', + 'heading' => 'About Danks & Strydom', + 'intro' => 'Danks & Strydom is a physiotherapy practice in Glen Marais, Kempton Park. Elize Strydom and Cheryl Myburgh both hold degrees in physiotherapy.', + 'sections' => [ + 'Care that starts with you' => 'Your first visit starts with an assessment of what is troubling you and the activities you want help with. Both physiotherapists provide back and neck pain care, sports injury rehabilitation and post-operative rehabilitation.', + ], + 'practitioners' => [ + [ + 'name' => 'Cheryl Myburgh', + 'title' => 'Physiotherapist', + // Editable draft using supplied facts only; equine experience is biographical, not a practice service. + 'biography' => 'Cheryl Myburgh is a physiotherapist with a degree in physiotherapy and extensive experience in human and equine physiotherapy.', + // Supply exact degree/qualification titles and awarding universities as lists. + // Physiotherapy degrees and the facts used in the draft above are already confirmed. + 'qualifications' => [], + 'universities' => [], + // Optional longer factual biography; do not replace confirmed wording with filler. + 'expanded_biography' => null, + // Supply the languages this practitioner offers consultations in. + 'languages' => [], + // Supply an approved portrait's public-relative asset path and descriptive alt text together. + 'photo' => ['path' => null, 'alt' => null], + ], + [ + 'name' => 'Elize Strydom', + 'title' => 'Physiotherapist', + // Editable draft using supplied facts only. + 'biography' => 'Elize Strydom is a physiotherapist with a degree in physiotherapy and experience in human physiotherapy.', + // Supply exact degree/qualification titles and awarding universities as lists. + // Physiotherapy degrees and the facts used in the draft above are already confirmed. + 'qualifications' => [], + 'universities' => [], + // Optional longer factual biography; do not replace confirmed wording with filler. + 'expanded_biography' => null, + // Supply the languages this practitioner offers consultations in. + 'languages' => [], + // Supply an approved portrait's public-relative asset path and descriptive alt text together. + 'photo' => ['path' => null, 'alt' => null], + ], + ], + ], + 'contact' => [ + 'path' => '/contact', + 'published' => true, + 'title' => 'Contact & Directions', + 'description' => 'Contact Danks & Strydom Physiotherapy in Glen Marais, Kempton Park. Request an appointment and confirm directions before your visit.', + 'heading' => 'Contact and directions', + 'intro' => 'Get in touch with reception to request a time, ask a practical question or find the rooms.', + 'sections' => [ + 'Arranging an appointment' => 'Call during opening hours for availability, including same-day appointments. You can also send a request by email or through the form below.', + 'When your booking is confirmed' => 'Reception will confirm a date and time with you. For preparation, payment and cancellation details, read Patient Information before your visit.', + ], + ], + 'services' => [ + 'path' => '/services', + 'published' => true, + 'title' => 'Physiotherapy Services in Kempton Park', + 'description' => 'Explore physiotherapy services at Danks & Strydom in Glen Marais, Kempton Park. Find care for pain, movement and rehabilitation.', + 'heading' => 'Physiotherapy services', + 'intro' => 'Find support for pain, movement difficulties and rehabilitation. Explore the eight services below to see which focus most closely matches what you want help with.', + 'sections' => [ + 'Not sure where to start?' => 'You do not need to identify the cause of a problem or choose a treatment before booking. Tell reception what you would like help with and arrange a first assessment.', + ], + ], + 'sports-injury-rehabilitation' => [ + 'path' => '/services/sports-injury-rehabilitation', + 'published' => true, + 'title' => 'Sports Physio in Kempton Park', + 'description' => 'Physiotherapy for sports injuries, from assessing painful movements to planning a gradual return to activity at Danks & Strydom in Kempton Park.', + 'heading' => 'Sports injury rehabilitation', + 'intro' => 'An injury can interrupt training and make movements you normally take for granted feel difficult. Sports rehabilitation focuses on the demands of your activity and the steps towards taking part again.', + 'sections' => [ + 'Your sport and your starting point' => 'Tell us how the injury happened, what has changed in your training and which activities you want to return to. Pain, weakness or difficulty with a particular movement can all be useful starting points for an assessment.', + 'Looking beyond the painful movement' => 'The assessment may explore movement, strength and how the affected area responds to tasks relevant to your sport. Your recent activity and current abilities help establish a starting point for rehabilitation.', + 'Building towards activity' => 'Rehabilitation may involve guided exercise and changes to activity while you recover. The aim is to work towards the movements your sport requires, with progression based on your response rather than a fixed return date.', + 'Reviewing readiness' => 'Follow-up can revisit the tasks that were difficult, your confidence in movement and how you have managed activity between visits. Together, you and your physiotherapist can decide what to build on and what still needs attention.', + ], + 'card_title' => 'Sports Injury Rehabilitation', + 'service_icon' => 'pulse', + 'card_description' => 'Care for injuries affecting training, sport and a return to activity.', + 'related_services' => ['injury-prevention', 'rehabilitation-exercise-programmes'], + ], + 'back-neck-pain' => [ + 'path' => '/services/back-neck-pain', + 'published' => true, + 'title' => 'Back & Neck Pain Physio in Kempton Park', + 'description' => 'Physiotherapy for back and neck pain at Danks & Strydom in Glen Marais. Explore support for everyday movement, comfort and activity.', + 'heading' => 'Back and neck pain', + 'intro' => 'Back or neck pain can make sitting, turning, lifting or sleeping uncomfortable. Physiotherapy looks at how symptoms affect your day and helps you find a practical way forward.', + 'sections' => [ + 'How pain affects your day' => 'You might want help with stiffness on turning your head, discomfort during desk work or difficulty bending and lifting. Explain when symptoms appear, what changes them and which parts of your routine have become harder.', + 'Understanding movement' => 'Your physiotherapist may check comfortable movement, the affected area and relevant everyday tasks. The assessment brings your symptoms and activity together rather than assuming that one posture or movement explains every problem.', + 'Finding workable next steps' => 'Care may include exercise guidance and advice on approaching activities more comfortably. Treatment is selected after the assessment, with room to ask questions and understand how the plan fits your routine.', + 'Noticing meaningful changes' => 'At follow-up, changes in movement and day-to-day comfort help show how the plan is working. Your physiotherapist can adjust guidance if difficulties continue or your activity changes.', + ], + 'card_title' => 'Back & Neck Pain Treatment', + 'service_icon' => 'spine', + 'card_description' => 'Support for back and neck discomfort affecting work, rest and daily movement.', + 'related_services' => ['joint-muscle-pain', 'chronic-pain-management'], + ], + 'post-operative-rehabilitation' => [ + 'path' => '/services/post-operative-rehabilitation', + 'published' => true, + 'title' => 'Post-Operative Physio in Kempton Park', + 'description' => 'Physiotherapy after surgery in Glen Marais, Kempton Park. Rehabilitation shaped around your operation, surgical guidance and everyday recovery goals.', + 'heading' => 'Rehabilitation after surgery', + 'intro' => 'After surgery, everyday tasks can take more effort. Post-operative rehabilitation supports movement and function while taking account of your procedure and the guidance from your surgical team.', + 'sections' => [ + 'Starting from your operation' => 'Tell your physiotherapist about the operation, your current difficulties and any restrictions or instructions from your surgical team. Getting around, managing stairs or returning to ordinary tasks may be priorities, depending on the procedure.', + 'Checking your current abilities' => 'The assessment may look at movement, strength and how you manage relevant activities within those restrictions. Your stage of recovery matters when deciding which tasks are appropriate to assess.', + 'Rehabilitation at an appropriate pace' => 'Treatment may include guided movement and rehabilitation exercises suited to your current abilities. Progression should remain consistent with surgical instructions; a programme for one operation will not necessarily suit another.', + 'Following your recovery' => 'Reviews provide time to check function, ask questions about the programme and consider the next steps. Changes in surgical advice or difficulties between visits can affect how rehabilitation progresses.', + ], + 'card_title' => 'Post-Operative Rehabilitation', + 'service_icon' => 'recovery', + 'card_description' => 'Rehabilitation shaped around your operation, recovery stage and daily activities.', + 'related_services' => ['rehabilitation-exercise-programmes', 'mobility-movement-assessment'], + ], + 'joint-muscle-pain' => [ + 'path' => '/services/joint-muscle-pain', + 'published' => true, + 'title' => 'Joint & Muscle Pain Physio in Kempton Park', + 'description' => 'Assessment and physiotherapy for joint discomfort and muscular aches at Danks & Strydom in Kempton Park, with everyday movement in mind.', + 'heading' => 'Joint & Muscle Pain', + 'intro' => 'A sore joint or aching muscle can make reaching, walking or carrying things uncomfortable. This service focuses on the affected area and the everyday movements you want to manage more easily.', + 'card_title' => 'Joint & Muscle Pain', + 'service_icon' => 'joint', + 'sections' => [ + 'The difficulties you notice' => 'Pain, stiffness or weakness may affect one movement or several activities. You can describe where you feel it, when it started and whether the pattern changes through the day or after activity.', + 'Examining the affected area' => 'An assessment may explore joint movement, muscle function and tasks that reproduce your difficulty. Your physiotherapist will consider your account alongside these findings before explaining possible next steps.', + 'Treatment with a practical focus' => 'Care may involve movement guidance or exercises suited to the affected area. The plan connects treatment to a useful goal, such as reaching more comfortably or managing a daily task, rather than treating discomfort in isolation.', + 'Checking the response' => 'Follow-up can compare comfort, movement and the tasks that first brought you in. Your response helps determine whether to continue, adjust the approach or reconsider what support is needed.', + ], + 'card_description' => 'Assessment of joint discomfort, muscular aches and difficulties with everyday movement.', + 'related_services' => ['mobility-movement-assessment', 'back-neck-pain'], + ], + 'mobility-movement-assessment' => [ + 'path' => '/services/mobility-movement-assessment', + 'published' => true, + 'title' => 'Movement Assessment in Glen Marais, Kempton Park', + 'description' => 'Understand difficulties with walking, bending or reaching through a movement assessment at Danks & Strydom in Glen Marais, Kempton Park.', + 'heading' => 'Mobility & Movement Assessment', + 'intro' => 'Movement can feel limited even when pain is not the main concern. A mobility and movement assessment looks at what is difficult, how you manage it and what you would like to do more easily.', + 'card_title' => 'Mobility & Movement Assessment', + 'service_icon' => 'mobility', + 'sections' => [ + 'Start with an everyday task' => 'Perhaps reaching a shelf feels restricted, getting up from a chair takes effort or walking feels less comfortable than before. Choose the activities that matter most so the assessment has a clear focus.', + 'Looking at how you move' => 'Your physiotherapist may observe those tasks and check relevant movement, strength or balance. The aim is to understand the limitation in context, including how it affects your independence and routine.', + 'Turning findings into a plan' => 'The next step may be guidance for a particular task, an exercise approach or further treatment. The assessment helps identify which support is relevant; you do not need to choose a programme before your first visit.', + 'Revisiting the same tasks' => 'If follow-up is useful, repeating meaningful tasks can help track change. You can also raise new difficulties or activities you would like to work towards.', + ], + 'card_description' => 'Understand movement limitations and how they affect the activities that matter to you.', + 'related_services' => ['joint-muscle-pain', 'rehabilitation-exercise-programmes'], + ], + 'chronic-pain-management' => [ + 'path' => '/services/chronic-pain-management', + 'published' => true, + 'title' => 'Persistent Pain Support in Kempton Park', + 'description' => 'Physiotherapy support for persistent pain in Glen Marais. Work towards manageable activity and everyday function with Danks & Strydom.', + 'heading' => 'Chronic Pain Management', + 'intro' => 'Living with persistent pain can affect your routine, energy and confidence in movement. Physiotherapy offers support for finding manageable ways to stay involved in the activities that matter to you.', + 'card_title' => 'Chronic Pain Management', + 'service_icon' => 'chronic', + 'sections' => [ + 'Your experience matters' => 'There is time to explain how pain affects work, rest and the things you enjoy, including what you have already tried. A useful goal might be doing an everyday activity more comfortably or feeling more confident about moving.', + 'A broader view of activity' => 'Assessment may explore relevant movements, your usual activity pattern and how symptoms respond. Both easier and more difficult days help build a picture of what is manageable for you.', + 'Working towards manageable goals' => 'Guidance may involve finding a suitable starting level of activity and gradually adapting it. The focus is on practical steps that fit your circumstances, with treatment selected around your assessment and priorities.', + 'Progress beyond a pain score' => 'Reviews can consider what you are able to do, how confident you feel and how the plan works on difficult days. These changes matter alongside symptoms and can help shape ongoing support.', + ], + 'card_description' => 'Support for living with persistent pain and building manageable everyday activity.', + 'related_services' => ['joint-muscle-pain', 'rehabilitation-exercise-programmes'], + ], + 'injury-prevention' => [ + 'path' => '/services/injury-prevention', + 'published' => true, + 'title' => 'Injury Risk & Movement Assessment in Kempton Park', + 'description' => 'Prepare for activity with movement assessment and injury-risk guidance at Danks & Strydom Physiotherapy in Glen Marais, Kempton Park.', + 'heading' => 'Injury Prevention', + 'intro' => 'Starting an activity, returning after a break or increasing its demands can raise questions about preparation. Injury prevention focuses on understanding those demands and reducing avoidable risks.', + 'card_title' => 'Injury Prevention', + 'service_icon' => 'shield', + 'sections' => [ + 'Preparing for your activity' => 'You may be starting a sport, returning after time away or finding that an old difficulty affects a new activity. Your history, current activity and intended demands provide a useful starting point.', + 'Assessing relevant movement' => 'The assessment may explore movements used in your activity and areas where strength or control make a task difficult. It is a way to identify useful preparation, not a test that can predict every future injury.', + 'Making practical adjustments' => 'Guidance may cover building activity gradually, preparing for its demands and recognising when to adjust the workload. Recommendations depend on your assessment and the activity you want to pursue.', + 'Reviewing as demands change' => 'A return visit may help when your activity increases or a difficulty persists. You can review how the advice is working and whether preparation needs to change with your goals.', + ], + 'card_description' => 'Prepare for activity with guidance informed by your movement and its demands.', + 'related_services' => ['mobility-movement-assessment', 'sports-injury-rehabilitation'], + ], + 'rehabilitation-exercise-programmes' => [ + 'path' => '/services/rehabilitation-exercise-programmes', + 'published' => true, + 'title' => 'Rehabilitation Exercise Guidance in Kempton Park', + 'description' => 'Rehabilitation exercise guidance at Danks & Strydom in Kempton Park. Individual starting points, clear progression and follow-up for your movement goals.', + 'heading' => 'Rehabilitation Exercise Programmes', + 'intro' => 'Knowing how to begin and when to progress can make rehabilitation exercises easier to follow. An individual programme connects exercise to the movement or activity you are working towards.', + 'card_title' => 'Rehabilitation Exercise Programmes', + 'service_icon' => 'program', + 'sections' => [ + 'A starting point that fits you' => 'Bring your questions about exercise, including movements you find difficult or parts of a previous programme you are unsure about. Your physiotherapist will consider your current function and rehabilitation goals before selecting exercises.', + 'Understanding the programme' => 'Guidance may cover the purpose of an exercise, how to approach it and what to do if it is difficult. Exercises can focus on relevant movement, strength or control, depending on the assessment.', + 'Progression with a reason' => 'A programme can change as tasks become more manageable. Progression may mean adjusting the exercise or its demands rather than simply adding more; the next step should relate to your goal and response.', + 'Feedback and follow-up' => 'At review, explain what has worked in your routine and where you have struggled. This gives your physiotherapist a basis for adapting the programme and clarifying how to continue between appointments.', + ], + 'card_description' => 'Individual exercise guidance, progression and follow-up to support your rehabilitation.', + 'related_services' => ['post-operative-rehabilitation', 'mobility-movement-assessment'], + ], + 'patient-information' => [ + 'path' => '/patient-information', + 'published' => true, + 'title' => 'Patient Information', + 'description' => 'Prepare for your visit to Danks & Strydom Physiotherapy in Glen Marais, Kempton Park. Contact the practice for appointment and payment details.', + 'heading' => 'Before your first appointment', + 'intro' => 'Practical information to help you arrange your appointment and prepare for your visit.', + // Optional additions: null bodies are omitted in every environment. Track missing facts in CONTENT-TODO.md. + 'optional_sections' => [ + // Supply current consultation fees and currency, only if amounts should be displayed. + 'consultation_fees' => ['heading' => 'Current consultation fees', 'body' => null], + // Supply when payment is due; cash/card acceptance is already confirmed. + 'payment_timing' => ['heading' => 'When payment is due', 'body' => null], + // Supply who submits claims and whether patients pay first; do not infer scheme coverage. + 'medical_aid_claims' => ['heading' => 'Submitting medical-aid claims', 'body' => null], + // Supply responsibility and next steps if a medical-aid claim is declined. + 'declined_claims' => ['heading' => 'Declined medical-aid claims', 'body' => null], + // Supply a separate missed-appointment policy, if applicable. The 24-hour cancellation policy is confirmed. + 'missed_appointments' => ['heading' => 'Missed appointments', 'body' => null], + ], + 'sections' => [ + 'Arranging an appointment' => 'Please arrange an appointment before visiting. Call the practice during opening hours to check availability, including appointments for the same day. You can also request an appointment by email or through our website.', + 'When your booking is confirmed' => 'Walk-ins can be accommodated only when an appointment slot is available; please speak to reception on arrival. Website and email enquiries are appointment requests. Your appointment is confirmed once the practice agrees a date and time with you.', + 'Your first appointment' => 'Appointments are one hour. New patients complete a patient-information form. Your physiotherapist will assess you before providing appropriate treatment.', + 'What to bring' => 'Patients are not expected to bring anything to their appointment.', + 'Follow-up appointments' => 'Your physiotherapist will discuss whether further appointments are needed and when you should return. Follow-up timing depends on your assessment and progress. If you need advice before your next appointment, contact the practice by telephone or message to discuss whether an earlier visit is needed.', + 'Referrals' => 'You can book directly with the practice without a doctor’s referral. If you intend to use medical aid, check whether your scheme has any separate requirements.', + 'Fees and payment' => 'Contact the practice for current consultation fees. Cash and card payments are accepted. Please ask reception about medical-aid payment arrangements.', + 'Medical aid' => 'Please contact reception to confirm medical-aid claim arrangements and any amount payable by you. Check your benefits and requirements with your scheme before your appointment.', + 'Cancellations and rescheduling' => 'Please give at least 24 hours’ notice if you need to cancel or reschedule. With less than 24 hours’ notice, you may be liable for the full appointment fee. Contact the practice to change your booking.', + ], + ], + ], +]; diff --git a/deploy.sh b/deploy.sh index 1bb8fcf..4d0604d 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,19 +1,18 @@ #!/bin/bash set -euo pipefail -APP_DIR="/home/danks/repositories/DanksAndStrydom" -PUBLIC_DIR="/home/danks/public_html" -PHP_BIN="/usr/local/bin/php" -COMPOSER="/home/danks/composer.phar" +APP_DIR="${DANKS_DEPLOY_APP_DIR:-/home/danks/repositories/DanksAndStrydom}" +PUBLIC_DIR="${DANKS_DEPLOY_PUBLIC_DIR:-/home/danks/public_html}" +PHP_BIN="${DANKS_DEPLOY_PHP_BIN:-/usr/local/bin/php}" +COMPOSER="${DANKS_DEPLOY_COMPOSER:-/home/danks/composer.phar}" BRANCH="main" LOG_FILE="$APP_DIR/storage/logs/deploy.log" -LOCK_FILE="/tmp/danksandstrydom-deploy.lock" +LOCK_FILE="${DANKS_DEPLOY_LOCK_FILE:-/tmp/danksandstrydom-deploy.lock}" # Required when running Composer from a web-triggered process. -export HOME="/home/danks" -export COMPOSER_HOME="/home/danks/.composer" -export COMPOSER_CACHE_DIR="/home/danks/.composer/cache" +export COMPOSER_HOME="${COMPOSER_HOME:-/home/danks/.composer}" +export COMPOSER_CACHE_DIR="${COMPOSER_CACHE_DIR:-/home/danks/.composer/cache}" mkdir -p "$APP_DIR/storage/logs" mkdir -p "$COMPOSER_HOME" @@ -31,8 +30,23 @@ bring_app_up() { exit 1 } - # If anything fails after maintenance mode starts, this ensures the site is not left down. - trap bring_app_up EXIT + umask 077 + CANDIDATE_DIR="$(mktemp -d /tmp/danks-release.XXXXXXXX)" + MAINTENANCE_STARTED=false + RELEASE_READY=false + cleanup() { + rm -rf "$CANDIDATE_DIR" + if [ "$MAINTENANCE_STARTED" = true ]; then + if [ "$RELEASE_READY" = true ]; then + bring_app_up + else + echo "Deployment did not complete; maintenance mode retained. Restore the prior release or resolve the failure before bringing the site up." + fi + fi + } + trap cleanup EXIT + trap 'exit 130' INT + trap 'exit 143' TERM echo "" echo "==================================================" @@ -41,14 +55,37 @@ bring_app_up() { cd "$APP_DIR" - echo "Putting app into maintenance mode..." - $PHP_BIN artisan down || true - - echo "Fetching latest code..." + echo "Fetching candidate code without changing the active checkout..." git fetch origin "$BRANCH" + TARGET_SHA="$(git rev-parse "origin/$BRANCH^{commit}")" + git archive "$TARGET_SHA" | tar -x -C "$CANDIDATE_DIR" - echo "Resetting working tree to origin/$BRANCH..." - git reset --hard "origin/$BRANCH" + # Explicit .env semantics must match the later Artisan cache rebuild. + if [ ! -f "$APP_DIR/.env" ] || [ -f "$APP_DIR/.env.production" ]; then + echo "Preflight requires an existing .env and no overriding .env.production. Resolve privately." + exit 1 + fi + ENV_DIGEST="$(sha256sum "$APP_DIR/.env")" + cp "$APP_DIR/.env" "$CANDIDATE_DIR/.env" + chmod 600 "$CANDIDATE_DIR/.env" + + # Independent vendor avoids loading the active checkout's classes via its classmap. + # No candidate Composer scripts or plugins may run before validation. + echo "Preparing isolated candidate dependencies..." + (cd "$CANDIDATE_DIR" && "$PHP_BIN" "$COMPOSER" install --no-dev --no-scripts --no-plugins --prefer-dist --optimize-autoloader --no-interaction) + "$PHP_BIN" "$CANDIDATE_DIR/preflight.php" "$CANDIDATE_DIR/.env" + test -f "$CANDIDATE_DIR/public/build/manifest.json" + if [ "$ENV_DIGEST" != "$(sha256sum "$APP_DIR/.env")" ]; then + echo "Environment changed during preflight; aborting before activation." + exit 1 + fi + + echo "Preflight passed. Putting app into maintenance mode..." + "$PHP_BIN" artisan down + MAINTENANCE_STARTED=true + + echo "Activating the exact validated revision..." + git reset --hard "$TARGET_SHA" echo "Installing Composer dependencies..." $PHP_BIN "$COMPOSER" install --no-dev --prefer-dist --optimize-autoloader --no-interaction @@ -63,15 +100,13 @@ bring_app_up() { cp -R "$APP_DIR/public/images" "$PUBLIC_DIR/images" fi - echo "Copying SEO discovery files..." - cp "$APP_DIR/public/robots.txt" "$PUBLIC_DIR/robots.txt" - cp "$APP_DIR/public/sitemap.xml" "$PUBLIC_DIR/sitemap.xml" + echo "Removing obsolete static SEO files so Laravel handles environment-aware discovery..." + rm -f "$PUBLIC_DIR/robots.txt" "$PUBLIC_DIR/sitemap.xml" echo "Fixing permissions..." chmod -R 775 "$APP_DIR/storage" || true chmod -R 775 "$APP_DIR/bootstrap/cache" || true chmod -R 755 "$PUBLIC_DIR/build" || true - chmod 644 "$PUBLIC_DIR/robots.txt" "$PUBLIC_DIR/sitemap.xml" || true if [ -d "$PUBLIC_DIR/images" ]; then chmod -R 755 "$PUBLIC_DIR/images" || true @@ -86,6 +121,10 @@ bring_app_up() { echo "Rebuilding Laravel caches..." $PHP_BIN artisan optimize + echo "Checking the rebuilt effective production configuration..." + "$PHP_BIN" "$APP_DIR/preflight.php" "$APP_DIR/.env" --cached + RELEASE_READY=true + echo "Deployment completed: $(date)" echo "==================================================" diff --git a/phpstan.neon b/phpstan.neon index 1ca23a4..7c37a28 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,5 +9,6 @@ parameters: - config/ - database/ - routes/ + - preflight.php level: 7 diff --git a/preflight.php b/preflight.php new file mode 100644 index 0000000..47bf467 --- /dev/null +++ b/preflight.php @@ -0,0 +1,47 @@ +load(); + $cached = in_array('--cached', $argv ?? [], true); + $app = $cached ? new Application(__DIR__) : new class(__DIR__) extends Application + { + public function getCachedConfigPath(): string + { + return $this->bootstrapPath('cache/preflight-uncached.php'); + } + }; + if ($app->configurationIsCached() !== $cached) { + throw new RuntimeException('Unexpected configuration cache state.'); + } + (new LoadConfiguration)->bootstrap($app); + $failures = (new ProductionPreflight)->failures($app->make('config')); + if ($failures !== []) { + fwrite(STDERR, 'Production preflight rejected configuration keys: '.implode(', ', $failures).PHP_EOL); + exit(1); + } + fwrite(STDOUT, 'Production preflight passed; configuration values were not printed.'.PHP_EOL); +} catch (Throwable) { + fwrite(STDERR, 'Production preflight could not validate configuration. Check the environment file and candidate dependencies privately.'.PHP_EOL); + exit(1); +} diff --git a/public/build/assets/app-Cyd-Yril.css b/public/build/assets/app-Cyd-Yril.css new file mode 100644 index 0000000..d38fab3 --- /dev/null +++ b/public/build/assets/app-Cyd-Yril.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-serif:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-100:oklch(95.3% .051 180.801);--color-teal-200:oklch(91% .096 180.426);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-teal-800:oklch(43.7% .078 188.216);--color-teal-900:oklch(38.6% .063 188.416);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-800:oklch(45% .085 224.283);--color-cyan-900:oklch(39.8% .07 227.392);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-100:oklch(95.2% .037 318.852);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-fuchsia-600:oklch(59.1% .293 322.896);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-fuchsia-800:oklch(45.2% .211 324.591);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-fuchsia-950:oklch(29.3% .136 325.661);--color-pink-50:oklch(97.1% .014 343.198);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-600:oklch(59.2% .249 .584);--color-pink-700:oklch(52.5% .223 3.958);--color-pink-800:oklch(45.9% .187 3.815);--color-pink-900:oklch(40.8% .153 2.432);--color-pink-950:oklch(28.4% .109 3.907);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--radius-4xl:2rem;--shadow-2xs:0 1px #0000000d;--shadow-xs:0 1px 2px 0 #0000000d;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--shadow-lg:0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;--shadow-xl:0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;--shadow-2xl:0 25px 50px -12px #00000040;--inset-shadow-2xs:inset 0 1px #0000000d;--inset-shadow-xs:inset 0 1px 1px #0000000d;--inset-shadow-sm:inset 0 2px 4px #0000000d;--drop-shadow-xs:0 1px 1px #0000000d;--drop-shadow-sm:0 1px 2px #00000026;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--drop-shadow-xl:0 9px 7px #0000001a;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-xs:4px;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--perspective-dramatic:100px;--perspective-near:300px;--perspective-normal:500px;--perspective-midrange:800px;--perspective-distant:1200px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-display:"Fraunces", ui-serif, Georgia, Cambria, "Times New Roman", serif;--color-brand:#174780;--color-brand-hover:#123a69;--color-navy:#102b4e;--color-link:#006d88;--color-accent:#009bc2;--color-card:#fff;--color-surface-50:#fafcfe;--color-surface-100:#edf5fa;--color-surface-200:#dcebf5;--color-ink-100:#dfecf5;--color-ink-200:#c3d8e8;--color-ink-300:#a6c3d9;--color-ink-400:#91aec5;--color-ink-500:#4c6478;--color-ink-600:#4c6478;--color-ink-700:#354f68;--color-ink-800:#243e58;--color-ink-900:#172c42;--color-accent-100:#dcf3fa;--color-accent-200:#b9e7f3;--color-accent-300:#8bd9ed;--color-accent-400:#5ccae4;--color-accent-500:#009bc2;--color-accent-600:#006d88;--color-accent-700:#006d88;--color-accent-900:#12385a;--color-success-surface:#d8f2e7;--color-success-ink:#1c6054;--shadow-ink:16, 43, 78;--animate-drift:drift 11s ease-in-out infinite;--animate-drift-slow:drift 16s ease-in-out infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(a,button,input,textarea,select,[tabindex]):focus-visible{outline:2px solid var(--color-link);outline-offset:2px;box-shadow:0 0 0 2px var(--color-card)}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.-top-4{top:calc(var(--spacing) * -4)}.-top-16{top:calc(var(--spacing) * -16)}.-top-24{top:calc(var(--spacing) * -24)}.-top-40{top:calc(var(--spacing) * -40)}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-10{top:calc(var(--spacing) * 10)}.top-16{top:calc(var(--spacing) * 16)}.top-24{top:calc(var(--spacing) * 24)}.top-32{top:calc(var(--spacing) * 32)}.top-44{top:calc(var(--spacing) * 44)}.-right-2{right:calc(var(--spacing) * -2)}.-right-5{right:calc(var(--spacing) * -5)}.-right-6{right:calc(var(--spacing) * -6)}.-right-16{right:calc(var(--spacing) * -16)}.-right-20{right:calc(var(--spacing) * -20)}.-right-32{right:calc(var(--spacing) * -32)}.-right-40{right:calc(var(--spacing) * -40)}.-bottom-6{bottom:calc(var(--spacing) * -6)}.-bottom-px{bottom:-1px}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-5{bottom:calc(var(--spacing) * 5)}.-left-14{left:calc(var(--spacing) * -14)}.-left-24{left:calc(var(--spacing) * -24)}.-left-32{left:calc(var(--spacing) * -32)}.-left-48{left:calc(var(--spacing) * -48)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-8{left:calc(var(--spacing) * 8)}.isolate{isolation:isolate}.-z-10{z-index:calc(10 * -1)}.-z-20{z-index:calc(20 * -1)}.z-50{z-index:50}.order-last{order:9999}.mx-auto{margin-inline:auto}.ms-1{margin-inline-start:calc(var(--spacing) * 1)}.-mt-2{margin-top:calc(var(--spacing) * -2)}.-mt-4{margin-top:calc(var(--spacing) * -4)}.-mt-6{margin-top:calc(var(--spacing) * -6)}.-mt-7{margin-top:calc(var(--spacing) * -7)}.-mt-\[6\.6rem\]{margin-top:-6.6rem}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-9{margin-top:calc(var(--spacing) * 9)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-14{margin-top:calc(var(--spacing) * 14)}.mt-20{margin-top:calc(var(--spacing) * 20)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.-ml-8{margin-left:calc(var(--spacing) * -8)}.-ml-px{margin-left:-1px}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-4\/3{aspect-ratio:4/3}.aspect-\[335\/364\]{aspect-ratio:335/364}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-5\.5{height:calc(var(--spacing) * 5.5)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-14\.5{height:calc(var(--spacing) * 14.5)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-44{height:calc(var(--spacing) * 44)}.h-56{height:calc(var(--spacing) * 56)}.h-72{height:calc(var(--spacing) * 72)}.h-80{height:calc(var(--spacing) * 80)}.h-96{height:calc(var(--spacing) * 96)}.h-112{height:calc(var(--spacing) * 112)}.h-136{height:calc(var(--spacing) * 136)}.h-full{height:100%}.h-px{height:1px}.min-h-80{min-height:calc(var(--spacing) * 80)}.min-h-screen{min-height:100vh}.w-1{width:calc(var(--spacing) * 1)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-2xl{width:var(--container-2xl)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-5\.5{width:calc(var(--spacing) * 5.5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-52{width:calc(var(--spacing) * 52)}.w-56{width:calc(var(--spacing) * 56)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-136{width:calc(var(--spacing) * 136)}.w-\[438px\]{width:438px}.w-fit{width:fit-content}.w-full{width:100%}.w-md{width:var(--container-md)}.max-w-2xl{max-width:var(--container-2xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-\[335px\]{max-width:335px}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.origin-center{transform-origin:50%}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-x-0{--tw-scale-x:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-drift{animation:var(--animate-drift)}.animate-drift-slow{animation:var(--animate-drift-slow)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.resize-y{resize:vertical}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-items-center{justify-items:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-7{gap:calc(var(--spacing) * 7)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-12{gap:calc(var(--spacing) * 12)}.gap-14{gap:calc(var(--spacing) * 14)}.gap-16{gap:calc(var(--spacing) * 16)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-7>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 7) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 7) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-8{column-gap:calc(var(--spacing) * 8)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-3\.5{row-gap:calc(var(--spacing) * 3.5)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-ink-900\/6>:not(:last-child)){border-color:#172c420f}@supports (color:color-mix(in lab, red, red)){:where(.divide-ink-900\/6>:not(:last-child)){border-color:color-mix(in oklab, var(--color-ink-900) 6%, transparent)}}:where(.divide-ink-900\/8>:not(:last-child)){border-color:#172c4214}@supports (color:color-mix(in lab, red, red)){:where(.divide-ink-900\/8>:not(:last-child)){border-color:color-mix(in oklab, var(--color-ink-900) 8%, transparent)}}:where(.divide-ink-900\/10>:not(:last-child)){border-color:#172c421a}@supports (color:color-mix(in lab, red, red)){:where(.divide-ink-900\/10>:not(:last-child)){border-color:color-mix(in oklab, var(--color-ink-900) 10%, transparent)}}:where(.divide-surface-50\/10>:not(:last-child)){border-color:#fafcfe1a}@supports (color:color-mix(in lab, red, red)){:where(.divide-surface-50\/10>:not(:last-child)){border-color:color-mix(in oklab, var(--color-surface-50) 10%, transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[2\.5rem\]{border-radius:2.5rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-ee-lg{border-end-end-radius:var(--radius-lg)}.rounded-es-lg{border-end-start-radius:var(--radius-lg)}.rounded-t-full{border-top-left-radius:3.40282e38px;border-top-right-radius:3.40282e38px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-b-\[2\.5rem\]{border-bottom-right-radius:2.5rem;border-bottom-left-radius:2.5rem}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-\[\#e3e3e0\]{border-color:#e3e3e0}.border-accent-500\/50{border-color:#009bc280}@supports (color:color-mix(in lab, red, red)){.border-accent-500\/50{border-color:color-mix(in oklab, var(--color-accent-500) 50%, transparent)}}.border-black{border-color:var(--color-black)}.border-gray-300{border-color:var(--color-gray-300)}.border-ink-900\/8{border-color:#172c4214}@supports (color:color-mix(in lab, red, red)){.border-ink-900\/8{border-color:color-mix(in oklab, var(--color-ink-900) 8%, transparent)}}.border-ink-900\/10{border-color:#172c421a}@supports (color:color-mix(in lab, red, red)){.border-ink-900\/10{border-color:color-mix(in oklab, var(--color-ink-900) 10%, transparent)}}.border-ink-900\/15{border-color:#172c4226}@supports (color:color-mix(in lab, red, red)){.border-ink-900\/15{border-color:color-mix(in oklab, var(--color-ink-900) 15%, transparent)}}.border-red-200{border-color:var(--color-red-200)}.border-surface-50\/10{border-color:#fafcfe1a}@supports (color:color-mix(in lab, red, red)){.border-surface-50\/10{border-color:color-mix(in oklab, var(--color-surface-50) 10%, transparent)}}.border-surface-50\/15{border-color:#fafcfe26}@supports (color:color-mix(in lab, red, red)){.border-surface-50\/15{border-color:color-mix(in oklab, var(--color-surface-50) 15%, transparent)}}.border-surface-50\/20{border-color:#fafcfe33}@supports (color:color-mix(in lab, red, red)){.border-surface-50\/20{border-color:color-mix(in oklab, var(--color-surface-50) 20%, transparent)}}.border-surface-50\/25{border-color:#fafcfe40}@supports (color:color-mix(in lab, red, red)){.border-surface-50\/25{border-color:color-mix(in oklab, var(--color-surface-50) 25%, transparent)}}.border-transparent{border-color:#0000}.bg-\[\#1b1b18\]{background-color:#1b1b18}.bg-\[\#FDFDFC\]{background-color:#fdfdfc}.bg-\[\#dbdbd7\]{background-color:#dbdbd7}.bg-\[\#fff2f2\]{background-color:#fff2f2}.bg-accent-100{background-color:var(--color-accent-100)}.bg-accent-100\/60{background-color:#dcf3fa99}@supports (color:color-mix(in lab, red, red)){.bg-accent-100\/60{background-color:color-mix(in oklab, var(--color-accent-100) 60%, transparent)}}.bg-accent-100\/70{background-color:#dcf3fab3}@supports (color:color-mix(in lab, red, red)){.bg-accent-100\/70{background-color:color-mix(in oklab, var(--color-accent-100) 70%, transparent)}}.bg-accent-400\/60{background-color:#5ccae499}@supports (color:color-mix(in lab, red, red)){.bg-accent-400\/60{background-color:color-mix(in oklab, var(--color-accent-400) 60%, transparent)}}.bg-accent-500{background-color:var(--color-accent-500)}.bg-accent-500\/10{background-color:#009bc21a}@supports (color:color-mix(in lab, red, red)){.bg-accent-500\/10{background-color:color-mix(in oklab, var(--color-accent-500) 10%, transparent)}}.bg-accent-500\/60{background-color:#009bc299}@supports (color:color-mix(in lab, red, red)){.bg-accent-500\/60{background-color:color-mix(in oklab, var(--color-accent-500) 60%, transparent)}}.bg-accent-600\/20{background-color:#006d8833}@supports (color:color-mix(in lab, red, red)){.bg-accent-600\/20{background-color:color-mix(in oklab, var(--color-accent-600) 20%, transparent)}}.bg-accent-600\/25{background-color:#006d8840}@supports (color:color-mix(in lab, red, red)){.bg-accent-600\/25{background-color:color-mix(in oklab, var(--color-accent-600) 25%, transparent)}}.bg-accent-700\/15{background-color:#006d8826}@supports (color:color-mix(in lab, red, red)){.bg-accent-700\/15{background-color:color-mix(in oklab, var(--color-accent-700) 15%, transparent)}}.bg-brand{background-color:var(--color-brand)}.bg-card{background-color:var(--color-card)}.bg-card\/70{background-color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.bg-card\/70{background-color:color-mix(in oklab, var(--color-card) 70%, transparent)}}.bg-card\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.bg-card\/90{background-color:color-mix(in oklab, var(--color-card) 90%, transparent)}}.bg-current{background-color:currentColor}.bg-gray-200{background-color:var(--color-gray-200)}.bg-ink-100{background-color:var(--color-ink-100)}.bg-navy{background-color:var(--color-navy)}.bg-navy\/55{background-color:#102b4e8c}@supports (color:color-mix(in lab, red, red)){.bg-navy\/55{background-color:color-mix(in oklab, var(--color-navy) 55%, transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-success-surface{background-color:var(--color-success-surface)}.bg-surface-50{background-color:var(--color-surface-50)}.bg-surface-50\/5{background-color:#fafcfe0d}@supports (color:color-mix(in lab, red, red)){.bg-surface-50\/5{background-color:color-mix(in oklab, var(--color-surface-50) 5%, transparent)}}.bg-surface-50\/15{background-color:#fafcfe26}@supports (color:color-mix(in lab, red, red)){.bg-surface-50\/15{background-color:color-mix(in oklab, var(--color-surface-50) 15%, transparent)}}.bg-surface-50\/95{background-color:#fafcfef2}@supports (color:color-mix(in lab, red, red)){.bg-surface-50\/95{background-color:color-mix(in oklab, var(--color-surface-50) 95%, transparent)}}.bg-surface-100{background-color:var(--color-surface-100)}.bg-surface-100\/80{background-color:#edf5facc}@supports (color:color-mix(in lab, red, red)){.bg-surface-100\/80{background-color:color-mix(in oklab, var(--color-surface-100) 80%, transparent)}}.bg-surface-200{background-color:var(--color-surface-200)}.bg-surface-200\/80{background-color:#dcebf5cc}@supports (color:color-mix(in lab, red, red)){.bg-surface-200\/80{background-color:color-mix(in oklab, var(--color-surface-200) 80%, transparent)}}.bg-white{background-color:var(--color-white)}.bg-linear-to-b{--tw-gradient-position:to bottom}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-b{--tw-gradient-position:to bottom in oklab}}.bg-linear-to-b{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-t{--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-t{--tw-gradient-position:to top in oklab}}.bg-linear-to-t{background-image:linear-gradient(var(--tw-gradient-stops))}.from-ink-900{--tw-gradient-from:var(--color-ink-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-navy{--tw-gradient-from:var(--color-navy);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-navy\/65{--tw-gradient-from:#102b4ea6}@supports (color:color-mix(in lab, red, red)){.from-navy\/65{--tw-gradient-from:color-mix(in oklab, var(--color-navy) 65%, transparent)}}.from-navy\/65{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-surface-50{--tw-gradient-from:var(--color-surface-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-surface-100{--tw-gradient-from:var(--color-surface-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-navy{--tw-gradient-via:var(--color-navy);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-navy\/5{--tw-gradient-via:#102b4e0d}@supports (color:color-mix(in lab, red, red)){.via-navy\/5{--tw-gradient-via:color-mix(in oklab, var(--color-navy) 5%, transparent)}}.via-navy\/5{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-navy\/15{--tw-gradient-via:#102b4e26}@supports (color:color-mix(in lab, red, red)){.via-navy\/15{--tw-gradient-via:color-mix(in oklab, var(--color-navy) 15%, transparent)}}.via-navy\/15{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-surface-50{--tw-gradient-via:var(--color-surface-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-surface-100{--tw-gradient-via:var(--color-surface-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white{--tw-gradient-via:var(--color-white);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-accent-900{--tw-gradient-to:var(--color-accent-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-surface-50{--tw-gradient-to:var(--color-surface-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-surface-100{--tw-gradient-to:var(--color-surface-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.box-decoration-clone{-webkit-box-decoration-break:clone;box-decoration-break:clone}.box-decoration-slice{-webkit-box-decoration-break:slice;box-decoration-break:slice}.decoration-clone{-webkit-box-decoration-break:clone;box-decoration-break:clone}.decoration-slice{-webkit-box-decoration-break:slice;box-decoration-break:slice}.object-cover{object-fit:cover}.object-\[center_48\%\]{object-position:center 48%}.object-center{object-position:center}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-7{padding:calc(var(--spacing) * 7)}.p-8{padding:calc(var(--spacing) * 8)}.p-10{padding:calc(var(--spacing) * 10)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-7{padding-block:calc(var(--spacing) * 7)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-14{padding-block:calc(var(--spacing) * 14)}.py-20{padding-block:calc(var(--spacing) * 20)}.py-24{padding-block:calc(var(--spacing) * 24)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-20{padding-top:calc(var(--spacing) * 20)}.pt-32{padding-top:calc(var(--spacing) * 32)}.pr-2\.5{padding-right:calc(var(--spacing) * 2.5)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-3\.5{padding-right:calc(var(--spacing) * 3.5)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-9{padding-bottom:calc(var(--spacing) * 9)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-14{padding-bottom:calc(var(--spacing) * 14)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pb-20{padding-bottom:calc(var(--spacing) * 20)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-7{padding-left:calc(var(--spacing) * 7)}.text-center{text-align:center}.text-left{text-align:left}.font-display{font-family:var(--font-display)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.62rem\]{font-size:.62rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[1\.05rem\]{font-size:1.05rem}.text-\[1\.6rem\]{font-size:1.6rem}.text-\[1\.7rem\]{font-size:1.7rem}.text-\[1\.15rem\]{font-size:1.15rem}.text-\[1\.45rem\]{font-size:1.45rem}.text-\[2\.1rem\]{font-size:2.1rem}.text-\[2\.7rem\]{font-size:2.7rem}.text-\[11\.5vw\]{font-size:11.5vw}.text-\[13px\]{font-size:13px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-\[1\.06\]{--tw-leading:1.06;line-height:1.06}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[1\.12\]{--tw-leading:1.12;line-height:1.12}.leading-\[20px\]{--tw-leading:20px;line-height:20px}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.24em\]{--tw-tracking:.24em;letter-spacing:.24em}.tracking-\[0\.28em\]{--tw-tracking:.28em;letter-spacing:.28em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.text-pretty{text-wrap:pretty}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.overflow-ellipsis,.text-ellipsis{text-overflow:ellipsis}.text-\[\#1B1B18\],.text-\[\#1b1b18\]{color:#1b1b18}.text-\[\#706f6c\]{color:#706f6c}.text-\[\#F3BEC7\]{color:#f3bec7}.text-\[\#F8B803\]{color:#f8b803}.text-\[\#F53003\],.text-\[\#f53003\]{color:#f53003}.text-accent{color:var(--color-accent)}.text-accent-300{color:var(--color-accent-300)}.text-accent-400{color:var(--color-accent-400)}.text-accent-400\/80{color:#5ccae4cc}@supports (color:color-mix(in lab, red, red)){.text-accent-400\/80{color:color-mix(in oklab, var(--color-accent-400) 80%, transparent)}}.text-accent-600{color:var(--color-accent-600)}.text-accent-700{color:var(--color-accent-700)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-ink-200{color:var(--color-ink-200)}.text-ink-300{color:var(--color-ink-300)}.text-ink-400{color:var(--color-ink-400)}.text-ink-500{color:var(--color-ink-500)}.text-ink-600{color:var(--color-ink-600)}.text-ink-700{color:var(--color-ink-700)}.text-ink-800{color:var(--color-ink-800)}.text-ink-900{color:var(--color-ink-900)}.text-link{color:var(--color-link)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-success-ink{color:var(--color-success-ink)}.text-white{color:var(--color-white)}.text-white\/6{color:#ffffff0f}@supports (color:color-mix(in lab, red, red)){.text-white\/6{color:color-mix(in oklab, var(--color-white) 6%, transparent)}}.text-white\/40{color:#fff6}@supports (color:color-mix(in lab, red, red)){.text-white\/40{color:color-mix(in oklab, var(--color-white) 40%, transparent)}}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-25{opacity:.25}.opacity-100{opacity:1}.mix-blend-color{mix-blend-mode:color}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-hard-light{mix-blend-mode:hard-light}.mix-blend-multiply{mix-blend-mode:multiply}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_18px_40px_-18px_rgba\(var\(--shadow-ink\)\,0\.6\)\]{--tw-shadow:0 18px 40px -18px var(--tw-shadow-color,rgba(var(--shadow-ink),.6));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_20px_45px_-22px_rgba\(var\(--shadow-ink\)\,0\.4\)\]{--tw-shadow:0 20px 45px -22px var(--tw-shadow-color,rgba(var(--shadow-ink),.4));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_20px_45px_-22px_rgba\(var\(--shadow-ink\)\,0\.35\)\]{--tw-shadow:0 20px 45px -22px var(--tw-shadow-color,rgba(var(--shadow-ink),.35));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_30px_60px_-40px_rgba\(var\(--shadow-ink\)\,0\.8\)\]{--tw-shadow:0 30px 60px -40px var(--tw-shadow-color,rgba(var(--shadow-ink),.8));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_35px_70px_-42px_rgba\(var\(--shadow-ink\)\,0\.65\)\]{--tw-shadow:0 35px 70px -42px var(--tw-shadow-color,rgba(var(--shadow-ink),.65));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_40px_80px_-40px_rgba\(var\(--shadow-ink\)\,0\.7\)\]{--tw-shadow:0 40px 80px -40px var(--tw-shadow-color,rgba(var(--shadow-ink),.7));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_40px_90px_-50px_rgba\(var\(--shadow-ink\)\,0\.5\)\]{--tw-shadow:0 40px 90px -50px var(--tw-shadow-color,rgba(var(--shadow-ink),.5));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0px_0px_1px_0px_rgba\(0\,0\,0\,0\.03\)\,0px_1px_2px_0px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0px 0px 1px 0px var(--tw-shadow-color,#00000008), 0px 1px 2px 0px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[inset_0px_0px_0px_1px_rgba\(26\,26\,0\,0\.16\)\]{--tw-shadow:inset 0px 0px 0px 1px var(--tw-shadow-color,#1a1a0029);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-navy\/10{--tw-shadow-color:#102b4e1a}@supports (color:color-mix(in lab, red, red)){.shadow-navy\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-navy) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-navy\/25{--tw-shadow-color:#102b4e40}@supports (color:color-mix(in lab, red, red)){.shadow-navy\/25{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-navy) 25%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-gray-300{--tw-ring-color:var(--color-gray-300)}.ring-red-300{--tw-ring-color:var(--color-red-300)}.ring-transparent{--tw-ring-color:transparent}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.delay-300{transition-delay:.3s}.delay-400{transition-delay:.4s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-750{--tw-duration:.75s;transition-duration:.75s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.will-change-transform{will-change:transform}.select-none{-webkit-user-select:none;user-select:none}.\[--stroke-color\:\#1B1B18\]{--stroke-color:#1b1b18}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-y-0\.5:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:border-accent-400:is(:where(.group):hover *){border-color:var(--color-accent-400)}.group-hover\:border-accent-400\/40:is(:where(.group):hover *){border-color:#5ccae466}@supports (color:color-mix(in lab, red, red)){.group-hover\:border-accent-400\/40:is(:where(.group):hover *){border-color:color-mix(in oklab, var(--color-accent-400) 40%, transparent)}}.group-hover\:bg-accent-500:is(:where(.group):hover *){background-color:var(--color-accent-500)}.group-hover\:text-accent-300:is(:where(.group):hover *){color:var(--color-accent-300)}.group-hover\:text-accent-400\/70:is(:where(.group):hover *){color:#5ccae4b3}@supports (color:color-mix(in lab, red, red)){.group-hover\:text-accent-400\/70:is(:where(.group):hover *){color:color-mix(in oklab, var(--color-accent-400) 70%, transparent)}}.group-hover\:text-ink-200:is(:where(.group):hover *){color:var(--color-ink-200)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/link\:scale-x-100:is(:where(.group\/link):hover *){--tw-scale-x:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.group-data-\[menu-open\=true\]\/header\:w-6:is(:where(.group\/header)[data-menu-open=true] *){width:calc(var(--spacing) * 6)}.group-data-\[menu-open\=true\]\/header\:translate-y-\[-7\.5px\]:is(:where(.group\/header)[data-menu-open=true] *){--tw-translate-y:-7.5px;translate:var(--tw-translate-x) var(--tw-translate-y)}.group-data-\[menu-open\=true\]\/header\:translate-y-\[7\.5px\]:is(:where(.group\/header)[data-menu-open=true] *){--tw-translate-y:7.5px;translate:var(--tw-translate-x) var(--tw-translate-y)}.group-data-\[menu-open\=true\]\/header\:-rotate-45:is(:where(.group\/header)[data-menu-open=true] *){rotate:-45deg}.group-data-\[menu-open\=true\]\/header\:rotate-45:is(:where(.group\/header)[data-menu-open=true] *){rotate:45deg}.group-data-\[menu-open\=true\]\/header\:opacity-0:is(:where(.group\/header)[data-menu-open=true] *){opacity:0}.selection\:bg-accent-200 ::selection{background-color:var(--color-accent-200)}.selection\:bg-accent-200::selection{background-color:var(--color-accent-200)}.selection\:text-ink-900 ::selection{color:var(--color-ink-900)}.selection\:text-ink-900::selection{color:var(--color-ink-900)}.placeholder\:text-ink-500::placeholder{color:var(--color-ink-500)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:start-\[0\.4rem\]:before{content:var(--tw-content);inset-inline-start:.4rem}.before\:top-0:before{content:var(--tw-content);top:calc(var(--spacing) * 0)}.before\:top-1\/2:before{content:var(--tw-content);top:50%}.before\:bottom-0:before{content:var(--tw-content);bottom:calc(var(--spacing) * 0)}.before\:bottom-1\/2:before{content:var(--tw-content);bottom:50%}.before\:left-\[0\.4rem\]:before{content:var(--tw-content);left:.4rem}.before\:border-l:before{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:1px}.before\:border-\[\#e3e3e0\]:before{content:var(--tw-content);border-color:#e3e3e0}@media (hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-accent-400:hover{border-color:var(--color-accent-400)}.hover\:border-black:hover{border-color:var(--color-black)}.hover\:border-ink-900:hover{border-color:var(--color-ink-900)}.hover\:border-navy:hover{border-color:var(--color-navy)}.hover\:bg-black:hover{background-color:var(--color-black)}.hover\:bg-brand:hover{background-color:var(--color-brand)}.hover\:bg-brand-hover:hover{background-color:var(--color-brand-hover)}.hover\:bg-brand\/5:hover{background-color:#1747800d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-brand\/5:hover{background-color:color-mix(in oklab, var(--color-brand) 5%, transparent)}}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-navy:hover{background-color:var(--color-navy)}.hover\:bg-surface-50\/10:hover{background-color:#fafcfe1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-surface-50\/10:hover{background-color:color-mix(in oklab, var(--color-surface-50) 10%, transparent)}}.hover\:text-accent-300:hover{color:var(--color-accent-300)}.hover\:text-accent-700:hover{color:var(--color-accent-700)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-ink-900:hover{color:var(--color-ink-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:shadow-\[0_24px_50px_-26px_rgba\(var\(--shadow-ink\)\,0\.35\)\]:hover{--tw-shadow:0 24px 50px -26px var(--tw-shadow-color,rgba(var(--shadow-ink),.35));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-\[0_28px_60px_-28px_rgba\(var\(--shadow-ink\)\,0\.55\)\]:hover{--tw-shadow:0 28px 60px -28px var(--tw-shadow-color,rgba(var(--shadow-ink),.55));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-100:focus{z-index:100}.focus\:rounded-full:focus{border-radius:3.40282e38px}.focus\:border-blue-300:focus{border-color:var(--color-blue-300)}.focus\:bg-brand:focus{background-color:var(--color-brand)}.focus\:bg-card:focus{background-color:var(--color-card)}.focus\:px-5:focus{padding-inline:calc(var(--spacing) * 5)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:text-sm:focus{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.focus\:font-semibold:focus{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.focus\:text-white:focus{color:var(--color-white)}.focus\:shadow-lg:focus{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:shadow-navy\/25:focus{--tw-shadow-color:#102b4e40}@supports (color:color-mix(in lab, red, red)){.focus\:shadow-navy\/25:focus{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-navy) 25%, transparent) var(--tw-shadow-alpha), transparent)}}.focus\:ring-link:focus{--tw-ring-color:var(--color-link)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-accent-600:focus-visible{outline-color:var(--color-accent-600)}.active\:bg-gray-100:active{background-color:var(--color-gray-100)}.active\:text-gray-500:active{color:var(--color-gray-500)}.active\:text-gray-700:active{color:var(--color-gray-700)}.active\:text-gray-800:active{color:var(--color-gray-800)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:aspect-16\/10{aspect-ratio:16/10}.sm\:h-48{height:calc(var(--spacing) * 48)}.sm\:h-80{height:calc(var(--spacing) * 80)}.sm\:flex-1{flex:1}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:gap-10{gap:calc(var(--spacing) * 10)}.sm\:p-10{padding:calc(var(--spacing) * 10)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:px-9{padding-inline:calc(var(--spacing) * 9)}.sm\:px-10{padding-inline:calc(var(--spacing) * 10)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:py-10{padding-block:calc(var(--spacing) * 10)}.sm\:py-28{padding-block:calc(var(--spacing) * 28)}.sm\:pt-40{padding-top:calc(var(--spacing) * 40)}.sm\:pb-8{padding-bottom:calc(var(--spacing) * 8)}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-\[1\.7rem\]{font-size:1.7rem}.sm\:text-\[1\.35rem\]{font-size:1.35rem}.sm\:text-\[2\.6rem\]{font-size:2.6rem}}@media (width>=48rem){.md\:grid{display:grid}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:gap-20{gap:calc(var(--spacing) * 20)}}@media (width>=64rem){.lg\:sticky{position:sticky}.lg\:top-32{top:calc(var(--spacing) * 32)}.lg\:-right-10{right:calc(var(--spacing) * -10)}.lg\:order-first{order:-9999}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:mr-8{margin-right:calc(var(--spacing) * 8)}.lg\:mb-0{margin-bottom:calc(var(--spacing) * 0)}.lg\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.lg\:-ml-px{margin-left:-1px}.lg\:ml-0{margin-left:calc(var(--spacing) * 0)}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:inline-flex{display:inline-flex}.lg\:aspect-4\/3{aspect-ratio:4/3}.lg\:aspect-auto{aspect-ratio:auto}.lg\:h-24{height:calc(var(--spacing) * 24)}.lg\:h-40{height:calc(var(--spacing) * 40)}.lg\:h-80{height:calc(var(--spacing) * 80)}.lg\:h-88{height:calc(var(--spacing) * 88)}.lg\:min-h-full{min-height:100%}.lg\:w-60{width:calc(var(--spacing) * 60)}.lg\:w-\[438px\]{width:438px}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:max-w-none{max-width:none}.lg\:flex-none{flex:none}.lg\:grow{flex-grow:1}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[0\.9fr_1\.1fr\]{grid-template-columns:.9fr 1.1fr}.lg\:grid-cols-\[0\.65fr_1\.35fr\]{grid-template-columns:.65fr 1.35fr}.lg\:grid-cols-\[0\.85fr_1\.15fr\]{grid-template-columns:.85fr 1.15fr}.lg\:grid-cols-\[0\.92fr_1\.08fr\]{grid-template-columns:.92fr 1.08fr}.lg\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:justify-center{justify-content:center}.lg\:gap-2{gap:calc(var(--spacing) * 2)}.lg\:gap-12{gap:calc(var(--spacing) * 12)}.lg\:gap-20{gap:calc(var(--spacing) * 20)}.lg\:self-start{align-self:flex-start}.lg\:rounded-ss-lg{border-start-start-radius:var(--radius-lg)}.lg\:rounded-ee-none{border-end-end-radius:0}.lg\:rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.lg\:rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.lg\:p-8{padding:calc(var(--spacing) * 8)}.lg\:p-12{padding:calc(var(--spacing) * 12)}.lg\:p-20{padding:calc(var(--spacing) * 20)}.lg\:py-36{padding-block:calc(var(--spacing) * 36)}.lg\:pt-10{padding-top:calc(var(--spacing) * 10)}.lg\:pt-20{padding-top:calc(var(--spacing) * 20)}.lg\:pt-24{padding-top:calc(var(--spacing) * 24)}.lg\:pt-36{padding-top:calc(var(--spacing) * 36)}.lg\:pt-48{padding-top:calc(var(--spacing) * 48)}.lg\:pb-28{padding-bottom:calc(var(--spacing) * 28)}.lg\:pb-36{padding-bottom:calc(var(--spacing) * 36)}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-\[4\.1rem\]{font-size:4.1rem}.lg\:text-\[7\.5rem\]{font-size:7.5rem}.lg\:leading-\[1\.08\]{--tw-leading:1.08;line-height:1.08}}.rtl\:flex-row-reverse:where(:is(:lang(ae),:lang(ar),:lang(arc),:lang(bcc),:lang(bqi),:lang(ckb),:lang(dv),:lang(fa),:lang(glk),:lang(he),:lang(ku),:lang(mzn),:lang(nqo),:lang(pnb),:lang(ps),:lang(sd),:lang(ug),:lang(ur),:lang(yi)),[dir=rtl],[dir=rtl] *){flex-direction:row-reverse}@media (prefers-color-scheme:dark){.dark\:border-\[\#3E3E3A\]{border-color:#3e3e3a}.dark\:border-\[\#eeeeec\]{border-color:#eeeeec}.dark\:border-gray-600{border-color:var(--color-gray-600)}.dark\:bg-\[\#0a0a0a\]{background-color:#0a0a0a}.dark\:bg-\[\#1D0002\]{background-color:#1d0002}.dark\:bg-\[\#3E3E3A\]{background-color:#3e3e3a}.dark\:bg-\[\#161615\]{background-color:#161615}.dark\:bg-\[\#eeeeec\]{background-color:#eeeeec}.dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-\[\#1C1C1A\]{color:#1c1c1a}.dark\:text-\[\#4B0600\]{color:#4b0600}.dark\:text-\[\#391800\]{color:#391800}.dark\:text-\[\#733000\]{color:#733000}.dark\:text-\[\#A1A09A\]{color:#a1a09a}.dark\:text-\[\#EDEDEC\]{color:#ededec}.dark\:text-\[\#F61500\]{color:#f61500}.dark\:text-\[\#FF4433\]{color:#f43}.dark\:text-black{color:var(--color-black)}.dark\:text-gray-200{color:var(--color-gray-200)}.dark\:text-gray-300{color:var(--color-gray-300)}.dark\:text-gray-400{color:var(--color-gray-400)}.dark\:text-gray-600{color:var(--color-gray-600)}.dark\:text-white{color:var(--color-white)}.dark\:mix-blend-hard-light{mix-blend-mode:hard-light}.dark\:mix-blend-normal{mix-blend-mode:normal}.dark\:shadow-\[inset_0px_0px_0px_1px_\#fffaed2d\]{--tw-shadow:inset 0px 0px 0px 1px var(--tw-shadow-color,#fffaed2d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.dark\:\[--stroke-color\:\#FF750F\]{--stroke-color:#ff750f}.dark\:before\:border-\[\#3E3E3A\]:before{content:var(--tw-content);border-color:#3e3e3a}@media (hover:hover){.dark\:hover\:border-white:hover{border-color:var(--color-white)}.dark\:hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.dark\:hover\:bg-white:hover{background-color:var(--color-white)}.dark\:hover\:text-gray-200:hover{color:var(--color-gray-200)}.dark\:hover\:text-gray-300:hover{color:var(--color-gray-300)}}.dark\:focus\:border-blue-700:focus{border-color:var(--color-blue-700)}.dark\:focus\:border-blue-800:focus{border-color:var(--color-blue-800)}.dark\:active\:bg-gray-700:active{background-color:var(--color-gray-700)}.dark\:active\:text-gray-300:active{color:var(--color-gray-300)}}@starting-style{.starting\:opacity-0{opacity:0}}@media (prefers-reduced-motion:no-preference){@starting-style{.motion-safe\:starting\:-translate-x-\[26px\]{--tw-translate-x:calc(26px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:-translate-x-\[51px\]{--tw-translate-x:calc(51px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:-translate-x-\[78px\]{--tw-translate-x:calc(78px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:-translate-x-\[102px\]{--tw-translate-x:calc(102px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:translate-y-6{--tw-translate-y:calc(var(--spacing) * 6);translate:var(--tw-translate-x) var(--tw-translate-y)}}}.\[\&_\[data-contact-action\]\]\:text-ink-200 [data-contact-action]{color:var(--color-ink-200)}.\[\&\.is-scrolled\]\:border-ink-900\/8.is-scrolled{border-color:#172c4214}@supports (color:color-mix(in lab, red, red)){.\[\&\.is-scrolled\]\:border-ink-900\/8.is-scrolled{border-color:color-mix(in oklab, var(--color-ink-900) 8%, transparent)}}.\[\&\.is-scrolled\]\:bg-surface-50\/80.is-scrolled{background-color:#fafcfecc}@supports (color:color-mix(in lab, red, red)){.\[\&\.is-scrolled\]\:bg-surface-50\/80.is-scrolled{background-color:color-mix(in oklab, var(--color-surface-50) 80%, transparent)}}.\[\&\.is-scrolled\]\:shadow-\[0_1px_0_0_rgba\(var\(--shadow-ink\)\,0\.04\)\,0_12px_32px_-20px_rgba\(var\(--shadow-ink\)\,0\.25\)\].is-scrolled{--tw-shadow:0 1px 0 0 var(--tw-shadow-color,rgba(var(--shadow-ink),.04)), 0 12px 32px -20px var(--tw-shadow-color,rgba(var(--shadow-ink),.25));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.\[\&\.is-scrolled\]\:backdrop-blur-xl.is-scrolled{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}}@keyframes drift{0%,to{transform:translateY(0)rotate(0)}50%{transform:translateY(-18px)rotate(1.2deg)}}@keyframes fade-up{0%{opacity:0;transform:translateY(26px)}to{opacity:1;transform:translateY(0)}}@keyframes carousel-right{0%{transform:translate(-50%)}to{transform:translate(0)}}html{scroll-behavior:smooth}body{background-color:var(--color-surface-50);color:var(--color-ink-900);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;overflow-x:hidden}.font-display{font-optical-sizing:auto;letter-spacing:-.015em}@keyframes reveal-enter{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:no-preference){.reveal.is-visible{animation:.85s cubic-bezier(.16,1,.3,1) backwards reveal-enter;animation-delay:var(--reveal-delay,0s)}.reveal:focus-within{animation:none}}.carousel-track{will-change:transform;animation:42s linear infinite carousel-right}.carousel-window:hover .carousel-track,.carousel-window:focus-within .carousel-track{animation-play-state:paused}[data-carousel-dialog]::backdrop{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.parallax-fixed{background-position:50%;background-size:cover;background-attachment:scroll}@media (width>=768px) and (prefers-reduced-motion:no-preference){.parallax-fixed{background-attachment:fixed}}.grain:before{content:"";pointer-events:none;opacity:.5;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.04 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");position:absolute;inset:0}@media (width>=768px){::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--color-surface-100)}::-webkit-scrollbar-thumb{background:var(--color-ink-200);border:2px solid var(--color-surface-100);border-radius:9999px}::-webkit-scrollbar-thumb:hover{background:var(--color-ink-300)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}html.js .reveal{opacity:1;transform:none}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} diff --git a/public/build/assets/app-DCb96HGE.js b/public/build/assets/app-DCb96HGE.js new file mode 100644 index 0000000..1a28f1a --- /dev/null +++ b/public/build/assets/app-DCb96HGE.js @@ -0,0 +1 @@ +function e(e=document,t=window.matchMedia(`(prefers-reduced-motion: reduce)`),n=window.IntersectionObserver){let r=[...e.querySelectorAll(`.reveal:not([data-reveal-ready])`)];if(t.matches||!n||r.length===0)return;let i;try{i=new n(e=>{e.forEach(({isIntersecting:e,target:t})=>{e&&(t.classList.add(`is-visible`),i.unobserve(t))})},{rootMargin:`0px 0px -10% 0px`,threshold:.12}),r.forEach(e=>{i.observe(e),e.dataset.revealReady=`true`})}catch{i?.disconnect(),r.forEach(e=>e.classList.remove(`is-visible`))}}function t(e){let t=new Set;return(n,r)=>{if([`enquiry_accepted`,`phone_click`,`directions_click`].includes(n)){if(n===`enquiry_accepted`){if(typeof r!=`string`||!/^[0-9a-f-]{36}$/.test(r)||t.has(r))return;t.add(r)}e({event:n})}}}function n(){if(document.body.dataset.analyticsEnabled!==`true`)return;let e=t(e=>window.dispatchEvent(new CustomEvent(`site:analytics`,{detail:e})));window.addEventListener(`enquiry-accepted`,t=>e(`enquiry_accepted`,t.detail?.id)),document.addEventListener(`click`,t=>{let n=t.target.closest?.(`a[data-contact-action]`);n?.dataset.contactAction===`phone`&&e(`phone_click`),n?.dataset.contactAction===`directions`&&e(`directions_click`)})}document.documentElement.classList.add(`js`);function r(){let e=document.querySelector(`[data-site-header]`);if(!e)return;let t=()=>{window.scrollY>24?e.classList.add(`is-scrolled`):e.classList.remove(`is-scrolled`)};t(),window.addEventListener(`scroll`,t,{passive:!0})}function i(){let e=document.querySelector(`[data-site-header]`);if(!e||e.dataset.mobileMenuInitialized===`true`)return;let t=e.querySelector(`[data-mobile-menu-toggle]`),n=e.querySelector(`[data-mobile-menu]`);if(!t||!n)return;e.dataset.mobileMenuInitialized=`true`;let r=r=>{e.dataset.menuOpen=String(r),n.hidden=!r,t.setAttribute(`aria-expanded`,String(r)),t.setAttribute(`aria-label`,r?`Close menu`:`Open menu`),document.body.classList.toggle(`overflow-hidden`,r)},i=()=>t.getAttribute(`aria-expanded`)===`true`;t.addEventListener(`click`,()=>{r(!i())}),e.querySelectorAll(`[data-mobile-menu-close]`).forEach(e=>{e.addEventListener(`click`,()=>r(!1))}),e.addEventListener(`keydown`,e=>{e.key===`Escape`&&i()&&(r(!1),t.focus())}),window.matchMedia(`(min-width: 1024px)`).addEventListener(`change`,e=>{e.matches&&r(!1)})}function a(){let e=window.matchMedia(`(prefers-reduced-motion: reduce)`),t=window.matchMedia(`(min-width: 768px)`),n=Array.from(document.querySelectorAll(`[data-parallax]`));if(n.length===0)return;let r=!1,i=()=>{let i=window.scrollY;for(let r of n){if(e.matches||!t.matches){r.style.transform=``;continue}let n=parseFloat(r.dataset.parallax)||.15;r.style.transform=`translate3d(0, ${i*n}px, 0)`}r=!1};i(),e.addEventListener(`change`,i),t.addEventListener(`change`,i),window.addEventListener(`scroll`,()=>{r||=(window.requestAnimationFrame(i),!0)},{passive:!0})}function o(){document.querySelectorAll(`[data-image-carousel]`).forEach(e=>{if(e.dataset.initialized===`true`)return;let t=e.querySelector(`[data-carousel-dialog]`),n=e.querySelector(`[data-carousel-dialog-image]`),r=e.querySelector(`[data-carousel-dialog-close]`);if(!t||!n||!r)return;e.dataset.initialized=`true`;let i=null,a=()=>{t.open&&t.close()};e.querySelectorAll(`[data-carousel-image]`).forEach(e=>{e.addEventListener(`click`,()=>{window.matchMedia(`(min-width: 768px)`).matches&&(i=e,n.src=e.dataset.imageSrc,n.alt=e.dataset.imageAlt,t.showModal(),document.body.classList.add(`overflow-hidden`))})}),r.addEventListener(`click`,a),t.addEventListener(`click`,e=>{e.target===t&&a()}),t.addEventListener(`close`,()=>{document.body.classList.remove(`overflow-hidden`),n.src=``,n.alt=``,i?.focus()}),window.matchMedia(`(max-width: 767px)`).addEventListener(`change`,e=>{e.matches&&a()})})}function s(){r(),i(),a(),e(),o()}document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,s):s(),document.addEventListener(`livewire:navigated`,s),n(),window.addEventListener(`enquiry-accepted`,()=>{requestAnimationFrame(()=>document.querySelector(`[data-enquiry-success]`)?.focus())}); \ No newline at end of file diff --git a/public/build/assets/app-DyABguZv.css b/public/build/assets/app-DyABguZv.css deleted file mode 100644 index 26c4fdf..0000000 --- a/public/build/assets/app-DyABguZv.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-outline-style:solid}}}@layer theme{:root,:host{--font-sans:"Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-serif:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-100:oklch(95.3% .051 180.801);--color-teal-200:oklch(91% .096 180.426);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-teal-800:oklch(43.7% .078 188.216);--color-teal-900:oklch(38.6% .063 188.416);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-800:oklch(45% .085 224.283);--color-cyan-900:oklch(39.8% .07 227.392);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-100:oklch(95.2% .037 318.852);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-fuchsia-600:oklch(59.1% .293 322.896);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-fuchsia-800:oklch(45.2% .211 324.591);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-fuchsia-950:oklch(29.3% .136 325.661);--color-pink-50:oklch(97.1% .014 343.198);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-600:oklch(59.2% .249 .584);--color-pink-700:oklch(52.5% .223 3.958);--color-pink-800:oklch(45.9% .187 3.815);--color-pink-900:oklch(40.8% .153 2.432);--color-pink-950:oklch(28.4% .109 3.907);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--radius-4xl:2rem;--shadow-2xs:0 1px #0000000d;--shadow-xs:0 1px 2px 0 #0000000d;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--shadow-lg:0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;--shadow-xl:0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;--shadow-2xl:0 25px 50px -12px #00000040;--inset-shadow-2xs:inset 0 1px #0000000d;--inset-shadow-xs:inset 0 1px 1px #0000000d;--inset-shadow-sm:inset 0 2px 4px #0000000d;--drop-shadow-xs:0 1px 1px #0000000d;--drop-shadow-sm:0 1px 2px #00000026;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--drop-shadow-xl:0 9px 7px #0000001a;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-xs:4px;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--perspective-dramatic:100px;--perspective-near:300px;--perspective-normal:500px;--perspective-midrange:800px;--perspective-distant:1200px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-display:"Fraunces", ui-serif, Georgia, Cambria, "Times New Roman", serif;--color-bone-50:#fbfaf7;--color-bone-100:#f6f3ec;--color-pine-100:#dce8e4;--color-pine-200:#bad1ca;--color-pine-300:#8fb3a9;--color-pine-400:#659186;--color-pine-500:#4b766c;--color-pine-600:#3a5e56;--color-pine-700:#314d47;--color-pine-800:#25403a;--color-pine-900:#16302b;--color-pine-950:#0a1f1b;--color-sea-100:#d8f2e7;--color-sea-200:#b3e4d2;--color-sea-300:#82cfb7;--color-sea-400:#50b298;--color-sea-500:#2f967e;--color-sea-600:#217866;--color-sea-700:#1c6054;--color-sea-900:#16403a;--color-sand-100:#f3ead8;--color-sand-200:#e9dbbd;--color-sand-400:#c9ab74;--animate-drift:drift 11s ease-in-out infinite;--animate-drift-slow:drift 16s ease-in-out infinite}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.-top-4{top:calc(var(--spacing) * -4)}.-top-16{top:calc(var(--spacing) * -16)}.-top-24{top:calc(var(--spacing) * -24)}.-top-40{top:calc(var(--spacing) * -40)}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-10{top:calc(var(--spacing) * 10)}.top-16{top:calc(var(--spacing) * 16)}.top-24{top:calc(var(--spacing) * 24)}.top-32{top:calc(var(--spacing) * 32)}.top-44{top:calc(var(--spacing) * 44)}.-right-2{right:calc(var(--spacing) * -2)}.-right-4{right:calc(var(--spacing) * -4)}.-right-5{right:calc(var(--spacing) * -5)}.-right-6{right:calc(var(--spacing) * -6)}.-right-12{right:calc(var(--spacing) * -12)}.-right-16{right:calc(var(--spacing) * -16)}.-right-20{right:calc(var(--spacing) * -20)}.-right-32{right:calc(var(--spacing) * -32)}.-right-40{right:calc(var(--spacing) * -40)}.-bottom-6{bottom:calc(var(--spacing) * -6)}.-bottom-px{bottom:-1px}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-5{bottom:calc(var(--spacing) * 5)}.-left-14{left:calc(var(--spacing) * -14)}.-left-24{left:calc(var(--spacing) * -24)}.-left-32{left:calc(var(--spacing) * -32)}.-left-48{left:calc(var(--spacing) * -48)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-8{left:calc(var(--spacing) * 8)}.isolate{isolation:isolate}.-z-10{z-index:calc(10 * -1)}.-z-20{z-index:calc(20 * -1)}.z-50{z-index:50}.order-last{order:9999}.m-auto{margin:auto}.mx-auto{margin-inline:auto}.ms-1{margin-inline-start:calc(var(--spacing) * 1)}.-mt-2{margin-top:calc(var(--spacing) * -2)}.-mt-4{margin-top:calc(var(--spacing) * -4)}.-mt-7{margin-top:calc(var(--spacing) * -7)}.-mt-\[6\.6rem\]{margin-top:-6.6rem}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-9{margin-top:calc(var(--spacing) * 9)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-14{margin-top:calc(var(--spacing) * 14)}.mt-20{margin-top:calc(var(--spacing) * 20)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.-ml-8{margin-left:calc(var(--spacing) * -8)}.-ml-px{margin-left:-1px}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-4\/3{aspect-ratio:4/3}.aspect-\[335\/364\]{aspect-ratio:335/364}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-5\.5{height:calc(var(--spacing) * 5.5)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-14\.5{height:calc(var(--spacing) * 14.5)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-40{height:calc(var(--spacing) * 40)}.h-44{height:calc(var(--spacing) * 44)}.h-56{height:calc(var(--spacing) * 56)}.h-72{height:calc(var(--spacing) * 72)}.h-80{height:calc(var(--spacing) * 80)}.h-96{height:calc(var(--spacing) * 96)}.h-112{height:calc(var(--spacing) * 112)}.h-136{height:calc(var(--spacing) * 136)}.h-full{height:100%}.h-px{height:1px}.max-h-\[75vh\]{max-height:75vh}.max-h-none{max-height:none}.min-h-80{min-height:calc(var(--spacing) * 80)}.min-h-screen{min-height:100vh}.w-1{width:calc(var(--spacing) * 1)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-2xl{width:var(--container-2xl)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-5\.5{width:calc(var(--spacing) * 5.5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-40{width:calc(var(--spacing) * 40)}.w-52{width:calc(var(--spacing) * 52)}.w-56{width:calc(var(--spacing) * 56)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-136{width:calc(var(--spacing) * 136)}.w-\[438px\]{width:438px}.w-fit{width:fit-content}.w-full{width:100%}.w-max{width:max-content}.w-md{width:var(--container-md)}.max-w-2xl{max-width:var(--container-2xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-60{max-width:calc(var(--spacing) * 60)}.max-w-\[75vw\]{max-width:75vw}.max-w-\[335px\]{max-width:335px}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.origin-center{transform-origin:50%}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-x-0{--tw-scale-x:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-drift{animation:var(--animate-drift)}.animate-drift-slow{animation:var(--animate-drift-slow)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-zoom-in{cursor:zoom-in}.resize-y{resize:vertical}.appearance-none{appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-items-center{justify-items:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-7{gap:calc(var(--spacing) * 7)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-14{gap:calc(var(--spacing) * 14)}.gap-16{gap:calc(var(--spacing) * 16)}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-7>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 7) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 7) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-8{column-gap:calc(var(--spacing) * 8)}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-3\.5{row-gap:calc(var(--spacing) * 3.5)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-bone-50\/10>:not(:last-child)){border-color:#fbfaf71a}@supports (color:color-mix(in lab, red, red)){:where(.divide-bone-50\/10>:not(:last-child)){border-color:color-mix(in oklab, var(--color-bone-50) 10%, transparent)}}:where(.divide-pine-900\/6>:not(:last-child)){border-color:#16302b0f}@supports (color:color-mix(in lab, red, red)){:where(.divide-pine-900\/6>:not(:last-child)){border-color:color-mix(in oklab, var(--color-pine-900) 6%, transparent)}}:where(.divide-pine-900\/8>:not(:last-child)){border-color:#16302b14}@supports (color:color-mix(in lab, red, red)){:where(.divide-pine-900\/8>:not(:last-child)){border-color:color-mix(in oklab, var(--color-pine-900) 8%, transparent)}}:where(.divide-pine-900\/10>:not(:last-child)){border-color:#16302b1a}@supports (color:color-mix(in lab, red, red)){:where(.divide-pine-900\/10>:not(:last-child)){border-color:color-mix(in oklab, var(--color-pine-900) 10%, transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[2\.5rem\]{border-radius:2.5rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-ee-lg{border-end-end-radius:var(--radius-lg)}.rounded-es-lg{border-end-start-radius:var(--radius-lg)}.rounded-t-full{border-top-left-radius:3.40282e38px;border-top-right-radius:3.40282e38px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-b-\[2\.5rem\]{border-bottom-right-radius:2.5rem;border-bottom-left-radius:2.5rem}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-\[\#e3e3e0\]{border-color:#e3e3e0}.border-black{border-color:var(--color-black)}.border-bone-50\/10{border-color:#fbfaf71a}@supports (color:color-mix(in lab, red, red)){.border-bone-50\/10{border-color:color-mix(in oklab, var(--color-bone-50) 10%, transparent)}}.border-bone-50\/15{border-color:#fbfaf726}@supports (color:color-mix(in lab, red, red)){.border-bone-50\/15{border-color:color-mix(in oklab, var(--color-bone-50) 15%, transparent)}}.border-bone-50\/20{border-color:#fbfaf733}@supports (color:color-mix(in lab, red, red)){.border-bone-50\/20{border-color:color-mix(in oklab, var(--color-bone-50) 20%, transparent)}}.border-gray-300{border-color:var(--color-gray-300)}.border-pine-900\/8{border-color:#16302b14}@supports (color:color-mix(in lab, red, red)){.border-pine-900\/8{border-color:color-mix(in oklab, var(--color-pine-900) 8%, transparent)}}.border-pine-900\/10{border-color:#16302b1a}@supports (color:color-mix(in lab, red, red)){.border-pine-900\/10{border-color:color-mix(in oklab, var(--color-pine-900) 10%, transparent)}}.border-pine-900\/15{border-color:#16302b26}@supports (color:color-mix(in lab, red, red)){.border-pine-900\/15{border-color:color-mix(in oklab, var(--color-pine-900) 15%, transparent)}}.border-red-200{border-color:var(--color-red-200)}.border-sea-500\/50{border-color:#2f967e80}@supports (color:color-mix(in lab, red, red)){.border-sea-500\/50{border-color:color-mix(in oklab, var(--color-sea-500) 50%, transparent)}}.border-transparent{border-color:#0000}.bg-\[\#1b1b18\]{background-color:#1b1b18}.bg-\[\#FDFDFC\]{background-color:#fdfdfc}.bg-\[\#dbdbd7\]{background-color:#dbdbd7}.bg-\[\#fff2f2\]{background-color:#fff2f2}.bg-bone-50{background-color:var(--color-bone-50)}.bg-bone-50\/5{background-color:#fbfaf70d}@supports (color:color-mix(in lab, red, red)){.bg-bone-50\/5{background-color:color-mix(in oklab, var(--color-bone-50) 5%, transparent)}}.bg-bone-50\/15{background-color:#fbfaf726}@supports (color:color-mix(in lab, red, red)){.bg-bone-50\/15{background-color:color-mix(in oklab, var(--color-bone-50) 15%, transparent)}}.bg-bone-50\/95{background-color:#fbfaf7f2}@supports (color:color-mix(in lab, red, red)){.bg-bone-50\/95{background-color:color-mix(in oklab, var(--color-bone-50) 95%, transparent)}}.bg-bone-100{background-color:var(--color-bone-100)}.bg-current{background-color:currentColor}.bg-gray-200{background-color:var(--color-gray-200)}.bg-pine-100{background-color:var(--color-pine-100)}.bg-pine-900{background-color:var(--color-pine-900)}.bg-pine-950{background-color:var(--color-pine-950)}.bg-pine-950\/55{background-color:#0a1f1b8c}@supports (color:color-mix(in lab, red, red)){.bg-pine-950\/55{background-color:color-mix(in oklab, var(--color-pine-950) 55%, transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-sand-100\/80{background-color:#f3ead8cc}@supports (color:color-mix(in lab, red, red)){.bg-sand-100\/80{background-color:color-mix(in oklab, var(--color-sand-100) 80%, transparent)}}.bg-sand-200{background-color:var(--color-sand-200)}.bg-sand-200\/80{background-color:#e9dbbdcc}@supports (color:color-mix(in lab, red, red)){.bg-sand-200\/80{background-color:color-mix(in oklab, var(--color-sand-200) 80%, transparent)}}.bg-sand-400\/10{background-color:#c9ab741a}@supports (color:color-mix(in lab, red, red)){.bg-sand-400\/10{background-color:color-mix(in oklab, var(--color-sand-400) 10%, transparent)}}.bg-sea-100{background-color:var(--color-sea-100)}.bg-sea-100\/60{background-color:#d8f2e799}@supports (color:color-mix(in lab, red, red)){.bg-sea-100\/60{background-color:color-mix(in oklab, var(--color-sea-100) 60%, transparent)}}.bg-sea-100\/70{background-color:#d8f2e7b3}@supports (color:color-mix(in lab, red, red)){.bg-sea-100\/70{background-color:color-mix(in oklab, var(--color-sea-100) 70%, transparent)}}.bg-sea-400\/60{background-color:#50b29899}@supports (color:color-mix(in lab, red, red)){.bg-sea-400\/60{background-color:color-mix(in oklab, var(--color-sea-400) 60%, transparent)}}.bg-sea-500{background-color:var(--color-sea-500)}.bg-sea-500\/20{background-color:#2f967e33}@supports (color:color-mix(in lab, red, red)){.bg-sea-500\/20{background-color:color-mix(in oklab, var(--color-sea-500) 20%, transparent)}}.bg-sea-500\/60{background-color:#2f967e99}@supports (color:color-mix(in lab, red, red)){.bg-sea-500\/60{background-color:color-mix(in oklab, var(--color-sea-500) 60%, transparent)}}.bg-sea-600\/20{background-color:#21786633}@supports (color:color-mix(in lab, red, red)){.bg-sea-600\/20{background-color:color-mix(in oklab, var(--color-sea-600) 20%, transparent)}}.bg-sea-600\/25{background-color:#21786640}@supports (color:color-mix(in lab, red, red)){.bg-sea-600\/25{background-color:color-mix(in oklab, var(--color-sea-600) 25%, transparent)}}.bg-sea-700\/15{background-color:#1c605426}@supports (color:color-mix(in lab, red, red)){.bg-sea-700\/15{background-color:color-mix(in oklab, var(--color-sea-700) 15%, transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/70{background-color:#ffffffb3}@supports (color:color-mix(in lab, red, red)){.bg-white\/70{background-color:color-mix(in oklab, var(--color-white) 70%, transparent)}}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.bg-white\/90{background-color:color-mix(in oklab, var(--color-white) 90%, transparent)}}.bg-linear-to-b{--tw-gradient-position:to bottom}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-b{--tw-gradient-position:to bottom in oklab}}.bg-linear-to-b{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-br{--tw-gradient-position:to bottom right}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-br{--tw-gradient-position:to bottom right in oklab}}.bg-linear-to-br{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-linear-to-t{--tw-gradient-position:to top}@supports (background-image:linear-gradient(in lab, red, red)){.bg-linear-to-t{--tw-gradient-position:to top in oklab}}.bg-linear-to-t{background-image:linear-gradient(var(--tw-gradient-stops))}.bg-\[linear-gradient\(var\(--color-pine-100\)_1px\,transparent_1px\)\,linear-gradient\(90deg\,var\(--color-pine-100\)_1px\,transparent_1px\)\]{background-image:linear-gradient(var(--color-pine-100) 1px,transparent 1px),linear-gradient(90deg,var(--color-pine-100) 1px,transparent 1px)}.bg-\[radial-gradient\(circle_at_50\%_45\%\,transparent_30\%\,var\(--color-bone-100\)_75\%\)\]{background-image:radial-gradient(circle at 50% 45%,transparent 30%,var(--color-bone-100) 75%)}.bg-\[url\(\'data\:image\/svg\+xml\;charset\=utf-8\,\%3Csvg\%20xmlns\=\%22http\:\/\/www\.w3\.org\/2000\/svg\%22\%20fill\=\%22none\%22\%20viewBox\=\%220\%200\%2024\%2024\%22\%20stroke\=\%22\%234b766c\%22\%20stroke-width\=\%222\%22\%3E\%3Cpath\%20stroke-linecap\=\%22round\%22\%20stroke-linejoin\=\%22round\%22\%20d\=\%22M6\%209l6\%206\%206-6\%22\/\%3E\%3C\/svg\%3E\'\)\]{background-image:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20fill=%22none%22%20viewBox=%220%200%2024%2024%22%20stroke=%22%234b766c%22%20stroke-width=%222%22%3E%3Cpath%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%20d=%22M6%209l6%206%206-6%22/%3E%3C/svg%3E)}.from-bone-50{--tw-gradient-from:var(--color-bone-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-bone-100{--tw-gradient-from:var(--color-bone-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pine-900{--tw-gradient-from:var(--color-pine-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pine-950{--tw-gradient-from:var(--color-pine-950);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pine-950\/25{--tw-gradient-from:#0a1f1b40}@supports (color:color-mix(in lab, red, red)){.from-pine-950\/25{--tw-gradient-from:color-mix(in oklab, var(--color-pine-950) 25%, transparent)}}.from-pine-950\/25{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pine-950\/65{--tw-gradient-from:#0a1f1ba6}@supports (color:color-mix(in lab, red, red)){.from-pine-950\/65{--tw-gradient-from:color-mix(in oklab, var(--color-pine-950) 65%, transparent)}}.from-pine-950\/65{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-bone-50{--tw-gradient-via:var(--color-bone-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-bone-100{--tw-gradient-via:var(--color-bone-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-pine-950{--tw-gradient-via:var(--color-pine-950);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-pine-950\/5{--tw-gradient-via:#0a1f1b0d}@supports (color:color-mix(in lab, red, red)){.via-pine-950\/5{--tw-gradient-via:color-mix(in oklab, var(--color-pine-950) 5%, transparent)}}.via-pine-950\/5{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-pine-950\/15{--tw-gradient-via:#0a1f1b26}@supports (color:color-mix(in lab, red, red)){.via-pine-950\/15{--tw-gradient-via:color-mix(in oklab, var(--color-pine-950) 15%, transparent)}}.via-pine-950\/15{--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white{--tw-gradient-via:var(--color-white);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-bone-50{--tw-gradient-to:var(--color-bone-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-bone-100{--tw-gradient-to:var(--color-bone-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-sea-900{--tw-gradient-to:var(--color-sea-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.box-decoration-clone{-webkit-box-decoration-break:clone;box-decoration-break:clone}.box-decoration-slice{-webkit-box-decoration-break:slice;box-decoration-break:slice}.decoration-clone{-webkit-box-decoration-break:clone;box-decoration-break:clone}.decoration-slice{-webkit-box-decoration-break:slice;box-decoration-break:slice}.bg-size-\[1\.1rem\]{background-size:1.1rem}.bg-size-\[42px_42px\]{background-size:42px 42px}.bg-position-\[right_1rem_center\]{background-position:right 1rem center}.bg-no-repeat{background-repeat:no-repeat}.fill-current{fill:currentColor}.fill-pine-100{fill:var(--color-pine-100)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-\[center_48\%\]{object-position:center 48%}.object-center{object-position:center}.p-0{padding:calc(var(--spacing) * 0)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-7{padding:calc(var(--spacing) * 7)}.p-8{padding:calc(var(--spacing) * 8)}.p-10{padding:calc(var(--spacing) * 10)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-7{padding-block:calc(var(--spacing) * 7)}.py-14{padding-block:calc(var(--spacing) * 14)}.py-24{padding-block:calc(var(--spacing) * 24)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-20{padding-top:calc(var(--spacing) * 20)}.pt-32{padding-top:calc(var(--spacing) * 32)}.pr-2\.5{padding-right:calc(var(--spacing) * 2.5)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-3\.5{padding-right:calc(var(--spacing) * 3.5)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pr-11{padding-right:calc(var(--spacing) * 11)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-9{padding-bottom:calc(var(--spacing) * 9)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-14{padding-bottom:calc(var(--spacing) * 14)}.pb-20{padding-bottom:calc(var(--spacing) * 20)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-7{padding-left:calc(var(--spacing) * 7)}.text-center{text-align:center}.text-left{text-align:left}.font-display{font-family:var(--font-display)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.62rem\]{font-size:.62rem}.text-\[0\.68rem\]{font-size:.68rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[1\.05rem\]{font-size:1.05rem}.text-\[1\.6rem\]{font-size:1.6rem}.text-\[1\.7rem\]{font-size:1.7rem}.text-\[1\.15rem\]{font-size:1.15rem}.text-\[1\.45rem\]{font-size:1.45rem}.text-\[2\.1rem\]{font-size:2.1rem}.text-\[2\.7rem\]{font-size:2.7rem}.text-\[11\.5vw\]{font-size:11.5vw}.text-\[13px\]{font-size:13px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-\[1\.06\]{--tw-leading:1.06;line-height:1.06}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[1\.12\]{--tw-leading:1.12;line-height:1.12}.leading-\[20px\]{--tw-leading:20px;line-height:20px}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.14em\]{--tw-tracking:.14em;letter-spacing:.14em}.tracking-\[0\.22em\]{--tw-tracking:.22em;letter-spacing:.22em}.tracking-\[0\.24em\]{--tw-tracking:.24em;letter-spacing:.24em}.tracking-\[0\.28em\]{--tw-tracking:.28em;letter-spacing:.28em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.text-pretty{text-wrap:pretty}.overflow-ellipsis,.text-ellipsis{text-overflow:ellipsis}.text-\[\#1B1B18\],.text-\[\#1b1b18\]{color:#1b1b18}.text-\[\#706f6c\]{color:#706f6c}.text-\[\#F3BEC7\]{color:#f3bec7}.text-\[\#F8B803\]{color:#f8b803}.text-\[\#F53003\],.text-\[\#f53003\]{color:#f53003}.text-bone-50{color:var(--color-bone-50)}.text-bone-50\/6{color:#fbfaf70f}@supports (color:color-mix(in lab, red, red)){.text-bone-50\/6{color:color-mix(in oklab, var(--color-bone-50) 6%, transparent)}}.text-bone-50\/40{color:#fbfaf766}@supports (color:color-mix(in lab, red, red)){.text-bone-50\/40{color:color-mix(in oklab, var(--color-bone-50) 40%, transparent)}}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-pine-100{color:var(--color-pine-100)}.text-pine-200{color:var(--color-pine-200)}.text-pine-300{color:var(--color-pine-300)}.text-pine-400{color:var(--color-pine-400)}.text-pine-500{color:var(--color-pine-500)}.text-pine-600{color:var(--color-pine-600)}.text-pine-700{color:var(--color-pine-700)}.text-pine-800{color:var(--color-pine-800)}.text-pine-900{color:var(--color-pine-900)}.text-pine-950{color:var(--color-pine-950)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-sand-400{color:var(--color-sand-400)}.text-sea-200{color:var(--color-sea-200)}.text-sea-300{color:var(--color-sea-300)}.text-sea-400{color:var(--color-sea-400)}.text-sea-400\/80{color:#50b298cc}@supports (color:color-mix(in lab, red, red)){.text-sea-400\/80{color:color-mix(in oklab, var(--color-sea-400) 80%, transparent)}}.text-sea-600{color:var(--color-sea-600)}.text-sea-700{color:var(--color-sea-700)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-100{opacity:1}.mix-blend-color{mix-blend-mode:color}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-hard-light{mix-blend-mode:hard-light}.mix-blend-multiply{mix-blend-mode:multiply}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_18px_40px_-16px_rgba\(10\,31\,27\,0\.6\)\]{--tw-shadow:0 18px 40px -16px var(--tw-shadow-color,#0a1f1b99);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_18px_40px_-18px_rgba\(10\,31\,27\,0\.6\)\]{--tw-shadow:0 18px 40px -18px var(--tw-shadow-color,#0a1f1b99);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_20px_45px_-22px_rgba\(10\,31\,27\,0\.4\)\]{--tw-shadow:0 20px 45px -22px var(--tw-shadow-color,#0a1f1b66);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_20px_45px_-22px_rgba\(10\,31\,27\,0\.35\)\]{--tw-shadow:0 20px 45px -22px var(--tw-shadow-color,#0a1f1b59);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_30px_60px_-40px_rgba\(0\,0\,0\,0\.8\)\]{--tw-shadow:0 30px 60px -40px var(--tw-shadow-color,#000c);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_35px_70px_-42px_rgba\(10\,31\,27\,0\.65\)\]{--tw-shadow:0 35px 70px -42px var(--tw-shadow-color,#0a1f1ba6);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_40px_80px_-40px_rgba\(10\,31\,27\,0\.7\)\]{--tw-shadow:0 40px 80px -40px var(--tw-shadow-color,#0a1f1bb3);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_40px_90px_-50px_rgba\(10\,31\,27\,0\.5\)\]{--tw-shadow:0 40px 90px -50px var(--tw-shadow-color,#0a1f1b80);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0px_0px_1px_0px_rgba\(0\,0\,0\,0\.03\)\,0px_1px_2px_0px_rgba\(0\,0\,0\,0\.06\)\]{--tw-shadow:0px 0px 1px 0px var(--tw-shadow-color,#00000008), 0px 1px 2px 0px var(--tw-shadow-color,#0000000f);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[inset_0px_0px_0px_1px_rgba\(26\,26\,0\,0\.16\)\]{--tw-shadow:inset 0px 0px 0px 1px var(--tw-shadow-color,#1a1a0029);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-gray-300{--tw-ring-color:var(--color-gray-300)}.ring-red-300{--tw-ring-color:var(--color-red-300)}.ring-transparent{--tw-ring-color:transparent}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.delay-300{transition-delay:.3s}.delay-400{transition-delay:.4s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-750{--tw-duration:.75s;transition-duration:.75s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.will-change-transform{will-change:transform}.select-none{-webkit-user-select:none;user-select:none}.\[--stroke-color\:\#1B1B18\]{--stroke-color:#1b1b18}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.group-hover\:translate-x-0\.5:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:-translate-y-0\.5:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-y-0\.5:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:border-sea-400:is(:where(.group):hover *){border-color:var(--color-sea-400)}.group-hover\:border-sea-400\/40:is(:where(.group):hover *){border-color:#50b29866}@supports (color:color-mix(in lab, red, red)){.group-hover\:border-sea-400\/40:is(:where(.group):hover *){border-color:color-mix(in oklab, var(--color-sea-400) 40%, transparent)}}.group-hover\:bg-sea-500:is(:where(.group):hover *){background-color:var(--color-sea-500)}.group-hover\:text-bone-50:is(:where(.group):hover *){color:var(--color-bone-50)}.group-hover\:text-pine-200:is(:where(.group):hover *){color:var(--color-pine-200)}.group-hover\:text-sea-300:is(:where(.group):hover *){color:var(--color-sea-300)}.group-hover\:text-sea-400\/70:is(:where(.group):hover *){color:#50b298b3}@supports (color:color-mix(in lab, red, red)){.group-hover\:text-sea-400\/70:is(:where(.group):hover *){color:color-mix(in oklab, var(--color-sea-400) 70%, transparent)}}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/link\:scale-x-100:is(:where(.group\/link):hover *){--tw-scale-x:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}}.group-focus-visible\:scale-105:is(:where(.group):focus-visible *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-focus-visible\:opacity-100:is(:where(.group):focus-visible *){opacity:1}.group-data-\[menu-open\=true\]\/header\:w-6:is(:where(.group\/header)[data-menu-open=true] *){width:calc(var(--spacing) * 6)}.group-data-\[menu-open\=true\]\/header\:translate-y-\[-7\.5px\]:is(:where(.group\/header)[data-menu-open=true] *){--tw-translate-y:-7.5px;translate:var(--tw-translate-x) var(--tw-translate-y)}.group-data-\[menu-open\=true\]\/header\:translate-y-\[7\.5px\]:is(:where(.group\/header)[data-menu-open=true] *){--tw-translate-y:7.5px;translate:var(--tw-translate-x) var(--tw-translate-y)}.group-data-\[menu-open\=true\]\/header\:-rotate-45:is(:where(.group\/header)[data-menu-open=true] *){rotate:-45deg}.group-data-\[menu-open\=true\]\/header\:rotate-45:is(:where(.group\/header)[data-menu-open=true] *){rotate:45deg}.group-data-\[menu-open\=true\]\/header\:opacity-0:is(:where(.group\/header)[data-menu-open=true] *){opacity:0}.selection\:bg-sea-200 ::selection{background-color:var(--color-sea-200)}.selection\:bg-sea-200::selection{background-color:var(--color-sea-200)}.selection\:text-pine-950 ::selection{color:var(--color-pine-950)}.selection\:text-pine-950::selection{color:var(--color-pine-950)}.placeholder\:text-pine-300::placeholder{color:var(--color-pine-300)}.backdrop\:bg-pine-950\/85::backdrop{background-color:#0a1f1bd9}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-pine-950\/85::backdrop{background-color:color-mix(in oklab, var(--color-pine-950) 85%, transparent)}}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:start-\[0\.4rem\]:before{content:var(--tw-content);inset-inline-start:.4rem}.before\:top-0:before{content:var(--tw-content);top:calc(var(--spacing) * 0)}.before\:top-1\/2:before{content:var(--tw-content);top:50%}.before\:bottom-0:before{content:var(--tw-content);bottom:calc(var(--spacing) * 0)}.before\:bottom-1\/2:before{content:var(--tw-content);bottom:50%}.before\:left-\[0\.4rem\]:before{content:var(--tw-content);left:.4rem}.before\:border-l:before{content:var(--tw-content);border-left-style:var(--tw-border-style);border-left-width:1px}.before\:border-\[\#e3e3e0\]:before{content:var(--tw-content);border-color:#e3e3e0}@media (hover:hover){.hover\:-translate-y-1:hover{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-black:hover{border-color:var(--color-black)}.hover\:border-pine-900:hover{border-color:var(--color-pine-900)}.hover\:border-pine-950:hover{border-color:var(--color-pine-950)}.hover\:border-sea-400:hover{border-color:var(--color-sea-400)}.hover\:bg-black:hover{background-color:var(--color-black)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-pine-900:hover{background-color:var(--color-pine-900)}.hover\:bg-pine-900\/5:hover{background-color:#16302b0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pine-900\/5:hover{background-color:color-mix(in oklab, var(--color-pine-900) 5%, transparent)}}.hover\:bg-pine-950:hover{background-color:var(--color-pine-950)}.hover\:bg-sea-300:hover{background-color:var(--color-sea-300)}.hover\:bg-sea-700:hover{background-color:var(--color-sea-700)}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:text-bone-50:hover{color:var(--color-bone-50)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-pine-950:hover{color:var(--color-pine-950)}.hover\:text-sea-300:hover{color:var(--color-sea-300)}.hover\:text-sea-700:hover{color:var(--color-sea-700)}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-\[0_24px_50px_-26px_rgba\(10\,31\,27\,0\.35\)\]:hover{--tw-shadow:0 24px 50px -26px var(--tw-shadow-color,#0a1f1b59);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:shadow-\[0_28px_60px_-28px_rgba\(10\,31\,27\,0\.55\)\]:hover{--tw-shadow:0 28px 60px -28px var(--tw-shadow-color,#0a1f1b8c);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-4:focus{top:calc(var(--spacing) * 4)}.focus\:left-4:focus{left:calc(var(--spacing) * 4)}.focus\:z-100:focus{z-index:100}.focus\:rounded-full:focus{border-radius:3.40282e38px}.focus\:border-blue-300:focus{border-color:var(--color-blue-300)}.focus\:bg-pine-900:focus{background-color:var(--color-pine-900)}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:px-5:focus{padding-inline:calc(var(--spacing) * 5)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:text-sm:focus{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.focus\:font-semibold:focus{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.focus\:text-bone-50:focus{color:var(--color-bone-50)}.focus\:shadow-lg:focus{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-sea-500:focus{--tw-ring-color:var(--color-sea-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-offset-4:focus-visible{outline-offset:4px}.focus-visible\:outline-sea-400:focus-visible{outline-color:var(--color-sea-400)}.focus-visible\:outline-sea-600:focus-visible{outline-color:var(--color-sea-600)}.active\:bg-gray-100:active{background-color:var(--color-gray-100)}.active\:text-gray-500:active{color:var(--color-gray-500)}.active\:text-gray-700:active{color:var(--color-gray-700)}.active\:text-gray-800:active{color:var(--color-gray-800)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:aspect-16\/10{aspect-ratio:16/10}.sm\:h-48{height:calc(var(--spacing) * 48)}.sm\:h-80{height:calc(var(--spacing) * 80)}.sm\:max-w-56{max-width:calc(var(--spacing) * 56)}.sm\:flex-1{flex:1}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-10{gap:calc(var(--spacing) * 10)}.sm\:p-10{padding:calc(var(--spacing) * 10)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:px-9{padding-inline:calc(var(--spacing) * 9)}.sm\:px-10{padding-inline:calc(var(--spacing) * 10)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:py-28{padding-block:calc(var(--spacing) * 28)}.sm\:pt-40{padding-top:calc(var(--spacing) * 40)}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.sm\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-\[1\.7rem\]{font-size:1.7rem}.sm\:text-\[1\.35rem\]{font-size:1.35rem}.sm\:text-\[2\.6rem\]{font-size:2.6rem}}@media (width>=48rem){.md\:block{display:block}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width>=64rem){.lg\:sticky{position:sticky}.lg\:top-32{top:calc(var(--spacing) * 32)}.lg\:-right-10{right:calc(var(--spacing) * -10)}.lg\:order-first{order:-9999}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:mr-8{margin-right:calc(var(--spacing) * 8)}.lg\:mb-0{margin-bottom:calc(var(--spacing) * 0)}.lg\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.lg\:-ml-px{margin-left:-1px}.lg\:ml-0{margin-left:calc(var(--spacing) * 0)}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:inline-flex{display:inline-flex}.lg\:aspect-4\/3{aspect-ratio:4/3}.lg\:aspect-auto{aspect-ratio:auto}.lg\:h-24{height:calc(var(--spacing) * 24)}.lg\:h-40{height:calc(var(--spacing) * 40)}.lg\:h-80{height:calc(var(--spacing) * 80)}.lg\:h-88{height:calc(var(--spacing) * 88)}.lg\:min-h-full{min-height:100%}.lg\:w-60{width:calc(var(--spacing) * 60)}.lg\:w-\[438px\]{width:438px}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:max-w-none{max-width:none}.lg\:flex-none{flex:none}.lg\:grow{flex-grow:1}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[0\.9fr_1\.1fr\]{grid-template-columns:.9fr 1.1fr}.lg\:grid-cols-\[0\.85fr_1\.15fr\]{grid-template-columns:.85fr 1.15fr}.lg\:grid-cols-\[0\.92fr_1\.08fr\]{grid-template-columns:.92fr 1.08fr}.lg\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:justify-center{justify-content:center}.lg\:gap-2{gap:calc(var(--spacing) * 2)}.lg\:gap-12{gap:calc(var(--spacing) * 12)}.lg\:gap-20{gap:calc(var(--spacing) * 20)}.lg\:self-start{align-self:flex-start}.lg\:rounded-ss-lg{border-start-start-radius:var(--radius-lg)}.lg\:rounded-ee-none{border-end-end-radius:0}.lg\:rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.lg\:rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.lg\:p-8{padding:calc(var(--spacing) * 8)}.lg\:p-12{padding:calc(var(--spacing) * 12)}.lg\:p-20{padding:calc(var(--spacing) * 20)}.lg\:py-36{padding-block:calc(var(--spacing) * 36)}.lg\:pt-10{padding-top:calc(var(--spacing) * 10)}.lg\:pt-20{padding-top:calc(var(--spacing) * 20)}.lg\:pt-24{padding-top:calc(var(--spacing) * 24)}.lg\:pt-36{padding-top:calc(var(--spacing) * 36)}.lg\:pt-48{padding-top:calc(var(--spacing) * 48)}.lg\:pb-28{padding-bottom:calc(var(--spacing) * 28)}.lg\:pb-36{padding-bottom:calc(var(--spacing) * 36)}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-\[4\.1rem\]{font-size:4.1rem}.lg\:text-\[7\.5rem\]{font-size:7.5rem}.lg\:leading-\[1\.08\]{--tw-leading:1.08;line-height:1.08}}.rtl\:flex-row-reverse:where(:is(:lang(ae),:lang(ar),:lang(arc),:lang(bcc),:lang(bqi),:lang(ckb),:lang(dv),:lang(fa),:lang(glk),:lang(he),:lang(ku),:lang(mzn),:lang(nqo),:lang(pnb),:lang(ps),:lang(sd),:lang(ug),:lang(ur),:lang(yi)),[dir=rtl],[dir=rtl] *){flex-direction:row-reverse}@media (prefers-color-scheme:dark){.dark\:border-\[\#3E3E3A\]{border-color:#3e3e3a}.dark\:border-\[\#eeeeec\]{border-color:#eeeeec}.dark\:border-gray-600{border-color:var(--color-gray-600)}.dark\:bg-\[\#0a0a0a\]{background-color:#0a0a0a}.dark\:bg-\[\#1D0002\]{background-color:#1d0002}.dark\:bg-\[\#3E3E3A\]{background-color:#3e3e3a}.dark\:bg-\[\#161615\]{background-color:#161615}.dark\:bg-\[\#eeeeec\]{background-color:#eeeeec}.dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark\:text-\[\#1C1C1A\]{color:#1c1c1a}.dark\:text-\[\#4B0600\]{color:#4b0600}.dark\:text-\[\#391800\]{color:#391800}.dark\:text-\[\#733000\]{color:#733000}.dark\:text-\[\#A1A09A\]{color:#a1a09a}.dark\:text-\[\#EDEDEC\]{color:#ededec}.dark\:text-\[\#F61500\]{color:#f61500}.dark\:text-\[\#FF4433\]{color:#f43}.dark\:text-black{color:var(--color-black)}.dark\:text-gray-200{color:var(--color-gray-200)}.dark\:text-gray-300{color:var(--color-gray-300)}.dark\:text-gray-400{color:var(--color-gray-400)}.dark\:text-gray-600{color:var(--color-gray-600)}.dark\:text-white{color:var(--color-white)}.dark\:mix-blend-hard-light{mix-blend-mode:hard-light}.dark\:mix-blend-normal{mix-blend-mode:normal}.dark\:shadow-\[inset_0px_0px_0px_1px_\#fffaed2d\]{--tw-shadow:inset 0px 0px 0px 1px var(--tw-shadow-color,#fffaed2d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.dark\:\[--stroke-color\:\#FF750F\]{--stroke-color:#ff750f}.dark\:before\:border-\[\#3E3E3A\]:before{content:var(--tw-content);border-color:#3e3e3a}@media (hover:hover){.dark\:hover\:border-white:hover{border-color:var(--color-white)}.dark\:hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.dark\:hover\:bg-white:hover{background-color:var(--color-white)}.dark\:hover\:text-gray-200:hover{color:var(--color-gray-200)}.dark\:hover\:text-gray-300:hover{color:var(--color-gray-300)}}.dark\:focus\:border-blue-700:focus{border-color:var(--color-blue-700)}.dark\:focus\:border-blue-800:focus{border-color:var(--color-blue-800)}.dark\:active\:bg-gray-700:active{background-color:var(--color-gray-700)}.dark\:active\:text-gray-300:active{color:var(--color-gray-300)}}@starting-style{.starting\:opacity-0{opacity:0}}@media (prefers-reduced-motion:no-preference){@starting-style{.motion-safe\:starting\:-translate-x-\[26px\]{--tw-translate-x:calc(26px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:-translate-x-\[51px\]{--tw-translate-x:calc(51px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:-translate-x-\[78px\]{--tw-translate-x:calc(78px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:-translate-x-\[102px\]{--tw-translate-x:calc(102px * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}}@starting-style{.motion-safe\:starting\:translate-y-6{--tw-translate-y:calc(var(--spacing) * 6);translate:var(--tw-translate-x) var(--tw-translate-y)}}}.\[\&\.is-scrolled\]\:border-pine-900\/8.is-scrolled{border-color:#16302b14}@supports (color:color-mix(in lab, red, red)){.\[\&\.is-scrolled\]\:border-pine-900\/8.is-scrolled{border-color:color-mix(in oklab, var(--color-pine-900) 8%, transparent)}}.\[\&\.is-scrolled\]\:bg-bone-50\/80.is-scrolled{background-color:#fbfaf7cc}@supports (color:color-mix(in lab, red, red)){.\[\&\.is-scrolled\]\:bg-bone-50\/80.is-scrolled{background-color:color-mix(in oklab, var(--color-bone-50) 80%, transparent)}}.\[\&\.is-scrolled\]\:shadow-\[0_1px_0_0_rgba\(10\,31\,27\,0\.04\)\,0_12px_32px_-20px_rgba\(10\,31\,27\,0\.25\)\].is-scrolled{--tw-shadow:0 1px 0 0 var(--tw-shadow-color,#0a1f1b0a), 0 12px 32px -20px var(--tw-shadow-color,#0a1f1b40);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.\[\&\.is-scrolled\]\:backdrop-blur-xl.is-scrolled{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}}@keyframes drift{0%,to{transform:translateY(0)rotate(0)}50%{transform:translateY(-18px)rotate(1.2deg)}}@keyframes fade-up{0%{opacity:0;transform:translateY(26px)}to{opacity:1;transform:translateY(0)}}@keyframes carousel-right{0%{transform:translate(-50%)}to{transform:translate(0)}}html{scroll-behavior:smooth}body{background-color:var(--color-bone-50);color:var(--color-pine-900);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;overflow-x:hidden}.font-display{font-optical-sizing:auto;letter-spacing:-.015em}html.js .reveal{opacity:0;transition:opacity .85s cubic-bezier(.16,1,.3,1),transform .85s cubic-bezier(.16,1,.3,1);transition-delay:var(--reveal-delay,0s);will-change:opacity, transform;transform:translateY(30px)}html.js .reveal.is-visible{opacity:1;transform:none}.carousel-track{will-change:transform;animation:42s linear infinite carousel-right}.carousel-window:hover .carousel-track,.carousel-window:focus-within .carousel-track{animation-play-state:paused}[data-carousel-dialog]::backdrop{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.parallax-fixed{background-position:50%;background-size:cover;background-attachment:scroll}@media (width>=768px) and (prefers-reduced-motion:no-preference){.parallax-fixed{background-attachment:fixed}}.grain:before{content:"";pointer-events:none;opacity:.5;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.04 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");position:absolute;inset:0}@media (width>=768px){::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background:var(--color-bone-100)}::-webkit-scrollbar-thumb{background:var(--color-pine-200);border:2px solid var(--color-bone-100);border-radius:9999px}::-webkit-scrollbar-thumb:hover{background:var(--color-pine-300)}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}html.js .reveal{opacity:1;transform:none}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}} diff --git a/public/build/assets/app-x-0rverM.js b/public/build/assets/app-x-0rverM.js deleted file mode 100644 index 02df891..0000000 --- a/public/build/assets/app-x-0rverM.js +++ /dev/null @@ -1 +0,0 @@ -document.documentElement.classList.add(`js`);var e=window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;function t(){let e=document.querySelector(`[data-site-header]`);if(!e)return;let t=()=>{window.scrollY>24?e.classList.add(`is-scrolled`):e.classList.remove(`is-scrolled`)};t(),window.addEventListener(`scroll`,t,{passive:!0})}function n(){let e=document.querySelector(`[data-site-header]`);if(!e||e.dataset.mobileMenuInitialized===`true`)return;let t=e.querySelector(`[data-mobile-menu-toggle]`),n=e.querySelector(`[data-mobile-menu]`);if(!t||!n)return;e.dataset.mobileMenuInitialized=`true`;let r=r=>{e.dataset.menuOpen=String(r),n.hidden=!r,t.setAttribute(`aria-expanded`,String(r)),t.setAttribute(`aria-label`,r?`Close menu`:`Open menu`),document.body.classList.toggle(`overflow-hidden`,r)},i=()=>t.getAttribute(`aria-expanded`)===`true`;t.addEventListener(`click`,()=>{r(!i())}),e.querySelectorAll(`[data-mobile-menu-close]`).forEach(e=>{e.addEventListener(`click`,()=>r(!1))}),e.addEventListener(`keydown`,e=>{e.key===`Escape`&&i()&&(r(!1),t.focus())}),window.matchMedia(`(min-width: 1024px)`).addEventListener(`change`,e=>{e.matches&&r(!1)})}function r(){if(e||!window.matchMedia(`(min-width: 768px)`).matches)return;let t=Array.from(document.querySelectorAll(`[data-parallax]`));if(t.length===0)return;let n=!1,r=()=>{let e=window.scrollY;for(let n of t){let t=parseFloat(n.dataset.parallax)||.15;n.style.transform=`translate3d(0, ${e*t}px, 0)`}n=!1};r(),window.addEventListener(`scroll`,()=>{n||=(window.requestAnimationFrame(r),!0)},{passive:!0})}function i(){let t=document.querySelectorAll(`.reveal`);if(t.length===0)return;if(e||!(`IntersectionObserver`in window)){t.forEach(e=>e.classList.add(`is-visible`));return}let n=new IntersectionObserver((e,t)=>{e.forEach(e=>{e.isIntersecting&&(e.target.classList.add(`is-visible`),t.unobserve(e.target))})},{rootMargin:`0px 0px -10% 0px`,threshold:.12});t.forEach(e=>n.observe(e))}function a(){document.querySelectorAll(`[data-image-carousel]`).forEach(e=>{if(e.dataset.initialized===`true`)return;let t=e.querySelector(`[data-carousel-dialog]`),n=e.querySelector(`[data-carousel-dialog-image]`),r=e.querySelector(`[data-carousel-dialog-close]`);if(!t||!n||!r)return;e.dataset.initialized=`true`;let i=null,a=()=>{t.open&&t.close()};e.querySelectorAll(`[data-carousel-image]`).forEach(e=>{e.addEventListener(`click`,()=>{window.matchMedia(`(min-width: 768px)`).matches&&(i=e,n.src=e.dataset.imageSrc,n.alt=e.dataset.imageAlt,t.showModal(),document.body.classList.add(`overflow-hidden`))})}),r.addEventListener(`click`,a),t.addEventListener(`click`,e=>{e.target===t&&a()}),t.addEventListener(`close`,()=>{document.body.classList.remove(`overflow-hidden`),n.src=``,n.alt=``,i?.focus()}),window.matchMedia(`(max-width: 767px)`).addEventListener(`change`,e=>{e.matches&&a()})})}function o(){t(),n(),r(),i(),a()}document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,o):o(),document.addEventListener(`livewire:navigated`,o); \ No newline at end of file diff --git a/public/build/manifest.json b/public/build/manifest.json index 9e2979f..f4cb887 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -196,7 +196,7 @@ "src": "_instrument-sans-600-normal-DMks36a2.woff" }, "resources/css/app.css": { - "file": "assets/app-DyABguZv.css", + "file": "assets/app-Cyd-Yril.css", "name": "app", "names": [ "app.css" @@ -205,7 +205,7 @@ "isEntry": true }, "resources/js/app.js": { - "file": "assets/app-x-0rverM.js", + "file": "assets/app-DCb96HGE.js", "name": "app", "src": "resources/js/app.js", "isEntry": true diff --git a/public/images/illustrations/consultation-1200.webp b/public/images/illustrations/consultation-1200.webp new file mode 100644 index 0000000..7c85807 Binary files /dev/null and b/public/images/illustrations/consultation-1200.webp differ diff --git a/public/images/illustrations/consultation-480.webp b/public/images/illustrations/consultation-480.webp new file mode 100644 index 0000000..f5f7202 Binary files /dev/null and b/public/images/illustrations/consultation-480.webp differ diff --git a/public/images/illustrations/consultation-768.webp b/public/images/illustrations/consultation-768.webp new file mode 100644 index 0000000..7ef73d4 Binary files /dev/null and b/public/images/illustrations/consultation-768.webp differ diff --git a/public/images/illustrations/exercise-1200.webp b/public/images/illustrations/exercise-1200.webp new file mode 100644 index 0000000..e3c2960 Binary files /dev/null and b/public/images/illustrations/exercise-1200.webp differ diff --git a/public/images/illustrations/exercise-480.webp b/public/images/illustrations/exercise-480.webp new file mode 100644 index 0000000..848865b Binary files /dev/null and b/public/images/illustrations/exercise-480.webp differ diff --git a/public/images/illustrations/exercise-768.webp b/public/images/illustrations/exercise-768.webp new file mode 100644 index 0000000..da60b6c Binary files /dev/null and b/public/images/illustrations/exercise-768.webp differ diff --git a/public/images/illustrations/knee-1200.webp b/public/images/illustrations/knee-1200.webp new file mode 100644 index 0000000..a865bf9 Binary files /dev/null and b/public/images/illustrations/knee-1200.webp differ diff --git a/public/images/illustrations/knee-480.webp b/public/images/illustrations/knee-480.webp new file mode 100644 index 0000000..182e58b Binary files /dev/null and b/public/images/illustrations/knee-480.webp differ diff --git a/public/images/illustrations/knee-768.webp b/public/images/illustrations/knee-768.webp new file mode 100644 index 0000000..1c5aa53 Binary files /dev/null and b/public/images/illustrations/knee-768.webp differ diff --git a/public/images/illustrations/mobility-1200.webp b/public/images/illustrations/mobility-1200.webp new file mode 100644 index 0000000..0b36f72 Binary files /dev/null and b/public/images/illustrations/mobility-1200.webp differ diff --git a/public/images/illustrations/mobility-480.webp b/public/images/illustrations/mobility-480.webp new file mode 100644 index 0000000..bf420aa Binary files /dev/null and b/public/images/illustrations/mobility-480.webp differ diff --git a/public/images/illustrations/mobility-768.webp b/public/images/illustrations/mobility-768.webp new file mode 100644 index 0000000..73718bb Binary files /dev/null and b/public/images/illustrations/mobility-768.webp differ diff --git a/public/images/illustrations/neck-1200.webp b/public/images/illustrations/neck-1200.webp new file mode 100644 index 0000000..5b4fa93 Binary files /dev/null and b/public/images/illustrations/neck-1200.webp differ diff --git a/public/images/illustrations/neck-480.webp b/public/images/illustrations/neck-480.webp new file mode 100644 index 0000000..87f7a35 Binary files /dev/null and b/public/images/illustrations/neck-480.webp differ diff --git a/public/images/illustrations/neck-768.webp b/public/images/illustrations/neck-768.webp new file mode 100644 index 0000000..68c6336 Binary files /dev/null and b/public/images/illustrations/neck-768.webp differ diff --git a/public/images/illustrations/shoulder-1200.webp b/public/images/illustrations/shoulder-1200.webp new file mode 100644 index 0000000..77b8718 Binary files /dev/null and b/public/images/illustrations/shoulder-1200.webp differ diff --git a/public/images/illustrations/shoulder-480.webp b/public/images/illustrations/shoulder-480.webp new file mode 100644 index 0000000..8dd374e Binary files /dev/null and b/public/images/illustrations/shoulder-480.webp differ diff --git a/public/images/illustrations/shoulder-768.webp b/public/images/illustrations/shoulder-768.webp new file mode 100644 index 0000000..1f25223 Binary files /dev/null and b/public/images/illustrations/shoulder-768.webp differ diff --git a/public/images/illustrations/sports-1200.webp b/public/images/illustrations/sports-1200.webp new file mode 100644 index 0000000..9132105 Binary files /dev/null and b/public/images/illustrations/sports-1200.webp differ diff --git a/public/images/illustrations/sports-480.webp b/public/images/illustrations/sports-480.webp new file mode 100644 index 0000000..ebd9a2b Binary files /dev/null and b/public/images/illustrations/sports-480.webp differ diff --git a/public/images/illustrations/sports-768.webp b/public/images/illustrations/sports-768.webp new file mode 100644 index 0000000..4dd593c Binary files /dev/null and b/public/images/illustrations/sports-768.webp differ diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 1bf570c..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://danksandstrydom.co.za/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 303bdb1..0000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - https://danksandstrydom.co.za/ - monthly - 1.0 - - diff --git a/resources/css/app.css b/resources/css/app.css index b634a4d..56272ed 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -7,47 +7,46 @@ --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-display: 'Fraunces', ui-serif, Georgia, Cambria, 'Times New Roman', serif; - /* ------------------------------------------------------------------ - Danks & Strydom palette — editorial spa-clinical - ------------------------------------------------------------------ */ - - /* Bone — warm ivory surfaces */ - --color-bone-50: #fbfaf7; - --color-bone-100: #f6f3ec; - --color-bone-200: #ece7db; - --color-bone-300: #ddd5c2; - - /* Pine — deep blue-green ink, dark sections */ - --color-pine-50: #f1f6f4; - --color-pine-100: #dce8e4; - --color-pine-200: #bad1ca; - --color-pine-300: #8fb3a9; - --color-pine-400: #659186; - --color-pine-500: #4b766c; - --color-pine-600: #3a5e56; - --color-pine-700: #314d47; - --color-pine-800: #25403a; - --color-pine-900: #16302b; - --color-pine-950: #0a1f1b; - - /* Sea — soft sea-green accent */ - --color-sea-50: #effaf6; - --color-sea-100: #d8f2e7; - --color-sea-200: #b3e4d2; - --color-sea-300: #82cfb7; - --color-sea-400: #50b298; - --color-sea-500: #2f967e; - --color-sea-600: #217866; - --color-sea-700: #1c6054; - --color-sea-800: #194d44; - --color-sea-900: #16403a; - - /* Sand — warm accent, used sparingly */ - --color-sand-100: #f3ead8; - --color-sand-200: #e9dbbd; - --color-sand-300: #dcc69a; - --color-sand-400: #c9ab74; - --color-sand-500: #b18f53; + /* Danks & Strydom — blue brand, cool surfaces and readable ink. */ + --color-brand: #174780; + --color-brand-hover: #123A69; + --color-navy: #102B4E; + --color-link: #006D88; + --color-accent: #009BC2; + --color-card: #FFFFFF; + + --color-surface-50: #FAFCFE; + --color-surface-100: #EDF5FA; + --color-surface-200: #DCEBF5; + --color-surface-300: #C4D9E8; + + --color-ink-50: #F1F7FB; + --color-ink-100: #DFECF5; + --color-ink-200: #C3D8E8; + --color-ink-300: #A6C3D9; + --color-ink-400: #91AEC5; + --color-ink-500: #4C6478; + --color-ink-600: #4C6478; + --color-ink-700: #354F68; + --color-ink-800: #243E58; + --color-ink-900: #172C42; + + /* Light cyan for text on navy; darker shades for text on light surfaces. */ + --color-accent-50: #EFFAFF; + --color-accent-100: #DCF3FA; + --color-accent-200: #B9E7F3; + --color-accent-300: #8BD9ED; + --color-accent-400: #5CCAE4; + --color-accent-500: #009BC2; + --color-accent-600: #006D88; + --color-accent-700: #006D88; + --color-accent-800: #075870; + --color-accent-900: #12385A; + + /* Semantic confirmation colours remain independent of the brand. */ + --color-success-surface: #D8F2E7; + --color-success-ink: #1C6054; + --shadow-ink: 16, 43, 78; /* Custom animations */ --animate-drift: drift 11s ease-in-out infinite; @@ -77,8 +76,8 @@ html { } body { - background-color: var(--color-bone-50); - color: var(--color-pine-900); + background-color: var(--color-surface-50); + color: var(--color-ink-900); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; overflow-x: hidden; @@ -90,20 +89,19 @@ body { letter-spacing: -0.015em; } -/* ---- Scroll reveal (driven by app.js IntersectionObserver) ---- - Hidden state only applies once JS has tagged , - so content stays visible if JavaScript never loads. */ -html.js .reveal { - opacity: 0; - transform: translateY(30px); - transition: opacity 0.85s cubic-bezier(0.16, 1, 0.3, 1), - transform 0.85s cubic-bezier(0.16, 1, 0.3, 1); - transition-delay: var(--reveal-delay, 0ms); - will-change: opacity, transform; +/* Original 30px / 850ms entrance, triggered only after intersection. + No hidden waiting state: failed or absent JS leaves all content readable. + No forwards fill: completed entrances do not override card hover transforms. */ +@keyframes reveal-enter { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } } -html.js .reveal.is-visible { - opacity: 1; - transform: none; +@media (prefers-reduced-motion: no-preference) { + .reveal.is-visible { + animation: reveal-enter 0.85s cubic-bezier(0.16, 1, 0.3, 1) backwards; + animation-delay: var(--reveal-delay, 0ms); + } + .reveal:focus-within { animation: none; } } /* ---- Desktop image carousel ---- */ @@ -146,13 +144,13 @@ html.js .reveal.is-visible { /* Soft scrollbar (desktop) */ @media (min-width: 768px) { ::-webkit-scrollbar { width: 10px; } - ::-webkit-scrollbar-track { background: var(--color-bone-100); } + ::-webkit-scrollbar-track { background: var(--color-surface-100); } ::-webkit-scrollbar-thumb { - background: var(--color-pine-200); + background: var(--color-ink-200); border-radius: 9999px; - border: 2px solid var(--color-bone-100); + border: 2px solid var(--color-surface-100); } - ::-webkit-scrollbar-thumb:hover { background: var(--color-pine-300); } + ::-webkit-scrollbar-thumb:hover { background: var(--color-ink-300); } } /* Respect reduced motion */ @@ -165,3 +163,12 @@ html.js .reveal.is-visible { } html.js .reveal { opacity: 1; transform: none; } } + +/* Two contrasting edges keep keyboard focus visible on light and navy surfaces. */ +@layer base { + :where(a, button, input, textarea, select, [tabindex]):focus-visible { + outline: 2px solid var(--color-link); + outline-offset: 2px; + box-shadow: 0 0 0 2px var(--color-card); + } +} diff --git a/resources/js/analytics.js b/resources/js/analytics.js new file mode 100644 index 0000000..6c4187b --- /dev/null +++ b/resources/js/analytics.js @@ -0,0 +1,23 @@ +// Only generic events leave this adapter. A reviewed provider may listen for site:analytics. +export function createTracker(send) { + const accepted = new Set(); + return (name, id) => { + if (!['enquiry_accepted', 'phone_click', 'directions_click'].includes(name)) return; + if (name === 'enquiry_accepted') { + if (typeof id !== 'string' || !/^[0-9a-f-]{36}$/.test(id) || accepted.has(id)) return; + accepted.add(id); + } + send({ event: name }); + }; +} + +export function installTracking() { + if (document.body.dataset.analyticsEnabled !== 'true') return; + const track = createTracker((payload) => window.dispatchEvent(new CustomEvent('site:analytics', { detail: payload }))); + window.addEventListener('enquiry-accepted', (event) => track('enquiry_accepted', event.detail?.id)); + document.addEventListener('click', (event) => { + const link = event.target.closest?.('a[data-contact-action]'); + if (link?.dataset.contactAction === 'phone') track('phone_click'); + if (link?.dataset.contactAction === 'directions') track('directions_click'); + }); +} diff --git a/resources/js/app.js b/resources/js/app.js index a339c91..865b49a 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,3 +1,5 @@ +import { initReveal } from './motion'; + /** * Danks & Strydom Physiotherapy — front-end interactions. * @@ -9,14 +11,9 @@ * - desktop image carousel lightbox */ -// Tag the document so CSS can gate "hidden until revealed" styles -// behind JS actually being available (no-JS users see everything). +// Mark JavaScript availability without making content visibility depend on it. document.documentElement.classList.add('js'); -const prefersReducedMotion = window.matchMedia( - '(prefers-reduced-motion: reduce)' -).matches; - /* ------------------------------------------------------------------ */ /* Sticky header state */ /* ------------------------------------------------------------------ */ @@ -86,8 +83,8 @@ function initMobileMenu() { /* Parallax — translate decorative layers a fraction of scroll */ /* ------------------------------------------------------------------ */ function initParallax() { - if (prefersReducedMotion) return; - if (!window.matchMedia('(min-width: 768px)').matches) return; + const motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + const desktop = window.matchMedia('(min-width: 768px)'); const layers = Array.from(document.querySelectorAll('[data-parallax]')); if (layers.length === 0) return; @@ -97,6 +94,10 @@ function initParallax() { const update = () => { const scrollY = window.scrollY; for (const el of layers) { + if (motion.matches || !desktop.matches) { + el.style.transform = ''; + continue; + } const speed = parseFloat(el.dataset.parallax) || 0.15; el.style.transform = `translate3d(0, ${scrollY * speed}px, 0)`; } @@ -111,36 +112,11 @@ function initParallax() { }; update(); + motion.addEventListener('change', update); + desktop.addEventListener('change', update); window.addEventListener('scroll', onScroll, { passive: true }); } -/* ------------------------------------------------------------------ */ -/* Scroll reveal */ -/* ------------------------------------------------------------------ */ -function initReveal() { - const items = document.querySelectorAll('.reveal'); - if (items.length === 0) return; - - if (prefersReducedMotion || !('IntersectionObserver' in window)) { - items.forEach((el) => el.classList.add('is-visible')); - return; - } - - const observer = new IntersectionObserver( - (entries, obs) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.classList.add('is-visible'); - obs.unobserve(entry.target); - } - }); - }, - { rootMargin: '0px 0px -10% 0px', threshold: 0.12 } - ); - - items.forEach((el) => observer.observe(el)); -} - /* ------------------------------------------------------------------ */ /* Desktop image carousel lightbox */ /* ------------------------------------------------------------------ */ @@ -215,3 +191,9 @@ if (document.readyState === 'loading') { // Re-run reveal after Livewire DOM updates (e.g. contact form success) document.addEventListener('livewire:navigated', boot); + +import { installTracking } from './analytics'; +installTracking(); +window.addEventListener('enquiry-accepted', () => { + requestAnimationFrame(() => document.querySelector('[data-enquiry-success]')?.focus()); +}); diff --git a/resources/js/motion.js b/resources/js/motion.js new file mode 100644 index 0000000..0338b92 --- /dev/null +++ b/resources/js/motion.js @@ -0,0 +1,24 @@ +/** Reveal only when observed: uninitialised or failed JavaScript never hides content. */ +export function initReveal(root = document, media = window.matchMedia('(prefers-reduced-motion: reduce)'), Observer = window.IntersectionObserver) { + const items = [...root.querySelectorAll('.reveal:not([data-reveal-ready])')]; + if (media.matches || !Observer || items.length === 0) return; + + let observer; + try { + observer = new Observer((entries) => { + entries.forEach(({ isIntersecting, target }) => { + if (!isIntersecting) return; + target.classList.add('is-visible'); + observer.unobserve(target); + }); + }, { rootMargin: '0px 0px -10% 0px', threshold: 0.12 }); + items.forEach((item) => { + observer.observe(item); + item.dataset.revealReady = 'true'; + }); + } catch { + observer?.disconnect(); + // Base CSS is visible, including if observer construction or setup fails. + items.forEach((item) => item.classList.remove('is-visible')); + } +} diff --git a/resources/views/components/contact-form.blade.php b/resources/views/components/contact-form.blade.php index b8d0265..5148e29 100644 --- a/resources/views/components/contact-form.blade.php +++ b/resources/views/components/contact-form.blade.php @@ -4,6 +4,10 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Livewire\Attributes\Validate; +use Livewire\Attributes\Locked; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Str; use Livewire\Component; new class extends Component @@ -23,23 +27,31 @@ #[Validate('required|string|min:10|max:2500')] public string $message = ''; + #[Locked] public bool $sent = false; - /** @var list */ - public array $services = [ - 'Sports injury rehabilitation', - 'Back & neck pain treatment', - 'Post-operative rehabilitation', - 'Joint & muscle pain', - 'Mobility & movement assessment', - 'Chronic pain management', - 'Injury prevention', - 'Rehabilitation exercise programmes', - 'Something else / not sure', - ]; + public string $website = ''; + + #[Locked] + public string $submissionId = ''; + + public function mount(): void + { + $this->submissionId = (string) Str::uuid(); + } public function submit(): void { + $this->resetErrorBag('form'); + if ($this->sent || $this->website !== '') { + return; + } + $key = 'contact:'.hash('sha256', (string) request()->ip()); + if (RateLimiter::tooManyAttempts($key, 5)) { + $this->addError('form', 'Too many attempts. Please try again later or call the practice.'); + return; + } + RateLimiter::hit($key, 600); $validated = $this->validate(); $recipients = config('contact.recipients', []); @@ -52,57 +64,97 @@ public function submit(): void } try { - //Do not run on a queue as no queue is available - Mail::to($recipients)->send(new ContactFormSubmitted( - data: [ - 'name' => $validated['name'], - 'email' => $validated['email'], - 'phone' => $validated['phone'] ?? '', - 'service' => $validated['service'] ?? '', - 'message' => $validated['message'], - ], - submittedAt: now(), - )); + $lock = Cache::lock('enquiry-lock:'.$this->submissionId, 120); + if (! $lock->get()) { + return; + } } catch (\Throwable $e) { - Log::error('Contact form mail failed: '.$e->getMessage()); - $this->addError('form', 'Something went wrong sending your message. Please try again or call us.'); - + Log::warning('Contact form replay protection unavailable.', ['exception_type' => $e::class]); + $this->addError('form', 'We are unable to send messages right now. Please try later or call the practice.'); return; } - $this->reset(['name', 'email', 'phone', 'service', 'message']); - $this->sent = true; + try { + try { + $alreadySent = Cache::has('enquiry-sent:'.$this->submissionId); + } catch (\Throwable $e) { + Log::warning('Contact form replay check failed.', ['exception_type' => $e::class]); + $this->addError('form', 'We are unable to send messages right now. Please try later or call the practice.'); + return; + } + if ($alreadySent) { + $this->sent = true; + return; + } + + try { + // The hosting environment has no queue worker; preserve synchronous mail. + Mail::to($recipients)->send(new ContactFormSubmitted( + data: [ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'phone' => $validated['phone'] ?? '', + 'service' => $validated['service'] ?? '', + 'message' => $validated['message'], + ], + submittedAt: now(), + )); + } catch (\Throwable $e) { + Log::error('Contact form mail failed.', ['exception_type' => $e::class]); + $this->addError('form', 'Something went wrong sending your message. Please try again or call us.'); + return; + } + + $this->sent = true; + $this->reset(['name', 'email', 'phone', 'service', 'message']); + $this->dispatch('enquiry-accepted', id: $this->submissionId); + + try { + if (! Cache::put('enquiry-sent:'.$this->submissionId, true, now()->addDay())) { + Log::warning('Contact form accepted-submission bookkeeping failed.'); + } + } catch (\Throwable $e) { + Log::warning('Contact form accepted-submission bookkeeping failed.', ['exception_type' => $e::class]); + } + } finally { + try { + $lock->release(); + } catch (\Throwable $e) { + Log::warning('Contact form submission lock release failed.', ['exception_type' => $e::class]); + } + } } public function sendAnother(): void { $this->sent = false; + $this->submissionId = (string) Str::uuid(); } }; ?>
-
+
@if ($sent) {{-- Success state --}} -
- +
+ -

Thank you — message sent.

-

- We've received your enquiry and aim to get back to you within one business day. We look forward to helping you. +

Thank you — message sent.

+

+ Your enquiry has been accepted for sending to the practice. Your appointment is not confirmed until the practice confirms it.

@else
-

Send us a message

-

Fields marked * are required.

+

Send us a message

+

Fields marked * are required.

@error('form') @@ -112,19 +164,23 @@ class="mt-9 inline-flex items-center gap-2 rounded-full border border-pine-900/1
@enderror + {{-- Name + Phone --}}
- - Name * + - @error('name')

{{ $message }}

@enderror + @error('name') @enderror
- + @error('phone')

{{ $message }}

@enderror
@@ -132,42 +188,29 @@ class="mt-2 w-full rounded-xl border-0 bg-bone-100 px-4 py-3.5 text-[0.95rem] te {{-- Email --}}
- - Email * + - @error('email')

{{ $message }}

@enderror -
- - {{-- Service interest --}} -
- - - @error('service')

{{ $message }}

@enderror + @error('email') @enderror
{{-- Message --}}
- - - @error('message')

{{ $message }}

@enderror + @error('message') @enderror
-
+
-

- We respect your privacy and won't share your details. +

+ Your contact details and message are emailed to the practice to handle this enquiry. Please avoid including detailed clinical information.

diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index 5c196fc..1b3b7c3 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -10,52 +10,51 @@ $practice = config('contact.practice'); $siteName = $practice['name'] ?? config('app.name'); $pageTitle = $title ? "{$title} | {$siteName}" : $siteName; - $canonicalUrl = $canonical ?: route('home'); - $socialImage = $image ?: asset('images/back_strapping.webp'); + $canonicalUrl = $canonical ?: \App\Support\Site::url(request()->path() === '/' ? '/' : '/'.request()->path()); + $rootUrl = \App\Support\Site::url(); + $robots = \App\Support\Site::indexable() ? $robots : 'noindex, nofollow'; + $photo = config('imagery.images.'.($image ?: 'consultation')); + $socialImage = \App\Support\Site::url('/'.$photo['path']); + $heroSrcset = collect($photo['variants'])->map(fn ($path, $width) => asset($path).' '.$width.'w')->implode(', '); $structuredData = [ '@context' => 'https://schema.org', '@graph' => [ [ '@type' => 'WebSite', - '@id' => "{$canonicalUrl}#website", - 'url' => $canonicalUrl, + '@id' => "{$rootUrl}#website", + 'url' => $rootUrl, 'name' => $siteName, 'inLanguage' => str_replace('_', '-', app()->getLocale()), ], [ - '@type' => 'Physiotherapy', - '@id' => "{$canonicalUrl}#practice", + '@type' => 'MedicalClinic', + '@id' => "{$rootUrl}#practice", 'name' => $siteName, - 'url' => $canonicalUrl, - 'description' => $description, - 'image' => $socialImage, + 'url' => $rootUrl, + 'description' => 'Danks & Strydom Physiotherapy in Glen Marais, Kempton Park.', 'telephone' => $practice['phone'], 'email' => $practice['email'], - 'address' => [ - '@type' => 'PostalAddress', - 'streetAddress' => $practice['address'], - 'addressCountry' => 'ZA', - ], 'medicalSpecialty' => 'https://schema.org/Physiotherapy', - 'openingHoursSpecification' => [ - [ - '@type' => 'OpeningHoursSpecification', - 'dayOfWeek' => [ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - ], - 'opens' => '07:30', - 'closes' => '17:30', - ], - ], - 'hasMap' => 'https://www.google.com/maps/search/?api=1&query='.urlencode($practice['address']), ], ], ]; + $clinic = &$structuredData['@graph'][1]; + foreach (['telephone', 'email'] as $field) { + if (empty($clinic[$field])) { unset($clinic[$field]); } + } + if ($practice['location_verified']) { + $clinic['address'] = array_filter([ + '@type' => 'PostalAddress', + 'streetAddress' => $practice['street'], + 'addressLocality' => $practice['locality'], + 'addressRegion' => $practice['region'], + 'postalCode' => $practice['postcode'], + 'addressCountry' => $practice['country'], + ]); + if ($practice['directions_url']) { $clinic['hasMap'] = $practice['directions_url']; } + } + @endphp @@ -63,15 +62,17 @@ - + {{ $pageTitle }} - - + + @if (request()->routeIs('home')) + + @endif @@ -79,14 +80,15 @@ - - - + + + + '], + 'site.pages.about.practitioners.0.universities' => ['Test university'], + 'site.pages.about.practitioners.0.expanded_biography' => 'Test expanded biography.', + 'site.pages.about.practitioners.0.languages' => ['Test language'], + 'site.pages.about.practitioners.0.photo' => ['path' => 'images/back_strapping.webp', 'alt' => 'Synthetic test portrait'], + 'site.pages.patient-information.optional_sections.payment_timing.body' => 'Test payment timing.', + ]); + $html = $this->get('/about')->assertSee('Test qualification ') + ->assertSee('Test university')->assertSee('Test expanded biography.')->assertSee('Test language') + ->assertSee('data-practitioner-photo', false)->assertSee('Synthetic test portrait') + ->assertDontSee('', false)->getContent(); + $head = explode('', $html)[0]; + expect($head)->not->toContain('Test qualification', 'Test university', 'Test expanded biography', 'Test language', 'Synthetic test portrait'); + $this->get('/patient-information')->assertSee('When payment is due')->assertSee('Test payment timing.'); +}); diff --git a/tests/Feature/ResponsiveImageryTest.php b/tests/Feature/ResponsiveImageryTest.php new file mode 100644 index 0000000..3591c74 --- /dev/null +++ b/tests/Feature/ResponsiveImageryTest.php @@ -0,0 +1,58 @@ + $path) { + $file = public_path($path); + expect($file)->toBeFile(); + $dimensions = getimagesize($file); + expect($dimensions[0])->toBe($width) + ->and($dimensions[1])->toBe((int) ($width * $photo['height'] / $photo['width'])) + ->and($dimensions['mime'])->toBe('image/webp') + ->and(filesize($file))->toBeLessThan(160000); + } + } +}); + +it('uses page specific heroes and accurate illustrative social metadata on every public page', function () { + foreach (config('imagery.pages') as $path => $key) { + $photo = config('imagery.images.'.$key); + $response = $this->get($path)->assertSuccessful() + ->assertSee('src="'.asset($photo['path']).'"', false) + ->assertSee('width="'.$photo['width'].'" height="'.$photo['height'].'"', false) + ->assertSee('', false) + ->assertSee('', false) + ->assertSee('', false) + ->assertSee('', false) + ->assertSee('Images are AI-generated illustrations') + ->assertDontSee('Physiotherapy treatment at'); + $dom = new DOMDocument; + @$dom->loadHTML($response->getContent()); + $xpath = new DOMXPath($dom); + expect($xpath->query('//img[@fetchpriority="high"]')->length)->toBe(1); + $hero = $xpath->query('//img[@fetchpriority="high"]')->item(0); + expect($hero->getAttribute('loading'))->toBe('eager'); + foreach ($photo['variants'] as $width => $variant) { + expect($hero->getAttribute('srcset'))->toContain(asset($variant).' '.$width.'w'); + } + $data = json_decode($xpath->query('//script[@type="application/ld+json"]')->item(0)->textContent, true, flags: JSON_THROW_ON_ERROR); + expect(collect($data['@graph'])->firstWhere('@type', 'MedicalClinic'))->not->toHaveKey('image'); + if ($path === '/') { + $preload = $xpath->query('//link[@rel="preload" and @as="image"]')->item(0); + expect($preload->getAttribute('href'))->toBe($hero->getAttribute('src')) + ->and($preload->getAttribute('imagesrcset'))->toBe($hero->getAttribute('srcset')) + ->and($preload->getAttribute('imagesizes'))->toBe($hero->getAttribute('sizes')); + $images = $xpath->query('//img'); + $sources = []; + foreach ($images as $image) { + $sources[] = $image->getAttribute('src'); + if ($image !== $hero && $image->getAttribute('fetchpriority') !== 'high') { + expect($image->getAttribute('loading'))->toBe('lazy'); + } + } + expect(array_unique($sources))->toHaveCount(7)->and($sources)->toHaveCount(7); + } + } +}); diff --git a/tests/Feature/ServiceInventoryTest.php b/tests/Feature/ServiceInventoryTest.php new file mode 100644 index 0000000..63bca06 --- /dev/null +++ b/tests/Feature/ServiceInventoryTest.php @@ -0,0 +1,61 @@ +detectEnvironment(fn () => 'production'); + config(['site.indexable' => true, 'app.url' => 'https://danksandstrydom.co.za']); + $path = '/services/'.$name; + config(["site.pages.$name.published" => false]); + $this->get($path.'?preview=true')->assertNotFound(); + $this->get('/services')->assertDontSee('href="'.route($name).'"', false); + $this->get('/sitemap.xml')->assertDontSee(''.Site::url($path).'', false); + config(["site.pages.$name.published" => true]); + $response = $this->get($path)->assertOk()->assertSee(config("site.pages.$name.heading")) + ->assertSee('', false) + ->assertSee('content="'.e(config("site.pages.$name.description")).'"', false); + expect(substr_count($response->getContent(), 'toBe(1); + $this->get('/services')->assertSee('href="'.route($name).'"', false); + $this->get('/sitemap.xml')->assertSee(''.Site::url($path).'', false); +})->with('new service pages'); + +it('offers eight ordered crawlable staging cards with matching icons and unique page metadata', function () { + app()->detectEnvironment(fn () => 'staging'); + $expected = [ + 'sports-injury-rehabilitation' => 'pulse', 'back-neck-pain' => 'spine', 'post-operative-rehabilitation' => 'recovery', + 'joint-muscle-pain' => 'joint', 'mobility-movement-assessment' => 'mobility', 'chronic-pain-management' => 'chronic', + 'injury-prevention' => 'shield', 'rehabilitation-exercise-programmes' => 'program', + ]; + $html = $this->get('https://staging.example.test/services')->assertOk()->getContent(); + $dom = new DOMDocument; + @$dom->loadHTML($html); + $xpath = new DOMXPath($dom); + $links = $xpath->query('//*[@data-service-cards]//article/a'); + expect($links->length)->toBe(8); + $titles = $descriptions = []; + foreach (array_keys($expected) as $index => $name) { + $path = config("site.pages.$name.path"); + expect($links->item($index)->getAttribute('href'))->toBe(route($name)) + ->and(config("site.pages.$name.service_icon"))->toBe($expected[$name]); + $page = $this->get('https://staging.example.test'.$path)->assertOk()->assertHeader('X-Robots-Tag', 'noindex, nofollow')->getContent(); + preg_match('/(.*?)<\/title>/', $page, $title); + $titles[] = $title[1]; + $descriptions[] = config("site.pages.$name.description"); + } + expect(array_unique($titles))->toHaveCount(8)->and(array_unique($descriptions))->toHaveCount(8); + $this->get('https://staging.example.test/sitemap.xml')->assertDontSee('<loc>', false); +}); + +it('limits related services to relevant published destinations', function () { + config(['site.pages.joint-muscle-pain.published' => true, 'site.pages.mobility-movement-assessment.published' => false, 'site.pages.back-neck-pain.published' => false]); + $this->get('/services/joint-muscle-pain')->assertOk()->assertDontSee('aria-label="Related services"', false); + config(['site.pages.mobility-movement-assessment.published' => true]); + $this->get('/services/joint-muscle-pain')->assertSee('href="'.route('mobility-movement-assessment').'"', false) + ->assertDontSee('href="'.route('chronic-pain-management').'"', false) + ->assertDontSee('href="'.route('back-neck-pain').'"', false); +}); diff --git a/tests/Feature/SiteHeaderTest.php b/tests/Feature/SiteHeaderTest.php index 4334a42..254cd48 100644 --- a/tests/Feature/SiteHeaderTest.php +++ b/tests/Feature/SiteHeaderTest.php @@ -4,7 +4,7 @@ $this->get('/') ->assertSuccessful() ->assertSee('Danks') - ->assertSee('Book an appointment') + ->assertSee('Request an appointment') ->assertSee('data-mobile-menu-toggle', false) ->assertSee('aria-expanded="false"', false) ->assertSee('data-mobile-menu', false) diff --git a/tests/Feature/SitePagesTest.php b/tests/Feature/SitePagesTest.php new file mode 100644 index 0000000..a30c4c3 --- /dev/null +++ b/tests/Feature/SitePagesTest.php @@ -0,0 +1,119 @@ +<?php + +use App\Support\Site; + +it('renders each published page with distinct initial HTML metadata', function () { + foreach (array_keys(config('site.pages')) as $name) { + config(["site.pages.$name.published" => true]); + } + $titles = []; + foreach (['/' => 'home', '/about' => 'about', '/contact' => 'contact', '/services' => 'services', '/services/back-neck-pain' => 'back-neck-pain', '/services/sports-injury-rehabilitation' => 'sports-injury-rehabilitation', '/services/post-operative-rehabilitation' => 'post-operative-rehabilitation', '/patient-information' => 'patient-information'] as $path => $name) { + $html = $this->get($path)->assertSuccessful()->getContent(); + expect(substr_count($html, '<h1'))->toBe(1) + ->and(substr_count($html, '<title>'))->toBe(1) + ->and($html)->toContain('<link rel="canonical" href="'.Site::url($path).'">'); + preg_match('/<title>(.*?)<\/title>/', $html, $match); + $titles[] = $match[1]; + } + expect(array_unique($titles))->toHaveCount(8); +}); + +it('keeps drafts and unknown paths unavailable even with preview query strings', function (string $path) { + foreach (['back-neck-pain', 'sports-injury-rehabilitation', 'post-operative-rehabilitation', 'patient-information'] as $name) { + config(["site.pages.$name.published" => false]); + } + $this->get($path.'?preview=true')->assertNotFound(); +})->with(['/services/back-neck-pain', '/services/sports-injury-rehabilitation', '/services/post-operative-rehabilitation', '/patient-information', '/services/mckenzie-assessment', '/serengeti', '/missing']); + +it('publishes only enabled canonical pages in the production sitemap', function () { + app()->detectEnvironment(fn () => 'production'); + config(['site.indexable' => true, 'app.url' => 'https://danksandstrydom.co.za']); + config(['site.pages.back-neck-pain.published' => false, 'site.pages.patient-information.published' => false]); + $this->get('/services')->assertDontSee('href="'.route('back-neck-pain').'"', false); + $xml = $this->get('/sitemap.xml')->assertSuccessful()->getContent(); + expect(simplexml_load_string($xml))->not->toBeFalse(); + foreach (['/', '/services', '/contact'] as $path) { + expect($xml)->toContain('<loc>'.Site::url($path).'</loc>'); + } + expect($xml)->not->toContain('back-neck-pain', 'patient-information', 'lastmod'); + $this->get('/robots.txt')->assertSee('Sitemap: https://danksandstrydom.co.za/sitemap.xml'); +}); + +it('keeps nonproduction out of search even when indexing is configured', function () { + config(['site.indexable' => true]); + $this->get('/')->assertHeader('X-Robots-Tag', 'noindex, nofollow'); + $this->get('/robots.txt')->assertSee('Disallow: /'); + $this->get('/sitemap.xml')->assertDontSee('<loc>', false); +}); + +it('redirects known production variants once with path and query intact', function () { + app()->detectEnvironment(fn () => 'production'); + config(['site.canonical_redirects' => true, 'app.url' => 'https://danksandstrydom.co.za']); + $this->get('http://www.danksandstrydom.co.za/contact?utm_source=test&x=1') + ->assertStatus(301)->assertRedirect('https://danksandstrydom.co.za/contact?utm_source=test&x=1'); + $this->get('https://danksandstrydom.co.za/contact')->assertSuccessful(); + $this->get('https://danksandstrydom.co.za/missing')->assertNotFound(); +}); + +it('does not send staging or development to production or trust arbitrary hosts', function () { + config(['site.canonical_redirects' => true, 'app.url' => 'https://danksandstrydom.co.za']); + $this->get('http://localhost/contact')->assertSuccessful(); + app()->detectEnvironment(fn () => 'production'); + $this->get('https://staging.example.test/contact')->assertSuccessful(); + $this->get('https://attacker.example/contact?private=value') + ->assertSee('<link rel="canonical" href="https://danksandstrydom.co.za/contact">', false); +}); + +it('publishes an enabled service link and sitemap entry', function () { + config(['site.pages.back-neck-pain.published' => true, 'site.indexable' => true]); + app()->detectEnvironment(fn () => 'production'); + $this->get('/services/back-neck-pain')->assertSuccessful()->assertSee('Back and neck pain'); + $this->get('/')->assertSee('href="'.route('back-neck-pain').'"', false); + $this->get('/sitemap.xml')->assertSee(Site::url('/services/back-neck-pain')); +}); + +it('provides a direct enquiry without an empty service grid or explore self-link', function () { + foreach (array_keys(Site::services()) as $name) { + config(["site.pages.$name.published" => false]); + } + foreach (['/', '/services'] as $path) { + $this->get($path)->assertSuccessful()->assertSee('data-service-enquiry', false) + ->assertSee('href="'.route('contact').'#contact"', false) + ->assertDontSee('data-service-cards', false)->assertDontSee('Explore physiotherapy enquiries'); + } +}); + +it('automatically shows only published services without an explore self-link', function () { + config(['site.pages.back-neck-pain.published' => true, 'site.pages.sports-injury-rehabilitation.published' => true, 'site.pages.post-operative-rehabilitation.published' => false]); + $this->get('/services')->assertSuccessful()->assertSee('data-service-cards', false) + ->assertSee('href="'.route('back-neck-pain').'"', false) + ->assertSee('href="'.route('sports-injury-rehabilitation').'"', false) + ->assertDontSee('href="'.route('post-operative-rehabilitation').'"', false) + ->assertDontSee('data-service-enquiry', false)->assertDontSee('Explore physiotherapy enquiries'); +}); + +it('renders confirmed copy on published pages', function () { + app()->detectEnvironment(fn () => 'production'); + config(['site.indexable' => true]); + config(['site.pages.about.published' => true]); + foreach (['back-neck-pain', 'sports-injury-rehabilitation', 'post-operative-rehabilitation', 'patient-information'] as $name) { + config(["site.pages.$name.published" => true]); + $this->get(route($name))->assertSuccessful() + ->assertDontSee('Placeholder'); + if ($name !== 'patient-information') { + $this->get(route($name))->assertSee('Fees, bookings and patient information')->assertDontSee('Appointments are one hour'); + } else { + $this->get(route($name))->assertSee('Patients are not expected to bring anything'); + } + $this->get('/sitemap.xml')->assertSee(Site::url(config("site.pages.$name.path"))); + } + $this->get('/about')->assertSee('Elize Strydom')->assertSee('Cheryl Myburgh') + ->assertSee('Physiotherapist')->assertDontSee('Placeholder'); +}); + +it('shows the supplied contact address while keeping unverified maps and schema gated', function () { + config(['contact.practice.location_verified' => false, 'contact.practice.address' => 'Surgiklin Studios, Unit 12, Koorsboom Ave, Glen Marais, Kempton Park, 1619', 'contact.practice.phone' => '011 391 3126', 'contact.practice.email' => 'admin@danksandstrydom.co.za']); + $this->get('/contact')->assertSuccessful()->assertSee('Surgiklin Studios, Unit 12, Koorsboom Ave') + ->assertSee('011 391 3126')->assertSee('admin@danksandstrydom.co.za') + ->assertDontSee('Practice location map')->assertDontSee('streetAddress')->assertDontSee('Monument Road'); +}); diff --git a/tests/Feature/TestimonialsTest.php b/tests/Feature/TestimonialsTest.php new file mode 100644 index 0000000..a150492 --- /dev/null +++ b/tests/Feature/TestimonialsTest.php @@ -0,0 +1,64 @@ +<?php + +it('shows three labelled design samples only on local and staging in the requested order', function (string $environment) { + app()->detectEnvironment(fn () => $environment); + config(['site.testimonials.entries' => []]); + $html = $this->get('/')->assertSuccessful()->assertHeader('X-Robots-Tag', 'noindex, nofollow') + ->assertSeeInOrder(['data-practice-images', 'What our patients say', 'Meet the people behind your care']) + ->assertDontSee('Read original testimonial')->getContent(); + expect(substr_count($html, 'Sample testimonial — replace before publishing'))->toBe(3); + expect(explode('</head>', $html)[0])->not->toContain('Sample testimonial', 'Reviewer display name', 'aggregateRating', '"@type":"Review"'); +})->with(['local', 'staging']); + +it('omits the entire section outside local and staging when there are no approved entries', function (string $environment) { + app()->detectEnvironment(fn () => $environment); + config(['site.testimonials.entries' => [], 'site.testimonials.samples.0.approved' => true]); + $html = $this->get('/?preview=true')->assertSuccessful()->assertDontSee('id="testimonials"', false) + ->assertDontSee('What our patients say')->assertDontSee('Sample testimonial')->getContent(); + foreach (config('site.testimonials.samples') as $sample) { + expect($html)->not->toContain($sample['quote'], $sample['display_name']); + } +})->with(['production', 'testing', 'preview']); + +it('renders only explicitly approved complete entries and keeps all quotes out of metadata and schema', function () { + app()->detectEnvironment(fn () => 'production'); + config(['site.testimonials.entries' => [ + ['quote' => 'Synthetic approved <script>quote</script>', 'display_name' => 'Synthetic <name>', 'source_url' => 'https://example.test/review', 'approved' => true], + ['quote' => 'Synthetic no-source quote', 'display_name' => 'Synthetic no-source name', 'source_url' => null, 'approved' => true], + ['quote' => 'Unapproved sentinel', 'display_name' => 'Draft name', 'approved' => false], + ['quote' => 'String approval sentinel', 'display_name' => 'Draft name', 'approved' => 'true'], + ['quote' => 'Missing approval sentinel', 'display_name' => 'Draft name'], + ['quote' => 'Incomplete sentinel', 'display_name' => ' ', 'approved' => true], + ]]); + $html = $this->get('/')->assertSuccessful()->assertSee('What our patients say') + ->assertSee('Synthetic approved <script>quote</script>')->assertSee('Synthetic <name>') + ->assertDontSee('<script>quote</script>', false)->assertSee('href="https://example.test/review"', false) + ->assertDontSee('Unapproved sentinel')->assertDontSee('String approval sentinel')->assertDontSee('Missing approval sentinel') + ->assertDontSee('Incomplete sentinel')->assertDontSee('Sample testimonial')->assertDontSee('out of 5 stars')->getContent(); + expect(substr_count($html, 'Read original testimonial'))->toBe(1); + expect(explode('</head>', $html)[0])->not->toContain('Synthetic', 'aggregateRating', '"@type":"Review"'); +}); + +it('never renders unsafe or invalid source links', function (string $source) { + app()->detectEnvironment(fn () => 'production'); + config(['site.testimonials.entries' => [['quote' => 'Synthetic quote', 'display_name' => 'Synthetic name', 'source_url' => $source, 'approved' => true]]]); + $this->get('/')->assertSee('Synthetic quote')->assertDontSee('Read original testimonial')->assertDontSee('href="'.$source.'"', false); +})->with(['javascript:alert(1)', 'not-a-url', '//example.test/review', '']); + +it('uses approved entries instead of filling staging with sample cards', function () { + app()->detectEnvironment(fn () => 'staging'); + config(['site.testimonials.entries' => [['quote' => 'Synthetic approved quote', 'display_name' => 'Synthetic name', 'approved' => true]]]); + $this->get('/')->assertSee('Synthetic approved quote')->assertDontSee('Sample testimonial'); +}); + +it('publishes the three user-confirmed testimonials without samples ratings or invented source links', function (string $environment) { + app()->detectEnvironment(fn () => $environment); + $html = $this->get('/')->assertSuccessful()->assertSeeInOrder(['Sarah M.', 'Priya N.', 'Lerato K.']) + ->assertSee('After months of lower back pain, I finally feel like myself again.') + ->assertSee('The guidance after my surgery was clear and reassuring at every step.') + ->assertSee('What stood out was how much they listened.') + ->assertDontSee('Sample testimonial')->assertDontSee('Reviewer display name') + ->assertDontSee('Read original testimonial')->assertDontSee('out of 5 stars')->getContent(); + expect(config('site.testimonials.entries'))->toHaveCount(3); + expect(explode('</head>', $html)[0])->not->toContain('Sarah M.', 'Priya N.', 'Lerato K.', 'aggregateRating', '"@type":"Review"'); +})->with(['local', 'staging', 'production']); diff --git a/tests/analytics.test.js b/tests/analytics.test.js new file mode 100644 index 0000000..581b214 --- /dev/null +++ b/tests/analytics.test.js @@ -0,0 +1,23 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createTracker } from '../resources/js/analytics.js'; + +test('deduplicates success and sends only generic allowlisted events', () => { + const events = []; + const track = createTracker((event) => events.push(event)); + const id = '00000000-0000-4000-8000-000000000001'; + track('enquiry_accepted', id); + track('enquiry_accepted', id); + track('enquiry_accepted', 'private@example.test'); + track('clinical_selection', 'back pain'); + track('phone_click', '+27115550000'); + track('directions_click', 'private address'); + assert.deepEqual(events, [{ event: 'enquiry_accepted' }, { event: 'phone_click' }, { event: 'directions_click' }]); +}); + +test('disabled integration registers no listeners or provider', async () => { + const { installTracking } = await import('../resources/js/analytics.js'); + globalThis.document = { body: { dataset: { analyticsEnabled: 'false' } } }; + assert.doesNotThrow(() => installTracking()); + delete globalThis.document; +}); diff --git a/tests/motion.test.js b/tests/motion.test.js new file mode 100644 index 0000000..842d48d --- /dev/null +++ b/tests/motion.test.js @@ -0,0 +1,37 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { initReveal } from '../resources/js/motion.js'; + +function fixture() { + const classes = new Set(); + const item = { dataset: {}, classList: { add: x => classes.add(x), remove: x => classes.delete(x) } }; + return { item, classes, root: { querySelectorAll: () => item.dataset.revealReady ? [] : [item] } }; +} + +test('reveals on intersection once without reobserving initialized elements', () => { + const { item, classes, root } = fixture(); + let notify, observed = 0, unobserved = 0; + class Observer { + constructor(callback) { notify = callback; } + observe() { observed++; } + unobserve() { unobserved++; } + } + initReveal(root, { matches: false }, Observer); + assert.equal(classes.size, 0); + notify([{ target: item, isIntersecting: false }]); + assert.equal(classes.size, 0); + notify([{ target: item, isIntersecting: true }]); + assert.equal(classes.has('is-visible'), true); + assert.equal(unobserved, 1); + initReveal(root, { matches: false }, Observer); + assert.equal(observed, 1); +}); + +test('reduced motion and missing or failing observers leave content unmodified', () => { + for (const [matches, Observer] of [[true, class { constructor() { throw Error(); } }], [false, null], [false, class { constructor() { throw Error(); } }]]) { + const { root, classes, item } = fixture(); + assert.doesNotThrow(() => initReveal(root, { matches }, Observer)); + assert.equal(classes.size, 0); + assert.equal(item.dataset.revealReady, undefined); + } +});