sonarqube fixes
This commit is contained in:
parent
ce79a800f2
commit
acbdb1ea3c
|
|
@ -14,7 +14,7 @@ type Props = {
|
||||||
export default function Navbar({
|
export default function Navbar({
|
||||||
theme,
|
theme,
|
||||||
setTheme
|
setTheme
|
||||||
}: Props) {
|
}: Readonly<Props>) {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Personal Photo and Bio */}
|
{/* Personal Photo and Bio */}
|
||||||
<div className="about-section__content" role="main">
|
<main className="about-section__content" role="main">
|
||||||
<div className="about-section__image-wrapper">
|
<div className="about-section__image-wrapper">
|
||||||
<img
|
<img
|
||||||
src={profileImage}
|
src={profileImage}
|
||||||
|
|
@ -88,19 +88,18 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
Hi, I'm {name}!
|
Hi, I'm {name}!
|
||||||
</h3>
|
</h3>
|
||||||
<div className="about-section__bio-text">
|
<div className="about-section__bio-text">
|
||||||
{bio.map((paragraph, index) => (
|
{bio.map((paragraph) => (
|
||||||
<p key={index}>{paragraph}</p>
|
<p key={paragraph.substring(0, 50)}>{paragraph}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
{/* Feature Cards Grid */}
|
{/* Feature Cards Grid */}
|
||||||
<div
|
<div
|
||||||
id="features"
|
id="features"
|
||||||
className="about-section__features"
|
className="about-section__features"
|
||||||
role="region"
|
|
||||||
aria-labelledby="features-heading"
|
aria-labelledby="features-heading"
|
||||||
>
|
>
|
||||||
<h3 id="features-heading" className="sr-only">My Areas of Expertise</h3>
|
<h3 id="features-heading" className="sr-only">My Areas of Expertise</h3>
|
||||||
|
|
@ -108,23 +107,21 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
<div className="about-section__features-grid">
|
<div className="about-section__features-grid">
|
||||||
{featureCards.map((card, index) => {
|
{featureCards.map((card, index) => {
|
||||||
const IconComponent = card.icon;
|
const IconComponent = card.icon;
|
||||||
|
const cardId = card.title.toLowerCase().replaceAll(/\s+/g, '-');
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
key={index}
|
key={cardId}
|
||||||
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
|
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
|
||||||
role="group"
|
aria-labelledby={`feature-${cardId}-title`}
|
||||||
aria-labelledby={`feature-${index}-title`}
|
aria-describedby={`feature-${cardId}-description`}
|
||||||
aria-describedby={`feature-${index}-description`}
|
|
||||||
>
|
>
|
||||||
<header className="about-section__feature-header">
|
<header className="about-section__feature-header">
|
||||||
<IconComponent
|
<IconComponent
|
||||||
className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`}
|
className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
role="img"
|
|
||||||
aria-label={`${card.title} icon`}
|
|
||||||
/>
|
/>
|
||||||
<h4
|
<h4
|
||||||
id={`feature-${index}-title`}
|
id={`feature-${cardId}-title`}
|
||||||
className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}
|
className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}
|
||||||
>
|
>
|
||||||
{card.title}
|
{card.title}
|
||||||
|
|
@ -132,7 +129,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
</header>
|
</header>
|
||||||
<div className="about-section__feature-content">
|
<div className="about-section__feature-content">
|
||||||
<p
|
<p
|
||||||
id={`feature-${index}-description`}
|
id={`feature-${cardId}-description`}
|
||||||
className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}
|
className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}
|
||||||
>
|
>
|
||||||
{card.description}
|
{card.description}
|
||||||
|
|
@ -149,6 +146,6 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -83,16 +83,14 @@ export default function CertificationsSection(props: CertificationsSectionProps
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Certifications Grid */}
|
{/* Certifications Grid */}
|
||||||
<div
|
<section
|
||||||
className="certifications-section__grid"
|
className="certifications-section__grid"
|
||||||
role="region"
|
|
||||||
aria-label="Professional certifications and qualifications"
|
aria-label="Professional certifications and qualifications"
|
||||||
>
|
>
|
||||||
{sortedCertifications.map((cert, index) => (
|
{sortedCertifications.map((cert, index) => (
|
||||||
<article
|
<div
|
||||||
key={index}
|
key={`${cert.name}-${cert.year}-${cert.issuer}`}
|
||||||
className="certifications-section__card"
|
className="certifications-section__card"
|
||||||
role="group"
|
|
||||||
aria-labelledby={`cert-${index}-title`}
|
aria-labelledby={`cert-${index}-title`}
|
||||||
aria-describedby={`cert-${index}-details`}
|
aria-describedby={`cert-${index}-details`}
|
||||||
>
|
>
|
||||||
|
|
@ -108,7 +106,6 @@ export default function CertificationsSection(props: CertificationsSectionProps
|
||||||
<Award
|
<Award
|
||||||
className="certifications-section__card-icon"
|
className="certifications-section__card-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
role="img"
|
|
||||||
aria-label="Certification award icon"
|
aria-label="Certification award icon"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -140,14 +137,13 @@ export default function CertificationsSection(props: CertificationsSectionProps
|
||||||
Certification {index + 1} of {sortedCertifications.length}.
|
Certification {index + 1} of {sortedCertifications.length}.
|
||||||
{cert.name} from {cert.issuer}, completed in {cert.year}.
|
{cert.name} from {cert.issuer}, completed in {cert.year}.
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{/* Certifications summary for screen readers */}
|
{/* Certifications summary for screen readers */}
|
||||||
<div className="certifications-section__summary sr-only" aria-live="polite">
|
<div className="certifications-section__summary sr-only" aria-live="polite">
|
||||||
<p>
|
<p>
|
||||||
Certifications timeline: Spanning from {sortedCertifications[sortedCertifications.length - 1]?.year} to {sortedCertifications[0]?.year},
|
Certifications timeline: Spanning from {sortedCertifications.at(-1)?.year} to {sortedCertifications.at(0)?.year},
|
||||||
including {sortedCertifications.filter(cert => cert.issuer.includes('Udemy')).length} online courses and
|
including {sortedCertifications.filter(cert => cert.issuer.includes('Udemy')).length} online courses and
|
||||||
{sortedCertifications.filter(cert => cert.issuer.includes('TU Darmstadt')).length} academic degree.
|
{sortedCertifications.filter(cert => cert.issuer.includes('TU Darmstadt')).length} academic degree.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Mail, Github, Linkedin, Send } from 'lucide-react';
|
import { Mail, ExternalLink, Send } from 'lucide-react';
|
||||||
import { personalConfig, getObfuscatedEmail } from '../../config/personal';
|
import { personalConfig, getObfuscatedEmail } from '../../config/personal';
|
||||||
import { getTexts } from '../../config/texts';
|
import { getTexts } from '../../config/texts';
|
||||||
|
|
||||||
|
|
@ -50,10 +50,10 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
announcement.textContent = 'Opening email client with pre-filled message';
|
announcement.textContent = 'Opening email client with pre-filled message';
|
||||||
document.body.appendChild(announcement);
|
document.body.appendChild(announcement);
|
||||||
|
|
||||||
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
|
globalThis.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.removeChild(announcement);
|
announcement.remove();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -70,10 +70,10 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
announcement.textContent = `Opening ${platform} profile in new tab`;
|
announcement.textContent = `Opening ${platform} profile in new tab`;
|
||||||
document.body.appendChild(announcement);
|
document.body.appendChild(announcement);
|
||||||
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer');
|
globalThis.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.removeChild(announcement);
|
announcement.remove();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -98,15 +98,13 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Content Grid */}
|
{/* Contact Content Grid */}
|
||||||
<div className="contact-section__grid" role="main">
|
<main className="contact-section__grid">
|
||||||
{/* Let's Connect Card */}
|
{/* Let's Connect Card */}
|
||||||
<div className="contact-section__connect-card" role="region" aria-labelledby="connect-heading">
|
<div className="contact-section__connect-card" role="region" aria-labelledby="connect-heading">
|
||||||
<div className="contact-section__connect-header">
|
<div className="contact-section__connect-header">
|
||||||
<Mail
|
<Mail
|
||||||
className="contact-section__email-icon"
|
className="contact-section__email-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
role="img"
|
|
||||||
aria-label="Email icon"
|
|
||||||
/>
|
/>
|
||||||
<div className="contact-section__connect-text">
|
<div className="contact-section__connect-text">
|
||||||
<h3 id="connect-heading" className="contact-section__email-title">
|
<h3 id="connect-heading" className="contact-section__email-title">
|
||||||
|
|
@ -141,15 +139,13 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
<div className="contact-section__info-card">
|
<div className="contact-section__info-card">
|
||||||
<h3 id="info-heading" className="sr-only">Contact Information</h3>
|
<h3 id="info-heading" className="sr-only">Contact Information</h3>
|
||||||
|
|
||||||
<div className="contact-section__availability" role="status" aria-live="polite">
|
<output className="contact-section__availability" aria-live="polite">
|
||||||
<div
|
<div
|
||||||
className="contact-section__status-indicator"
|
className="contact-section__status-indicator"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
role="img"
|
|
||||||
aria-label="Available status indicator"
|
|
||||||
></div>
|
></div>
|
||||||
<span className="contact-section__status-text">{availabilityText}</span>
|
<span className="contact-section__status-text">{availabilityText}</span>
|
||||||
</div>
|
</output>
|
||||||
|
|
||||||
<div className="contact-section__quick-facts">
|
<div className="contact-section__quick-facts">
|
||||||
<dl className="contact-section__fact-list">
|
<dl className="contact-section__fact-list">
|
||||||
|
|
@ -171,11 +167,11 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Social Contact Methods */}
|
{/* Social Contact Methods */}
|
||||||
<div className="contact-section__social-card" role="region" aria-labelledby="social-heading">
|
<div className="contact-section__social-card" aria-labelledby="social-heading">
|
||||||
<h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4>
|
<h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4>
|
||||||
<div className="contact-section__social-list" role="list">
|
<ul className="contact-section__social-list">
|
||||||
|
|
||||||
<div className="contact-section__detail-item" role="listitem">
|
<li className="contact-section__detail-item">
|
||||||
<button
|
<button
|
||||||
className="contact-section__social-button contact-section__social-button--email"
|
className="contact-section__social-button contact-section__social-button--email"
|
||||||
onClick={handleEmailClick}
|
onClick={handleEmailClick}
|
||||||
|
|
@ -187,38 +183,38 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
<span className="contact-section__detail-text" aria-label="Email address">
|
<span className="contact-section__detail-text" aria-label="Email address">
|
||||||
{getObfuscatedEmail()}
|
{getObfuscatedEmail()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div className="contact-section__detail-item" role="listitem">
|
<li className="contact-section__detail-item">
|
||||||
<button
|
<button
|
||||||
className="contact-section__social-button contact-section__social-button--github"
|
className="contact-section__social-button contact-section__social-button--github"
|
||||||
onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
|
onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
|
||||||
aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`}
|
aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Github className="contact-section__social-icon" aria-hidden="true" />
|
<ExternalLink className="contact-section__social-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<span className="contact-section__detail-text" aria-label="GitHub username">
|
<span className="contact-section__detail-text" aria-label="GitHub username">
|
||||||
{personalConfig.social.github.username}
|
{personalConfig.social.github.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<div className="contact-section__detail-item" role="listitem">
|
<li className="contact-section__detail-item">
|
||||||
<button
|
<button
|
||||||
className="contact-section__social-button contact-section__social-button--linkedin"
|
className="contact-section__social-button contact-section__social-button--linkedin"
|
||||||
onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
|
onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
|
||||||
aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`}
|
aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Linkedin className="contact-section__social-icon" aria-hidden="true" />
|
<ExternalLink className="contact-section__social-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<span className="contact-section__detail-text" aria-label="LinkedIn username">
|
<span className="contact-section__detail-text" aria-label="LinkedIn username">
|
||||||
{personalConfig.social.linkedin.username}
|
{personalConfig.social.linkedin.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
|
|
||||||
document.body.appendChild(announcement);
|
document.body.appendChild(announcement);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.removeChild(announcement);
|
announcement.remove();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
document.body.removeChild(link);
|
link.remove();
|
||||||
|
|
||||||
console.log('✅ Resume download initiated');
|
console.log('✅ Resume download initiated');
|
||||||
announceToScreenReader('Resume download started successfully');
|
announceToScreenReader('Resume download started successfully');
|
||||||
|
|
@ -93,7 +93,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="hero-section" role="main" aria-labelledby="hero-title">
|
<main className="hero-section" aria-labelledby="hero-title">
|
||||||
<div className="hero-section__container">
|
<div className="hero-section__container">
|
||||||
{/* Skip link for keyboard navigation */}
|
{/* Skip link for keyboard navigation */}
|
||||||
<div className="hero-section__skip">
|
<div className="hero-section__skip">
|
||||||
|
|
@ -167,6 +167,6 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
))}
|
))}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Github, ExternalLink } from 'lucide-react';
|
import { GitBranch, ExternalLink } from 'lucide-react';
|
||||||
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
||||||
import { personalConfig } from '../../config/personal';
|
import { personalConfig } from '../../config/personal';
|
||||||
import { getTexts } from '../../config/texts';
|
import { getTexts } from '../../config/texts';
|
||||||
|
|
@ -167,7 +167,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
|
|
||||||
document.body.appendChild(announcement);
|
document.body.appendChild(announcement);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.removeChild(announcement);
|
announcement.remove();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -248,9 +248,8 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
<div className="projects-section__overlay" aria-hidden="true"></div>
|
<div className="projects-section__overlay" aria-hidden="true"></div>
|
||||||
|
|
||||||
{/* Conditionally visible action buttons */}
|
{/* Conditionally visible action buttons */}
|
||||||
<div
|
<fieldset
|
||||||
className={`projects-section__actions ${isActive ? 'projects-section__actions--visible' : ''}`}
|
className={`projects-section__actions ${isActive ? 'projects-section__actions--visible' : ''}`}
|
||||||
role="group"
|
|
||||||
aria-label={`Actions for ${project.title}`}
|
aria-label={`Actions for ${project.title}`}
|
||||||
aria-hidden={!isActive}
|
aria-hidden={!isActive}
|
||||||
>
|
>
|
||||||
|
|
@ -265,7 +264,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
tabIndex={isActive ? 0 : -1}
|
tabIndex={isActive ? 0 : -1}
|
||||||
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, 0)}
|
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, 0)}
|
||||||
>
|
>
|
||||||
<Github className="projects-section__action-icon" aria-hidden="true" />
|
<GitBranch className="projects-section__action-icon" aria-hidden="true" />
|
||||||
<span className="projects-section__action-text">{texts.codeButtonText}</span>
|
<span className="projects-section__action-text">{texts.codeButtonText}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
@ -283,8 +282,9 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
<ExternalLink className="projects-section__action-icon" aria-hidden="true" />
|
<ExternalLink className="projects-section__action-icon" aria-hidden="true" />
|
||||||
<span className="projects-section__action-text">{texts.liveButtonText}</span>
|
<span className="projects-section__action-text">{texts.liveButtonText}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="projects-section__content">
|
<div className="projects-section__content">
|
||||||
|
|
@ -305,18 +305,17 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="projects-section__technologies" role="list" aria-label="Technologies used">
|
<ul className="projects-section__technologies" aria-label="Technologies used">
|
||||||
{project.technologies.map((tech, index) => (
|
{project.technologies.map((tech, index) => (
|
||||||
<span
|
<li
|
||||||
key={tech}
|
key={tech}
|
||||||
className="projects-section__tech-badge"
|
className="projects-section__tech-badge"
|
||||||
role="listitem"
|
|
||||||
aria-label={`Technology ${index + 1}: ${tech}`}
|
aria-label={`Technology ${index + 1}: ${tech}`}
|
||||||
>
|
>
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
{/* Enhanced status indicator for active card */}
|
{/* Enhanced status indicator for active card */}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,8 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
|
||||||
const IconComponent = service.icon;
|
const IconComponent = service.icon;
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
key={index}
|
key={`${service.title}-${service.colorClass}`}
|
||||||
className={`services-section__card services-section__card--${service.colorClass}`}
|
className={`services-section__card services-section__card--${service.colorClass}`}
|
||||||
tabIndex={0}
|
|
||||||
role="group"
|
|
||||||
aria-labelledby={`service-${index}-title`}
|
aria-labelledby={`service-${index}-title`}
|
||||||
aria-describedby={`service-${index}-description`}
|
aria-describedby={`service-${index}-description`}
|
||||||
>
|
>
|
||||||
|
|
@ -96,8 +94,6 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
|
||||||
<IconComponent
|
<IconComponent
|
||||||
className={`services-section__icon services-section__icon--${service.colorClass}`}
|
className={`services-section__icon services-section__icon--${service.colorClass}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
role="img"
|
|
||||||
aria-label={`${service.title} service icon`}
|
|
||||||
/>
|
/>
|
||||||
<h3
|
<h3
|
||||||
id={`service-${index}-title`}
|
id={`service-${index}-title`}
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,9 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
|
||||||
{skillCategories.map((category, categoryIndex) => {
|
{skillCategories.map((category, categoryIndex) => {
|
||||||
const colorClass = getColorByPosition(categoryIndex);
|
const colorClass = getColorByPosition(categoryIndex);
|
||||||
return (
|
return (
|
||||||
<article
|
<fieldset
|
||||||
key={category.title}
|
key={category.title}
|
||||||
className={`skills-section__category skills-section__category--${colorClass}`}
|
className={`skills-section__category skills-section__category--${colorClass}`}
|
||||||
role="group"
|
|
||||||
aria-labelledby={`category-${categoryIndex}-title`}
|
aria-labelledby={`category-${categoryIndex}-title`}
|
||||||
>
|
>
|
||||||
<header className="skills-section__category-header">
|
<header className="skills-section__category-header">
|
||||||
|
|
@ -139,18 +138,16 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<ul
|
||||||
className="skills-section__skills-list"
|
className="skills-section__skills-list"
|
||||||
role="list"
|
|
||||||
aria-label={`${category.title} skills`}
|
aria-label={`${category.title} skills`}
|
||||||
>
|
>
|
||||||
{category.skills.map((skillItem, skillIndex) => {
|
{category.skills.map((skillItem, skillIndex) => {
|
||||||
const displayLevel = calculateSkillLevel(skillItem.value);
|
const displayLevel = calculateSkillLevel(skillItem.value);
|
||||||
return (
|
return (
|
||||||
<div
|
<li
|
||||||
key={skillItem.skill}
|
key={skillItem.skill}
|
||||||
className="skills-section__skill-item"
|
className="skills-section__skill-item"
|
||||||
role="listitem"
|
|
||||||
aria-label={`${skillItem.skill}: ${displayLevel} level`}
|
aria-label={`${skillItem.skill}: ${displayLevel} level`}
|
||||||
>
|
>
|
||||||
<div className="skills-section__skill-header">
|
<div className="skills-section__skill-header">
|
||||||
|
|
@ -195,11 +192,12 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
|
||||||
Proficiency level: {displayLevel} at {skillItem.value} percent.
|
Proficiency level: {displayLevel} at {skillItem.value} percent.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</ul>
|
||||||
</article>
|
|
||||||
|
</fieldset>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -278,17 +278,23 @@
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced focus states for AAA compliance
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
// Brand-specific icon colors with better contrast
|
// Brand-specific icon colors with better contrast
|
||||||
&--email .contact-section__social-icon {
|
&--email .contact-section__social-icon {
|
||||||
color: #2563eb; // Stronger blue for email
|
color: var(--social-icon-email, #2563eb); // Use theme variable
|
||||||
}
|
}
|
||||||
|
|
||||||
&--github .contact-section__social-icon {
|
&--github .contact-section__social-icon {
|
||||||
color: #1f2937; // Darker for GitHub
|
color: var(--social-icon-github, #1f2937); // Use theme variable
|
||||||
}
|
}
|
||||||
|
|
||||||
&--linkedin .contact-section__social-icon {
|
&--linkedin .contact-section__social-icon {
|
||||||
color: #0d67b5; // Stronger LinkedIn blue
|
color: var(--social-icon-linkedin, #0d67b5); // Use theme variable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,7 +506,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__email-icon {
|
&__email-card-icon {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
|
@ -508,19 +514,6 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__email-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__email-description {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__email-button {
|
&__email-button {
|
||||||
background: var(--contact-button-bg);
|
background: var(--contact-button-bg);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -669,26 +662,6 @@
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
background-color: var(--contact-button-bg) !important;
|
background-color: var(--contact-button-bg) !important;
|
||||||
color: var(--contact-button-text) !important;
|
color: var(--contact-button-text) !important;
|
||||||
border: 2px solid var(--contact-social-border) !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
text-decoration: none !important;
|
|
||||||
z-index: 1000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update social button icons for AAA compliance
|
|
||||||
.contact-section__social-button {
|
|
||||||
&--email .contact-section__social-icon {
|
|
||||||
color: var(--social-icon-email); // Use theme variable
|
|
||||||
}
|
|
||||||
|
|
||||||
&--github .contact-section__social-icon {
|
|
||||||
color: var(--social-icon-github); // Use theme variable
|
|
||||||
}
|
|
||||||
|
|
||||||
&--linkedin .contact-section__social-icon {
|
|
||||||
color: var(--social-icon-linkedin); // Use theme variable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced focus states for AAA compliance
|
// Enhanced focus states for AAA compliance
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 3px solid var(--color-focus-ring, #2563eb);
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,9 @@
|
||||||
border-radius: $card-border-radius-md;
|
border-radius: $card-border-radius-md;
|
||||||
box-shadow: var(--projects-card-shadow);
|
box-shadow: var(--projects-card-shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: $transition-base;
|
transition: all 0.3s ease;
|
||||||
border: 1px solid var(--projects-card-border);
|
border: 1px solid var(--projects-card-border);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-0.25rem);
|
transform: translateY(-0.25rem);
|
||||||
|
|
@ -73,7 +74,7 @@
|
||||||
|
|
||||||
.projects-section__actions {
|
.projects-section__actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +90,7 @@
|
||||||
|
|
||||||
.projects-section__actions {
|
.projects-section__actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +107,7 @@
|
||||||
|
|
||||||
.projects-section__actions {
|
.projects-section__actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +119,7 @@
|
||||||
|
|
||||||
.projects-section__actions {
|
.projects-section__actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -404,65 +405,6 @@
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card states
|
|
||||||
.projects-section__card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 3px solid var(--color-focus-ring, #2563eb);
|
|
||||||
outline-offset: 2px;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: var(--projects-card-shadow-hover);
|
|
||||||
border-color: var(--color-focus-ring, #2563eb);
|
|
||||||
border-width: 2px;
|
|
||||||
|
|
||||||
.projects-section__actions {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action buttons visibility
|
|
||||||
.projects-section__actions {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%) translateY(10px);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
z-index: 10; // Above overlay
|
|
||||||
|
|
||||||
&--visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(-50%, -50%) translateY(0);
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override for when actions are shown (hover, focus, active)
|
|
||||||
.projects-section__card {
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&--active,
|
|
||||||
&:focus-within {
|
|
||||||
.projects-section__actions {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(-50%, -50%) translateY(0);
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// High contrast mode
|
// High contrast mode
|
||||||
@media (prefers-contrast: high) {
|
@media (prefers-contrast: high) {
|
||||||
.projects-section__card {
|
.projects-section__card {
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@
|
||||||
min-height: 250px; // Ensures consistent card heights
|
min-height: 250px; // Ensures consistent card heights
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
animation: cardFadeIn 0.6s ease-out;
|
||||||
|
|
||||||
// Add subtle backdrop blur for depth
|
// Add subtle backdrop blur for depth
|
||||||
&::before {
|
&::before {
|
||||||
|
|
@ -132,6 +133,52 @@
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make cards keyboard accessible
|
||||||
|
&[tabindex='0'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AAA-compliant focus indicator (3px minimum)
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced hover and focus interaction
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: $shadow-card-hover;
|
||||||
|
|
||||||
|
// When both focused and hovered
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-6px); // Slightly more lift when focused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation delays for cards
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
// Responsive card heights - more flexible approach
|
// Responsive card heights - more flexible approach
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
min-height: 280px; // Use min-height instead of fixed height
|
min-height: 280px; // Use min-height instead of fixed height
|
||||||
|
|
@ -150,6 +197,54 @@
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// High contrast mode support
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 4px solid currentColor !important;
|
||||||
|
outline-offset: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhance icon visibility in high contrast
|
||||||
|
.services-section__icon {
|
||||||
|
filter: contrast(1.5) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduced motion support
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition: none !important;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch device optimizations
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
min-height: 320px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: $shadow-card-hover;
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme enhancements
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
&--primary {
|
&--primary {
|
||||||
background: var(--service-card-primary-bg);
|
background: var(--service-card-primary-bg);
|
||||||
}
|
}
|
||||||
|
|
@ -277,131 +372,25 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
// ================================
|
||||||
|
// WCAG Level AAA Accessibility Enhancements
|
||||||
|
// ================================
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
// High contrast mode support for better accessibility
|
||||||
padding: 0 1.25rem 1.25rem 1.25rem;
|
@media (prefers-contrast: high) {
|
||||||
|
// Ensure text remains readable in high contrast mode
|
||||||
|
.services-section__card-title,
|
||||||
|
.services-section__card-description {
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
animation-delay: 0.4s;
|
||||||
}
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
&__card-description {
|
animation-delay: 0.5s;
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--primary {
|
|
||||||
color: var(--service-description-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--secondary {
|
|
||||||
color: var(--service-description-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--tertiary {
|
|
||||||
color: var(--service-description-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--quaternary {
|
|
||||||
color: var(--service-description-quaternary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--quinary {
|
|
||||||
color: var(--service-description-quinary);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--senary {
|
|
||||||
color: var(--service-description-senary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
&:nth-child(6) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
// Handle 6 cards specifically for better distribution
|
|
||||||
.services-section__grid:has(.services-section__card:nth-child(6):last-child) {
|
|
||||||
// For exactly 6 cards
|
|
||||||
@media (min-width: 768px) and (max-width: 1023px) {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
|
|
||||||
// Make last 2 cards center themselves if needed
|
|
||||||
.services-section__card:nth-child(5),
|
|
||||||
.services-section__card:nth-child(6) {
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ultra-compact mobile layout for very small screens
|
|
||||||
@media (max-height: 700px) and (max-width: 767px) {
|
|
||||||
.services-section {
|
|
||||||
min-height: auto;
|
|
||||||
padding: 1rem 0;
|
|
||||||
|
|
||||||
&__header {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__card {
|
|
||||||
min-height: 200px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__card-header {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
width: 1.75rem;
|
|
||||||
height: 1.75rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================
|
|
||||||
// WCAG Level AAA Accessibility Enhancements
|
|
||||||
// ================================
|
|
||||||
|
|
||||||
// Enhanced focus states for keyboard navigation
|
|
||||||
.services-section__card {
|
|
||||||
// Make cards keyboard accessible
|
|
||||||
&[tabindex='0'] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// AAA-compliant focus indicator (3px minimum)
|
|
||||||
&:focus {
|
|
||||||
outline: 3px solid var(--color-focus-ring, #2563eb);
|
|
||||||
outline-offset: 2px;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced hover and focus interaction
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: $shadow-card-hover;
|
|
||||||
|
|
||||||
// When both focused and hovered
|
|
||||||
&:focus {
|
|
||||||
outline: 3px solid var(--color-focus-ring, #2563eb);
|
|
||||||
outline-offset: 2px;
|
|
||||||
transform: translateY(-6px); // Slightly more lift when focused
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -468,47 +457,25 @@
|
||||||
// Enhance card visibility with better shadows in dark mode
|
// Enhance card visibility with better shadows in dark mode
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
animation: cardFadeIn 0.6s ease-out;
|
||||||
|
|
||||||
&:hover,
|
&:nth-child(1) {
|
||||||
&:focus {
|
animation-delay: 0.1s;
|
||||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4),
|
}
|
||||||
0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtle loading animation for cards
|
|
||||||
@keyframes cardFadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation delays for staggered effect
|
|
||||||
.services-section__card {
|
|
||||||
animation: cardFadeIn 0.6s ease-out;
|
|
||||||
|
|
||||||
&:nth-child(1) {
|
|
||||||
animation-delay: 0.1s;
|
|
||||||
}
|
|
||||||
&:nth-child(2) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
&:nth-child(3) {
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
&:nth-child(4) {
|
|
||||||
animation-delay: 0.4s;
|
|
||||||
}
|
|
||||||
&:nth-child(5) {
|
|
||||||
animation-delay: 0.5s;
|
|
||||||
}
|
|
||||||
&:nth-child(6) {
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue