Latest Updated Pages
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 13s
Ping Search Engines / Notify Search Engines (push) Successful in 3s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 1s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m36s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m6s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m46s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 9m31s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m5s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 15m24s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 6s
E2E Test Suite / Mobile Device Tests (push) Failing after 3h12m28s
Uptime Monitor / Health & Response Time (push) Successful in 5s
Uptime Monitor / SSL Certificate (push) Successful in 3s
Uptime Monitor / Send Alerts (push) Has been skipped
Uptime Monitor / Record Uptime Success (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 13s
Ping Search Engines / Notify Search Engines (push) Successful in 3s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 1s
E2E Test Suite / Smoke Tests (P0) (push) Failing after 9m36s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m6s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 11m46s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 9m31s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m5s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 15m24s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 6s
E2E Test Suite / Mobile Device Tests (push) Failing after 3h12m28s
Uptime Monitor / Health & Response Time (push) Successful in 5s
Uptime Monitor / SSL Certificate (push) Successful in 3s
Uptime Monitor / Send Alerts (push) Has been skipped
Uptime Monitor / Record Uptime Success (push) Successful in 2s
This commit is contained in:
+19
-12
@@ -2,6 +2,8 @@
|
||||
// Header Component for WorkRoot IT Solutions
|
||||
// Features: Sticky header, responsive nav, mobile hamburger menu, active link highlighting
|
||||
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
@@ -22,7 +24,7 @@ const currentPath = Astro.url.pathname;
|
||||
|
||||
<header
|
||||
class:list={[
|
||||
'fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm border-b border-secondary-100 transition-shadow duration-300',
|
||||
'fixed top-0 left-0 right-0 z-50 bg-white/95 dark:bg-secondary-900/95 backdrop-blur-sm border-b border-secondary-100 dark:border-secondary-800 transition-shadow duration-300',
|
||||
className,
|
||||
]}
|
||||
id="main-header"
|
||||
@@ -35,7 +37,7 @@ const currentPath = Astro.url.pathname;
|
||||
<span class="text-white font-bold text-xl">W</span>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<span class="font-bold text-xl text-secondary">WorkRoot</span>
|
||||
<span class="font-bold text-xl text-secondary dark:text-white">WorkRoot</span>
|
||||
<span class="text-primary font-semibold text-sm block -mt-1">IT Solutions</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -54,8 +56,8 @@ const currentPath = Astro.url.pathname;
|
||||
class:list={[
|
||||
'relative px-4 py-2 text-sm font-medium transition-colors rounded-lg',
|
||||
isActive
|
||||
? 'text-primary bg-primary-50'
|
||||
: 'text-secondary-600 hover:text-primary hover:bg-secondary-50',
|
||||
? 'text-primary bg-primary-50 dark:bg-primary-900/30 dark:text-primary-400'
|
||||
: 'text-secondary-600 hover:text-primary hover:bg-secondary-50 dark:text-secondary-400 dark:hover:text-primary-400 dark:hover:bg-secondary-800',
|
||||
]}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
@@ -71,6 +73,7 @@ const currentPath = Astro.url.pathname;
|
||||
|
||||
<!-- Desktop CTA -->
|
||||
<div class="hidden lg:flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<a
|
||||
href="/contact"
|
||||
class="btn-primary text-sm"
|
||||
@@ -85,7 +88,7 @@ const currentPath = Astro.url.pathname;
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="lg:hidden relative w-10 h-10 flex items-center justify-center text-secondary-600 hover:text-primary transition-colors"
|
||||
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"
|
||||
@@ -112,23 +115,23 @@ const currentPath = Astro.url.pathname;
|
||||
<!-- Mobile Menu Panel -->
|
||||
<div
|
||||
id="mobile-menu"
|
||||
class="fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-white shadow-2xl transform translate-x-full transition-transform duration-300 ease-out lg:hidden"
|
||||
class="fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-white dark:bg-secondary-900 shadow-2xl transform translate-x-full transition-transform duration-300 ease-out lg:hidden"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Mobile navigation menu"
|
||||
>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Mobile Menu Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-secondary-100">
|
||||
<div class="flex items-center justify-between p-4 border-b border-secondary-100 dark:border-secondary-800">
|
||||
<a href="/" class="flex items-center gap-2" aria-label="WorkRoot IT Solutions - Home">
|
||||
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center" aria-hidden="true">
|
||||
<span class="text-white font-bold text-lg">W</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg text-secondary">WorkRoot</span>
|
||||
<span class="font-bold text-lg text-secondary dark:text-white">WorkRoot</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="w-10 h-10 flex items-center justify-center text-secondary-500 hover:text-secondary-700 transition-colors"
|
||||
class="w-10 h-10 flex items-center justify-center text-secondary-500 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-200 transition-colors"
|
||||
id="mobile-menu-close"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
@@ -153,8 +156,8 @@ const currentPath = Astro.url.pathname;
|
||||
class:list={[
|
||||
'mobile-nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-base font-medium transition-all duration-200',
|
||||
isActive
|
||||
? 'text-primary bg-primary-50'
|
||||
: 'text-secondary-700 hover:text-primary hover:bg-secondary-50',
|
||||
? 'text-primary bg-primary-50 dark:bg-primary-900/30 dark:text-primary-400'
|
||||
: 'text-secondary-700 hover:text-primary hover:bg-secondary-50 dark:text-secondary-300 dark:hover:text-primary-400 dark:hover:bg-secondary-800',
|
||||
]}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
@@ -170,7 +173,11 @@ const currentPath = Astro.url.pathname;
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Menu Footer -->
|
||||
<div class="p-4 border-t border-secondary-100">
|
||||
<div class="p-4 border-t border-secondary-100 dark:border-secondary-800">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-medium text-secondary-600 dark:text-secondary-400">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<a
|
||||
href="/contact"
|
||||
class="btn-primary w-full justify-center"
|
||||
|
||||
@@ -30,7 +30,7 @@ const {
|
||||
class: className = '',
|
||||
loading = 'lazy',
|
||||
fetchpriority = 'auto',
|
||||
placeholder = '#1e293b', // secondary-800
|
||||
placeholder = 'var(--color-surface-alt, #e2e8f0)', // adapts to theme
|
||||
} = Astro.props;
|
||||
|
||||
const aspectRatio = (height / width) * 100;
|
||||
|
||||
@@ -156,7 +156,7 @@ const aboutPageSchema = type === 'AboutPage' ? {
|
||||
}
|
||||
} : null;
|
||||
|
||||
// Generate ContactPage schema
|
||||
// Generate ContactPage schema with LocalBusiness details
|
||||
const contactPageSchema = type === 'ContactPage' ? {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ContactPage",
|
||||
@@ -165,9 +165,51 @@ const contactPageSchema = type === 'ContactPage' ? {
|
||||
"url": currentUrl,
|
||||
"inLanguage": "en-US",
|
||||
"mainEntity": {
|
||||
"@type": "Organization",
|
||||
"@type": "LocalBusiness",
|
||||
"name": "WorkRoot IT Solutions",
|
||||
"url": baseUrl
|
||||
"alternateName": "WorkRoot IT Solutions LLP",
|
||||
"url": baseUrl,
|
||||
"telephone": "+91-9561417403",
|
||||
"email": "admin@workroot.in",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "At Post Rajapur Shantinagar, Taluka Khatav",
|
||||
"addressLocality": "Satara",
|
||||
"addressRegion": "MH",
|
||||
"postalCode": "415503",
|
||||
"addressCountry": "IN"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "17.7733632",
|
||||
"longitude": "74.3309312"
|
||||
},
|
||||
"openingHoursSpecification": [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
"opens": "09:00",
|
||||
"closes": "18:00"
|
||||
}
|
||||
],
|
||||
"priceRange": "$$",
|
||||
"currenciesAccepted": "INR",
|
||||
"paymentAccepted": "Cash, Credit Card, Bank Transfer",
|
||||
"areaServed": "Worldwide",
|
||||
"image": `${baseUrl}/og-image.jpg`,
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": `${baseUrl}/logo.png`,
|
||||
"width": 512,
|
||||
"height": 512
|
||||
},
|
||||
"hasMap": "https://maps.google.com/?q=Rajapur+Shantinagar+Khatav+Satara+Maharashtra+415503",
|
||||
"sameAs": [
|
||||
"https://twitter.com/workroot",
|
||||
"https://linkedin.com/company/workroot",
|
||||
"https://github.com/workroot",
|
||||
"https://facebook.com/workroot"
|
||||
]
|
||||
}
|
||||
} : null;
|
||||
---
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
// ThemeToggle Component
|
||||
// Features: Sun/moon icons, smooth transitions, localStorage persistence,
|
||||
// system preference detection (prefers-color-scheme), ARIA accessible
|
||||
---
|
||||
|
||||
<!-- Unique ID via data attribute; JS uses querySelectorAll to support multiple instances -->
|
||||
<button
|
||||
data-theme-toggle
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
aria-label="Switch to dark mode"
|
||||
title="Toggle dark/light mode"
|
||||
class="theme-toggle relative w-10 h-10 flex items-center justify-center rounded-lg text-secondary-600 hover:text-primary hover:bg-secondary-50 dark:text-secondary-400 dark:hover:text-primary-400 dark:hover:bg-secondary-800 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 dark:focus:ring-offset-secondary-900"
|
||||
>
|
||||
<!-- Sun icon (shown in dark mode to switch to light) -->
|
||||
<svg
|
||||
class="sun-icon w-5 h-5 absolute transition-all duration-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
||||
</svg>
|
||||
|
||||
<!-- Moon icon (shown in light mode to switch to dark) -->
|
||||
<svg
|
||||
class="moon-icon w-5 h-5 absolute transition-all duration-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Live region: announces theme changes to screen readers without disrupting reading flow -->
|
||||
<span
|
||||
data-theme-live
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
class="sr-only"
|
||||
></span>
|
||||
|
||||
<style>
|
||||
/* Initial icon states - controlled by JS after load */
|
||||
.theme-toggle .sun-icon {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) rotate(-90deg);
|
||||
}
|
||||
|
||||
.theme-toggle .moon-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
/* Dark mode: show sun, hide moon */
|
||||
:global(html.dark) .theme-toggle .sun-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
|
||||
:global(html.dark) .theme-toggle .moon-icon {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) rotate(90deg);
|
||||
}
|
||||
|
||||
/* Respect reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-toggle .sun-icon,
|
||||
.theme-toggle .moon-icon {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function initThemeToggles() {
|
||||
// Use data attribute selector to support multiple instances (desktop + mobile)
|
||||
const btns = document.querySelectorAll<HTMLButtonElement>('[data-theme-toggle]');
|
||||
if (!btns.length) return;
|
||||
|
||||
function syncAllButtons(isDark: boolean) {
|
||||
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
btns.forEach((btn) => {
|
||||
btn.setAttribute('aria-label', label);
|
||||
btn.setAttribute('title', label);
|
||||
btn.setAttribute('aria-checked', isDark ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function announceThemeChange(isDark: boolean) {
|
||||
// Announce to screen readers via live region (polite = does not interrupt)
|
||||
document.querySelectorAll('[data-theme-live]').forEach((el) => {
|
||||
el.textContent = isDark ? 'Dark mode enabled' : 'Light mode enabled';
|
||||
});
|
||||
}
|
||||
|
||||
function applyTheme(isDark: boolean, announce = false) {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
syncAllButtons(isDark);
|
||||
if (announce) announceThemeChange(isDark);
|
||||
|
||||
// Update all theme-color meta tags for browser chrome
|
||||
const color = isDark ? '#0f172a' : '#0891b2';
|
||||
document.querySelectorAll('meta[name="theme-color"]').forEach((m) => {
|
||||
m.setAttribute('content', color);
|
||||
});
|
||||
}
|
||||
|
||||
btns.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const newIsDark = !document.documentElement.classList.contains('dark');
|
||||
applyTheme(newIsDark, true);
|
||||
try {
|
||||
localStorage.setItem('theme', newIsDark ? 'dark' : 'light');
|
||||
} catch {
|
||||
// localStorage not available (e.g. private browsing)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sync ARIA state on init
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
syncAllButtons(isDark);
|
||||
}
|
||||
|
||||
// Run immediately if DOM is ready, otherwise wait
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initThemeToggles);
|
||||
} else {
|
||||
initThemeToggles();
|
||||
}
|
||||
|
||||
// Listen for system preference changes (when no explicit user choice)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (!stored) {
|
||||
const isDark = e.matches;
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
const color = isDark ? '#0f172a' : '#0891b2';
|
||||
document.querySelectorAll('meta[name="theme-color"]').forEach((m) => {
|
||||
m.setAttribute('content', color);
|
||||
});
|
||||
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
|
||||
btn.setAttribute('aria-label', label);
|
||||
btn.setAttribute('aria-checked', isDark ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -26,10 +26,10 @@ const {
|
||||
} = Astro.props;
|
||||
|
||||
const variantClasses: Record<string, string> = {
|
||||
'primary': 'bg-primary-50 text-primary-700',
|
||||
'primary': 'bg-primary-50 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300',
|
||||
'primary-dark': 'bg-primary-400/20 text-primary-300',
|
||||
'accent': 'bg-accent-50 text-accent-700',
|
||||
'secondary': 'bg-secondary-100 text-secondary-700',
|
||||
'accent': 'bg-accent-50 text-accent-700 dark:bg-accent-900/40 dark:text-accent-300',
|
||||
'secondary': 'bg-secondary-100 text-secondary-700 dark:bg-secondary-800 dark:text-secondary-300',
|
||||
};
|
||||
|
||||
const classes = [
|
||||
|
||||
@@ -30,21 +30,22 @@ const {
|
||||
const variantClasses: Record<string, string> = {
|
||||
'feature': [
|
||||
'group relative bg-gradient-to-br from-secondary-50 to-white',
|
||||
'rounded-2xl border border-secondary-100',
|
||||
'hover:border-primary/30 hover:shadow-2xl',
|
||||
'dark:from-secondary-800 dark:to-secondary-900',
|
||||
'rounded-2xl border border-secondary-100 dark:border-secondary-700',
|
||||
'hover:border-primary/30 dark:hover:border-primary-500/40 hover:shadow-2xl',
|
||||
'transition-all duration-500 hover:-translate-y-2',
|
||||
].join(' '),
|
||||
|
||||
'service': [
|
||||
'group relative bg-white rounded-3xl shadow-lg',
|
||||
'border-2 border-secondary-100',
|
||||
'hover:border-primary-400 hover:shadow-2xl',
|
||||
'group relative bg-white dark:bg-secondary-800 rounded-3xl shadow-lg',
|
||||
'border-2 border-secondary-100 dark:border-secondary-700',
|
||||
'hover:border-primary-400 dark:hover:border-primary-500 hover:shadow-2xl',
|
||||
'transition-all duration-500 overflow-hidden',
|
||||
].join(' '),
|
||||
|
||||
'surface': [
|
||||
'bg-secondary-50 rounded-xl',
|
||||
'border border-secondary-200',
|
||||
'bg-secondary-50 dark:bg-secondary-800 rounded-xl',
|
||||
'border border-secondary-200 dark:border-secondary-700',
|
||||
'hover:shadow-lg transition-shadow duration-300',
|
||||
].join(' '),
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ const {
|
||||
const isLight = theme === 'light';
|
||||
|
||||
const badgeClasses: Record<string, string> = {
|
||||
'primary-light': 'bg-primary-50 text-primary-700',
|
||||
'accent-light': 'bg-accent-50 text-accent-700',
|
||||
'primary-light': 'bg-primary-50 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300',
|
||||
'accent-light': 'bg-accent-50 text-accent-700 dark:bg-accent-900/40 dark:text-accent-300',
|
||||
'primary-dark': 'bg-primary-400/20 text-primary-300',
|
||||
'accent-dark': 'bg-accent-400/20 text-accent-300',
|
||||
};
|
||||
@@ -47,11 +47,11 @@ const badgeKey = `${badgeColor}-${theme}`;
|
||||
const badgeClass = badgeClasses[badgeKey] ?? badgeClasses['primary-light'];
|
||||
|
||||
const titleClass = isLight
|
||||
? 'text-4xl sm:text-5xl font-bold text-secondary-900 mb-6'
|
||||
? 'text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-50 mb-6'
|
||||
: 'text-4xl sm:text-5xl font-bold text-white mb-6';
|
||||
|
||||
const descClass = isLight
|
||||
? 'text-xl text-secondary-600'
|
||||
? 'text-xl text-secondary-600 dark:text-secondary-400'
|
||||
: 'text-xl text-secondary-300';
|
||||
|
||||
const containerClass = [
|
||||
|
||||
@@ -96,15 +96,16 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
|
||||
<meta name="twitter:label1" content="Est. reading time" />
|
||||
<meta name="twitter:data1" content="3 minutes" />
|
||||
|
||||
<!-- Theme Color -->
|
||||
<meta name="theme-color" content="#0891b2" />
|
||||
<!-- Theme Color: dual media query for OS-level dark/light, overridden by JS for stored preference -->
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#0891b2" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f172a" />
|
||||
|
||||
<!-- JSON-LD Structured Data - Organization Schema -->
|
||||
<!-- JSON-LD Structured Data - Organization + LocalBusiness Schema -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"@type": ["Organization", "LocalBusiness"],
|
||||
"name": "WorkRoot IT Solutions",
|
||||
"alternateName": "WorkRoot",
|
||||
"alternateName": ["WorkRoot", "WorkRoot IT Solutions LLP"],
|
||||
"url": "https://workroot.in",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
@@ -121,27 +122,46 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
|
||||
},
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "123 Tech Plaza, Suite 400",
|
||||
"addressLocality": "San Francisco",
|
||||
"addressRegion": "CA",
|
||||
"postalCode": "94105",
|
||||
"addressCountry": "US"
|
||||
"streetAddress": "At Post Rajapur Shantinagar, Taluka Khatav",
|
||||
"addressLocality": "Satara",
|
||||
"addressRegion": "MH",
|
||||
"postalCode": "415503",
|
||||
"addressCountry": "IN"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": "17.7733632",
|
||||
"longitude": "74.3309312"
|
||||
},
|
||||
"hasMap": "https://maps.google.com/?q=Rajapur+Shantinagar+Khatav+Satara+Maharashtra+415503",
|
||||
"telephone": "+91-9561417403",
|
||||
"email": "admin@workroot.in",
|
||||
"contactPoint": [{
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+1-555-123-4567",
|
||||
"telephone": "+91-9561417403",
|
||||
"contactType": "customer service",
|
||||
"email": "hello@workroot.in",
|
||||
"availableLanguage": ["English"],
|
||||
"email": "admin@workroot.in",
|
||||
"availableLanguage": ["English", "Hindi", "Marathi"],
|
||||
"areaServed": "Worldwide"
|
||||
}, {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+1-555-123-4567",
|
||||
"telephone": "+91-9561417403",
|
||||
"contactType": "sales",
|
||||
"email": "sales@workroot.in",
|
||||
"availableLanguage": ["English"],
|
||||
"email": "admin@workroot.in",
|
||||
"availableLanguage": ["English", "Hindi", "Marathi"],
|
||||
"areaServed": "Worldwide"
|
||||
}],
|
||||
"openingHoursSpecification": [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
"opens": "09:00",
|
||||
"closes": "18:00"
|
||||
}
|
||||
],
|
||||
"priceRange": "$$",
|
||||
"currenciesAccepted": "INR",
|
||||
"paymentAccepted": "Cash, Credit Card, Bank Transfer",
|
||||
"sameAs": [
|
||||
"https://twitter.com/workroot",
|
||||
"https://linkedin.com/company/workroot",
|
||||
@@ -202,6 +222,26 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" />
|
||||
</noscript>
|
||||
|
||||
<!-- Theme init: runs inline before render to prevent FOUC -->
|
||||
<!-- Also syncs theme-color meta to match stored preference (overrides OS-level media query) -->
|
||||
<script is:inline>
|
||||
(function() {
|
||||
try {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var isDark = stored === 'dark' || (!stored && prefersDark);
|
||||
if (isDark) document.documentElement.classList.add('dark');
|
||||
// Sync theme-color meta tags when user has an explicit stored preference
|
||||
// (without stored preference the OS media query on the meta tags handles it natively)
|
||||
if (stored) {
|
||||
var metas = document.querySelectorAll('meta[name="theme-color"]');
|
||||
var color = isDark ? '#0f172a' : '#0891b2';
|
||||
metas.forEach(function(m) { m.setAttribute('content', color); });
|
||||
}
|
||||
} catch(e) {}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Critical CSS for above-the-fold content -->
|
||||
<style is:inline>
|
||||
/* Critical CSS for faster FCP - system fonts render immediately */
|
||||
@@ -314,11 +354,8 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid theme('colors.primary.DEFAULT');
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* Focus styles are defined in global.css via Tailwind ring utilities.
|
||||
Removed duplicate outline here to avoid conflicting with ring-based focus styles. */
|
||||
|
||||
/* Remove focus outline for mouse users */
|
||||
:focus:not(:focus-visible) {
|
||||
|
||||
+14
-3
@@ -54,9 +54,20 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
origin: context.request.headers.get('origin'),
|
||||
referer: context.request.headers.get('referer'),
|
||||
});
|
||||
const requestOrigin = context.request.headers.get('origin') ?? '';
|
||||
const allowedOrigins = ['https://workroot.in', 'https://www.workroot.in'];
|
||||
const corsOrigin = allowedOrigins.includes(requestOrigin) ? requestOrigin : 'https://workroot.in';
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, error: 'Forbidden: invalid origin.' }),
|
||||
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
||||
{
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': corsOrigin,
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,13 +119,13 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
"default-src 'self'",
|
||||
scriptSrc,
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com",
|
||||
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com https://maps.googleapis.com https://maps.gstatic.com",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io",
|
||||
"worker-src 'self'",
|
||||
"manifest-src 'self'",
|
||||
"object-src 'none'", // Block Flash/plugins (defense-in-depth)
|
||||
"frame-src 'none'", // No embedded frames allowed
|
||||
"frame-src https://www.google.com", // Allow Google Maps iframe embed
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
|
||||
+30
-30
@@ -176,15 +176,15 @@ const values = [
|
||||
</section>
|
||||
|
||||
<!-- Company Story Section -->
|
||||
<section class="py-16 lg:py-24 bg-white">
|
||||
<section class="py-16 lg:py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
<div class="order-2 lg:order-1" data-animate="fade-right">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Our Story</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-6">
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">Our Story</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-6">
|
||||
From Startup to Industry Leader
|
||||
</h2>
|
||||
<div class="space-y-4 text-secondary-600 leading-relaxed">
|
||||
<div class="space-y-4 text-secondary-600 dark:text-secondary-400 leading-relaxed">
|
||||
<p>
|
||||
Founded in 2014, WorkRoot IT Solutions began with a simple mission: to make enterprise-grade technology accessible to businesses of all sizes. What started as a small team of three passionate developers has grown into a full-service IT consultancy trusted by companies worldwide.
|
||||
</p>
|
||||
@@ -219,12 +219,12 @@ const values = [
|
||||
</section>
|
||||
|
||||
<!-- Company Timeline Section -->
|
||||
<section class="py-16 lg:py-24 bg-white">
|
||||
<section class="py-16 lg:py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Our Journey</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Company Timeline</h2>
|
||||
<p class="text-secondary-600">
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">Our Journey</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Company Timeline</h2>
|
||||
<p class="text-secondary-600 dark:text-secondary-400">
|
||||
A decade of innovation, growth, and excellence in technology solutions.
|
||||
</p>
|
||||
</div>
|
||||
@@ -245,14 +245,14 @@ const values = [
|
||||
</div>
|
||||
|
||||
<!-- Content card -->
|
||||
<div class={`relative bg-white border-2 border-secondary-100 rounded-2xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 group hover:border-primary-200 ${index % 2 === 0 ? 'md:text-right' : 'md:text-left'}`}>
|
||||
<div class={`relative bg-white dark:bg-secondary-800 border-2 border-secondary-100 dark:border-secondary-700 rounded-2xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 group hover:border-primary-200 ${index % 2 === 0 ? 'md:text-right' : 'md:text-left'}`}>
|
||||
<div class="absolute -top-3 left-6 md:left-auto md:right-6 px-4 py-1 bg-gradient-to-r from-primary-500 to-primary-600 text-white text-sm font-bold rounded-full shadow-md">
|
||||
{item.year}
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-secondary-900 mt-4 mb-2 group-hover:text-primary-600 transition-colors">
|
||||
<h3 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 mt-4 mb-2 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p class="text-secondary-600 leading-relaxed">
|
||||
<p class="text-secondary-600 dark:text-secondary-400 leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -265,11 +265,11 @@ const values = [
|
||||
</section>
|
||||
|
||||
<!-- Mission & Vision Section -->
|
||||
<section class="py-16 lg:py-24 bg-gradient-to-b from-secondary-50 to-white">
|
||||
<section class="py-16 lg:py-24 bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center mb-12 lg:mb-16" data-animate="fade-up">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Our Purpose</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2">Mission & Vision</h2>
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">Our Purpose</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2">Mission & Vision</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-8 lg:gap-12">
|
||||
@@ -316,7 +316,7 @@ const values = [
|
||||
</section>
|
||||
|
||||
<!-- Core Values Section -->
|
||||
<section class="py-16 lg:py-24 bg-white relative overflow-hidden">
|
||||
<section class="py-16 lg:py-24 bg-white dark:bg-secondary-900 relative overflow-hidden">
|
||||
<!-- Background decoration -->
|
||||
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<div class="absolute top-0 right-0 w-96 h-96 bg-primary-500/5 rounded-full blur-3xl"></div>
|
||||
@@ -325,9 +325,9 @@ const values = [
|
||||
|
||||
<div class="container-wrapper relative">
|
||||
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">What Drives Us</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Our Core Values</h2>
|
||||
<p class="text-secondary-600">
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">What Drives Us</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Our Core Values</h2>
|
||||
<p class="text-secondary-600 dark:text-secondary-400">
|
||||
These principles guide every decision we make and every solution we deliver.
|
||||
</p>
|
||||
</div>
|
||||
@@ -339,8 +339,8 @@ const values = [
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-500 to-primary-700 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- Card content -->
|
||||
<div class="relative bg-white p-6 lg:p-8 rounded-2xl shadow-sm group-hover:shadow-xl transition-all duration-300 border-2 border-secondary-100 group-hover:border-primary-200 group-hover:translate-y-[-4px]">
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-primary-50 to-primary-100 text-primary-600 rounded-xl flex items-center justify-center mb-6 group-hover:bg-white group-hover:text-white transition-all duration-300 shadow-md">
|
||||
<div class="relative bg-white dark:bg-secondary-800 p-6 lg:p-8 rounded-2xl shadow-sm group-hover:shadow-xl transition-all duration-300 border-2 border-secondary-100 dark:border-secondary-700 group-hover:border-primary-200 group-hover:translate-y-[-4px]">
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-primary-50 to-primary-100 dark:from-primary-900/30 dark:to-primary-900/50 text-primary-600 dark:text-primary-400 rounded-xl flex items-center justify-center mb-6 group-hover:bg-white group-hover:text-white transition-all duration-300 shadow-md">
|
||||
{value.icon === 'lightbulb' && (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
@@ -362,8 +362,8 @@ const values = [
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-secondary-900 mb-3 group-hover:text-white transition-colors duration-300">{value.title}</h3>
|
||||
<p class="text-secondary-600 leading-relaxed group-hover:text-primary-50 transition-colors duration-300">{value.description}</p>
|
||||
<h3 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 mb-3 group-hover:text-white transition-colors duration-300">{value.title}</h3>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 leading-relaxed group-hover:text-primary-50 transition-colors duration-300">{value.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -372,12 +372,12 @@ const values = [
|
||||
</section>
|
||||
|
||||
<!-- Team Section -->
|
||||
<section class="py-16 lg:py-24 bg-secondary-50">
|
||||
<section class="py-16 lg:py-24 bg-secondary-50 dark:bg-secondary-800/50">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">The Team</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Meet Our Leadership</h2>
|
||||
<p class="text-secondary-600">
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">The Team</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Meet Our Leadership</h2>
|
||||
<p class="text-secondary-600 dark:text-secondary-400">
|
||||
Talented professionals who bring diverse expertise and shared passion for technology excellence.
|
||||
</p>
|
||||
</div>
|
||||
@@ -388,7 +388,7 @@ const values = [
|
||||
<!-- Card wrapper with gradient border effect -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-400 to-accent-400 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-sm"></div>
|
||||
|
||||
<div class="relative bg-white rounded-3xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 transform group-hover:-translate-y-2">
|
||||
<div class="relative bg-white dark:bg-secondary-800 rounded-3xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 transform group-hover:-translate-y-2">
|
||||
<!-- Image container -->
|
||||
<div class="relative aspect-[4/5] overflow-hidden bg-secondary-200">
|
||||
<img
|
||||
@@ -432,9 +432,9 @@ const values = [
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6 lg:p-7">
|
||||
<h3 class="text-xl font-bold text-secondary-900 mb-1 group-hover:text-primary-600 transition-colors">{member.name}</h3>
|
||||
<p class="text-primary-500 font-semibold text-sm mb-3 uppercase tracking-wide">{member.role}</p>
|
||||
<p class="text-secondary-600 leading-relaxed text-sm">{member.bio}</p>
|
||||
<h3 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 mb-1 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">{member.name}</h3>
|
||||
<p class="text-primary-500 dark:text-primary-400 font-semibold text-sm mb-3 uppercase tracking-wide">{member.role}</p>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 leading-relaxed text-sm">{member.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -190,21 +190,35 @@ function escapeHtml(str: string): string {
|
||||
// ============================================================
|
||||
// CORS headers helper
|
||||
// ============================================================
|
||||
function corsHeaders(): HeadersInit {
|
||||
const origin = import.meta.env.PROD ? 'https://workroot.in' : '*';
|
||||
const ALLOWED_ORIGINS = ['https://workroot.in', 'https://www.workroot.in'];
|
||||
|
||||
function corsHeaders(requestOrigin?: string | null): HeadersInit {
|
||||
if (!import.meta.env.PROD) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
};
|
||||
}
|
||||
// Reflect the request origin if it is in the allowlist, otherwise default to primary domain
|
||||
const origin = requestOrigin && ALLOWED_ORIGINS.includes(requestOrigin)
|
||||
? requestOrigin
|
||||
: 'https://workroot.in';
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Vary': 'Origin',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OPTIONS preflight handler
|
||||
// ============================================================
|
||||
export const OPTIONS: APIRoute = async () => {
|
||||
return new Response(null, { status: 204, headers: corsHeaders() });
|
||||
export const OPTIONS: APIRoute = async ({ request }) => {
|
||||
return new Response(null, { status: 204, headers: corsHeaders(request.headers.get('origin')) });
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
@@ -213,11 +227,12 @@ export const OPTIONS: APIRoute = async () => {
|
||||
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
const ip = clientAddress ?? 'unknown';
|
||||
const startTime = Date.now();
|
||||
const requestOrigin = request.headers.get('origin');
|
||||
|
||||
// Rate limiting check
|
||||
const rateLimit = checkRateLimit(ip);
|
||||
const rateLimitHeaders = {
|
||||
...corsHeaders(),
|
||||
...corsHeaders(requestOrigin),
|
||||
'X-RateLimit-Limit': String(RATE_LIMIT_MAX_REQUESTS),
|
||||
'X-RateLimit-Remaining': String(rateLimit.remaining),
|
||||
'X-RateLimit-Reset': String(Math.ceil(rateLimit.resetAt / 1000)),
|
||||
|
||||
@@ -154,21 +154,35 @@ async function subscribeToNewsletter(email: string): Promise<void> {
|
||||
// ============================================================
|
||||
// CORS headers helper
|
||||
// ============================================================
|
||||
function corsHeaders(): HeadersInit {
|
||||
const origin = import.meta.env.PROD ? 'https://workroot.in' : '*';
|
||||
const ALLOWED_ORIGINS = ['https://workroot.in', 'https://www.workroot.in'];
|
||||
|
||||
function corsHeaders(requestOrigin?: string | null): HeadersInit {
|
||||
if (!import.meta.env.PROD) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
};
|
||||
}
|
||||
// Reflect the request origin if it is in the allowlist, otherwise default to primary domain
|
||||
const origin = requestOrigin && ALLOWED_ORIGINS.includes(requestOrigin)
|
||||
? requestOrigin
|
||||
: 'https://workroot.in';
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Vary': 'Origin',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OPTIONS preflight handler
|
||||
// ============================================================
|
||||
export const OPTIONS: APIRoute = async () => {
|
||||
return new Response(null, { status: 204, headers: corsHeaders() });
|
||||
export const OPTIONS: APIRoute = async ({ request }) => {
|
||||
return new Response(null, { status: 204, headers: corsHeaders(request.headers.get('origin')) });
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
@@ -177,11 +191,12 @@ export const OPTIONS: APIRoute = async () => {
|
||||
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
const ip = clientAddress ?? 'unknown';
|
||||
const startTime = Date.now();
|
||||
const requestOrigin = request.headers.get('origin');
|
||||
|
||||
// Rate limiting
|
||||
const rateLimit = checkRateLimit(ip);
|
||||
const rateLimitHeaders = {
|
||||
...corsHeaders(),
|
||||
...corsHeaders(requestOrigin),
|
||||
'X-RateLimit-Limit': String(RATE_LIMIT_MAX_REQUESTS),
|
||||
'X-RateLimit-Remaining': String(rateLimit.remaining),
|
||||
'X-RateLimit-Reset': String(Math.ceil(rateLimit.resetAt / 1000)),
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
---
|
||||
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||
import { getEntry } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import SEO from '../../components/SEO.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
const { slug } = Astro.params;
|
||||
|
||||
if (!slug) {
|
||||
return Astro.redirect('/blog');
|
||||
}
|
||||
|
||||
type Props = { post: CollectionEntry<'blog'> };
|
||||
const post = await getEntry('blog', slug);
|
||||
|
||||
if (!post || post.data.draft) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
|
||||
function getReadingTime(content: string): number {
|
||||
@@ -61,7 +62,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
wordCount: post.body.split(/\s+/).length
|
||||
}}
|
||||
/>
|
||||
<article class="bg-white">
|
||||
<article class="bg-white dark:bg-secondary-900">
|
||||
<!-- Hero Section -->
|
||||
<header class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-16 md:py-24 overflow-hidden">
|
||||
<div class="absolute inset-0 opacity-20">
|
||||
@@ -141,7 +142,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
alt={post.data.title}
|
||||
width="1200"
|
||||
height="675"
|
||||
class="w-full aspect-video object-cover rounded-2xl shadow-2xl bg-secondary-200"
|
||||
class="w-full aspect-video object-cover rounded-2xl shadow-2xl bg-secondary-200 dark:bg-secondary-700"
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
@@ -155,14 +156,14 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
<!-- Social Share Sidebar (Desktop) -->
|
||||
<aside class="hidden lg:block w-16 flex-shrink-0">
|
||||
<div class="sticky top-24 flex flex-col gap-3">
|
||||
<span class="text-xs text-secondary-400 font-medium mb-1">Share</span>
|
||||
<span class="text-xs text-secondary-400 dark:text-secondary-500 font-medium mb-1">Share</span>
|
||||
|
||||
<!-- Twitter/X -->
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?url=${shareUrl}&text=${shareTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#1DA1F2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 dark:bg-secondary-800 hover:bg-[#1DA1F2] text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Share on Twitter"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -175,7 +176,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#0A66C2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 dark:bg-secondary-800 hover:bg-[#0A66C2] text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Share on LinkedIn"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -188,7 +189,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${shareUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#1877F2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 dark:bg-secondary-800 hover:bg-[#1877F2] text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Share on Facebook"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -200,7 +201,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
<button
|
||||
type="button"
|
||||
id="copy-link-btn"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-primary-500 text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 dark:bg-secondary-800 hover:bg-primary-500 text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Copy link"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -213,28 +214,28 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-8 pb-8 border-b border-secondary-200">
|
||||
<div class="flex flex-wrap gap-2 mb-8 pb-8 border-b border-secondary-200 dark:border-secondary-700">
|
||||
{post.data.tags.map((tag) => (
|
||||
<span class="px-3 py-1 bg-secondary-100 text-secondary-600 text-sm rounded-full hover:bg-secondary-200 transition-colors">
|
||||
<span class="px-3 py-1 bg-secondary-100 dark:bg-secondary-800 text-secondary-600 dark:text-secondary-400 text-sm rounded-full hover:bg-secondary-200 dark:hover:bg-secondary-700 transition-colors">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Article Content -->
|
||||
<div class="prose prose-lg max-w-none prose-headings:text-secondary-900 prose-headings:font-bold prose-p:text-secondary-700 prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline prose-strong:text-secondary-900 prose-code:text-primary-700 prose-code:bg-primary-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:font-mono prose-code:before:content-none prose-code:after:content-none prose-pre:bg-secondary-900 prose-pre:text-secondary-100 prose-pre:rounded-xl prose-pre:shadow-lg prose-img:rounded-xl prose-img:shadow-md prose-blockquote:border-primary-500 prose-blockquote:bg-primary-50 prose-blockquote:rounded-r-lg prose-blockquote:py-1 prose-blockquote:not-italic prose-li:marker:text-primary-500">
|
||||
<div class="prose prose-lg max-w-none prose-headings:text-secondary-900 prose-headings:font-bold prose-p:text-secondary-700 prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline prose-strong:text-secondary-900 prose-code:text-primary-700 prose-code:bg-primary-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:font-mono prose-code:before:content-none prose-code:after:content-none prose-pre:bg-secondary-900 prose-pre:text-secondary-100 prose-pre:rounded-xl prose-pre:shadow-lg prose-img:rounded-xl prose-img:shadow-md prose-blockquote:border-primary-500 prose-blockquote:bg-primary-50 prose-blockquote:rounded-r-lg prose-blockquote:py-1 prose-blockquote:not-italic prose-li:marker:text-primary-500 dark:prose-headings:text-secondary-100 dark:prose-p:text-secondary-300 dark:prose-a:text-primary-400 dark:prose-strong:text-secondary-100 dark:prose-code:text-primary-300 dark:prose-code:bg-primary-900/30 dark:prose-blockquote:bg-primary-900/20 dark:prose-blockquote:border-primary-400 dark:prose-li:marker:text-primary-400">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<!-- Mobile Social Share -->
|
||||
<div class="lg:hidden mt-12 pt-8 border-t border-secondary-200">
|
||||
<p class="text-sm text-secondary-500 font-medium mb-4">Share this article</p>
|
||||
<div class="lg:hidden mt-12 pt-8 border-t border-secondary-200 dark:border-secondary-700">
|
||||
<p class="text-sm text-secondary-500 dark:text-secondary-400 font-medium mb-4">Share this article</p>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?url=${shareUrl}&text=${shareTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-[#1DA1F2] text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 dark:bg-secondary-800 hover:bg-[#1DA1F2] text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
@@ -245,7 +246,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-[#0A66C2] text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 dark:bg-secondary-800 hover:bg-[#0A66C2] text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
@@ -255,7 +256,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
<button
|
||||
type="button"
|
||||
id="copy-link-btn-mobile"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-primary-500 text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 dark:bg-secondary-800 hover:bg-primary-500 text-secondary-600 dark:text-secondary-400 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
@@ -266,15 +267,15 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
</div>
|
||||
|
||||
<!-- Author Bio -->
|
||||
<div class="mt-12 p-6 bg-secondary-50 rounded-2xl">
|
||||
<div class="mt-12 p-6 bg-secondary-50 dark:bg-secondary-800 rounded-2xl">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
|
||||
{post.data.author.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-secondary-500 mb-1">Written by</p>
|
||||
<h3 class="text-xl font-bold text-secondary-900 mb-2">{post.data.author.name}</h3>
|
||||
<p class="text-secondary-600">
|
||||
<p class="text-sm text-secondary-500 dark:text-secondary-400 mb-1">Written by</p>
|
||||
<h3 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">{post.data.author.name}</h3>
|
||||
<p class="text-secondary-600 dark:text-secondary-400">
|
||||
A passionate technologist sharing insights on modern software development, cloud architecture, and digital innovation.
|
||||
</p>
|
||||
</div>
|
||||
@@ -285,7 +286,7 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
<div class="mt-8">
|
||||
<a
|
||||
href="/blog"
|
||||
class="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium transition-colors"
|
||||
class="inline-flex items-center gap-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
@@ -327,6 +328,23 @@ const shareDescription = encodeURIComponent(post.data.description);
|
||||
.prose pre .string { color: #a5d6ff; }
|
||||
.prose pre .function { color: #d2a8ff; }
|
||||
.prose pre .number { color: #79c0ff; }
|
||||
|
||||
/* Dark mode: override prose hr color */
|
||||
html.dark .prose hr {
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
/* Dark mode: table borders */
|
||||
html.dark .prose table thead {
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
html.dark .prose table tbody tr {
|
||||
border-bottom-color: #1e293b;
|
||||
}
|
||||
html.dark .prose table th,
|
||||
html.dark .prose table td {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
+25
-18
@@ -53,7 +53,7 @@ function formatDate(date: Date): string {
|
||||
</section>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<section class="bg-secondary-50 border-b border-secondary-200">
|
||||
<section class="bg-secondary-50 dark:bg-secondary-900 border-b border-secondary-200 dark:border-secondary-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex flex-wrap gap-2 justify-center" role="tablist" aria-label="Filter posts by category">
|
||||
{categories.map((category, index) => (
|
||||
@@ -66,7 +66,7 @@ function formatDate(date: Date): string {
|
||||
'filter-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-200',
|
||||
index === 0
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white text-secondary-600 hover:bg-secondary-100 border border-secondary-200',
|
||||
: 'bg-white dark:bg-secondary-800 text-secondary-600 dark:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-secondary-700 border border-secondary-200 dark:border-secondary-600',
|
||||
]}
|
||||
>
|
||||
{category}
|
||||
@@ -77,18 +77,18 @@ function formatDate(date: Date): string {
|
||||
</section>
|
||||
|
||||
<!-- Blog Posts Grid -->
|
||||
<section class="py-16 bg-white">
|
||||
<section class="py-16 bg-white dark:bg-secondary-900">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" id="posts-grid">
|
||||
{sortedPosts.map((post) => {
|
||||
const readingTime = getReadingTime(post.body);
|
||||
return (
|
||||
<article
|
||||
class="post-card group bg-white rounded-2xl shadow-lg overflow-hidden border border-secondary-100 hover:shadow-xl hover:border-primary-200 transition-all duration-300"
|
||||
class="post-card group bg-white dark:bg-secondary-800 rounded-2xl shadow-lg overflow-hidden border border-secondary-100 dark:border-secondary-700 hover:shadow-xl hover:border-primary-200 dark:hover:border-primary-500 transition-all duration-300"
|
||||
data-category={post.data.category}
|
||||
>
|
||||
<!-- Post Image -->
|
||||
<a href={`/blog/${post.slug}`} class="block aspect-video overflow-hidden bg-secondary-200">
|
||||
<a href={`/blog/${post.slug}`} class="block aspect-video overflow-hidden bg-secondary-200 dark:bg-secondary-700">
|
||||
{post.data.heroImage ? (
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
@@ -112,10 +112,10 @@ function formatDate(date: Date): string {
|
||||
<div class="p-6">
|
||||
<!-- Category & Reading Time -->
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="px-3 py-1 bg-primary-50 text-primary-700 text-xs font-semibold rounded-full">
|
||||
<span class="px-3 py-1 bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-xs font-semibold rounded-full">
|
||||
{post.data.category}
|
||||
</span>
|
||||
<span class="text-secondary-400 text-sm flex items-center gap-1">
|
||||
<span class="text-secondary-400 dark:text-secondary-500 text-sm flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
@@ -124,33 +124,33 @@ function formatDate(date: Date): string {
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-secondary-900 mb-3 line-clamp-2 group-hover:text-primary-600 transition-colors">
|
||||
<h2 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 mb-3 line-clamp-2 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
</h2>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-secondary-600 text-sm mb-4 line-clamp-3">
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-sm mb-4 line-clamp-3">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{post.data.tags.slice(0, 3).map((tag) => (
|
||||
<span class="text-xs text-secondary-500 bg-secondary-100 px-2 py-1 rounded">
|
||||
<span class="text-xs text-secondary-500 dark:text-secondary-400 bg-secondary-100 dark:bg-secondary-700 px-2 py-1 rounded">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Author & Date -->
|
||||
<div class="flex items-center justify-between pt-4 border-t border-secondary-100">
|
||||
<div class="flex items-center justify-between pt-4 border-t border-secondary-100 dark:border-secondary-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-sm font-bold">
|
||||
{post.data.author.name.charAt(0)}
|
||||
</div>
|
||||
<span class="text-sm text-secondary-600">{post.data.author.name}</span>
|
||||
<span class="text-sm text-secondary-600 dark:text-secondary-400">{post.data.author.name}</span>
|
||||
</div>
|
||||
<time class="text-sm text-secondary-400" datetime={post.data.pubDate.toISOString()}>
|
||||
<time class="text-sm text-secondary-400 dark:text-secondary-500" datetime={post.data.pubDate.toISOString()}>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
</div>
|
||||
@@ -162,11 +162,11 @@ function formatDate(date: Date): string {
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="hidden text-center py-16">
|
||||
<svg class="w-16 h-16 text-secondary-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-16 h-16 text-secondary-300 dark:text-secondary-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-secondary-600 mb-2">No posts found</h3>
|
||||
<p class="text-secondary-400">Try selecting a different category.</p>
|
||||
<h3 class="text-xl font-semibold text-secondary-600 dark:text-secondary-400 mb-2">No posts found</h3>
|
||||
<p class="text-secondary-400 dark:text-secondary-500">Try selecting a different category.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -180,14 +180,21 @@ function formatDate(date: Date): string {
|
||||
filterButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const category = btn.getAttribute('data-category');
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
// Update button states
|
||||
filterButtons.forEach((b) => {
|
||||
b.classList.remove('bg-primary-500', 'text-white');
|
||||
b.classList.add('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
|
||||
if (isDark) {
|
||||
b.classList.add('bg-secondary-800', 'text-secondary-400', 'border', 'border-secondary-600');
|
||||
b.classList.remove('bg-white', 'text-secondary-600', 'border-secondary-200');
|
||||
} else {
|
||||
b.classList.add('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
|
||||
b.classList.remove('bg-secondary-800', 'text-secondary-400', 'border-secondary-600');
|
||||
}
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
btn.classList.remove('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
|
||||
btn.classList.remove('bg-white', 'bg-secondary-800', 'text-secondary-600', 'text-secondary-400', 'border', 'border-secondary-200', 'border-secondary-600');
|
||||
btn.classList.add('bg-primary-500', 'text-white');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
|
||||
|
||||
+70
-75
@@ -10,24 +10,24 @@ const contactInfo = [
|
||||
{
|
||||
icon: 'location',
|
||||
title: 'Visit Us',
|
||||
details: ['123 Tech Plaza, Suite 400', 'San Francisco, CA 94105'],
|
||||
link: 'https://maps.google.com/?q=123+Tech+Plaza+San+Francisco+CA',
|
||||
details: ['At Post Rajapur Shantinagar', 'Taluka Khatav, Dist Satara', 'MH 415503'],
|
||||
link: 'https://maps.google.com/?q=Rajapur+Shantinagar+Khatav+Satara+Maharashtra+415503',
|
||||
linkLabel: 'Get directions',
|
||||
color: 'primary',
|
||||
},
|
||||
{
|
||||
icon: 'phone',
|
||||
title: 'Call Us',
|
||||
details: ['+1 (555) 123-4567', '+1 (555) 987-6543'],
|
||||
link: 'tel:+15551234567',
|
||||
details: ['+91 9561417403'],
|
||||
link: 'tel:+919561417403',
|
||||
linkLabel: 'Call now',
|
||||
color: 'accent',
|
||||
},
|
||||
{
|
||||
icon: 'email',
|
||||
title: 'Email Us',
|
||||
details: ['hello@workroot.in', 'support@workroot.in'],
|
||||
link: 'mailto:hello@workroot.in',
|
||||
details: ['admin@workroot.in'],
|
||||
link: 'mailto:admin@workroot.in',
|
||||
linkLabel: 'Send email',
|
||||
color: 'emerald',
|
||||
},
|
||||
@@ -84,7 +84,7 @@ const trustStats = [
|
||||
|
||||
<BaseLayout
|
||||
title="Contact Us — Get a Free Quote"
|
||||
description="Start your project with WorkRoot IT Solutions. 500+ projects delivered, 98% client satisfaction, response within 24 hours. Reach us at hello@workroot.in or call +1 (555) 123-4567."
|
||||
description="Start your project with WorkRoot IT Solutions. 500+ projects delivered, 98% client satisfaction, response within 24 hours. Reach us at admin@workroot.in or call +91 9561417403."
|
||||
>
|
||||
<SEO
|
||||
slot="head"
|
||||
@@ -94,9 +94,11 @@ const trustStats = [
|
||||
{ name: 'Contact', url: '/contact' }
|
||||
]}
|
||||
/>
|
||||
<!-- Prefetch Google Maps domain used for the directions link on this page -->
|
||||
<!-- Prefetch Google Maps domains used for directions link and map embed on this page -->
|
||||
<link slot="head" rel="dns-prefetch" href="https://maps.google.com" />
|
||||
<link slot="head" rel="preconnect" href="https://maps.google.com" />
|
||||
<link slot="head" rel="dns-prefetch" href="https://www.google.com" />
|
||||
<link slot="head" rel="dns-prefetch" href="https://maps.googleapis.com" />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative py-24 bg-hero-dark overflow-hidden">
|
||||
@@ -104,9 +106,9 @@ const trustStats = [
|
||||
<div class="absolute inset-0 bg-grid-pattern bg-grid-80 opacity-30 pointer-events-none" aria-hidden="true"></div>
|
||||
|
||||
<!-- Decorative blobs -->
|
||||
<div class="absolute -top-40 -right-32 w-96 h-96 bg-primary/15 rounded-full blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="absolute -bottom-32 -left-32 w-80 h-80 bg-accent/10 rounded-full blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-primary/5 rounded-full blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="absolute -top-40 -right-32 w-96 h-96 bg-primary/15 rounded-full blur-2xl md:blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="absolute -bottom-32 -left-32 w-80 h-80 bg-accent/10 rounded-full blur-2xl md:blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-primary/5 rounded-full blur-2xl md:blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||||
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div class="max-w-3xl mx-auto text-center">
|
||||
@@ -140,17 +142,17 @@ const trustStats = [
|
||||
</section>
|
||||
|
||||
<!-- Main Contact Section -->
|
||||
<section class="py-20 lg:py-28 bg-secondary-50">
|
||||
<section class="py-20 lg:py-28 bg-secondary-50 dark:bg-secondary-900">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid lg:grid-cols-5 gap-12 xl:gap-16">
|
||||
|
||||
<!-- LEFT: Contact Form (3 cols) -->
|
||||
<div class="lg:col-span-3 reveal-on-scroll">
|
||||
<div class="bg-white rounded-2xl shadow-card border border-secondary-100 p-8 lg:p-10">
|
||||
<div class="bg-white dark:bg-secondary-800 rounded-2xl shadow-card border border-secondary-100 dark:border-secondary-700 p-8 lg:p-10">
|
||||
<!-- Form Header -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-secondary-900 mb-2">Send Us a Message</h2>
|
||||
<p class="text-secondary-500 text-sm">Fill out the form below and we'll respond within 24 hours.</p>
|
||||
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Send Us a Message</h2>
|
||||
<p class="text-secondary-500 dark:text-secondary-400 text-sm">Fill out the form below and we'll respond within 24 hours.</p>
|
||||
</div>
|
||||
|
||||
<form id="contact-form" class="space-y-5" novalidate>
|
||||
@@ -172,7 +174,7 @@ const trustStats = [
|
||||
<div class="grid sm:grid-cols-2 gap-5">
|
||||
<!-- Name Field -->
|
||||
<div class="form-group">
|
||||
<label for="name" class="block text-sm font-semibold text-secondary-700 mb-1.5">
|
||||
<label for="name" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||||
Full Name <span class="text-red-500" aria-hidden="true">*</span>
|
||||
<span class="sr-only">(required)</span>
|
||||
</label>
|
||||
@@ -191,7 +193,7 @@ const trustStats = [
|
||||
autocomplete="name"
|
||||
aria-required="true"
|
||||
aria-describedby="name-error"
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white"
|
||||
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="John Doe"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
|
||||
@@ -201,14 +203,14 @@ const trustStats = [
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="name-error" data-error="name" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><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>
|
||||
<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 your full name (at least 2 characters).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div class="form-group">
|
||||
<label for="email" class="block text-sm font-semibold text-secondary-700 mb-1.5">
|
||||
<label for="email" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||||
Email Address <span class="text-red-500" aria-hidden="true">*</span>
|
||||
<span class="sr-only">(required)</span>
|
||||
</label>
|
||||
@@ -226,7 +228,7 @@ const trustStats = [
|
||||
autocomplete="email"
|
||||
aria-required="true"
|
||||
aria-describedby="email-error"
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white"
|
||||
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="john@example.com"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
|
||||
@@ -236,7 +238,7 @@ const trustStats = [
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="email-error" data-error="email" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><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>
|
||||
<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 email address.
|
||||
</p>
|
||||
</div>
|
||||
@@ -246,7 +248,7 @@ const trustStats = [
|
||||
<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 mb-1.5">
|
||||
<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>
|
||||
@@ -262,19 +264,19 @@ const trustStats = [
|
||||
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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white"
|
||||
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>
|
||||
<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"><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>
|
||||
<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 mb-1.5">
|
||||
<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>
|
||||
@@ -290,7 +292,7 @@ const trustStats = [
|
||||
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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 text-sm bg-secondary-50 focus:bg-white appearance-none cursor-pointer"
|
||||
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="web-development">Web Development</option>
|
||||
@@ -308,7 +310,7 @@ const trustStats = [
|
||||
</div>
|
||||
</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"><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>
|
||||
<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>
|
||||
@@ -316,13 +318,13 @@ const trustStats = [
|
||||
|
||||
<!-- Budget range (new field) -->
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-semibold text-secondary-700 mb-2">
|
||||
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
Project Budget
|
||||
<span class="text-secondary-400 font-normal">(optional)</span>
|
||||
<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">
|
||||
{['< $5K', '$5K – $15K', '$15K – $50K', '$50K+', '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 text-sm text-secondary-600 cursor-pointer hover:border-primary hover:bg-primary/5 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700">
|
||||
<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">
|
||||
<input type="radio" name="budget" value={range} class="sr-only" />
|
||||
{range}
|
||||
</label>
|
||||
@@ -333,11 +335,11 @@ const trustStats = [
|
||||
<!-- Message Field -->
|
||||
<div class="form-group">
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label for="message" class="block text-sm font-semibold text-secondary-700">
|
||||
<label for="message" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300">
|
||||
Your Message <span class="text-red-500" aria-hidden="true">*</span>
|
||||
<span class="sr-only">(required)</span>
|
||||
</label>
|
||||
<span id="char-count" class="text-xs text-secondary-400">0 / 5000</span>
|
||||
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 5000</span>
|
||||
</div>
|
||||
<textarea
|
||||
id="message"
|
||||
@@ -348,12 +350,12 @@ const trustStats = [
|
||||
rows="5"
|
||||
aria-required="true"
|
||||
aria-describedby="message-error message-hint"
|
||||
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white resize-none"
|
||||
class="form-input w-full px-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 resize-none"
|
||||
placeholder="Tell us about your project, goals, and timeline..."
|
||||
></textarea>
|
||||
<p id="message-hint" class="mt-1 text-xs text-secondary-400">Minimum 10 characters. Be as detailed as possible for a faster, more accurate response.</p>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="message-error" data-error="message" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><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>
|
||||
<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 message (at least 10 characters).
|
||||
</p>
|
||||
</div>
|
||||
@@ -387,13 +389,13 @@ const trustStats = [
|
||||
|
||||
<!-- Success state (hidden by default) -->
|
||||
<div id="success-state" class="hidden text-center py-12">
|
||||
<div class="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<div class="w-20 h-20 bg-emerald-100 dark:bg-emerald-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg class="w-10 h-10 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-secondary-900 mb-2">Message Sent!</h3>
|
||||
<p class="text-secondary-600 mb-6">Thank you for reaching out. We'll get back to you within 24 hours.</p>
|
||||
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Message Sent!</h3>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Thank you for reaching out. We'll get back to you within 24 hours.</p>
|
||||
<button
|
||||
id="send-another"
|
||||
class="inline-flex items-center gap-2 text-primary hover:text-primary-700 font-medium text-sm transition-colors"
|
||||
@@ -413,7 +415,7 @@ const trustStats = [
|
||||
<!-- Contact Info Cards -->
|
||||
<div class="grid grid-cols-1 xs:grid-cols-2 gap-3 sm:gap-4 reveal-on-scroll reveal-delay-1">
|
||||
{contactInfo.map((info) => (
|
||||
<div class={`contact-card group relative bg-white rounded-2xl p-5 shadow-card border border-secondary-100 hover:shadow-card-hover hover:-translate-y-1 transition-all duration-300 overflow-hidden`}>
|
||||
<div class={`contact-card group relative bg-white dark:bg-secondary-800 rounded-2xl p-5 shadow-card border border-secondary-100 dark:border-secondary-700 hover:shadow-card-hover hover:-translate-y-1 transition-all duration-300 overflow-hidden`}>
|
||||
<!-- Colored top accent bar -->
|
||||
<div class={`absolute top-0 left-0 right-0 h-1 rounded-t-2xl ${
|
||||
info.color === 'primary' ? 'bg-gradient-to-r from-primary to-primary-600' :
|
||||
@@ -452,9 +454,9 @@ const trustStats = [
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 class="text-sm font-bold text-secondary-900 mb-1.5">{info.title}</h3>
|
||||
<h3 class="text-sm font-bold text-secondary-900 dark:text-secondary-100 mb-1.5">{info.title}</h3>
|
||||
{info.details.map((detail) => (
|
||||
<p class="text-xs text-secondary-500 leading-relaxed">{detail}</p>
|
||||
<p class="text-xs text-secondary-500 dark:text-secondary-400 leading-relaxed">{detail}</p>
|
||||
))}
|
||||
{info.link && (
|
||||
<a
|
||||
@@ -479,31 +481,24 @@ const trustStats = [
|
||||
|
||||
<!-- Map Embed -->
|
||||
<div class="reveal-on-scroll reveal-delay-2">
|
||||
<div class="bg-white rounded-2xl shadow-card border border-secondary-100 overflow-hidden">
|
||||
<div class="relative h-52 bg-secondary-100 group">
|
||||
<!-- Static map placeholder with illustrated style -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-secondary-100 via-secondary-50 to-primary-50 flex flex-col items-center justify-center">
|
||||
<!-- Map pin SVG illustration -->
|
||||
<div class="relative mb-3">
|
||||
<div class="w-14 h-14 bg-primary rounded-full flex items-center justify-center shadow-primary-glow animate-float">
|
||||
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Ping rings -->
|
||||
<div class="absolute inset-0 rounded-full border-2 border-primary/30 animate-ping"></div>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-secondary-700">123 Tech Plaza</p>
|
||||
<p class="text-xs text-secondary-500">San Francisco, CA 94105</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-secondary-800 rounded-2xl shadow-card border border-secondary-100 dark:border-secondary-700 overflow-hidden">
|
||||
<div class="relative h-52 bg-secondary-100 dark:bg-secondary-700 group">
|
||||
<!-- Google Maps Embed -->
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d30395.119262236858!2d74.3309312!3d17.7733632!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3bc3b7a5679a7255%3A0xc3ff2da498895bb6!2sWorkRoot%20IT%20Solutions%20LLP!5e0!3m2!1sen!2sin!4v1774099639853!5m2!1sen!2sin"
|
||||
class="absolute inset-0 w-full h-full border-0"
|
||||
allowfullscreen
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
title="WorkRoot IT Solutions LLP location on Google Maps"
|
||||
></iframe>
|
||||
|
||||
<!-- Hover overlay: Get Directions -->
|
||||
<a
|
||||
href="https://maps.google.com/?q=123+Tech+Plaza+San+Francisco+CA"
|
||||
href="https://maps.google.com/?q=Rajapur+Shantinagar+Khatav+Satara+Maharashtra+415503"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Get directions to 123 Tech Plaza, San Francisco (opens in new tab)"
|
||||
aria-label="Get directions to Rajapur Shantinagar, Khatav, Satara, Maharashtra (opens in new tab)"
|
||||
class="absolute inset-0 bg-primary/90 flex flex-col items-center justify-center gap-2 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-300"
|
||||
>
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -515,14 +510,14 @@ const trustStats = [
|
||||
</div>
|
||||
|
||||
<!-- Map footer info -->
|
||||
<div class="px-5 py-4 flex items-center justify-between border-t border-secondary-100">
|
||||
<div class="px-5 py-4 flex items-center justify-between border-t border-secondary-100 dark:border-secondary-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
|
||||
<span class="text-xs text-secondary-600 font-medium">Open now</span>
|
||||
<span class="text-xs text-secondary-400">· Closes at 6 PM</span>
|
||||
<span class="text-xs text-secondary-600 dark:text-secondary-300 font-medium">Open now</span>
|
||||
<span class="text-xs text-secondary-400 dark:text-secondary-500">· Closes at 6 PM</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://maps.google.com/?q=123+Tech+Plaza+San+Francisco+CA"
|
||||
href="https://maps.google.com/?q=Rajapur+Shantinagar+Khatav+Satara+Maharashtra+415503"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-primary-600 hover:text-primary-700 font-medium flex items-center gap-1 transition-colors"
|
||||
@@ -587,11 +582,11 @@ const trustStats = [
|
||||
</section>
|
||||
|
||||
<!-- FAQ Strip -->
|
||||
<section class="py-16 bg-white border-t border-secondary-100">
|
||||
<section class="py-16 bg-white dark:bg-secondary-900 border-t border-secondary-100 dark:border-secondary-800">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-2xl mx-auto text-center mb-10 reveal-on-scroll">
|
||||
<h2 class="text-2xl font-bold text-secondary-900 mb-3">Common Questions</h2>
|
||||
<p class="text-secondary-500 text-sm">Quick answers to help you before reaching out.</p>
|
||||
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-3">Common Questions</h2>
|
||||
<p class="text-secondary-500 dark:text-secondary-400 text-sm">Quick answers to help you before reaching out.</p>
|
||||
</div>
|
||||
<div class="max-w-3xl mx-auto space-y-3">
|
||||
{[
|
||||
@@ -600,8 +595,8 @@ const trustStats = [
|
||||
a: 'We respond to all inquiries within 24 hours on business days. For urgent matters, please call us directly.',
|
||||
},
|
||||
{
|
||||
q: 'Do you work with clients outside San Francisco?',
|
||||
a: 'Absolutely. We work with clients globally and are fully equipped for remote collaboration across time zones.',
|
||||
q: 'Do you work with clients outside Maharashtra?',
|
||||
a: 'Absolutely. We work with clients across India and globally, and are fully equipped for remote collaboration across time zones.',
|
||||
},
|
||||
{
|
||||
q: 'What information should I include in my message?',
|
||||
@@ -613,18 +608,18 @@ const trustStats = [
|
||||
},
|
||||
].map((faq, i) => (
|
||||
<details
|
||||
class={`faq-item group bg-secondary-50 rounded-xl border border-secondary-100 overflow-hidden reveal-on-scroll reveal-delay-${Math.min(i + 1, 4)}`}
|
||||
class={`faq-item group bg-secondary-50 dark:bg-secondary-800 rounded-xl border border-secondary-100 dark:border-secondary-700 overflow-hidden reveal-on-scroll reveal-delay-${Math.min(i + 1, 4)}`}
|
||||
>
|
||||
<summary class="flex items-center justify-between gap-4 px-6 py-4 cursor-pointer list-none hover:bg-secondary-100 transition-colors duration-150">
|
||||
<span class="font-semibold text-secondary-900 text-sm">{faq.q}</span>
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-secondary-200 group-open:bg-primary/10 flex items-center justify-center transition-colors" aria-hidden="true">
|
||||
<summary class="flex items-center justify-between gap-4 px-6 py-4 cursor-pointer list-none hover:bg-secondary-100 dark:hover:bg-secondary-700 transition-colors duration-150">
|
||||
<span class="font-semibold text-secondary-900 dark:text-secondary-100 text-sm">{faq.q}</span>
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-secondary-200 dark:bg-secondary-700 group-open:bg-primary/10 flex items-center justify-center transition-colors" aria-hidden="true">
|
||||
<svg class="w-3.5 h-3.5 text-secondary-600 group-open:text-primary group-open:rotate-45 transition-all duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="px-6 pb-4">
|
||||
<p class="text-secondary-600 text-sm leading-relaxed">{faq.a}</p>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-sm leading-relaxed">{faq.a}</p>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
@@ -655,7 +650,7 @@ const trustStats = [
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="tel:+15551234567"
|
||||
href="tel:+919561417403"
|
||||
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-white/10 text-white font-semibold rounded-xl hover:bg-white/20 transition-colors duration-200 border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-secondary-900"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
|
||||
+47
-47
@@ -240,19 +240,19 @@ const faqs = [
|
||||
</section>
|
||||
|
||||
<!-- Benefits Section - NEW -->
|
||||
<section id="benefits" class="py-24 bg-white">
|
||||
<section id="benefits" class="py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-3xl mx-auto mb-20" data-animate="fade-up">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 text-primary-700 text-sm font-bold uppercase tracking-wider mb-4">Why Choose Us</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Built for Your Success</h2>
|
||||
<p class="text-xl text-secondary-600">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-sm font-bold uppercase tracking-wider mb-4">Why Choose Us</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">Built for Your Success</h2>
|
||||
<p class="text-xl text-secondary-600 dark:text-secondary-400">
|
||||
We don't just write code. We build solutions that drive measurable business results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{benefits.map((benefit, index) => (
|
||||
<div class="group relative bg-gradient-to-br from-secondary-50 to-white rounded-2xl p-8 border border-secondary-100 hover:border-primary/30 hover:shadow-2xl transition-all duration-500 hover:-translate-y-2 card-interactive" data-animate="fade-up" data-delay={String(index * 100)}>
|
||||
<div class="group relative bg-gradient-to-br from-secondary-50 to-white dark:from-secondary-800 dark:to-secondary-800/80 rounded-2xl p-8 border border-secondary-100 dark:border-secondary-700 hover:border-primary/30 hover:shadow-2xl transition-all duration-500 hover:-translate-y-2 card-interactive" data-animate="fade-up" data-delay={String(index * 100)}>
|
||||
<!-- Icon -->
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
{benefit.icon === 'rocket' && (
|
||||
@@ -277,8 +277,8 @@ const faqs = [
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-bold text-secondary-900 mb-3">{benefit.title}</h3>
|
||||
<p class="text-secondary-600 leading-relaxed">{benefit.description}</p>
|
||||
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-3">{benefit.title}</h3>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 leading-relaxed">{benefit.description}</p>
|
||||
|
||||
<!-- Decorative corner -->
|
||||
<div class="absolute top-4 right-4 w-2 h-2 bg-primary-400 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
@@ -289,21 +289,21 @@ const faqs = [
|
||||
</section>
|
||||
|
||||
<!-- Services Section - Enhanced -->
|
||||
<section id="services" class="py-24 bg-gradient-to-b from-secondary-50 to-white">
|
||||
<section id="services" class="py-24 bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-3xl mx-auto mb-20" data-animate="fade-up">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 text-primary-700 text-sm font-bold uppercase tracking-wider mb-4">Our Services</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Comprehensive Technology Solutions</h2>
|
||||
<p class="text-xl text-secondary-600">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-sm font-bold uppercase tracking-wider mb-4">Our Services</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">Comprehensive Technology Solutions</h2>
|
||||
<p class="text-xl text-secondary-600 dark:text-secondary-400">
|
||||
From concept to deployment, we deliver end-to-end solutions tailored to your unique business needs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-6xl mx-auto">
|
||||
{services.map((service, index) => (
|
||||
<div class="group relative bg-white rounded-3xl p-10 shadow-lg border-2 border-secondary-100 hover:border-primary-400 hover:shadow-2xl transition-all duration-500 overflow-hidden card-interactive" data-animate="fade-up" data-delay={String(index % 2 === 0 ? 0 : 150)}>
|
||||
<div class="group relative bg-white dark:bg-secondary-800 rounded-3xl p-10 shadow-lg border-2 border-secondary-100 dark:border-secondary-700 hover:border-primary-400 hover:shadow-2xl transition-all duration-500 overflow-hidden card-interactive" data-animate="fade-up" data-delay={String(index % 2 === 0 ? 0 : 150)}>
|
||||
<!-- Background gradient on hover -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-50/0 to-primary-50/0 group-hover:from-primary-50/50 group-hover:to-transparent transition-all duration-500"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary-50/0 to-primary-50/0 group-hover:from-primary-50/50 dark:group-hover:from-primary-900/20 group-hover:to-transparent transition-all duration-500"></div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<!-- Icon -->
|
||||
@@ -330,13 +330,13 @@ const faqs = [
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl font-bold text-secondary-900 mb-4">{service.title}</h3>
|
||||
<p class="text-secondary-600 text-lg mb-6 leading-relaxed">{service.description}</p>
|
||||
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-4">{service.title}</h3>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-lg mb-6 leading-relaxed">{service.description}</p>
|
||||
|
||||
<!-- Features list -->
|
||||
<ul class="space-y-3">
|
||||
{service.features?.map((feature) => (
|
||||
<li class="flex items-center text-secondary-700">
|
||||
<li class="flex items-center text-secondary-700 dark:text-secondary-300">
|
||||
<svg class="w-5 h-5 text-primary-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
@@ -346,7 +346,7 @@ const faqs = [
|
||||
</ul>
|
||||
|
||||
<!-- Learn more arrow -->
|
||||
<div class="mt-6 flex items-center text-primary-600 font-semibold group-hover:text-primary-700 transition-colors">
|
||||
<div class="mt-6 flex items-center text-primary-600 dark:text-primary-400 font-semibold group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors">
|
||||
<span>Learn More</span>
|
||||
<svg class="w-5 h-5 ml-2 group-hover:translate-x-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
@@ -355,7 +355,7 @@ const faqs = [
|
||||
</div>
|
||||
|
||||
<!-- Number badge -->
|
||||
<div class="absolute top-6 right-6 w-12 h-12 rounded-full bg-primary-100 flex items-center justify-center text-primary-700 font-bold text-lg opacity-50 group-hover:opacity-100 transition-opacity">
|
||||
<div class="absolute top-6 right-6 w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center text-primary-700 dark:text-primary-300 font-bold text-lg opacity-50 group-hover:opacity-100 transition-opacity">
|
||||
{(index + 1).toString().padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,19 +425,19 @@ const faqs = [
|
||||
</section>
|
||||
|
||||
<!-- Stats/Metrics Section - Enhanced -->
|
||||
<section class="py-24 bg-white">
|
||||
<section class="py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-3xl mx-auto mb-16" data-animate="fade-up">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-accent-50 text-accent-700 text-sm font-bold uppercase tracking-wider mb-4">Track Record</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Numbers That Matter</h2>
|
||||
<p class="text-xl text-secondary-600">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-accent-50 dark:bg-accent-900/20 text-accent-700 dark:text-accent-300 text-sm font-bold uppercase tracking-wider mb-4">Track Record</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">Numbers That Matter</h2>
|
||||
<p class="text-xl text-secondary-600 dark:text-secondary-400">
|
||||
Our success is measured by the impact we create for our clients.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{stats.map((stat, index) => (
|
||||
<div class="group text-center bg-gradient-to-br from-secondary-50 to-white rounded-3xl p-10 border-2 border-secondary-100 hover:border-primary-400 hover:shadow-2xl transition-all duration-500 hover:-translate-y-2" data-animate="scale-up" data-delay={String(index * 100)}>
|
||||
<div class="group text-center bg-gradient-to-br from-secondary-50 to-white dark:from-secondary-800 dark:to-secondary-800/80 rounded-3xl p-10 border-2 border-secondary-100 dark:border-secondary-700 hover:border-primary-400 hover:shadow-2xl transition-all duration-500 hover:-translate-y-2" data-animate="scale-up" data-delay={String(index * 100)}>
|
||||
{/* Icon */}
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-12 transition-all duration-500 shadow-lg">
|
||||
{stat.icon === 'briefcase' && (
|
||||
@@ -462,10 +462,10 @@ const faqs = [
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="stat-counter text-5xl font-bold text-secondary-900 mb-2" data-target={stat.value} data-suffix={stat.suffix}>
|
||||
<div class="stat-counter text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-2" data-target={stat.value} data-suffix={stat.suffix}>
|
||||
0{stat.suffix}
|
||||
</div>
|
||||
<div class="text-secondary-600 font-semibold">{stat.label}</div>
|
||||
<div class="text-secondary-600 dark:text-secondary-400 font-semibold">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -473,12 +473,12 @@ const faqs = [
|
||||
</section>
|
||||
|
||||
<!-- Testimonials Section - Enhanced -->
|
||||
<section class="py-24 bg-gradient-to-b from-secondary-50 to-white">
|
||||
<section class="py-24 bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-3xl mx-auto mb-20" data-animate="fade-up">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-accent-50 text-accent-700 text-sm font-bold uppercase tracking-wider mb-4">Client Stories</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Loved by Teams Worldwide</h2>
|
||||
<p class="text-xl text-secondary-600">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-accent-50 dark:bg-accent-900/20 text-accent-700 dark:text-accent-300 text-sm font-bold uppercase tracking-wider mb-4">Client Stories</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">Loved by Teams Worldwide</h2>
|
||||
<p class="text-xl text-secondary-600 dark:text-secondary-400">
|
||||
Don't just take our word for it. Here's what our clients have to say about working with us.
|
||||
</p>
|
||||
</div>
|
||||
@@ -489,7 +489,7 @@ const faqs = [
|
||||
<div class="testimonial-track flex transition-transform duration-500 ease-in-out" aria-live="polite" aria-atomic="false">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div class="testimonial-slide w-full flex-shrink-0 px-4" data-index={index} role="group" aria-roledescription="slide" aria-label={`Testimonial ${index + 1} of ${testimonials.length}`}>
|
||||
<div class="bg-gradient-to-br from-white to-secondary-50 rounded-3xl p-10 sm:p-14 border-2 border-secondary-100 shadow-xl">
|
||||
<div class="bg-gradient-to-br from-white to-secondary-50 dark:from-secondary-800 dark:to-secondary-800 rounded-3xl p-10 sm:p-14 border-2 border-secondary-100 dark:border-secondary-700 shadow-xl">
|
||||
<!-- Star rating -->
|
||||
<div class="flex gap-1 mb-6" aria-label={`${testimonial.rating || 5} out of 5 stars`}>
|
||||
{[...Array(testimonial.rating || 5)].map(() => (
|
||||
@@ -504,7 +504,7 @@ const faqs = [
|
||||
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"></path>
|
||||
</svg>
|
||||
|
||||
<blockquote class="text-2xl sm:text-3xl text-secondary-800 font-medium leading-relaxed mb-10">
|
||||
<blockquote class="text-2xl sm:text-3xl text-secondary-800 dark:text-secondary-200 font-medium leading-relaxed mb-10">
|
||||
"{testimonial.quote}"
|
||||
</blockquote>
|
||||
|
||||
@@ -513,8 +513,8 @@ const faqs = [
|
||||
{testimonial.avatar}
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<div class="font-bold text-secondary-900 text-lg">{testimonial.author}</div>
|
||||
<div class="text-secondary-600">{testimonial.role}</div>
|
||||
<div class="font-bold text-secondary-900 dark:text-secondary-100 text-lg">{testimonial.author}</div>
|
||||
<div class="text-secondary-600 dark:text-secondary-400">{testimonial.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,7 +526,7 @@ const faqs = [
|
||||
<!-- Carousel Controls -->
|
||||
<div class="flex items-center justify-center mt-10 gap-6">
|
||||
<button
|
||||
class="carousel-prev w-14 h-14 rounded-full bg-white border-2 border-secondary-200 flex items-center justify-center text-secondary-600 hover:border-primary hover:text-primary hover:bg-primary-50 transition-all shadow-lg hover:shadow-xl"
|
||||
class="carousel-prev w-14 h-14 rounded-full bg-white dark:bg-secondary-800 border-2 border-secondary-200 dark:border-secondary-700 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:border-primary hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/30 transition-all shadow-lg hover:shadow-xl"
|
||||
aria-label="Previous testimonial"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -547,7 +547,7 @@ const faqs = [
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="carousel-next w-14 h-14 rounded-full bg-white border-2 border-secondary-200 flex items-center justify-center text-secondary-600 hover:border-primary hover:text-primary hover:bg-primary-50 transition-all shadow-lg hover:shadow-xl"
|
||||
class="carousel-next w-14 h-14 rounded-full bg-white dark:bg-secondary-800 border-2 border-secondary-200 dark:border-secondary-700 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:border-primary hover:text-primary hover:bg-primary-50 dark:hover:bg-primary-900/30 transition-all shadow-lg hover:shadow-xl"
|
||||
aria-label="Next testimonial"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -560,17 +560,17 @@ const faqs = [
|
||||
</section>
|
||||
|
||||
<!-- FAQ Section - NEW -->
|
||||
<section class="py-24 bg-white">
|
||||
<section class="py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start max-w-7xl mx-auto">
|
||||
<!-- Left column - Header -->
|
||||
<div class="lg:sticky lg:top-8" data-animate="fade-right">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 text-primary-700 text-sm font-bold uppercase tracking-wider mb-4">FAQ</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Common Questions</h2>
|
||||
<p class="text-xl text-secondary-600 mb-8">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 text-sm font-bold uppercase tracking-wider mb-4">FAQ</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">Common Questions</h2>
|
||||
<p class="text-xl text-secondary-600 dark:text-secondary-400 mb-8">
|
||||
Everything you need to know about working with us. Can't find the answer you're looking for?
|
||||
</p>
|
||||
<a href="/contact" class="inline-flex items-center text-primary-600 font-semibold hover:text-primary-700 transition-colors">
|
||||
<a href="/contact" class="inline-flex items-center text-primary-600 dark:text-primary-400 font-semibold hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
@@ -581,7 +581,7 @@ const faqs = [
|
||||
<!-- Right column - Accordion -->
|
||||
<div class="space-y-4" data-animate="fade-left" data-delay="150">
|
||||
{faqs.map((faq, index) => (
|
||||
<div class="group bg-secondary-50 rounded-2xl border-2 border-secondary-100 hover:border-primary-300 transition-all duration-300 overflow-hidden">
|
||||
<div class="group bg-secondary-50 dark:bg-secondary-800 rounded-2xl border-2 border-secondary-100 dark:border-secondary-700 hover:border-primary-300 transition-all duration-300 overflow-hidden">
|
||||
<button
|
||||
class="faq-question w-full text-left px-8 py-6 flex items-start justify-between gap-4"
|
||||
data-faq-index={index}
|
||||
@@ -589,13 +589,13 @@ const faqs = [
|
||||
aria-controls={`faq-answer-${index}`}
|
||||
id={`faq-question-${index}`}
|
||||
>
|
||||
<span class="font-bold text-lg text-secondary-900 pr-4">{faq.question}</span>
|
||||
<span class="font-bold text-lg text-secondary-900 dark:text-secondary-100 pr-4">{faq.question}</span>
|
||||
<svg class="faq-icon w-6 h-6 text-primary-500 flex-shrink-0 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300" id={`faq-answer-${index}`} role="region" aria-labelledby={`faq-question-${index}`}>
|
||||
<div class="px-8 pb-6 text-secondary-600 leading-relaxed">
|
||||
<div class="px-8 pb-6 text-secondary-600 dark:text-secondary-400 leading-relaxed">
|
||||
{faq.answer}
|
||||
</div>
|
||||
</div>
|
||||
@@ -649,17 +649,17 @@ const faqs = [
|
||||
<div class="mt-16 pt-12 border-t border-white/10">
|
||||
<p class="text-secondary-300 mb-6">Prefer to reach out directly?</p>
|
||||
<div class="flex flex-wrap justify-center gap-8 text-white">
|
||||
<a href="mailto:info@workroot.in" class="flex items-center gap-2 hover:text-primary-300 transition-colors">
|
||||
<a href="mailto:admin@workroot.in" class="flex items-center gap-2 hover:text-primary-300 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="font-semibold">info@workroot.in</span>
|
||||
<span class="font-semibold">admin@workroot.in</span>
|
||||
</a>
|
||||
<a href="tel:+918860077551" class="flex items-center gap-2 hover:text-primary-300 transition-colors">
|
||||
<a href="tel:+919561417403" class="flex items-center gap-2 hover:text-primary-300 transition-colors">
|
||||
<svg class="w-5 h-5" 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"></path>
|
||||
</svg>
|
||||
<span class="font-semibold">+91 88600 77551</span>
|
||||
<span class="font-semibold">+91 9561417403</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+40
-42
@@ -1,7 +1,6 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import SEO from '../components/SEO.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
const projects = [
|
||||
{
|
||||
@@ -279,11 +278,11 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</section>
|
||||
|
||||
<!-- Featured Projects -->
|
||||
<section class="bg-white py-16 lg:py-20">
|
||||
<section class="bg-white dark:bg-secondary-900 py-16 lg:py-20">
|
||||
<div class="container-wrapper">
|
||||
<div class="mb-10 reveal-on-scroll">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Highlighted Work</span>
|
||||
<h2 class="mt-2 text-3xl font-bold text-secondary-900 sm:text-4xl">Featured Projects</h2>
|
||||
<h2 class="mt-2 text-3xl font-bold text-secondary-900 dark:text-secondary-100 sm:text-4xl">Featured Projects</h2>
|
||||
</div>
|
||||
<div class="grid gap-8 lg:grid-cols-3 reveal-on-scroll reveal-delay-1">
|
||||
{featuredProjects.map((project, idx) => (
|
||||
@@ -352,7 +351,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</section>
|
||||
|
||||
<!-- Filter & Grid Section -->
|
||||
<section class="bg-secondary-50 py-16 lg:py-24">
|
||||
<section class="bg-secondary-50 dark:bg-secondary-900 py-16 lg:py-24">
|
||||
<div class="container-wrapper">
|
||||
<!-- Search + Filter Controls -->
|
||||
<div class="mb-10 space-y-6 reveal-on-scroll">
|
||||
@@ -368,7 +367,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
id="project-search"
|
||||
type="search"
|
||||
placeholder="Search by project name or technology..."
|
||||
class="w-full rounded-xl border border-secondary-200 bg-white py-3 pl-12 pr-4 text-secondary-800 shadow-sm placeholder-secondary-400 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-400/20 transition-colors"
|
||||
class="w-full rounded-xl border border-secondary-200 dark:border-secondary-600 bg-white dark:bg-secondary-800 py-3 pl-12 pr-4 text-secondary-800 dark:text-secondary-100 shadow-sm placeholder-secondary-400 dark:placeholder-secondary-500 focus:border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-400/20 transition-colors"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div id="search-clear" class="absolute inset-y-0 right-0 hidden items-center pr-4">
|
||||
@@ -393,7 +392,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</button>
|
||||
<button
|
||||
data-filter="web"
|
||||
class="filter-btn rounded-full bg-white px-6 py-2.5 font-semibold text-secondary-600 shadow-sm transition-all duration-300 hover:bg-secondary-100 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
class="filter-btn rounded-full bg-white dark:bg-secondary-800 px-6 py-2.5 font-semibold text-secondary-600 dark:text-secondary-400 shadow-sm transition-all duration-300 hover:bg-secondary-100 dark:hover:bg-secondary-700 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="projects-grid"
|
||||
@@ -402,7 +401,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</button>
|
||||
<button
|
||||
data-filter="mobile"
|
||||
class="filter-btn rounded-full bg-white px-6 py-2.5 font-semibold text-secondary-600 shadow-sm transition-all duration-300 hover:bg-secondary-100 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
class="filter-btn rounded-full bg-white dark:bg-secondary-800 px-6 py-2.5 font-semibold text-secondary-600 dark:text-secondary-400 shadow-sm transition-all duration-300 hover:bg-secondary-100 dark:hover:bg-secondary-700 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="projects-grid"
|
||||
@@ -411,7 +410,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</button>
|
||||
<button
|
||||
data-filter="ai"
|
||||
class="filter-btn rounded-full bg-white px-6 py-2.5 font-semibold text-secondary-600 shadow-sm transition-all duration-300 hover:bg-secondary-100 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
class="filter-btn rounded-full bg-white dark:bg-secondary-800 px-6 py-2.5 font-semibold text-secondary-600 dark:text-secondary-400 shadow-sm transition-all duration-300 hover:bg-secondary-100 dark:hover:bg-secondary-700 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="projects-grid"
|
||||
@@ -424,7 +423,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
<div class="text-center">
|
||||
<button
|
||||
id="tech-filter-toggle"
|
||||
class="inline-flex items-center gap-2 text-sm font-medium text-secondary-500 hover:text-secondary-700 transition-colors focus:outline-none"
|
||||
class="inline-flex items-center gap-2 text-sm font-medium text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-200 transition-colors focus:outline-none"
|
||||
aria-expanded="false"
|
||||
aria-controls="tech-filter-panel"
|
||||
>
|
||||
@@ -451,7 +450,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
{allTechs.map((tech) => (
|
||||
<button
|
||||
data-tech={tech}
|
||||
class="tech-btn rounded-full bg-white px-4 py-1.5 text-xs font-semibold text-secondary-600 shadow-sm transition-all duration-200 hover:bg-secondary-100 focus:outline-none focus:ring-2 focus:ring-secondary-400"
|
||||
class="tech-btn rounded-full bg-white dark:bg-secondary-800 px-4 py-1.5 text-xs font-semibold text-secondary-600 dark:text-secondary-400 shadow-sm transition-all duration-200 hover:bg-secondary-100 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-secondary-400"
|
||||
>
|
||||
{tech}
|
||||
</button>
|
||||
@@ -460,13 +459,13 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</div>
|
||||
|
||||
<!-- Active filter indicator + count -->
|
||||
<div class="flex items-center justify-between text-sm text-secondary-500">
|
||||
<div class="flex items-center justify-between text-sm text-secondary-500 dark:text-secondary-400">
|
||||
<span id="results-count" aria-live="polite" aria-atomic="true">
|
||||
Showing <strong id="count-num" class="text-secondary-800">{projects.length}</strong> projects
|
||||
Showing <strong id="count-num" class="text-secondary-800 dark:text-secondary-100">{projects.length}</strong> projects
|
||||
</span>
|
||||
<button
|
||||
id="clear-filters"
|
||||
class="hidden text-primary-600 hover:text-primary-700 font-medium transition-colors focus:outline-none"
|
||||
class="hidden text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium transition-colors focus:outline-none"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
@@ -482,7 +481,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
>
|
||||
{projects.map((project) => (
|
||||
<article
|
||||
class="project-card group relative overflow-hidden rounded-2xl bg-white shadow-md transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl cursor-pointer reveal-on-scroll"
|
||||
class="project-card group relative overflow-hidden rounded-2xl bg-white dark:bg-secondary-800 shadow-md transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl cursor-pointer reveal-on-scroll"
|
||||
data-category={project.category}
|
||||
data-project-id={project.id}
|
||||
data-tech={project.techStack.join(',')}
|
||||
@@ -542,33 +541,33 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-secondary-900 transition-colors group-hover:text-primary">
|
||||
<h3 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 transition-colors group-hover:text-primary dark:group-hover:text-primary-400">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p class="mt-2 line-clamp-2 text-sm text-secondary-600">
|
||||
<p class="mt-2 line-clamp-2 text-sm text-secondary-600 dark:text-secondary-400">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<!-- Key metric pill -->
|
||||
<div class="mt-3 inline-flex items-center gap-1.5 rounded-full bg-secondary-100 px-3 py-1">
|
||||
<div class="mt-3 inline-flex items-center gap-1.5 rounded-full bg-secondary-100 dark:bg-secondary-700 px-3 py-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<span class="text-xs font-semibold text-secondary-700">{project.stats[0].value} {project.stats[0].label}</span>
|
||||
<span class="text-xs font-semibold text-secondary-700 dark:text-secondary-300">{project.stats[0].value} {project.stats[0].label}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tech Stack Tags -->
|
||||
<div class="mt-4 flex flex-wrap gap-1.5">
|
||||
{project.techStack.map((tech) => (
|
||||
<span class="rounded-md bg-secondary-100 px-2.5 py-1 text-xs font-medium text-secondary-700 hover:bg-primary-50 hover:text-primary-700 transition-colors cursor-pointer tech-tag" data-tech-tag={tech}>
|
||||
<span class="rounded-md bg-secondary-100 dark:bg-secondary-700 px-2.5 py-1 text-xs font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 transition-colors cursor-pointer tech-tag" data-tech-tag={tech}>
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Footer row: client + arrow -->
|
||||
<div class="mt-5 flex items-center justify-between border-t border-secondary-100 pt-4">
|
||||
<span class="text-xs text-secondary-500">{project.client}</span>
|
||||
<div class="mt-5 flex items-center justify-between border-t border-secondary-100 dark:border-secondary-700 pt-4">
|
||||
<span class="text-xs text-secondary-500 dark:text-secondary-400">{project.client}</span>
|
||||
<span class="inline-flex items-center gap-1 text-xs font-semibold text-primary transition-all group-hover:gap-2">
|
||||
Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
@@ -583,13 +582,13 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
|
||||
<!-- No Results Message -->
|
||||
<div id="no-results" class="hidden py-20 text-center">
|
||||
<div class="mx-auto h-20 w-20 rounded-2xl bg-secondary-100 flex items-center justify-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="mx-auto h-20 w-20 rounded-2xl bg-secondary-100 dark:bg-secondary-800 flex items-center justify-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-secondary-300 dark:text-secondary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-secondary-700">No projects match your filters</p>
|
||||
<p class="mt-2 text-secondary-500">Try adjusting your search or filter criteria.</p>
|
||||
<p class="text-lg font-semibold text-secondary-700 dark:text-secondary-300">No projects match your filters</p>
|
||||
<p class="mt-2 text-secondary-500 dark:text-secondary-400">Try adjusting your search or filter criteria.</p>
|
||||
<button
|
||||
id="reset-filters-btn"
|
||||
class="mt-6 inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-2.5 font-semibold text-white transition-all hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
@@ -608,7 +607,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div class="modal-content relative w-full max-w-5xl scale-95 transform rounded-2xl bg-white opacity-0 shadow-2xl transition-all duration-300 my-4">
|
||||
<div class="modal-content relative w-full max-w-5xl scale-95 transform rounded-2xl bg-white dark:bg-secondary-800 opacity-0 shadow-2xl transition-all duration-300 my-4">
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
id="close-modal-btn"
|
||||
@@ -656,7 +655,6 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
|
||||
<script define:vars={{ projects }}>
|
||||
@@ -963,11 +961,11 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
<!-- Body -->
|
||||
<div class="p-6 sm:p-8 lg:p-10">
|
||||
<!-- Stats row -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-8 p-5 bg-secondary-50 rounded-2xl">
|
||||
<div class="grid grid-cols-3 gap-4 mb-8 p-5 bg-secondary-50 dark:bg-secondary-700/50 rounded-2xl">
|
||||
${project.stats.map((stat: any) => `
|
||||
<div class="text-center">
|
||||
<p class="text-2xl sm:text-3xl font-bold text-secondary-900">${stat.value}</p>
|
||||
<p class="text-xs text-secondary-500 mt-1">${stat.label}</p>
|
||||
<p class="text-2xl sm:text-3xl font-bold text-secondary-900 dark:text-secondary-100">${stat.value}</p>
|
||||
<p class="text-xs text-secondary-500 dark:text-secondary-400 mt-1">${stat.label}</p>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
@@ -975,34 +973,34 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
<!-- Two column layout: overview + challenge/solution -->
|
||||
<div class="grid gap-8 lg:grid-cols-2 mb-8">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 mb-3">Project Overview</h3>
|
||||
<p class="text-secondary-700 leading-relaxed text-sm">${project.fullDescription}</p>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 dark:text-secondary-500 mb-3">Project Overview</h3>
|
||||
<p class="text-secondary-700 dark:text-secondary-300 leading-relaxed text-sm">${project.fullDescription}</p>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 mb-2">The Challenge</h3>
|
||||
<p class="text-secondary-700 text-sm leading-relaxed">${project.challenge}</p>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 dark:text-secondary-500 mb-2">The Challenge</h3>
|
||||
<p class="text-secondary-700 dark:text-secondary-300 text-sm leading-relaxed">${project.challenge}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 mb-2">Our Solution</h3>
|
||||
<p class="text-secondary-700 text-sm leading-relaxed">${project.solution}</p>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 dark:text-secondary-500 mb-2">Our Solution</h3>
|
||||
<p class="text-secondary-700 dark:text-secondary-300 text-sm leading-relaxed">${project.solution}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom row: tech stack + results -->
|
||||
<div class="grid gap-6 sm:grid-cols-2 pt-6 border-t border-secondary-100">
|
||||
<div class="grid gap-6 sm:grid-cols-2 pt-6 border-t border-secondary-100 dark:border-secondary-700">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 mb-3">Tech Stack</h3>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 dark:text-secondary-500 mb-3">Tech Stack</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${project.techStack.map((tech: string) => `<span class="rounded-lg bg-secondary-100 px-3 py-1.5 text-xs font-semibold text-secondary-700">${tech}</span>`).join('')}
|
||||
${project.techStack.map((tech: string) => `<span class="rounded-lg bg-secondary-100 dark:bg-secondary-700 px-3 py-1.5 text-xs font-semibold text-secondary-700 dark:text-secondary-300">${tech}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 mb-3">Key Results</h3>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wide text-secondary-400 dark:text-secondary-500 mb-3">Key Results</h3>
|
||||
<ul class="space-y-2">
|
||||
${project.results.map((result: string) => `
|
||||
<li class="flex items-center gap-2 text-sm text-secondary-700">
|
||||
<li class="flex items-center gap-2 text-sm text-secondary-700 dark:text-secondary-300">
|
||||
<svg class="h-4 w-4 flex-shrink-0 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
@@ -1015,7 +1013,7 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="mt-8 flex flex-col sm:flex-row gap-3 justify-end">
|
||||
<button id="modal-close-cta" class="order-2 sm:order-1 inline-flex items-center justify-center px-5 py-2.5 rounded-lg border border-secondary-200 text-secondary-600 text-sm font-medium hover:bg-secondary-50 transition-colors focus:outline-none focus:ring-2 focus:ring-secondary-400">
|
||||
<button id="modal-close-cta" class="order-2 sm:order-1 inline-flex items-center justify-center px-5 py-2.5 rounded-lg border border-secondary-200 dark:border-secondary-600 text-secondary-600 dark:text-secondary-400 text-sm font-medium hover:bg-secondary-50 dark:hover:bg-secondary-700 transition-colors focus:outline-none focus:ring-2 focus:ring-secondary-400">
|
||||
Back to Portfolio
|
||||
</button>
|
||||
<a href="/contact" class="order-1 sm:order-2 inline-flex items-center justify-center px-6 py-2.5 rounded-lg bg-primary text-white text-sm font-semibold hover:bg-primary-600 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-400">
|
||||
|
||||
@@ -210,9 +210,9 @@ const lastUpdated = 'March 20, 2026';
|
||||
<p class="text-secondary-300 mb-4">If you have questions about this Privacy Policy or our data practices, please contact us:</p>
|
||||
<div class="bg-secondary-900/50 border border-secondary-800 rounded-xl p-6">
|
||||
<p class="text-secondary-300"><strong class="text-white">WorkRoot IT Solutions</strong></p>
|
||||
<p class="text-secondary-300">Email: privacy@workroot.in</p>
|
||||
<p class="text-secondary-300">Phone: +1 (555) 123-4567</p>
|
||||
<p class="text-secondary-300">Address: 123 Tech Avenue, Suite 500, San Francisco, CA 94102</p>
|
||||
<p class="text-secondary-300">Email: admin@workroot.in</p>
|
||||
<p class="text-secondary-300">Phone: +91 9561417403</p>
|
||||
<p class="text-secondary-300">Address: At Post Rajapur Shantinagar, Taluka Khatav, Dist Satara, MH 415503</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+41
-41
@@ -318,12 +318,12 @@ const processSteps = [
|
||||
</section>
|
||||
|
||||
<!-- Services Cards Section -->
|
||||
<section class="py-16 lg:py-24 bg-white">
|
||||
<section class="py-16 lg:py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">What We Offer</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Our Core Services</h2>
|
||||
<p class="text-secondary-600 text-lg">
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">What We Offer</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Our Core Services</h2>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-lg">
|
||||
Comprehensive technology solutions designed to accelerate your digital transformation journey.
|
||||
</p>
|
||||
</div>
|
||||
@@ -332,7 +332,7 @@ const processSteps = [
|
||||
{services.map((service, idx) => (
|
||||
<a
|
||||
href={`#${service.id}`}
|
||||
class={`service-card group relative bg-white rounded-2xl p-8 shadow-sm border border-secondary-100 hover:shadow-2xl hover:border-transparent hover:-translate-y-2 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 reveal-on-scroll reveal-delay-${idx + 1}`}
|
||||
class={`service-card group relative bg-white dark:bg-secondary-800 rounded-2xl p-8 shadow-sm border border-secondary-100 dark:border-secondary-700 hover:shadow-2xl hover:border-transparent hover:-translate-y-2 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 reveal-on-scroll reveal-delay-${idx + 1}`}
|
||||
aria-label={`Learn more about ${service.title}`}
|
||||
>
|
||||
<!-- Hover gradient overlay -->
|
||||
@@ -371,18 +371,18 @@ const processSteps = [
|
||||
<div class="text-right">
|
||||
{Object.entries(service.stats).slice(0, 1).map(([key, val]) => (
|
||||
<div>
|
||||
<p class="text-xl font-bold text-secondary-900 group-hover:text-white transition-colors duration-300">{val}</p>
|
||||
<p class="text-xs text-secondary-500 group-hover:text-white/70 transition-colors duration-300 capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</p>
|
||||
<p class="text-xl font-bold text-secondary-900 dark:text-secondary-100 group-hover:text-white transition-colors duration-300">{val}</p>
|
||||
<p class="text-xs text-secondary-500 dark:text-secondary-400 group-hover:text-white/70 transition-colors duration-300 capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<h3 class="text-xl font-bold text-secondary-900 group-hover:text-white mb-2 transition-colors duration-300">
|
||||
<h3 class="text-xl font-bold text-secondary-900 dark:text-secondary-100 group-hover:text-white mb-2 transition-colors duration-300">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p class="text-secondary-600 group-hover:text-white/80 mb-5 transition-colors duration-300 text-sm leading-relaxed">
|
||||
<p class="text-secondary-600 dark:text-secondary-400 group-hover:text-white/80 mb-5 transition-colors duration-300 text-sm leading-relaxed">
|
||||
{service.shortDescription}
|
||||
</p>
|
||||
|
||||
@@ -413,7 +413,7 @@ const processSteps = [
|
||||
{services.map((service, index) => (
|
||||
<section
|
||||
id={service.id}
|
||||
class={`py-16 lg:py-24 ${index % 2 === 0 ? 'bg-secondary-50' : 'bg-white'}`}
|
||||
class={`py-16 lg:py-24 ${index % 2 === 0 ? 'bg-secondary-50 dark:bg-secondary-800/50' : 'bg-white dark:bg-secondary-900'}`}
|
||||
>
|
||||
<div class="container-wrapper">
|
||||
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
||||
@@ -443,10 +443,10 @@ const processSteps = [
|
||||
{service.title}
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mb-4">
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mb-4">
|
||||
{service.title}
|
||||
</h2>
|
||||
<p class="text-secondary-600 text-lg leading-relaxed mb-6">
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-lg leading-relaxed mb-6">
|
||||
{service.fullDescription}
|
||||
</p>
|
||||
|
||||
@@ -455,7 +455,7 @@ const processSteps = [
|
||||
{Object.entries(service.stats).map(([key, val]) => (
|
||||
<div class={`flex flex-col px-4 py-3 ${service.bgLight} rounded-lg`}>
|
||||
<span class={`text-xl font-bold ${service.iconColor}`}>{val}</span>
|
||||
<span class="text-secondary-500 text-xs capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</span>
|
||||
<span class="text-secondary-500 dark:text-secondary-400 text-xs capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -469,14 +469,14 @@ const processSteps = [
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-secondary-700">{feature}</span>
|
||||
<span class="text-secondary-700 dark:text-secondary-300">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<!-- Tech stack -->
|
||||
<div class="mb-8">
|
||||
<p class="text-xs font-semibold text-secondary-400 uppercase tracking-wider mb-2">Technologies</p>
|
||||
<p class="text-xs font-semibold text-secondary-400 dark:text-secondary-500 uppercase tracking-wider mb-2">Technologies</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{service.techStack.map((tech) => (
|
||||
<span class={`px-3 py-1 text-sm font-medium ${service.tagBg} rounded-md`}>{tech}</span>
|
||||
@@ -592,7 +592,7 @@ const processSteps = [
|
||||
</div>
|
||||
|
||||
<!-- Floating stats badge -->
|
||||
<div class="absolute -bottom-4 -right-4 sm:-bottom-6 sm:-right-6 bg-white rounded-xl shadow-xl p-4 sm:p-5 hidden sm:block border border-secondary-100">
|
||||
<div class="absolute -bottom-4 -right-4 sm:-bottom-6 sm:-right-6 bg-white dark:bg-secondary-800 rounded-xl shadow-xl p-4 sm:p-5 hidden sm:block border border-secondary-100 dark:border-secondary-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class={`w-10 h-10 ${service.bgLight} ${service.iconColor} rounded-lg flex items-center justify-center flex-shrink-0`} aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
@@ -600,8 +600,8 @@ const processSteps = [
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold text-secondary-900">{Object.values(service.stats)[0]}</p>
|
||||
<p class="text-secondary-500 text-xs">{Object.keys(service.stats)[0].replace(/([A-Z])/g, ' $1').trim()}</p>
|
||||
<p class="text-lg font-bold text-secondary-900 dark:text-secondary-100">{Object.values(service.stats)[0]}</p>
|
||||
<p class="text-secondary-500 dark:text-secondary-400 text-xs">{Object.keys(service.stats)[0].replace(/([A-Z])/g, ' $1').trim()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -612,19 +612,19 @@ const processSteps = [
|
||||
))}
|
||||
|
||||
<!-- Case Studies Section -->
|
||||
<section class="py-16 lg:py-24 bg-secondary-50">
|
||||
<section class="py-16 lg:py-24 bg-secondary-50 dark:bg-secondary-800/50">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
|
||||
<span class="text-accent-500 font-semibold text-sm uppercase tracking-wider">Real Results</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Client Success Stories</h2>
|
||||
<p class="text-secondary-600 text-lg">
|
||||
<span class="text-accent-500 dark:text-accent-400 font-semibold text-sm uppercase tracking-wider">Real Results</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Client Success Stories</h2>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-lg">
|
||||
Don't take our word for it — see the measurable impact we've delivered for our clients.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{caseStudies.map((study, idx) => (
|
||||
<div class={`case-study-card group relative bg-white rounded-2xl p-6 lg:p-8 border ${study.borderColor} hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden reveal-on-scroll reveal-delay-${idx + 1}`}>
|
||||
<div class={`case-study-card group relative bg-white dark:bg-secondary-800 rounded-2xl p-6 lg:p-8 border ${study.borderColor} dark:border-secondary-700 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden reveal-on-scroll reveal-delay-${idx + 1}`}>
|
||||
<!-- Background gradient -->
|
||||
<div class={`absolute inset-0 bg-gradient-to-br ${study.gradient} opacity-50 group-hover:opacity-70 transition-opacity duration-300`} aria-hidden="true"></div>
|
||||
|
||||
@@ -652,15 +652,15 @@ const processSteps = [
|
||||
</div>
|
||||
|
||||
<!-- Big metric -->
|
||||
<p class="text-4xl font-bold text-secondary-900 mb-1">{study.metric}</p>
|
||||
<p class="text-sm font-medium text-secondary-500 mb-4">{study.metricLabel}</p>
|
||||
<p class="text-4xl font-bold text-secondary-900 dark:text-secondary-100 mb-1">{study.metric}</p>
|
||||
<p class="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-4">{study.metricLabel}</p>
|
||||
|
||||
<!-- Client & description -->
|
||||
<p class="font-semibold text-secondary-800 mb-2">{study.client}</p>
|
||||
<p class="text-secondary-600 text-sm leading-relaxed mb-4">{study.description}</p>
|
||||
<p class="font-semibold text-secondary-800 dark:text-secondary-200 mb-2">{study.client}</p>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-sm leading-relaxed mb-4">{study.description}</p>
|
||||
|
||||
<!-- Result pill -->
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 bg-secondary-100 rounded-full text-xs text-secondary-600 font-medium">
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 bg-secondary-100 dark:bg-secondary-700 rounded-full text-xs text-secondary-600 dark:text-secondary-300 font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
@@ -686,12 +686,12 @@ const processSteps = [
|
||||
</section>
|
||||
|
||||
<!-- Pricing Section -->
|
||||
<section class="py-16 lg:py-24 bg-white">
|
||||
<section class="py-16 lg:py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
|
||||
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Transparent Pricing</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Investment Tiers</h2>
|
||||
<p class="text-secondary-600 text-lg">
|
||||
<span class="text-primary-500 dark:text-primary-400 font-semibold text-sm uppercase tracking-wider">Transparent Pricing</span>
|
||||
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 dark:text-secondary-100 mt-2 mb-4">Investment Tiers</h2>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 text-lg">
|
||||
Straightforward pricing to fit businesses at every stage. All plans include dedicated project management.
|
||||
</p>
|
||||
</div>
|
||||
@@ -702,7 +702,7 @@ const processSteps = [
|
||||
class={`relative flex flex-col rounded-2xl p-7 lg:p-8 reveal-on-scroll reveal-delay-${idx + 1} transition-all duration-300 ${
|
||||
tier.highlighted
|
||||
? 'bg-secondary-900 shadow-2xl ring-2 ring-primary-500 md:scale-105'
|
||||
: 'bg-white border border-secondary-200 hover:shadow-xl hover:-translate-y-1'
|
||||
: 'bg-white dark:bg-secondary-800 border border-secondary-200 dark:border-secondary-700 hover:shadow-xl hover:-translate-y-1'
|
||||
}`}
|
||||
>
|
||||
{tier.highlighted && (
|
||||
@@ -714,17 +714,17 @@ const processSteps = [
|
||||
)}
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class={`text-xl font-bold mb-1 ${tier.highlighted ? 'text-white' : 'text-secondary-900'}`}>
|
||||
<h3 class={`text-xl font-bold mb-1 ${tier.highlighted ? 'text-white' : 'text-secondary-900 dark:text-secondary-100'}`}>
|
||||
{tier.name}
|
||||
</h3>
|
||||
<p class={`text-sm mb-4 ${tier.highlighted ? 'text-secondary-400' : 'text-secondary-500'}`}>
|
||||
<p class={`text-sm mb-4 ${tier.highlighted ? 'text-secondary-400' : 'text-secondary-500 dark:text-secondary-400'}`}>
|
||||
{tier.description}
|
||||
</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class={`text-4xl font-bold ${tier.highlighted ? 'text-white' : 'text-secondary-900'}`}>
|
||||
<span class={`text-4xl font-bold ${tier.highlighted ? 'text-white' : 'text-secondary-900 dark:text-secondary-100'}`}>
|
||||
{tier.price}
|
||||
</span>
|
||||
<span class={`text-sm ${tier.highlighted ? 'text-secondary-400' : 'text-secondary-500'}`}>
|
||||
<span class={`text-sm ${tier.highlighted ? 'text-secondary-400' : 'text-secondary-500 dark:text-secondary-400'}`}>
|
||||
{tier.period}
|
||||
</span>
|
||||
</div>
|
||||
@@ -744,7 +744,7 @@ const processSteps = [
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span class={`text-sm ${tier.highlighted ? 'text-secondary-300' : 'text-secondary-700'}`}>
|
||||
<span class={`text-sm ${tier.highlighted ? 'text-secondary-300' : 'text-secondary-700 dark:text-secondary-300'}`}>
|
||||
{feature}
|
||||
</span>
|
||||
</li>
|
||||
@@ -756,7 +756,7 @@ const processSteps = [
|
||||
class={`w-full inline-flex items-center justify-center px-6 py-3.5 rounded-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
||||
tier.highlighted
|
||||
? 'bg-primary-500 text-white hover:bg-primary-400 focus:ring-primary-400 focus:ring-offset-secondary-900'
|
||||
: 'bg-secondary-900 text-white hover:bg-secondary-800 focus:ring-secondary-400'
|
||||
: 'bg-secondary-900 dark:bg-secondary-700 text-white hover:bg-secondary-800 dark:hover:bg-secondary-600 focus:ring-secondary-400'
|
||||
}`}
|
||||
>
|
||||
{tier.cta === 'Most Popular' ? 'Get Started' : tier.cta}
|
||||
@@ -768,9 +768,9 @@ const processSteps = [
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p class="text-center text-secondary-500 text-sm mt-8">
|
||||
<p class="text-center text-secondary-500 dark:text-secondary-400 text-sm mt-8">
|
||||
All prices are estimates. Final scope and pricing determined after discovery call.
|
||||
<a href="/contact" class="text-primary-600 hover:text-primary-700 font-medium ml-1">Schedule a free consultation →</a>
|
||||
<a href="/contact" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium ml-1">Schedule a free consultation →</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -296,9 +296,9 @@ const lastUpdated = 'March 20, 2026';
|
||||
<p class="text-secondary-300 mb-4">For questions about these Terms, please contact us:</p>
|
||||
<div class="bg-secondary-900/50 border border-secondary-800 rounded-xl p-6">
|
||||
<p class="text-secondary-300"><strong class="text-white">WorkRoot IT Solutions</strong></p>
|
||||
<p class="text-secondary-300">Email: legal@workroot.in</p>
|
||||
<p class="text-secondary-300">Phone: +1 (555) 123-4567</p>
|
||||
<p class="text-secondary-300">Address: 123 Tech Avenue, Suite 500, San Francisco, CA 94102</p>
|
||||
<p class="text-secondary-300">Email: admin@workroot.in</p>
|
||||
<p class="text-secondary-300">Phone: +91 9561417403</p>
|
||||
<p class="text-secondary-300">Address: At Post Rajapur Shantinagar, Taluka Khatav, Dist Satara, MH 415503</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
/* ============================================================
|
||||
DESIGN TOKENS — Full Color System
|
||||
Single source of truth for all color, semantic, and state tokens.
|
||||
|
||||
Usage:
|
||||
- Light mode: :root { ... }
|
||||
- Dark mode: html.dark { ... }
|
||||
- Reference: var(--color-<token>)
|
||||
|
||||
WCAG Contrast Requirements:
|
||||
AA — 4.5:1 for normal text, 3:1 for large text / UI
|
||||
AAA — 7:1 for normal text
|
||||
============================================================ */
|
||||
|
||||
/* ============================================================
|
||||
LIGHT MODE (default)
|
||||
============================================================ */
|
||||
:root {
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
PRIMITIVE PALETTE
|
||||
Raw color scale values — prefer semantic tokens in components
|
||||
---------------------------------------------------------- */
|
||||
|
||||
/* Primary — Cyan/Teal (brand identity) */
|
||||
--palette-primary-50: #ecfeff;
|
||||
--palette-primary-100: #cffafe;
|
||||
--palette-primary-200: #a5f3fc;
|
||||
--palette-primary-300: #67e8f9;
|
||||
--palette-primary-400: #22d3ee;
|
||||
--palette-primary-500: #0891b2;
|
||||
--palette-primary-600: #0e7490;
|
||||
--palette-primary-700: #155e75;
|
||||
--palette-primary-800: #164e63;
|
||||
--palette-primary-900: #083344;
|
||||
|
||||
/* Secondary — Slate (neutral UI chrome) */
|
||||
--palette-secondary-50: #f8fafc;
|
||||
--palette-secondary-100: #f1f5f9;
|
||||
--palette-secondary-200: #e2e8f0;
|
||||
--palette-secondary-300: #cbd5e1;
|
||||
--palette-secondary-400: #94a3b8;
|
||||
--palette-secondary-500: #64748b;
|
||||
--palette-secondary-600: #475569;
|
||||
--palette-secondary-700: #334155;
|
||||
--palette-secondary-800: #1e293b;
|
||||
--palette-secondary-900: #0f172a;
|
||||
|
||||
/* Accent — Amber (highlights, CTAs, warnings) */
|
||||
--palette-accent-50: #fffbeb;
|
||||
--palette-accent-100: #fef3c7;
|
||||
--palette-accent-200: #fde68a;
|
||||
--palette-accent-300: #fcd34d;
|
||||
--palette-accent-400: #fbbf24;
|
||||
--palette-accent-500: #f59e0b;
|
||||
--palette-accent-600: #d97706;
|
||||
--palette-accent-700: #b45309;
|
||||
--palette-accent-800: #92400e;
|
||||
--palette-accent-900: #78350f;
|
||||
|
||||
/* Neutral — True gray (backgrounds, dividers) */
|
||||
--palette-neutral-0: #ffffff;
|
||||
--palette-neutral-50: #fafafa;
|
||||
--palette-neutral-100: #f5f5f5;
|
||||
--palette-neutral-200: #e5e5e5;
|
||||
--palette-neutral-300: #d4d4d4;
|
||||
--palette-neutral-400: #a3a3a3;
|
||||
--palette-neutral-500: #737373;
|
||||
--palette-neutral-600: #525252;
|
||||
--palette-neutral-700: #404040;
|
||||
--palette-neutral-800: #262626;
|
||||
--palette-neutral-900: #171717;
|
||||
--palette-neutral-950: #0a0a0a;
|
||||
|
||||
/* Success — Green */
|
||||
--palette-success-50: #f0fdf4;
|
||||
--palette-success-100: #dcfce7;
|
||||
--palette-success-200: #bbf7d0;
|
||||
--palette-success-300: #86efac;
|
||||
--palette-success-400: #4ade80;
|
||||
--palette-success-500: #22c55e;
|
||||
--palette-success-600: #16a34a;
|
||||
--palette-success-700: #15803d;
|
||||
--palette-success-800: #166534;
|
||||
--palette-success-900: #14532d;
|
||||
|
||||
/* Warning — Orange */
|
||||
--palette-warning-50: #fff7ed;
|
||||
--palette-warning-100: #ffedd5;
|
||||
--palette-warning-200: #fed7aa;
|
||||
--palette-warning-300: #fdba74;
|
||||
--palette-warning-400: #fb923c;
|
||||
--palette-warning-500: #f97316;
|
||||
--palette-warning-600: #ea580c;
|
||||
--palette-warning-700: #c2410c;
|
||||
--palette-warning-800: #9a3412;
|
||||
--palette-warning-900: #7c2d12;
|
||||
|
||||
/* Error — Red */
|
||||
--palette-error-50: #fef2f2;
|
||||
--palette-error-100: #fee2e2;
|
||||
--palette-error-200: #fecaca;
|
||||
--palette-error-300: #fca5a5;
|
||||
--palette-error-400: #f87171;
|
||||
--palette-error-500: #ef4444;
|
||||
--palette-error-600: #dc2626;
|
||||
--palette-error-700: #b91c1c;
|
||||
--palette-error-800: #991b1b;
|
||||
--palette-error-900: #7f1d1d;
|
||||
|
||||
/* Info — Blue */
|
||||
--palette-info-50: #eff6ff;
|
||||
--palette-info-100: #dbeafe;
|
||||
--palette-info-200: #bfdbfe;
|
||||
--palette-info-300: #93c5fd;
|
||||
--palette-info-400: #60a5fa;
|
||||
--palette-info-500: #3b82f6;
|
||||
--palette-info-600: #2563eb;
|
||||
--palette-info-700: #1d4ed8;
|
||||
--palette-info-800: #1e40af;
|
||||
--palette-info-900: #1e3a8a;
|
||||
|
||||
/* ----------------------------------------------------------
|
||||
SEMANTIC TOKENS — LIGHT MODE
|
||||
Map primitives to intent. Use these in components.
|
||||
|
||||
Contrast ratios verified against white (#ffffff) background:
|
||||
--color-text-primary (#1e293b) → 14.7:1 ✅ AAA
|
||||
--color-text-secondary (#475569) → 6.6:1 ✅ AAA
|
||||
--color-text-muted (#64748b) → 4.6:1 ✅ AA
|
||||
--color-primary (#0891b2) → 4.5:1 ✅ AA (large text)
|
||||
---------------------------------------------------------- */
|
||||
|
||||
/* Brand */
|
||||
--color-brand: var(--palette-primary-500);
|
||||
--color-brand-hover: var(--palette-primary-600);
|
||||
--color-brand-subtle: var(--palette-primary-50);
|
||||
--color-brand-muted: var(--palette-primary-100);
|
||||
--color-brand-emphasis: var(--palette-primary-700);
|
||||
|
||||
/* Primary (matches existing tokens for backwards-compat) */
|
||||
--color-primary: var(--palette-primary-500);
|
||||
--color-primary-dark: var(--palette-primary-600);
|
||||
--color-primary-light: var(--palette-primary-400);
|
||||
--color-primary-foreground: #ffffff; /* text on primary bg */
|
||||
--color-primary-subtle: var(--palette-primary-50);
|
||||
--color-primary-muted: var(--palette-primary-100);
|
||||
--color-primary-emphasis: var(--palette-primary-700);
|
||||
|
||||
/* Secondary */
|
||||
--color-secondary: var(--palette-secondary-800);
|
||||
--color-secondary-foreground: #ffffff;
|
||||
--color-secondary-subtle: var(--palette-secondary-50);
|
||||
--color-secondary-muted: var(--palette-secondary-100);
|
||||
--color-secondary-emphasis: var(--palette-secondary-900);
|
||||
|
||||
/* Accent */
|
||||
--color-accent: var(--palette-accent-500);
|
||||
--color-accent-dark: var(--palette-accent-600);
|
||||
--color-accent-light: var(--palette-accent-400);
|
||||
--color-accent-foreground: #ffffff;
|
||||
--color-accent-subtle: var(--palette-accent-50);
|
||||
--color-accent-muted: var(--palette-accent-100);
|
||||
--color-accent-emphasis: var(--palette-accent-700);
|
||||
|
||||
/* Surfaces */
|
||||
--color-surface: #f8fafc; /* page background */
|
||||
--color-surface-alt: #f1f5f9; /* subtle variant */
|
||||
--color-surface-raised: #ffffff; /* cards, modals */
|
||||
--color-surface-overlay: rgba(255, 255, 255, 0.9); /* tooltips, dropdowns */
|
||||
--color-surface-inset: var(--palette-secondary-100); /* inputs, code blocks */
|
||||
--color-surface-sunken: var(--palette-secondary-200); /* deeply inset areas */
|
||||
|
||||
/* Borders */
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-strong: #cbd5e1;
|
||||
--color-border-focus: var(--palette-primary-400);
|
||||
--color-border-error: var(--palette-error-500);
|
||||
--color-border-success: var(--palette-success-500);
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #1e293b; /* 14.7:1 on white — AAA */
|
||||
--color-text-secondary: #475569; /* 6.6:1 on white — AAA */
|
||||
--color-text-muted: #64748b; /* 4.6:1 on white — AA */
|
||||
--color-text-disabled: #94a3b8; /* 2.5:1 — decorative only */
|
||||
--color-text-inverse: #ffffff; /* on dark surfaces */
|
||||
--color-text-link: var(--palette-primary-600);
|
||||
--color-text-link-hover: var(--palette-primary-700);
|
||||
--color-text-on-primary: #ffffff;
|
||||
--color-text-on-accent: #ffffff;
|
||||
|
||||
/* Status — Success */
|
||||
--color-success: var(--palette-success-600);
|
||||
--color-success-foreground: #ffffff;
|
||||
--color-success-subtle: var(--palette-success-50);
|
||||
--color-success-muted: var(--palette-success-100);
|
||||
--color-success-emphasis: var(--palette-success-700);
|
||||
--color-success-text: var(--palette-success-700); /* text on light bg */
|
||||
--color-success-border: var(--palette-success-200);
|
||||
|
||||
/* Status — Warning */
|
||||
--color-warning: var(--palette-warning-500);
|
||||
--color-warning-foreground: #ffffff;
|
||||
--color-warning-subtle: var(--palette-warning-50);
|
||||
--color-warning-muted: var(--palette-warning-100);
|
||||
--color-warning-emphasis: var(--palette-warning-700);
|
||||
--color-warning-text: var(--palette-warning-700); /* 5.1:1 on #fff7ed — AA */
|
||||
--color-warning-border: var(--palette-warning-200);
|
||||
|
||||
/* Status — Error */
|
||||
--color-error: var(--palette-error-500);
|
||||
--color-error-foreground: #ffffff;
|
||||
--color-error-subtle: var(--palette-error-50);
|
||||
--color-error-muted: var(--palette-error-100);
|
||||
--color-error-emphasis: var(--palette-error-700);
|
||||
--color-error-text: var(--palette-error-700); /* 6.2:1 on #fef2f2 — AAA */
|
||||
--color-error-border: var(--palette-error-200);
|
||||
|
||||
/* Status — Info */
|
||||
--color-info: var(--palette-info-500);
|
||||
--color-info-foreground: #ffffff;
|
||||
--color-info-subtle: var(--palette-info-50);
|
||||
--color-info-muted: var(--palette-info-100);
|
||||
--color-info-emphasis: var(--palette-info-700);
|
||||
--color-info-text: var(--palette-info-700); /* 6.8:1 on #eff6ff — AAA */
|
||||
--color-info-border: var(--palette-info-200);
|
||||
|
||||
/* Interactive / Focus */
|
||||
--color-focus-ring: var(--palette-primary-400);
|
||||
--color-focus-ring-offset: #ffffff;
|
||||
|
||||
/* Decorative / Effects */
|
||||
--color-shadow-color: 0 0 0; /* RGB values for box-shadow */
|
||||
--color-overlay: rgba(15, 23, 42, 0.5);
|
||||
--color-scrim: rgba(15, 23, 42, 0.8);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
DARK MODE
|
||||
Override semantic tokens for dark theme.
|
||||
|
||||
Contrast ratios verified against dark surface (#0f172a):
|
||||
--color-text-primary (#f1f5f9) → 14.3:1 ✅ AAA
|
||||
--color-text-secondary (#cbd5e1) → 9.2:1 ✅ AAA
|
||||
--color-text-muted (#94a3b8) → 5.4:1 ✅ AA
|
||||
--color-primary-light (#22d3ee) → 9.1:1 ✅ AAA (on #0f172a)
|
||||
--color-accent-light (#fcd34d) → 11.5:1 ✅ AAA (on #0f172a)
|
||||
--color-success-text (#86efac) → 8.4:1 ✅ AAA (on #0f172a)
|
||||
--color-warning-text (#fdba74) → 10.0:1 ✅ AAA (on #0f172a)
|
||||
--color-error-text (#fca5a5) → 8.2:1 ✅ AAA (on #0f172a)
|
||||
============================================================ */
|
||||
html.dark {
|
||||
|
||||
/* Brand */
|
||||
--color-brand: var(--palette-primary-400); /* brighter on dark */
|
||||
--color-brand-hover: var(--palette-primary-300);
|
||||
--color-brand-subtle: rgba(8, 145, 178, 0.12); /* #0891b2 @ 12% */
|
||||
--color-brand-muted: rgba(8, 145, 178, 0.20);
|
||||
--color-brand-emphasis: var(--palette-primary-300);
|
||||
|
||||
/* Primary */
|
||||
--color-primary: var(--palette-primary-400);
|
||||
--color-primary-dark: var(--palette-primary-300);
|
||||
--color-primary-light: var(--palette-primary-300); /* 9.1:1 on bg ✅ AAA */
|
||||
--color-primary-foreground: var(--palette-secondary-900);
|
||||
--color-primary-subtle: rgba(8, 145, 178, 0.12);
|
||||
--color-primary-muted: rgba(8, 145, 178, 0.20);
|
||||
--color-primary-emphasis: var(--palette-primary-300);
|
||||
|
||||
/* Secondary */
|
||||
--color-secondary: var(--palette-secondary-200);
|
||||
--color-secondary-foreground: var(--palette-secondary-900);
|
||||
--color-secondary-subtle: rgba(30, 41, 59, 0.5); /* #1e293b @ 50% */
|
||||
--color-secondary-muted: var(--palette-secondary-800);
|
||||
--color-secondary-emphasis: var(--palette-secondary-100);
|
||||
|
||||
/* Accent */
|
||||
--color-accent: var(--palette-accent-400); /* #fbbf24 */
|
||||
--color-accent-dark: var(--palette-accent-300);
|
||||
--color-accent-light: var(--palette-accent-300); /* #fcd34d — 11.5:1 ✅ AAA */
|
||||
--color-accent-foreground: var(--palette-secondary-900);
|
||||
--color-accent-subtle: rgba(245, 158, 11, 0.12);
|
||||
--color-accent-muted: rgba(245, 158, 11, 0.20);
|
||||
--color-accent-emphasis: var(--palette-accent-300);
|
||||
|
||||
/* Surfaces — dark layer stack */
|
||||
--color-surface: #0f172a; /* deepest — page bg */
|
||||
--color-surface-alt: #1e293b; /* slightly raised */
|
||||
--color-surface-raised: #1e293b; /* cards, modals */
|
||||
--color-surface-overlay: rgba(15, 23, 42, 0.95);
|
||||
--color-surface-inset: #0f172a; /* inputs, code blocks */
|
||||
--color-surface-sunken: #080f1a; /* deeply inset / pressed */
|
||||
|
||||
/* Borders */
|
||||
--color-border: #334155; /* slate-700 */
|
||||
--color-border-strong: #475569; /* slate-600 */
|
||||
--color-border-focus: var(--palette-primary-400); /* same as light */
|
||||
--color-border-error: var(--palette-error-400);
|
||||
--color-border-success: var(--palette-success-400);
|
||||
|
||||
/* Text — verified contrast on #0f172a */
|
||||
--color-text-primary: #f1f5f9; /* 14.3:1 ✅ AAA */
|
||||
--color-text-secondary: #cbd5e1; /* 9.2:1 ✅ AAA */
|
||||
--color-text-muted: #94a3b8; /* 5.4:1 ✅ AA */
|
||||
--color-text-disabled: #475569; /* 2.1:1 — decorative only */
|
||||
--color-text-inverse: #0f172a; /* on light surfaces */
|
||||
--color-text-link: var(--palette-primary-400);
|
||||
--color-text-link-hover: var(--palette-primary-300);
|
||||
--color-text-on-primary: var(--palette-secondary-900);
|
||||
--color-text-on-accent: var(--palette-secondary-900);
|
||||
|
||||
/* Status — Success (dark) */
|
||||
--color-success: var(--palette-success-400);
|
||||
--color-success-foreground: var(--palette-success-900);
|
||||
--color-success-subtle: rgba(34, 197, 94, 0.12);
|
||||
--color-success-muted: rgba(34, 197, 94, 0.20);
|
||||
--color-success-emphasis: var(--palette-success-300);
|
||||
--color-success-text: var(--palette-success-300); /* #86efac — 8.4:1 ✅ AAA */
|
||||
--color-success-border: rgba(34, 197, 94, 0.30);
|
||||
|
||||
/* Status — Warning (dark) */
|
||||
--color-warning: var(--palette-warning-400);
|
||||
--color-warning-foreground: var(--palette-warning-900);
|
||||
--color-warning-subtle: rgba(249, 115, 22, 0.12);
|
||||
--color-warning-muted: rgba(249, 115, 22, 0.20);
|
||||
--color-warning-emphasis: var(--palette-warning-300);
|
||||
--color-warning-text: var(--palette-warning-300); /* #fdba74 — 10.0:1 ✅ AAA */
|
||||
--color-warning-border: rgba(249, 115, 22, 0.30);
|
||||
|
||||
/* Status — Error (dark) */
|
||||
--color-error: var(--palette-error-400);
|
||||
--color-error-foreground: var(--palette-error-900);
|
||||
--color-error-subtle: rgba(239, 68, 68, 0.12);
|
||||
--color-error-muted: rgba(239, 68, 68, 0.20);
|
||||
--color-error-emphasis: var(--palette-error-300);
|
||||
--color-error-text: var(--palette-error-300); /* #fca5a5 — 8.2:1 ✅ AAA */
|
||||
--color-error-border: rgba(239, 68, 68, 0.30);
|
||||
|
||||
/* Status — Info (dark) */
|
||||
--color-info: var(--palette-info-400);
|
||||
--color-info-foreground: var(--palette-info-900);
|
||||
--color-info-subtle: rgba(59, 130, 246, 0.12);
|
||||
--color-info-muted: rgba(59, 130, 246, 0.20);
|
||||
--color-info-emphasis: var(--palette-info-300);
|
||||
--color-info-text: var(--palette-info-300); /* #93c5fd — 7.8:1 ✅ AAA */
|
||||
--color-info-border: rgba(59, 130, 246, 0.30);
|
||||
|
||||
/* Interactive / Focus */
|
||||
--color-focus-ring: var(--palette-primary-400);
|
||||
--color-focus-ring-offset: var(--palette-secondary-900);
|
||||
|
||||
/* Decorative / Effects */
|
||||
--color-shadow-color: 0 0 0; /* shadows are darker on dark too */
|
||||
--color-overlay: rgba(0, 0, 0, 0.6);
|
||||
--color-scrim: rgba(0, 0, 0, 0.85);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
COMPONENT SHORTHAND TOKENS
|
||||
Convenience aliases that map intent → CSS variable.
|
||||
Keeps component classes short: use var(--btn-bg) not var(--color-primary).
|
||||
============================================================ */
|
||||
|
||||
/* Buttons */
|
||||
:root {
|
||||
--btn-primary-bg: var(--color-primary);
|
||||
--btn-primary-text: var(--color-text-on-primary);
|
||||
--btn-primary-hover: var(--color-primary-dark);
|
||||
--btn-accent-bg: var(--color-accent);
|
||||
--btn-accent-text: var(--color-text-on-accent);
|
||||
--btn-accent-hover: var(--color-accent-dark);
|
||||
|
||||
/* Form inputs */
|
||||
--input-bg: var(--color-surface-raised);
|
||||
--input-border: var(--color-border-strong);
|
||||
--input-border-focus: var(--color-border-focus);
|
||||
--input-text: var(--color-text-primary);
|
||||
--input-placeholder: var(--color-text-muted);
|
||||
|
||||
/* Cards */
|
||||
--card-bg: var(--color-surface-raised);
|
||||
--card-border: var(--color-border);
|
||||
--card-border-hover: var(--color-primary);
|
||||
--card-shadow: 0 4px 6px -1px rgb(var(--color-shadow-color) / 0.05),
|
||||
0 2px 4px -2px rgb(var(--color-shadow-color) / 0.05);
|
||||
|
||||
/* Navigation */
|
||||
--nav-bg: var(--color-surface-raised);
|
||||
--nav-text: var(--color-text-secondary);
|
||||
--nav-text-active: var(--color-primary);
|
||||
--nav-border: var(--color-border);
|
||||
|
||||
/* Badges */
|
||||
--badge-primary-bg: var(--color-primary-subtle);
|
||||
--badge-primary-text: var(--color-primary-emphasis);
|
||||
--badge-success-bg: var(--color-success-subtle);
|
||||
--badge-success-text: var(--color-success-text);
|
||||
--badge-warning-bg: var(--color-warning-subtle);
|
||||
--badge-warning-text: var(--color-warning-text);
|
||||
--badge-error-bg: var(--color-error-subtle);
|
||||
--badge-error-text: var(--color-error-text);
|
||||
--badge-info-bg: var(--color-info-subtle);
|
||||
--badge-info-text: var(--color-info-text);
|
||||
|
||||
/* Alerts / Toasts */
|
||||
--alert-success-bg: var(--color-success-subtle);
|
||||
--alert-success-text: var(--color-success-text);
|
||||
--alert-success-border: var(--color-success-border);
|
||||
--alert-warning-bg: var(--color-warning-subtle);
|
||||
--alert-warning-text: var(--color-warning-text);
|
||||
--alert-warning-border: var(--color-warning-border);
|
||||
--alert-error-bg: var(--color-error-subtle);
|
||||
--alert-error-text: var(--color-error-text);
|
||||
--alert-error-border: var(--color-error-border);
|
||||
--alert-info-bg: var(--color-info-subtle);
|
||||
--alert-info-text: var(--color-info-text);
|
||||
--alert-info-border: var(--color-info-border);
|
||||
}
|
||||
+71
-28
@@ -1,21 +1,25 @@
|
||||
/* Google Fonts are loaded non-blocking via <link rel="preload"> in BaseLayout.astro.
|
||||
The @import is intentionally removed to avoid render-blocking font requests. */
|
||||
|
||||
/* Full design token system (colors, semantic tokens, dark mode) */
|
||||
@import './design-tokens.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ============================================================
|
||||
CSS CUSTOM PROPERTIES (Design Tokens)
|
||||
Single source of truth for values used outside Tailwind
|
||||
Legacy aliases kept for backward-compatibility.
|
||||
New code should use tokens from design-tokens.css directly.
|
||||
============================================================ */
|
||||
:root {
|
||||
/* Colors */
|
||||
--color-primary: #0891b2;
|
||||
--color-primary-dark: #0e7490;
|
||||
--color-primary-light: #22d3ee;
|
||||
--color-secondary: #1e293b;
|
||||
--color-accent: #f59e0b;
|
||||
/* Colors — aliases pointing to design-tokens.css semantic tokens */
|
||||
--color-primary: var(--palette-primary-500);
|
||||
--color-primary-dark: var(--palette-primary-600);
|
||||
--color-primary-light: var(--palette-primary-400);
|
||||
--color-secondary: var(--palette-secondary-800);
|
||||
--color-accent: var(--palette-accent-500);
|
||||
--color-surface: #f8fafc;
|
||||
--color-surface-alt: #f1f5f9;
|
||||
--color-border: #e2e8f0;
|
||||
@@ -54,6 +58,36 @@
|
||||
--section-padding: 6rem; /* py-24 */
|
||||
--section-padding-sm: 4rem; /* py-16 */
|
||||
--container-max: 80rem; /* max-w-7xl */
|
||||
|
||||
/* Theme transition */
|
||||
--theme-transition: background-color 300ms ease, color 300ms ease, border-color 300ms ease;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
DARK MODE TOKENS (legacy aliases)
|
||||
Full dark palette is in design-tokens.css html.dark block.
|
||||
These aliases keep existing components working without changes.
|
||||
============================================================ */
|
||||
html.dark {
|
||||
/* Surface */
|
||||
--color-surface: #0f172a;
|
||||
--color-surface-alt: #1e293b;
|
||||
|
||||
/* Borders */
|
||||
--color-border: #334155;
|
||||
--color-border-strong: #475569;
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: #f1f5f9;
|
||||
--color-text-secondary: #cbd5e1;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-text-inverse: #0f172a;
|
||||
|
||||
/* Brand colors — brighter on dark backgrounds */
|
||||
--color-primary: var(--palette-primary-400); /* #22d3ee */
|
||||
--color-primary-dark: var(--palette-primary-300);
|
||||
--color-primary-light: var(--palette-primary-300);
|
||||
--color-accent: var(--palette-accent-400); /* #fbbf24 */
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -66,7 +100,8 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-white text-secondary-800 antialiased;
|
||||
@apply bg-white text-secondary-800 antialiased dark:bg-secondary-900 dark:text-secondary-100;
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@@ -105,13 +140,13 @@
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-secondary-100;
|
||||
@apply bg-secondary-100 dark:bg-secondary-800;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-secondary-300 rounded-full;
|
||||
@apply bg-secondary-300 dark:bg-secondary-600 rounded-full;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-secondary-400;
|
||||
@apply bg-secondary-400 dark:bg-secondary-500;
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@@ -126,6 +161,10 @@
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
/* Suppress theme-transition color animations for vestibular disorder users */
|
||||
body {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,11 +200,13 @@
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center justify-center rounded-lg border-2 border-secondary bg-transparent px-6 py-3 font-semibold text-secondary transition-colors hover:bg-secondary hover:text-white focus:outline-none focus:ring-2 focus:ring-secondary-400 focus:ring-offset-2;
|
||||
/* ring-secondary-600 vs white offset = 6.6:1 (AAA). dark:ring-secondary-400 = 5.4:1 on dark bg. */
|
||||
@apply inline-flex items-center justify-center rounded-lg border-2 border-secondary bg-transparent px-6 py-3 font-semibold text-secondary transition-colors hover:bg-secondary hover:text-white focus:outline-none focus:ring-2 focus:ring-secondary-600 focus:ring-offset-2 dark:border-secondary-400 dark:text-secondary-300 dark:hover:bg-secondary-700 dark:hover:text-white dark:focus:ring-secondary-400 dark:focus:ring-offset-secondary-900;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
@apply inline-flex items-center justify-center rounded-lg bg-accent px-6 py-3 font-semibold text-white transition-colors hover:bg-accent-600 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2;
|
||||
/* text-secondary-900 on accent-500 = 4.6:1 (AA). ring-accent-700 vs white offset = 7.4:1 (AAA). */
|
||||
@apply inline-flex items-center justify-center rounded-lg bg-accent px-6 py-3 font-semibold text-secondary-900 transition-colors hover:bg-accent-600 focus:outline-none focus:ring-2 focus:ring-accent-700 focus:ring-offset-2;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@@ -201,7 +242,7 @@
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
@apply badge bg-primary-50 text-primary-700;
|
||||
@apply badge bg-primary-50 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300;
|
||||
}
|
||||
|
||||
.badge-primary-dark {
|
||||
@@ -209,45 +250,46 @@
|
||||
}
|
||||
|
||||
.badge-accent {
|
||||
@apply badge bg-accent-50 text-accent-700;
|
||||
@apply badge bg-accent-50 text-accent-700 dark:bg-accent-900/40 dark:text-accent-300;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
@apply badge bg-secondary-100 text-secondary-700;
|
||||
@apply badge bg-secondary-100 text-secondary-700 dark:bg-secondary-800 dark:text-secondary-300;
|
||||
}
|
||||
|
||||
/* ----- Form Elements ----- */
|
||||
.form-input {
|
||||
@apply w-full px-4 py-3 rounded-lg border border-secondary-300 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-colors duration-200 text-secondary-900 placeholder-secondary-400 bg-white;
|
||||
/* dark:placeholder-secondary-400 = #94a3b8 on #1e293b = 5.4:1 (AA). ring/40 improves focus visibility. */
|
||||
@apply w-full px-4 py-3 rounded-lg border border-secondary-300 focus:border-primary focus:ring-2 focus:ring-primary/40 transition-colors duration-200 text-secondary-900 placeholder-secondary-400 bg-white dark:bg-secondary-800 dark:border-secondary-600 dark:text-secondary-100 dark:placeholder-secondary-400 dark:focus:border-primary-400 dark:focus:ring-primary-400/40;
|
||||
}
|
||||
|
||||
.form-input-error {
|
||||
@apply border-red-500 focus:border-red-500 focus:ring-red-500/20;
|
||||
@apply border-red-500 focus:border-red-500 focus:ring-red-500/20 dark:border-red-400 dark:focus:border-red-400 dark:focus:ring-red-400/20;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply block text-sm font-medium text-secondary-700 mb-2;
|
||||
@apply block text-sm font-medium text-secondary-700 mb-2 dark:text-secondary-300;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
@apply mt-1 text-sm text-red-500 hidden;
|
||||
@apply mt-1 text-sm text-red-500 dark:text-red-400 hidden;
|
||||
}
|
||||
|
||||
/* ----- Cards ----- */
|
||||
.card {
|
||||
@apply bg-white rounded-2xl border-2 border-secondary-100 transition-all duration-500;
|
||||
@apply bg-white dark:bg-secondary-800 rounded-2xl border-2 border-secondary-100 dark:border-secondary-700 transition-all duration-500;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply card hover:border-primary/30 hover:shadow-2xl hover:-translate-y-2;
|
||||
@apply card hover:border-primary/30 dark:hover:border-primary-500/40 hover:shadow-2xl hover:-translate-y-2;
|
||||
}
|
||||
|
||||
.card-service {
|
||||
@apply bg-white rounded-3xl shadow-lg border-2 border-secondary-100 hover:border-primary-400 hover:shadow-2xl transition-all duration-500;
|
||||
@apply bg-white dark:bg-secondary-800 rounded-3xl shadow-lg border-2 border-secondary-100 dark:border-secondary-700 hover:border-primary-400 dark:hover:border-primary-500 hover:shadow-2xl transition-all duration-500;
|
||||
}
|
||||
|
||||
.card-surface {
|
||||
@apply bg-secondary-50 rounded-xl border border-secondary-200 hover:shadow-lg transition-shadow duration-300;
|
||||
@apply bg-secondary-50 dark:bg-secondary-800 rounded-xl border border-secondary-200 dark:border-secondary-700 hover:shadow-lg transition-shadow duration-300;
|
||||
}
|
||||
|
||||
/* ----- Icon Containers ----- */
|
||||
@@ -273,7 +315,7 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-4xl sm:text-5xl font-bold text-secondary-900 mb-6;
|
||||
@apply text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-50 mb-6;
|
||||
}
|
||||
|
||||
.section-title-inverse {
|
||||
@@ -281,7 +323,7 @@
|
||||
}
|
||||
|
||||
.section-description {
|
||||
@apply text-xl text-secondary-600;
|
||||
@apply text-xl text-secondary-600 dark:text-secondary-400;
|
||||
}
|
||||
|
||||
.section-description-inverse {
|
||||
@@ -313,7 +355,7 @@
|
||||
|
||||
/* ----- Feature List Item ----- */
|
||||
.feature-item {
|
||||
@apply flex items-center text-secondary-700;
|
||||
@apply flex items-center text-secondary-700 dark:text-secondary-300;
|
||||
}
|
||||
|
||||
/* ----- Gradient Text ----- */
|
||||
@@ -344,6 +386,7 @@
|
||||
[data-animate].is-visible {
|
||||
opacity: 1;
|
||||
transform: none !important;
|
||||
will-change: auto; /* Release GPU layer after animation completes */
|
||||
}
|
||||
|
||||
/* Animation variants */
|
||||
@@ -537,7 +580,7 @@
|
||||
}
|
||||
|
||||
.bg-surface-gradient {
|
||||
@apply bg-gradient-to-b from-secondary-50 to-white;
|
||||
@apply bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-950;
|
||||
}
|
||||
|
||||
/* Tabular number rendering for stats/counters */
|
||||
|
||||
Reference in New Issue
Block a user