agent: Fix mobile responsiveness and conversion across pa

This commit is contained in:
platform-mcp
2026-06-17 14:51:43 +05:30
parent ce67f90f06
commit 5c05779541
4 changed files with 241 additions and 117 deletions
+34 -17
View File
@@ -85,23 +85,40 @@ const currentPath = Astro.url.pathname;
</a>
</div>
<!-- Mobile Menu Button -->
<button
type="button"
class="lg:hidden relative w-10 h-10 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:text-primary dark:hover:text-primary-400 transition-colors"
id="mobile-menu-toggle"
aria-label="Toggle mobile menu"
aria-expanded="false"
aria-controls="mobile-menu"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<div class="hamburger-icon" aria-hidden="true">
<span class="hamburger-line hamburger-line-1"></span>
<span class="hamburger-line hamburger-line-2"></span>
<span class="hamburger-line hamburger-line-3"></span>
</div>
</button>
<!-- Mobile actions: prominent click-to-call + hamburger (shown <lg on every page) -->
<div class="flex items-center gap-1 lg:hidden">
<!-- Click-to-call — always visible in the mobile header so visitors on
any page can reach us in one tap without opening the menu. -->
<a
href="tel:+919561417403"
data-cta="header-mobile-call"
aria-label="Call WorkRoot at +91 95614 17403"
class="inline-flex items-center gap-1.5 h-10 px-3 rounded-lg bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 font-semibold text-sm hover:bg-primary-100 dark:hover:bg-primary-900/50 active:scale-95 transition-all focus:outline-none focus:ring-2 focus:ring-primary-400"
>
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span class="hidden xs:inline">Call</span>
</a>
<!-- Mobile Menu Button -->
<button
type="button"
class="relative w-10 h-10 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:text-primary dark:hover:text-primary-400 transition-colors"
id="mobile-menu-toggle"
aria-label="Toggle mobile menu"
aria-expanded="false"
aria-controls="mobile-menu"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<div class="hamburger-icon" aria-hidden="true">
<span class="hamburger-line hamburger-line-1"></span>
<span class="hamburger-line hamburger-line-2"></span>
<span class="hamburger-line hamburger-line-3"></span>
</div>
</button>
</div>
</nav>
</div>
+132 -98
View File
@@ -80,6 +80,23 @@ const trustStats = [
{ value: '< 24h', label: 'Response Time' },
{ value: '3+', label: 'Years Experience' },
];
// Service options — rendered as large, tappable radio tiles (replacing the
// old native <select>) so the choice is thumb-friendly on mobile. `value`
// is the code submitted to /api/contact (unchanged from the previous
// <option> values); `label` is the display text.
const serviceOptions = [
{ value: 'erp-solutions', label: 'ERP / Enterprise Systems' },
{ value: 'government-systems', label: 'Government IT' },
{ value: 'web-development', label: 'Web Development' },
{ value: 'mobile-development', label: 'Mobile Development' },
{ value: 'ui-ux', label: 'UI/UX Design' },
{ value: 'consulting', label: 'IT Consulting / DevOps' },
{ value: 'other', label: 'Other / General Inquiry' },
];
// Budget options — also rendered as large tappable tiles.
const budgetOptions = ['< ₹1L', '₹1L ₹5L', '₹5L ₹15L', '₹15L+', 'Not sure yet'];
---
<BaseLayout
@@ -273,107 +290,84 @@ const trustStats = [
</div>
</div>
<!-- Phone + Subject row -->
<div class="grid sm:grid-cols-2 gap-5">
<!-- Phone Field -->
<div class="form-group">
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
Phone Number
<span class="text-secondary-400 font-normal">(optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<input
type="tel"
id="phone"
name="phone"
autocomplete="tel"
aria-describedby="phone-error"
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
placeholder="+1 (555) 123-4567"
/>
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<!-- Phone Field (full width) -->
<div class="form-group">
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
Phone Number
<span class="text-secondary-400 font-normal">(optional)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please enter a valid phone number.
</p>
</div>
<!-- Subject Field -->
<div class="form-group">
<label for="subject" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<select
id="subject"
name="subject"
required
aria-required="true"
aria-describedby="subject-error"
class="form-input w-full pl-10 pr-8 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 appearance-none cursor-pointer"
>
<option value="">Select a service...</option>
<option value="erp-solutions">ERP / Enterprise Systems</option>
<option value="government-systems">Government IT</option>
<option value="web-development">Web Development</option>
<option value="mobile-development">Mobile Development</option>
<option value="ui-ux">UI/UX Design</option>
<option value="consulting">IT Consulting / DevOps</option>
<option value="other">Other / General Inquiry</option>
</select>
<div class="valid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Valid">
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="invalid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Invalid">
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<input
type="tel"
id="phone"
name="phone"
autocomplete="tel"
aria-describedby="phone-error"
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
placeholder="+1 (555) 123-4567"
/>
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please select a service.
</p>
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please enter a valid phone number.
</p>
</div>
<!-- Budget range (new field) -->
<!-- Service Needed — large tappable radio tiles (replaces the
old native <select>). Renders 2 columns on mobile (each tile
a comfortable thumb target) and 3 from sm up. The hidden
radios share name="subject" so the form submission and
analytics read the same value the <select> used to provide. -->
<div class="form-group">
<span id="subject-label" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</span>
<div
id="subject-group"
role="radiogroup"
aria-labelledby="subject-label"
aria-describedby="subject-error"
class="grid grid-cols-2 sm:grid-cols-3 gap-2.5"
>
{serviceOptions.map((opt) => (
<label class="subject-option group flex items-center justify-center text-center min-h-[52px] px-3 py-2.5 rounded-xl border border-secondary-200 dark:border-secondary-600 text-sm font-medium text-secondary-700 dark:text-secondary-300 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/40">
<input type="radio" name="subject" value={opt.value} class="sr-only" aria-describedby="subject-error" />
<span class="leading-snug">{opt.label}</span>
</label>
))}
</div>
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
Please select a service.
</p>
</div>
<!-- Project Budget — large tappable radio tiles. 2 cols on mobile
so each is a full thumb target; auto-fit row from sm up. -->
<div class="form-group">
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
Project Budget
<span class="text-secondary-400 dark:text-secondary-500 font-normal">(optional)</span>
</label>
<div class="flex flex-wrap gap-2" role="group" aria-label="Budget range options">
{['< ₹1L', '₹1L ₹5L', '₹5L ₹15L', '₹15L+', 'Not sure yet'].map((range, i) => (
<label class="budget-option flex items-center gap-1.5 px-3 py-2 rounded-lg border border-secondary-200 dark:border-secondary-600 text-sm text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2.5" role="group" aria-label="Budget range options">
{budgetOptions.map((range) => (
<label class="budget-option flex items-center justify-center text-center min-h-[48px] px-3 py-2.5 rounded-xl border border-secondary-200 dark:border-secondary-600 text-sm font-medium text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/40">
<input type="radio" name="budget" value={range} class="sr-only" />
{range}
</label>
@@ -806,6 +800,13 @@ const trustStats = [
color: #0e7490;
}
/* Service tiles: invalid-group highlight (shown only after a failed submit
attempt with no service chosen). Subtle red ring around the whole group. */
.subject-group-invalid {
border-radius: 0.75rem;
box-shadow: 0 0 0 2px rgb(220 38 38 / 0.35);
}
/* FAQ item */
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::marker { display: none; }
@@ -909,7 +910,7 @@ const trustStats = [
});
}
// ── Budget toggle (radio-as-chip) ──────────────────────────
// ── Budget toggle (radio-as-tile) ──────────────────────────
const budgetOptions = document.querySelectorAll('.budget-option');
budgetOptions.forEach((opt) => {
const radio = opt.querySelector('input[type="radio"]') as HTMLInputElement;
@@ -921,6 +922,31 @@ const trustStats = [
}
});
// ── Service tiles (radio group replacing the old native <select>) ──
// The hidden radios share name="subject" so `form.elements.subject` resolves
// to a RadioNodeList whose `.value` is the checked option — exactly what the
// submit handler and analytics previously read from the <select>.
const subjectErrorEl = document.querySelector('[data-error="subject"]');
const subjectGroupEl = document.getElementById('subject-group');
const subjectRadios = Array.from(
document.querySelectorAll<HTMLInputElement>('input[name="subject"]')
);
function getSubjectValue(): string {
return subjectRadios.find((r) => r.checked)?.value ?? '';
}
function validateSubject(touched = true): boolean {
const valid = getSubjectValue() !== '';
if (subjectErrorEl) subjectErrorEl.classList.toggle('hidden', valid || !touched);
subjectGroupEl?.classList.toggle('subject-group-invalid', !valid && touched);
return valid;
}
subjectRadios.forEach((radio) => {
radio.addEventListener('change', () => validateSubject());
});
// ── Form Validation ────────────────────────────────────────
const form = document.getElementById('contact-form') as HTMLFormElement;
const successState = document.getElementById('success-state');
@@ -1012,18 +1038,25 @@ const trustStats = [
return;
}
// Validate all fields
// Validate all fields. `subject` is now a radio group, so it is checked
// separately from the text/textarea fields handled by validateField.
let isValid = true;
['name', 'email', 'phone', 'subject', 'message'].forEach((fieldName) => {
['name', 'email', 'phone', 'message'].forEach((fieldName) => {
const field = form.elements.namedItem(fieldName) as HTMLInputElement;
if (field && !validateField(field)) isValid = false;
});
if (!validateSubject()) isValid = false;
if (!isValid) {
showToast('Please fix the highlighted fields before submitting.', 'error');
// Focus first invalid field
// Focus the first invalid text field, or the service tiles if that's
// the only thing missing.
const firstInvalid = form.querySelector('.is-invalid') as HTMLElement | null;
firstInvalid?.focus();
if (firstInvalid) {
firstInvalid.focus();
} else if (getSubjectValue() === '') {
subjectRadios[0]?.focus();
}
return;
}
@@ -1057,7 +1090,7 @@ const trustStats = [
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
phone: (form.elements.namedItem('phone') as HTMLInputElement)?.value.trim(),
subject: (form.elements.namedItem('subject') as HTMLSelectElement)?.value,
subject: getSubjectValue(),
message: (form.elements.namedItem('message') as HTMLTextAreaElement)?.value.trim(),
website: honeypot,
}),
@@ -1087,7 +1120,7 @@ const trustStats = [
}
// Track analytics
const subject = (form.elements.namedItem('subject') as HTMLSelectElement)?.value;
const subject = getSubjectValue();
trackFormSubmit('contact', success);
if (success) trackEvent('contact_form_submit', { subject_category: subject });
@@ -1122,6 +1155,7 @@ const trustStats = [
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
if (charCount) charCount.textContent = '0 / 2000';
budgetOptions.forEach((o) => o.classList.remove('selected'));
subjectGroupEl?.classList.remove('subject-group-invalid');
});
}
}
+46
View File
@@ -400,6 +400,52 @@ const techStack = [
</div>
</section>
<!-- Mid-page Conversion CTA — sits directly after the Services section so
mobile visitors hit the funnel early instead of having to scroll past
How-It-Works, testimonials, the tech stack and the FAQ to reach the
inline lead form. Primary action jumps straight to the on-page inline
lead form (#inline-lead-form, smooth-scrolled by the anchor handler at
the bottom of this page); secondary action is a one-tap phone call. -->
<section
aria-labelledby="midpage-cta-heading"
class="bg-gradient-to-r from-primary-600 to-primary-700 dark:from-primary-700 dark:to-primary-800"
>
<div class="container-wrapper py-10 sm:py-12">
<div class="flex flex-col items-center gap-5 text-center md:flex-row md:justify-between md:text-left md:gap-8 max-w-5xl mx-auto">
<div class="md:flex-1">
<h2 id="midpage-cta-heading" class="text-2xl sm:text-3xl font-bold text-white leading-tight">
Have a project in mind? Get a free proposal in 24 hours.
</h2>
<p class="mt-2 text-primary-100 text-sm sm:text-base">
No obligation · Free 30-min discovery call · NDA on request.
</p>
</div>
<div class="flex w-full flex-col sm:flex-row md:w-auto md:flex-shrink-0 gap-3">
<a
href="#inline-lead-form"
data-cta="midpage-primary"
class="inline-flex items-center justify-center rounded-xl bg-white px-7 py-4 text-base font-bold text-primary-700 shadow-lg transition-all hover:bg-primary-50 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-700"
>
Get my free proposal
<svg class="ml-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
<a
href="tel:+919561417403"
data-cta="midpage-call"
class="inline-flex items-center justify-center rounded-xl border-2 border-white/40 bg-white/5 px-7 py-4 text-base font-bold text-white transition-all hover:bg-white/15 hover:border-white/60 focus:outline-none focus:ring-2 focus:ring-white/60 focus:ring-offset-2 focus:ring-offset-primary-700"
>
<svg class="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
Call now
</a>
</div>
</div>
</div>
</section>
<!-- How It Works Section - NEW -->
<section class="py-24 bg-gradient-to-br from-secondary-900 via-secondary-800 to-primary-900 relative overflow-hidden">
<!-- Background pattern -->
+29 -2
View File
@@ -348,8 +348,11 @@ const featuredProjects = projects.filter(p => p.featured);
</div>
</div>
<!-- Category Filter Tabs -->
<div class="flex flex-wrap justify-center gap-3" role="tablist" aria-label="Filter projects by category">
<!-- Category Filter Tabs
Mobile (<sm): a single horizontally-scrollable row so the five
chips never wrap into a tall stack that pushes the project grid
below the fold. ≥sm: centered wrapping row as before. -->
<div class="filter-scroll flex flex-nowrap gap-3 overflow-x-auto pb-1 snap-x sm:flex-wrap sm:justify-center sm:overflow-visible sm:pb-0" role="tablist" aria-label="Filter projects by category">
<button
data-filter="all"
class="filter-btn active rounded-full bg-primary px-6 py-2.5 font-semibold text-white transition-all duration-300 hover:shadow-lg hover:shadow-primary/25 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
@@ -1190,4 +1193,28 @@ const featuredProjects = projects.filter(p => p.featured);
display: flex;
flex-wrap: wrap;
}
/* Horizontally-scrollable category filter row on mobile.
Chips keep their intrinsic width (no shrinking) and snap as the user
swipes; the scrollbar is hidden for a clean native-app feel. At >=sm the
row wraps normally so the scroll affordance is mobile-only. */
.filter-scroll {
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.filter-scroll > .filter-btn {
flex: 0 0 auto;
white-space: nowrap;
scroll-snap-align: start;
}
/* Hide the scrollbar (still scrollable) only while in the mobile scroll mode */
@media (max-width: 639px) {
.filter-scroll {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge legacy */
}
.filter-scroll::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
}
</style>