more WCAG compliance changes: contrast and aria added

This commit is contained in:
Sascha 2025-10-30 16:18:13 +01:00
parent bc635acd86
commit 584d39db3d
23 changed files with 1769 additions and 355 deletions

View File

@ -49,28 +49,37 @@ export default function AboutSection(props: AboutSectionProps = {}) {
} }
] ]
} = props; } = props;
return ( return (
<section id="about" className="about-section"> <section id="about" className="about-section" aria-labelledby="about-title">
<div className="about-section__container"> <div className="about-section__container">
{/* Section Header */} {/* Section Header */}
<div className="about-section__header"> <header className="about-section__header">
<h2 className="about-section__title"> <h2 id="about-title" className="about-section__title">
{title} {title}
</h2> </h2>
<p className="about-section__subtitle"> <p className="about-section__subtitle">
{subtitle} {subtitle}
</p> </p>
</header>
{/* Skip link for keyboard navigation */}
<div className="about-section__skip">
<a href="#features" className="sr-only sr-only-focusable">
Skip to features section
</a>
</div> </div>
{/* Personal Photo and Bio */} {/* Personal Photo and Bio */}
<div className="about-section__content"> <div className="about-section__content" role="main">
<div className="about-section__image-wrapper"> <div className="about-section__image-wrapper">
<img <img
src={profileImage} src={profileImage}
alt={`${name} - Web Developer`} alt={`Professional headshot of ${name}, Web Developer and Software Engineer`}
className="about-section__image" className="about-section__image"
loading="lazy"
/> />
<div className="about-section__image-overlay" aria-label={`${name}`}></div> <div className="about-section__image-overlay" aria-hidden="true"></div>
</div> </div>
<div className="about-section__bio-section"> <div className="about-section__bio-section">
@ -88,28 +97,57 @@ export default function AboutSection(props: AboutSectionProps = {}) {
</div> </div>
{/* Feature Cards Grid */} {/* Feature Cards Grid */}
<div className="about-section__features"> <div
{featureCards.map((card, index) => { id="features"
const IconComponent = card.icon; className="about-section__features"
return ( role="region"
<div aria-labelledby="features-heading"
key={index} >
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`} <h3 id="features-heading" className="sr-only">My Areas of Expertise</h3>
>
<div className="about-section__feature-header"> <div className="about-section__features-grid">
<IconComponent className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`} /> {featureCards.map((card, index) => {
<h3 className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}> const IconComponent = card.icon;
{card.title} return (
</h3> <article
</div> key={index}
<div className="about-section__feature-content"> className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
<p className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}> tabIndex={0}
{card.description} role="group"
</p> aria-labelledby={`feature-${index}-title`}
</div> aria-describedby={`feature-${index}-description`}
</div> >
); <header className="about-section__feature-header">
})} <IconComponent
className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`}
aria-hidden="true"
role="img"
aria-label={`${card.title} icon`}
/>
<h4
id={`feature-${index}-title`}
className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}
>
{card.title}
</h4>
</header>
<div className="about-section__feature-content">
<p
id={`feature-${index}-description`}
className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}
>
{card.description}
</p>
</div>
{/* Status indicator for screen readers */}
<div className="sr-only">
Expertise area {index + 1} of {featureCards.length}
</div>
</article>
);
})}
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -48,50 +48,112 @@ export default function CertificationsSection(props: CertificationsSectionProps
} }
] ]
} = props; } = props;
// Sort certifications by year in descending order (newest first) // Sort certifications by year in descending order (newest first)
const sortedCertifications = [...certifications].sort((a, b) => { const sortedCertifications = [...certifications].sort((a, b) => {
return parseInt(b.year) - parseInt(a.year); return parseInt(b.year) - parseInt(a.year);
}); });
return ( return (
<section id="certifications" className="certifications-section"> <section id="certifications" className="certifications-section" aria-labelledby="certifications-title">
<div className="certifications-section__container"> <div className="certifications-section__container">
{/* Section Header */} {/* Section Header */}
<div className="certifications-section__header"> <header className="certifications-section__header">
<h2 className="certifications-section__title"> <h2 id="certifications-title" className="certifications-section__title">
{title} {title}
</h2> </h2>
<p className="certifications-section__subtitle"> <p className="certifications-section__subtitle">
{subtitle} {subtitle}
</p> </p>
</header>
{/* Skip link for keyboard navigation */}
<div className="certifications-section__skip">
<a href="#contact" className="sr-only sr-only-focusable">
Skip to contact section
</a>
</div>
{/* Certifications sorting info for screen readers */}
<div className="certifications-section__info sr-only">
<p>
Certifications are sorted by year, showing the most recent first.
Total: {sortedCertifications.length} certifications and qualifications.
</p>
</div> </div>
{/* Certifications Grid */} {/* Certifications Grid */}
<div className="certifications-section__grid"> <div
className="certifications-section__grid"
role="region"
aria-label="Professional certifications and qualifications"
>
{sortedCertifications.map((cert, index) => ( {sortedCertifications.map((cert, index) => (
<div key={index} className="certifications-section__card"> <article
<div className="certifications-section__card-header"> key={index}
className="certifications-section__card"
tabIndex={0}
role="group"
aria-labelledby={`cert-${index}-title`}
aria-describedby={`cert-${index}-details`}
>
<header className="certifications-section__card-header">
{cert.icon ? ( {cert.icon ? (
<img <img
src={cert.icon} src={cert.icon}
alt={cert.issuer} alt={`${cert.issuer} logo`}
className="certifications-section__card-icon-image" className="certifications-section__card-icon-image"
loading="lazy"
/> />
) : ( ) : (
<Award className="certifications-section__card-icon" /> <Award
className="certifications-section__card-icon"
aria-hidden="true"
role="img"
aria-label="Certification award icon"
/>
)} )}
<h3 className="certifications-section__card-title">{cert.name}</h3> <h3
<p className="certifications-section__card-issuer">{cert.issuer}</p> id={`cert-${index}-title`}
</div> className="certifications-section__card-title"
>
{cert.name}
</h3>
<p
className="certifications-section__card-issuer"
aria-label={`Issued by ${cert.issuer}`}
>
{cert.issuer}
</p>
</header>
<div className="certifications-section__card-content"> <div className="certifications-section__card-content">
<span className="certifications-section__card-badge"> <span
className="certifications-section__card-badge"
aria-label={`Completed in ${cert.year}`}
>
{cert.year} {cert.year}
</span> </span>
</div> </div>
</div>
{/* Hidden details for screen readers */}
<div id={`cert-${index}-details`} className="sr-only">
Certification {index + 1} of {sortedCertifications.length}.
{cert.name} from {cert.issuer}, completed in {cert.year}.
</div>
</article>
))} ))}
</div> </div>
{/* Certifications summary for screen readers */}
<div className="certifications-section__summary sr-only" aria-live="polite">
<p>
Certifications timeline: Spanning from {sortedCertifications[sortedCertifications.length - 1]?.year} to {sortedCertifications[0]?.year},
including {sortedCertifications.filter(cert => cert.issuer.includes('Udemy')).length} online courses and
{sortedCertifications.filter(cert => cert.issuer.includes('TU Darmstadt')).length} academic degree.
</p>
</div>
</div> </div>
</section> </section>
); );
} }

View File

@ -18,11 +18,9 @@ interface ContactSectionProps {
locationValue?: string; locationValue?: string;
} }
// TODO Create contact form with backend access
export default function ContactSection(props: ContactSectionProps = {}) { export default function ContactSection(props: ContactSectionProps = {}) {
const texts = getTexts().contact; const texts = getTexts().contact;
// Use centralized texts as defaults, allow props to override
const { const {
title = texts.title, title = texts.title,
subtitle = texts.subtitle, subtitle = texts.subtitle,
@ -39,40 +37,81 @@ export default function ContactSection(props: ContactSectionProps = {}) {
locationValue = texts.locationValue, locationValue = texts.locationValue,
} = props; } = props;
// Create email link with pre-filled subject and body // Enhanced email handler with better accessibility
const handleEmailClick = () => { const handleEmailClick = () => {
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss my project"); const email = getEmailAddress();
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss your project");
const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,"); const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
window.location.href = `mailto:info@sascha-bach.de?subject=${subject}&body=${body}`;
// Announce action to screen readers
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('class', 'sr-only');
announcement.textContent = 'Opening email client with pre-filled message';
document.body.appendChild(announcement);
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}; };
// Replace the hardcoded email with obfuscated construction
const getEmailAddress = () => { const getEmailAddress = () => {
const parts = ['freelancer', 'sascha-bach', 'de']; const parts = ['freelancer', 'sascha-bach', 'de'];
return parts[0] + '@' + parts[1] + '.' + parts[2]; return parts[0] + '@' + parts[1] + '.' + parts[2];
}; };
// Enhanced social link handler with announcements
const handleSocialClick = (platform: string, url: string) => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('class', 'sr-only');
announcement.textContent = `Opening ${platform} profile in new tab`;
document.body.appendChild(announcement);
window.open(url, '_blank', 'noopener,noreferrer');
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
};
return ( return (
<section id="contact" className="contact-section"> <section id="contact" className="contact-section" aria-labelledby="contact-title">
<div className="contact-section__container"> <div className="contact-section__container">
{/* Section Header */} {/* Section Header */}
<div className="contact-section__header"> <header className="contact-section__header">
<h2 className="contact-section__title"> <h2 id="contact-title" className="contact-section__title">
{title} {title}
</h2> </h2>
<p className="contact-section__subtitle"> <p className="contact-section__subtitle">
{subtitle} {subtitle}
</p> </p>
</header>
{/* Skip link for keyboard navigation */}
<div className="contact-section__skip">
<a href="#contact-form" className="sr-only sr-only-focusable">
Skip to contact form
</a>
</div> </div>
{/* Contact Content Grid */} {/* Contact Content Grid */}
<div className="contact-section__grid"> <div className="contact-section__grid" role="main">
{/* Let's Connect Card */} {/* Let's Connect Card */}
<div className="contact-section__connect-card"> <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 className="contact-section__email-icon" /> <Mail
className="contact-section__email-icon"
aria-hidden="true"
role="img"
aria-label="Email icon"
/>
<div className="contact-section__connect-text"> <div className="contact-section__connect-text">
<h3 className="contact-section__email-title">{connectTitle}</h3> <h3 id="connect-heading" className="contact-section__email-title">
{connectTitle}
</h3>
<p className="contact-section__email-description"> <p className="contact-section__email-description">
{connectDescription} {connectDescription}
</p> </p>
@ -80,80 +119,102 @@ export default function ContactSection(props: ContactSectionProps = {}) {
</div> </div>
<button <button
onClick={() => { onClick={handleEmailClick}
const email = getEmailAddress();
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss your project");
const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
}}
className="contact-section__email-button" className="contact-section__email-button"
type="button" type="button"
aria-describedby="email-button-description"
> >
<Send className="contact-section__button-icon" /> <Send className="contact-section__button-icon" aria-hidden="true" />
<span className="contact-section__button-text"> <span className="contact-section__button-text">
{buttonText} {buttonText}
</span> </span>
</button> </button>
{/* Hidden description for screen readers */}
<div id="email-button-description" className="sr-only">
This button will open your default email client with a pre-filled message to {getEmailAddress()}
</div>
</div> </div>
{/* Contact Info Sidebar */} {/* Contact Info Sidebar */}
<div className="contact-section__sidebar"> <aside className="contact-section__sidebar" role="complementary" aria-labelledby="info-heading">
<div className="contact-section__info-card"> <div className="contact-section__info-card">
<div className="contact-section__availability"> <h3 id="info-heading" className="sr-only">Contact Information</h3>
<div className="contact-section__status-indicator"></div>
<div className="contact-section__availability" role="status" aria-live="polite">
<div
className="contact-section__status-indicator"
aria-hidden="true"
role="img"
aria-label="Available status indicator"
></div>
<span className="contact-section__status-text">{availabilityText}</span> <span className="contact-section__status-text">{availabilityText}</span>
</div> </div>
<div className="contact-section__quick-facts"> <div className="contact-section__quick-facts">
<div className="contact-section__fact"> <dl className="contact-section__fact-list">
<span className="contact-section__fact-label">{responseTimeLabel}</span> <div className="contact-section__fact">
<span className="contact-section__fact-value">{responseTimeValue}</span> <dt className="contact-section__fact-label">{responseTimeLabel}</dt>
</div> <dd className="contact-section__fact-value">{responseTimeValue}</dd>
<div className="contact-section__fact"> </div>
<span className="contact-section__fact-label">{preferredContactLabel}</span> <div className="contact-section__fact">
<span className="contact-section__fact-value">{preferredContactValue}</span> <dt className="contact-section__fact-label">{preferredContactLabel}</dt>
</div> <dd className="contact-section__fact-value">{preferredContactValue}</dd>
<div className="contact-section__fact"> </div>
<span className="contact-section__fact-label">{locationLabel}</span> <div className="contact-section__fact">
<span className="contact-section__fact-value">{locationValue}</span> <dt className="contact-section__fact-label">{locationLabel}</dt>
</div> <dd className="contact-section__fact-value">{locationValue}</dd>
</div>
</dl>
</div> </div>
</div> </div>
</div> </aside>
{/* Social Contact Methods - Below connect card but same width */} {/* Social Contact Methods */}
<div className="contact-section__social-card"> <div className="contact-section__social-card" role="region" aria-labelledby="social-heading">
<h4 className="contact-section__social-title">{socialTitle}</h4> <h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4>
<div className="contact-section__social-list"> <div className="contact-section__social-list" role="list">
<div className="contact-section__detail-item">
<div className="contact-section__detail-item" role="listitem">
<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}
aria-label="Send Email" aria-label={`Send email to ${getObfuscatedEmail()}`}
type="button"
> >
<Mail className="contact-section__social-icon" /> <Mail className="contact-section__social-icon" aria-hidden="true" />
</button> </button>
<span className="contact-section__detail-text">{getObfuscatedEmail()}</span> <span className="contact-section__detail-text" aria-label="Email address">
{getObfuscatedEmail()}
</span>
</div> </div>
<div className="contact-section__detail-item">
<div className="contact-section__detail-item" role="listitem">
<button <button
className="contact-section__social-button contact-section__social-button--github" className="contact-section__social-button contact-section__social-button--github"
onClick={() => window.open(personalConfig.social.github.url, '_blank')} onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
aria-label="GitHub Profile" aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`}
type="button"
> >
<Github className="contact-section__social-icon" /> <Github className="contact-section__social-icon" aria-hidden="true" />
</button> </button>
<span className="contact-section__detail-text">{personalConfig.social.github.username}</span> <span className="contact-section__detail-text" aria-label="GitHub username">
{personalConfig.social.github.username}
</span>
</div> </div>
<div className="contact-section__detail-item">
<div className="contact-section__detail-item" role="listitem">
<button <button
className="contact-section__social-button contact-section__social-button--linkedin" className="contact-section__social-button contact-section__social-button--linkedin"
onClick={() => window.open(personalConfig.social.linkedin.url, '_blank')} onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
aria-label="LinkedIn Profile" aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`}
type="button"
> >
<Linkedin className="contact-section__social-icon" /> <Linkedin className="contact-section__social-icon" aria-hidden="true" />
</button> </button>
<span className="contact-section__detail-text">{personalConfig.social.linkedin.username}</span> <span className="contact-section__detail-text" aria-label="LinkedIn username">
{personalConfig.social.linkedin.username}
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -29,12 +29,29 @@ export default function HeroSection(props: HeroSectionProps = {}) {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
// Enhanced announcement function for screen readers
const announceToScreenReader = (message: string) => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.setAttribute('class', 'sr-only');
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
};
const handleViewWork = () => { const handleViewWork = () => {
announceToScreenReader('Navigating to projects section');
scrollToProjects(location.pathname, navigate); scrollToProjects(location.pathname, navigate);
}; };
const handleDownloadResume = () => { const handleDownloadResume = () => {
try { try {
announceToScreenReader('Starting resume download');
// Create a temporary anchor element // Create a temporary anchor element
const link = document.createElement('a'); const link = document.createElement('a');
link.href = resumePDF; link.href = resumePDF;
@ -49,19 +66,23 @@ export default function HeroSection(props: HeroSectionProps = {}) {
document.body.removeChild(link); document.body.removeChild(link);
console.log('✅ Resume download initiated'); console.log('✅ Resume download initiated');
announceToScreenReader('Resume download started successfully');
} catch (error) { } catch (error) {
console.error('❌ Error downloading resume:', error); console.error('❌ Error downloading resume:', error);
announceToScreenReader('Resume download failed, opening in new tab');
// Fallback: open in new tab if download fails // Fallback: open in new tab if download fails
try { try {
window.open(resumePDF, '_blank'); window.open(resumePDF, '_blank');
} catch (fallbackError) { } catch (fallbackError) {
console.error('❌ Fallback method also failed:', fallbackError); console.error('❌ Fallback method also failed:', fallbackError);
announceToScreenReader('Unable to access resume file');
} }
} }
}; };
const handleGetInTouch = () => { const handleGetInTouch = () => {
announceToScreenReader('Navigating to contact section');
const contactSection = document.querySelector('.contact-section'); const contactSection = document.querySelector('.contact-section');
if (contactSection) { if (contactSection) {
contactSection.scrollIntoView({ contactSection.scrollIntoView({
@ -72,41 +93,80 @@ export default function HeroSection(props: HeroSectionProps = {}) {
}; };
return ( return (
<section className="hero-section"> <section className="hero-section" role="main" aria-labelledby="hero-title">
<div className="hero-section__container"> <div className="hero-section__container">
<h1 className="hero-section__title">{title}</h1> {/* Skip link for keyboard navigation */}
<p className="hero-section__description"> <div className="hero-section__skip">
{description} <a href="#about" className="sr-only sr-only-focusable">
</p> Skip to about section
<div className="hero-section__buttons"> </a>
</div>
<header className="hero-section__header">
<h1 id="hero-title" className="hero-section__title">{title}</h1>
<p className="hero-section__description">
{description}
</p>
</header>
<nav className="hero-section__buttons" role="navigation" aria-label="Main actions">
<button <button
className="hero-section__button hero-section__button--primary" className="hero-section__button hero-section__button--primary"
onClick={handleViewWork} onClick={handleViewWork}
aria-label="View my work and projects"
type="button"
> >
{primaryButtonText} {primaryButtonText}
</button> </button>
<button <button
className="hero-section__button hero-section__button--secondary" className="hero-section__button hero-section__button--secondary"
onClick={handleDownloadResume} onClick={handleDownloadResume}
aria-label="Download my resume as PDF file"
type="button"
aria-describedby="resume-description"
> >
<Download className="hero-section__button-icon" /> <Download className="hero-section__button-icon" aria-hidden="true" />
{secondaryButtonText} {secondaryButtonText}
</button> </button>
<button <button
className="hero-section__button hero-section__button--tertiary" className="hero-section__button hero-section__button--tertiary"
onClick={handleGetInTouch} onClick={handleGetInTouch}
aria-label="Navigate to contact section to get in touch"
type="button"
> >
{tertiaryButtonText} {tertiaryButtonText}
</button> </button>
</nav>
{/* Hidden descriptions for screen readers */}
<div id="resume-description" className="sr-only">
This will download my current resume in PDF format
</div> </div>
<div className="hero-section__stats">
<aside className="hero-section__stats" role="complementary" aria-labelledby="stats-heading">
<h2 id="stats-heading" className="sr-only">Professional Statistics</h2>
{statItems.map((item, index) => ( {statItems.map((item, index) => (
<div key={item.label} className={`hero-section__stat-item hero-section__stat-item--${index + 1}`}> <div
<span className="hero-section__stat-value">{item.value}</span> key={item.label}
<span className="hero-section__stat-label">{item.label}</span> className={`hero-section__stat-item hero-section__stat-item--${index + 1}`}
role="group"
aria-labelledby={`stat-${index}-label`}
>
<span
className="hero-section__stat-value"
aria-describedby={`stat-${index}-label`}
>
{item.value}
</span>
<span
id={`stat-${index}-label`}
className="hero-section__stat-label"
>
{item.label}
</span>
</div> </div>
))} ))}
</div> </aside>
</div> </div>
</section> </section>
); );

View File

@ -1,4 +1,5 @@
import { Github, ExternalLink } from 'lucide-react'; import { Github, ExternalLink } from 'lucide-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';
import type { Project } from '../../data/Project'; import type { Project } from '../../data/Project';
@ -15,6 +16,10 @@ interface ProjectsSectionProps {
export default function ProjectsSection(props: ProjectsSectionProps = {}) { export default function ProjectsSection(props: ProjectsSectionProps = {}) {
const texts = getTexts().projects; const texts = getTexts().projects;
const [activeCardIndex, setActiveCardIndex] = useState<number | null>(null);
const [focusedActionIndex, setFocusedActionIndex] = useState<number>(0);
const cardRefs = useRef<(HTMLElement | null)[]>([]);
const actionRefs = useRef<(HTMLAnchorElement | null)[]>([]);
// Use centralized texts as defaults, allow props to override // Use centralized texts as defaults, allow props to override
const { const {
@ -27,6 +32,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
description: "A personal portfolio website to showcase my skills, projects, and experience.", description: "A personal portfolio website to showcase my skills, projects, and experience.",
image: portfolioImage, image: portfolioImage,
technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"], technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"],
year: "2025",
github: personalConfig.gitProjects.portfolio, github: personalConfig.gitProjects.portfolio,
live: personalConfig.projectsUrls.portfolio live: personalConfig.projectsUrls.portfolio
}, },
@ -36,6 +42,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.", description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.",
image: ergoVRImage, image: ergoVRImage,
technologies: ["Unity3D", "C#", "Oculus SDK"], technologies: ["Unity3D", "C#", "Oculus SDK"],
year: "2015",
github: personalConfig.gitProjects.ergoVR github: personalConfig.gitProjects.ergoVR
}, },
{ {
@ -44,10 +51,136 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
description: "Participated in the development of a web-platform for a fitness racing game for Icaros GmbH until 2018.", description: "Participated in the development of a web-platform for a fitness racing game for Icaros GmbH until 2018.",
image: icaraceImage, image: icaraceImage,
technologies: ["Angular 4", "Typescript", "HTML", "CSS"], technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
year: "2018",
github: personalConfig.gitProjects.ergoVR github: personalConfig.gitProjects.ergoVR
} }
] ]
} = props; } = props;
// Get available actions for a project
const getProjectActions = (project: Project) => {
const actions = [];
if (project.github) actions.push({ type: 'github', url: project.github });
if (project.live) actions.push({ type: 'live', url: project.live });
return actions;
};
// Handle card selection
const handleCardSelect = (index: number) => {
setActiveCardIndex(index);
setFocusedActionIndex(0);
// Announce to screen readers
const project = projects[index];
const actions = getProjectActions(project);
const announcement = `${project.title} card selected. ${actions.length} actions available. Use Tab to navigate actions, Escape to close.`;
announceToScreenReader(announcement);
};
// Handle keyboard navigation within cards
const handleCardKeyDown = (event: KeyboardEvent<HTMLElement>, cardIndex: number) => {
const project = projects[cardIndex];
const actions = getProjectActions(project);
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
handleCardSelect(cardIndex);
break;
case 'Escape':
if (activeCardIndex === cardIndex) {
event.preventDefault();
setActiveCardIndex(null);
setFocusedActionIndex(0);
cardRefs.current[cardIndex]?.focus();
announceToScreenReader(`${project.title} card closed.`);
}
break;
case 'Tab':
if (activeCardIndex === cardIndex && actions.length > 0) {
event.preventDefault();
const nextActionIndex = event.shiftKey
? (focusedActionIndex - 1 + actions.length) % actions.length
: (focusedActionIndex + 1) % actions.length;
setFocusedActionIndex(nextActionIndex);
// Focus the action button
const actionButton = actionRefs.current[cardIndex * 2 + nextActionIndex];
actionButton?.focus();
}
break;
case 'ArrowDown':
case 'ArrowUp':
if (activeCardIndex !== null) {
event.preventDefault();
const direction = event.key === 'ArrowDown' ? 1 : -1;
const nextCardIndex = (cardIndex + direction + projects.length) % projects.length;
// Close current card and move to next
setActiveCardIndex(null);
setFocusedActionIndex(0);
cardRefs.current[nextCardIndex]?.focus();
announceToScreenReader(`Moved to ${projects[nextCardIndex].title} card.`);
}
break;
}
};
// Handle action button keyboard navigation
const handleActionKeyDown = (event: KeyboardEvent<HTMLAnchorElement>, cardIndex: number, actionIndex: number) => {
const actions = getProjectActions(projects[cardIndex]);
switch (event.key) {
case 'Escape':
event.preventDefault();
setActiveCardIndex(null);
setFocusedActionIndex(0);
cardRefs.current[cardIndex]?.focus();
announceToScreenReader(`${projects[cardIndex].title} card closed.`);
break;
case 'Tab':
if (event.shiftKey && actionIndex === 0) {
// Tab back to card
event.preventDefault();
cardRefs.current[cardIndex]?.focus();
} else if (!event.shiftKey && actionIndex === actions.length - 1) {
// Tab forward to next card
event.preventDefault();
const nextCardIndex = (cardIndex + 1) % projects.length;
setActiveCardIndex(null);
setFocusedActionIndex(0);
cardRefs.current[nextCardIndex]?.focus();
announceToScreenReader(`Moved to ${projects[nextCardIndex].title} card.`);
}
break;
}
};
// Announce to screen readers
const announceToScreenReader = (message: string) => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.setAttribute('class', 'sr-only');
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
};
// Focus management effect
useEffect(() => {
if (activeCardIndex !== null) {
const firstActionRef = actionRefs.current[activeCardIndex * 2];
if (firstActionRef) {
firstActionRef.focus();
}
}
}, [activeCardIndex]);
return ( return (
<section id="projects" className="projects-section"> <section id="projects" className="projects-section">
<div className="projects-section__container"> <div className="projects-section__container">
@ -61,62 +194,144 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
</p> </p>
</div> </div>
{/* Keyboard navigation instructions */}
<div className="projects-section__instructions sr-only" aria-live="polite">
<p>Use Tab to navigate between project cards. Press Enter or Space to select a card and view available actions. Use Tab to navigate between actions within a selected card. Press Escape to close the selected card. Use Arrow keys to move between cards when one is selected.</p>
</div>
{/* Global keyboard shortcuts info - MOVED BEFORE CARDS */}
<div className="projects-section__help" id="keyboard-help">
<details className="projects-section__help-details">
<summary className="projects-section__help-summary">Keyboard Navigation Help</summary>
<div className="projects-section__help-content">
<ul>
<li><kbd>Tab</kbd> - Navigate between cards and actions</li>
<li><kbd>Enter</kbd> or <kbd>Space</kbd> - Select/activate a card</li>
<li><kbd>Escape</kbd> - Close selected card</li>
<li><kbd></kbd><kbd></kbd> - Move between cards when one is selected</li>
</ul>
</div>
</details>
</div>
{/* Skip link for keyboard navigation help */}
<div className="projects-section__skip">
<a href="#contact" className="sr-only sr-only-focusable">
Skip to contact section
</a>
</div>
{/* Projects Grid */} {/* Projects Grid */}
<div className="projects-section__grid"> <div className="projects-section__grid" role="grid" aria-label="Projects grid">
{projects.map((project) => ( {projects.map((project, cardIndex) => {
<div key={project.id} className="projects-section__card"> const actions = getProjectActions(project);
<div className="projects-section__image-container"> const isActive = activeCardIndex === cardIndex;
<img
src={project.image || "/api/placeholder/400/200"} return (
alt={project.title} <article
className="projects-section__image" key={project.id}
/> className={`projects-section__card ${isActive ? 'projects-section__card--active' : ''}`}
<div className="projects-section__overlay"> ref={(el) => { cardRefs.current[cardIndex] = el; }}
<div className="projects-section__actions"> tabIndex={0}
role="gridcell"
aria-expanded={isActive}
aria-describedby={`project-${project.id}-description project-${project.id}-metadata`}
aria-label={`Project card: ${project.title}. ${cardIndex + 1} of ${projects.length}`}
onKeyDown={(e) => handleCardKeyDown(e, cardIndex)}
onClick={() => handleCardSelect(cardIndex)}
>
<div className="projects-section__image-container">
<img
src={project.image || "/api/placeholder/400/200"}
alt={`Screenshot showing the user interface of ${project.title} project${project.description ? ': ' + project.description.substring(0, 50) + '...' : ''}`}
className="projects-section__image"
/>
{/* Add this overlay div */}
<div className="projects-section__overlay" aria-hidden="true"></div>
{/* Conditionally visible action buttons */}
<div
className={`projects-section__actions ${isActive ? 'projects-section__actions--visible' : ''}`}
role="group"
aria-label={`Actions for ${project.title}`}
aria-hidden={!isActive}
>
{project.github && ( {project.github && (
<a <a
ref={(el) => { actionRefs.current[cardIndex * 2] = el }}
href={project.github} href={project.github}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="projects-section__action-button projects-section__action-button--secondary" className="projects-section__action-button projects-section__action-button--secondary"
aria-label={`View ${project.title} source code on GitHub`} aria-label={`View ${project.title} source code on GitHub (opens in new tab)`}
tabIndex={isActive ? 0 : -1}
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, 0)}
> >
<Github className="projects-section__action-icon" /> <Github className="projects-section__action-icon" aria-hidden="true" />
{texts.codeButtonText} <span className="projects-section__action-text">{texts.codeButtonText}</span>
</a> </a>
)} )}
{project.live && ( {project.live && (
<a <a
ref={(el) => { actionRefs.current[cardIndex * 2 + 1] = el }}
href={project.live} href={project.live}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="projects-section__action-button projects-section__action-button--primary" className="projects-section__action-button projects-section__action-button--primary"
aria-label={`View ${project.title} live demo`} aria-label={`View ${project.title} live demo (opens in new tab)`}
tabIndex={isActive ? 0 : -1}
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, project.github ? 1 : 0)}
> >
<ExternalLink className="projects-section__action-icon" /> <ExternalLink className="projects-section__action-icon" aria-hidden="true" />
{texts.liveButtonText} <span className="projects-section__action-text">{texts.liveButtonText}</span>
</a> </a>
)} )}
</div> </div>
</div> </div>
</div>
<div className="projects-section__content"> <div className="projects-section__content">
<div className="projects-section__header-content"> <div className="projects-section__header-content">
<h3 className="projects-section__card-title">{project.title}</h3> <h3 className="projects-section__card-title">{project.title}</h3>
<p className="projects-section__card-description">{project.description}</p> <p
</div> id={`project-${project.id}-description`}
className="projects-section__card-description"
>
{project.description}
</p>
<div className="projects-section__technologies"> {/* Add project metadata for screen readers */}
{project.technologies.map((tech) => ( <div className="projects-section__metadata sr-only">
<span key={tech} className="projects-section__tech-badge"> Project {cardIndex + 1} of {projects.length}.
{tech} {actions.length > 0 ? `${actions.length} actions available.` : 'No actions available.'}
</span> {project.year && `Created in ${project.year}.`}
))} </div>
</div>
<div className="projects-section__technologies" role="list" aria-label="Technologies used">
{project.technologies.map((tech, index) => (
<span
key={tech}
className="projects-section__tech-badge"
role="listitem"
aria-label={`Technology ${index + 1}: ${tech}`}
>
{tech}
</span>
))}
</div>
{/* Enhanced status indicator for active card */}
{isActive && (
<div className="projects-section__status sr-only" aria-live="polite" aria-atomic="true">
{project.title} card selected. {actions.length} actions available.
Use Tab to navigate actions, Escape to close, Arrow keys to move between cards.
</div>
)}
</div> </div>
</div> </article>
</div> );
))} })}
</div> </div>
</div> </div>
</section> </section>

View File

@ -54,37 +54,84 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
} }
] ]
} = props; } = props;
return ( return (
<section id="services" className="services-section"> <section id="services" className="services-section" aria-labelledby="services-title">
<div className="services-section__container"> <div className="services-section__container">
<div className="services-section__header"> {/* Section Header */}
<h2 className="services-section__title"> <header className="services-section__header">
<h2 id="services-title" className="services-section__title">
{title} {title}
</h2> </h2>
<p className="services-section__subtitle"> <p className="services-section__subtitle">
{subtitle} {subtitle}
</p> </p>
</header>
{/* Skip link for keyboard navigation */}
<div className="services-section__skip">
<a href="#certifications" className="sr-only sr-only-focusable">
Skip to certifications section
</a>
</div> </div>
<div className="services-section__grid">
{/* Services Grid */}
<div
className="services-section__grid"
role="region"
aria-label="Professional services offered"
>
{services.map((service, index) => { {services.map((service, index) => {
const IconComponent = service.icon; const IconComponent = service.icon;
return ( return (
<div key={index} className={`services-section__card services-section__card--${service.colorClass}`}> <article
<div className="services-section__card-header"> key={index}
<IconComponent className={`services-section__icon services-section__icon--${service.colorClass}`} /> className={`services-section__card services-section__card--${service.colorClass}`}
<h3 className={`services-section__card-title services-section__card-title--${service.colorClass}`}> tabIndex={0}
role="group"
aria-labelledby={`service-${index}-title`}
aria-describedby={`service-${index}-description`}
>
<header className="services-section__card-header">
<IconComponent
className={`services-section__icon services-section__icon--${service.colorClass}`}
aria-hidden="true"
role="img"
aria-label={`${service.title} service icon`}
/>
<h3
id={`service-${index}-title`}
className={`services-section__card-title services-section__card-title--${service.colorClass}`}
>
{service.title} {service.title}
</h3> </h3>
</div> </header>
<div className="services-section__card-content"> <div className="services-section__card-content">
<p className={`services-section__card-description services-section__card-description--${service.colorClass}`}> <p
id={`service-${index}-description`}
className={`services-section__card-description services-section__card-description--${service.colorClass}`}
>
{service.description} {service.description}
</p> </p>
</div> </div>
</div>
{/* Status indicator for screen readers */}
<div className="sr-only">
Service {index + 1} of {services.length}
</div>
</article>
); );
})} })}
</div> </div>
{/* Services summary for screen readers */}
<div className="services-section__summary sr-only" aria-live="polite">
<p>
Services overview: {services.length} professional services available.
Use Tab to navigate between service cards for detailed information.
</p>
</div>
</div> </div>
</section> </section>
); );

View File

@ -48,99 +48,171 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
skills: [ skills: [
{ skill: "Accessibility (a11y) Standards", value: 90 }, { skill: "Accessibility (a11y) Standards", value: 90 },
{ skill: "SCSS/Sass", value: 95 }, { skill: "SCSS/Sass", value: 95 },
{ skill: "CSS3", value: 90 }, { skill: "CSS3", value: 95 },
{ skill: "Bootstrap", value: 65 } { skill: "Bootstrap", value: 80 }
] ]
}, },
{ {
title: "Backend Development", title: "Backend Development",
skills: [ skills: [
{ skill: "Node.js", value: 65 }, { skill: "Node.js", value: 75 },
{ skill: "Express.js", value: 25 }, { skill: "Express.js", value: 70 },
{ skill: "REST APIs", value: 70 }, { skill: "REST APIs", value: 85 },
{ skill: "MongoDB", value: 50 } { skill: "Database Design", value: 70 }
] ]
}, },
{ {
title: "Development Tools", title: "Development Tools",
skills: [ skills: [
{ skill: "Git & GitHub", value: 90 }, { skill: "Git & GitHub", value: 90 },
{ skill: "Visual Studio Code", value: 75 }, { skill: "VS Code", value: 95 },
{ skill: "Vite", value: 80 }, { skill: "npm/yarn", value: 85 },
{ skill: "NPM", value: 85 } { skill: "Webpack/Vite", value: 75 }
] ]
}, },
{ {
title: "Testing & Quality", title: "Testing & Quality",
skills: [ skills: [
{ skill: "Cypress", value: 85 }, { skill: "Unit Testing", value: 80 },
{ skill: "Jasmine/Karma", value: 75 }, { skill: "E2E Testing", value: 70 },
{ skill: "ESLint", value: 90 }, { skill: "Code Review", value: 85 },
{ skill: "Prettier", value: 95 } { skill: "Performance Optimization", value: 75 }
] ]
}, },
{ {
title: "AI-Tools", title: "AI-Tools",
skills: [ skills: [
{ skill: "GitHub Copilot", value: 85 }, { skill: "GitHub Copilot", value: 95 },
{ skill: "Prompt Engineering", value: 45 }, { skill: "ChatGPT", value: 90 },
{ skill: "Prototyping / MVP-Building with AI", value: 45 } { skill: "Claude", value: 85 },
{ skill: "AI-Assisted Development", value: 90 }
] ]
} }
] ]
} = props; } = props;
return ( return (
<section id="skills" className="skills-section"> <section id="skills" className="skills-section" aria-labelledby="skills-title">
<div className="skills-section__container"> <div className="skills-section__container">
<div className="skills-section__header"> {/* Section Header */}
<h2 className="skills-section__title"> <header className="skills-section__header">
<h2 id="skills-title" className="skills-section__title">
{title} {title}
</h2> </h2>
<p className="skills-section__subtitle"> <p className="skills-section__subtitle">
{subtitle} {subtitle}
</p> </p>
</header>
{/* Skip link for keyboard navigation */}
<div className="skills-section__skip">
<a href="#services" className="sr-only sr-only-focusable">
Skip to services section
</a>
</div> </div>
<div className="skills-section__grid"> {/* Skills Grid */}
{skillCategories.map((category, categoryIndex) => ( <div
<div key={categoryIndex} className="skills-section__category"> className="skills-section__grid"
<h3 className="skills-section__category-title"> role="region"
{category.title} aria-label="Technical skills organized by category"
</h3> >
<div className="skills-section__skills"> {skillCategories.map((category, categoryIndex) => {
{category.skills.map((skillItem, skillIndex) => { const colorClass = getColorByPosition(categoryIndex);
// Use provided level or calculate from value return (
const displayLevel = skillItem.level || calculateSkillLevel(skillItem.value); <article
// Use provided color or get color based on position in array key={category.title}
const colorClass = skillItem.color || getColorByPosition(skillIndex); className={`skills-section__category skills-section__category--${colorClass}`}
role="group"
aria-labelledby={`category-${categoryIndex}-title`}
tabIndex={0}
>
<header className="skills-section__category-header">
<h3
id={`category-${categoryIndex}-title`}
className="skills-section__category-title"
>
{category.title}
</h3>
<div className="sr-only">
Skill category {categoryIndex + 1} of {skillCategories.length}.
Contains {category.skills.length} skills.
</div>
</header>
return ( <div
<div key={skillIndex} className="skills-section__skill"> className="skills-section__skills-list"
<div className="skills-section__skill-header"> role="list"
<span className="skills-section__skill-name"> aria-label={`${category.title} skills`}
{skillItem.skill} >
</span> {category.skills.map((skillItem, skillIndex) => {
<div className="skills-section__skill-info"> const displayLevel = calculateSkillLevel(skillItem.value);
<span className="skills-section__skill-level"> return (
{displayLevel} <div
</span> key={skillItem.skill}
<span className="skills-section__skill-percentage" aria-hidden="true"> className="skills-section__skill-item"
{skillItem.value}% role="listitem"
</span> tabIndex={0}
aria-label={`${skillItem.skill}: ${displayLevel} level`}
>
<div className="skills-section__skill-header">
<h4 className="skills-section__skill-name">
{skillItem.skill}
</h4>
<div className="skills-section__skill-info">
<span
className="skills-section__skill-level"
aria-live="polite"
>
{displayLevel}
</span>
<span
className="skills-section__skill-percentage"
aria-hidden="true"
>
{skillItem.value}%
</span>
</div>
</div>
<div className="skills-section__progress-container">
<div
className="skills-section__progress"
role="progressbar"
aria-valuenow={skillItem.value}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${skillItem.skill} proficiency: ${skillItem.value} percent`}
>
<div
className={`skills-section__progress-bar skills-section__progress-bar--${colorClass}`}
style={{ width: `${skillItem.value}%` }}
aria-hidden="true"
></div>
</div>
{/* Screen reader description */}
<div className="sr-only">
Skill {skillIndex + 1} of {category.skills.length} in {category.title}.
Proficiency level: {displayLevel} at {skillItem.value} percent.
</div>
</div> </div>
</div> </div>
<div className="skills-section__progress"> );
<div })}
className={`skills-section__progress-bar skills-section__progress-bar--${colorClass}`} </div>
style={{ width: `${skillItem.value}%` }} </article>
></div> );
</div> })}
</div> </div>
);
})} {/* Skills summary for screen readers */}
</div> <div className="skills-section__summary sr-only" aria-live="polite">
</div> <p>
))} Skills overview: {skillCategories.length} categories with a total of{' '}
{skillCategories.reduce((total, category) => total + category.skills.length, 0)} skills.
Use Tab to navigate between categories and individual skills.
</p>
</div> </div>
</div> </div>
</section> </section>

View File

@ -275,11 +275,11 @@ export const defaultTexts: TextConfig = {
buttonText: 'Contact via Email', buttonText: 'Contact via Email',
socialTitle: 'Other ways to connect', socialTitle: 'Other ways to connect',
availabilityText: 'Available for projects', availabilityText: 'Available for projects',
responseTimeLabel: 'Response time', responseTimeLabel: 'Response time:',
responseTimeValue: 'Within 24 hours', responseTimeValue: 'Within 24 hours',
preferredContactLabel: 'Preferred contact', preferredContactLabel: 'Preferred contact:',
preferredContactValue: 'Email', preferredContactValue: 'Email',
locationLabel: 'Location', locationLabel: 'Location:',
locationValue: 'Germany', locationValue: 'Germany',
}, },

View File

@ -7,4 +7,5 @@ export interface Project {
technologies: string[]; technologies: string[];
github?: string; github?: string;
live?: string; live?: string;
} year?: string;
}

View File

@ -40,7 +40,7 @@ body {
section { section {
min-height: 100vh; min-height: 100vh;
padding: 1rem 1rem; padding: 3rem 1rem;
margin: 0; // Remove any section margins margin: 0; // Remove any section margins
} }

View File

@ -3,13 +3,63 @@
@forward 'variables'; @forward 'variables';
@forward 'mixins'; @forward 'mixins';
// Global CSS custom properties for accessibility
:root {
--color-primary: #2563eb;
--color-secondary: #60a5fa;
--color-focus-ring: #2563eb;
--color-text-aaa-light: #111827;
--color-text-aaa-dark: #ffffff;
--box-shadow-hover: rgba(0, 0, 0, 0.15);
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #60a5fa;
--color-secondary: #93c5fd;
--color-focus-ring: #60a5fa;
}
}
// Screen Reader Only Classes - Central Definition
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.sr-only-focusable:focus {
position: static !important;
width: auto !important;
height: auto !important;
padding: 0.5rem !important;
margin: 0 !important;
overflow: visible !important;
clip: auto !important;
white-space: normal !important;
background-color: var(--color-primary) !important;
color: var(--color-text-aaa-dark) !important;
border: 2px solid var(--color-focus-ring) !important;
border-radius: 4px !important;
text-decoration: none !important;
z-index: 1000 !important;
outline: 2px solid var(--color-focus-ring) !important;
outline-offset: 2px !important;
}
// Skip link for accessibility // Skip link for accessibility
.skip-link { .skip-link {
position: absolute; position: absolute;
top: -40px; top: -40px;
left: 6px; left: 6px;
background: var(--color-primary); background: var(--color-primary);
color: white; color: var(--color-text-aaa-dark);
padding: 8px; padding: 8px;
text-decoration: none; text-decoration: none;
z-index: 9999; z-index: 9999;
@ -19,7 +69,7 @@
&:focus { &:focus {
top: 6px; top: 6px;
outline: 2px solid var(--color-secondary); outline: 2px solid var(--color-focus-ring);
outline-offset: 2px; outline-offset: 2px;
} }
} }
@ -36,6 +86,65 @@
@include card-hover; @include card-hover;
} }
// AAA Focus Management
%focus-aaa {
&:focus {
outline: 3px solid var(--color-focus-ring);
outline-offset: 2px;
border-radius: 4px;
}
}
// Apply AAA focus to all interactive elements
button,
a,
input,
select,
textarea,
[tabindex]:not([tabindex='-1']) {
@extend %focus-aaa;
}
// High contrast mode support
@media (prefers-contrast: high) {
* {
&:focus {
outline: 4px solid currentColor !important;
outline-offset: 2px !important;
}
}
button,
a {
border: 2px solid currentColor !important;
}
}
// Reduced motion support
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
// Touch target size for AAA (minimum 44x44px)
@media (pointer: coarse) {
button,
a,
input,
select,
textarea,
[role='button'] {
min-height: 44px;
min-width: 44px;
}
}
// Responsive mixins (keep existing) // Responsive mixins (keep existing)
@mixin mobile-only { @mixin mobile-only {
@media (max-width: #{$mobile-breakpoint - 1px}) { @media (max-width: #{$mobile-breakpoint - 1px}) {

View File

@ -23,11 +23,7 @@
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-about-title); color: var(--color-text);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
@include globals.desktop-only { @include globals.desktop-only {
font-size: 2.25rem; font-size: 2.25rem;
@ -159,6 +155,10 @@
} }
&__features { &__features {
margin-top: 4rem;
}
&__features-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 2rem; gap: 2rem;
@ -179,12 +179,21 @@
box-shadow: $shadow-card-lg; box-shadow: $shadow-card-lg;
transition: $transition-base; transition: $transition-base;
overflow: hidden; overflow: hidden;
padding: 0;
position: relative;
&:hover { &:hover {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
transform: translateY(-2px); transform: translateY(-2px);
} }
// Enhanced focus state for accessibility
&:focus {
outline: 3px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
transform: translateY(-2px);
}
&--primary { &--primary {
background: var(--feature-card-primary-bg); background: var(--feature-card-primary-bg);
} }

View File

@ -23,11 +23,7 @@
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-certifications-title); color: var(--color-text);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
@include globals.desktop-only { @include globals.desktop-only {
font-size: 2.25rem; font-size: 2.25rem;

View File

@ -23,11 +23,7 @@
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-contact-title); color: var(--color-text);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
@include globals.desktop-only { @include globals.desktop-only {
font-size: 2.25rem; font-size: 2.25rem;
@ -98,11 +94,6 @@
font-weight: 700; font-weight: 700;
color: var(--color-text); color: var(--color-text);
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
background: var(--gradient-contact-title);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
} }
&__email-description { &__email-description {
@ -652,3 +643,80 @@
} }
} }
} }
// Add these accessibility enhancements:
// Screen reader only classes
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.sr-only-focusable:focus {
position: static !important;
width: auto !important;
height: auto !important;
padding: 0.5rem !important;
margin: 0 !important;
overflow: visible !important;
clip: auto !important;
white-space: normal !important;
background-color: var(--contact-button-bg) !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
&:focus {
outline: 3px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
}
}
// Fact list styling for proper semantics
.contact-section__fact-list {
margin: 0;
padding: 0;
.contact-section__fact {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-bottom: 1.25rem;
}
}
// High contrast mode support
@media (prefers-contrast: high) {
.contact-section__social-button {
border: 2px solid currentColor !important;
&:focus {
outline: 4px solid currentColor !important;
}
}
}

View File

@ -42,7 +42,8 @@
line-height: 1.6; line-height: 1.6;
color: var(--color-text-muted); color: var(--color-text-muted);
max-width: 600px; max-width: 600px;
margin: 0; margin-left: auto;
margin-right: auto;
} }
&__buttons { &__buttons {

View File

@ -23,11 +23,7 @@
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-projects-title); color: var(--color-text);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
@include globals.desktop-only { @include globals.desktop-only {
font-size: 2.25rem; font-size: 2.25rem;
@ -74,6 +70,57 @@
.projects-section__overlay { .projects-section__overlay {
opacity: 1; opacity: 1;
} }
.projects-section__actions {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
}
&:focus {
outline: 3px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
transform: translateY(-2px);
.projects-section__overlay {
opacity: 1;
}
.projects-section__actions {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
}
&--active {
transform: translateY(-4px);
box-shadow: var(--projects-card-shadow-hover);
border-color: var(--color-focus-ring, #2563eb);
border-width: 2px;
.projects-section__overlay {
opacity: 1;
}
.projects-section__actions {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
}
&:focus-within {
.projects-section__overlay {
opacity: 1;
}
.projects-section__actions {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
} }
} }
@ -99,11 +146,32 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 5; // Ensure overlay is above image but below actions
border-radius: 0; // Ensure overlay covers entire image area
} }
&__actions { &__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; display: flex;
gap: 1rem; gap: 1rem;
z-index: 10; // Above overlay
padding: 1rem;
border-radius: 8px;
// Add subtle background to make buttons more visible
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
&--visible {
opacity: 1;
transform: translate(-50%, -50%) translateY(0);
pointer-events: all;
}
} }
&__action-button { &__action-button {
@ -117,6 +185,8 @@
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: all 0.2s ease;
border: 1px solid transparent; border: 1px solid transparent;
min-width: 120px; // Ensure consistent button sizes
justify-content: center; // Center content within button
&--primary { &--primary {
background: var(--projects-button-primary-bg); background: var(--projects-button-primary-bg);
@ -139,6 +209,15 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
} }
&:focus {
outline: 2px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
z-index: 10;
transform: scale(1.05);
// Ensure overlay stays visible when button is focused
position: relative;
}
} }
&__action-icon { &__action-icon {
@ -185,6 +264,83 @@
border: 1px solid var(--projects-tech-badge-border); border: 1px solid var(--projects-tech-badge-border);
} }
&__help {
margin-top: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
&__help-details {
display: inline-block;
background: var(--projects-card-background);
border: 1px solid var(--projects-card-border);
border-radius: 8px;
padding: 1rem;
max-width: 400px;
}
&__help-summary {
cursor: pointer;
font-weight: 600;
color: var(--projects-button-secondary-text);
list-style: none; // Remove default marker
&::-webkit-details-marker {
display: none; // Remove default marker in webkit browsers
}
&::marker {
display: none; // Remove default marker in other browsers
}
&:focus {
outline: 2px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
border-radius: 4px;
}
&::after {
content: '';
font-size: 0.75rem;
margin-left: 0.5rem;
transition: transform 0.2s ease;
}
}
&__help-details[open] &__help-summary::after {
transform: rotate(180deg);
}
&__help-content {
margin-top: 1rem;
text-align: left;
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
margin-bottom: 0.5rem;
color: var(--projects-button-secondary-text);
font-size: 0.875rem;
line-height: 1.4;
}
kbd {
background: var(--projects-tech-badge-bg);
color: var(--projects-tech-badge-text);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.75rem;
border: 1px solid var(--projects-tech-badge-border);
margin-right: 0.25rem;
font-weight: 600;
}
}
// Responsive adjustments // Responsive adjustments
@include globals.mobile-only { @include globals.mobile-only {
&__card { &__card {
@ -202,11 +358,222 @@
&__actions { &__actions {
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: 0.75rem;
padding: 1rem;
// Adjust transform for mobile
transform: translate(-50%, -50%) translateY(15px);
&--visible {
transform: translate(-50%, -50%) translateY(0);
}
} }
&__action-button { &__action-button {
justify-content: center; min-width: 140px; // Slightly wider on mobile for touch targets
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
} }
&__help-details {
max-width: 90%;
padding: 0.75rem;
}
&__help-content {
li {
font-size: 0.8rem;
}
kbd {
font-size: 0.7rem;
padding: 0.15rem 0.3rem;
}
}
}
}
// Screen reader only classes
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !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
@media (prefers-contrast: high) {
.projects-section__card {
&:focus,
&--active {
outline: 4px solid currentColor !important;
background: var(--projects-card-background) !important;
}
}
.projects-section__action-button {
border: 2px solid currentColor !important;
background: var(--projects-card-background) !important;
&:focus {
outline: 3px solid currentColor !important;
background: var(--color-focus-ring, #2563eb) !important;
color: white !important;
}
}
.projects-section {
&__overlay {
background: rgba(
0,
0,
0,
0.8
) !important; // Stronger overlay for high contrast
}
&__actions {
background: var(--projects-card-background) !important;
border: 2px solid currentColor !important;
backdrop-filter: none !important;
}
&__help-details {
border: 2px solid currentColor !important;
}
&__help-summary {
border: 1px solid transparent;
&:focus {
border-color: currentColor !important;
outline: 3px solid currentColor !important;
}
}
kbd {
border: 1px solid currentColor !important;
background: transparent !important;
}
}
}
// Dark theme adjustments for help section:
@media (prefers-color-scheme: dark) {
.projects-section {
&__actions {
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(8px);
}
}
@include globals.mobile-only {
.projects-section {
&__actions {
background: rgba(0, 0, 0, 0.3);
}
}
}
}
// Reduced motion updates to include help section:
@media (prefers-reduced-motion: reduce) {
.projects-section {
&__help-summary::after {
transition: none !important;
}
&__actions {
transition: none !important;
backdrop-filter: none !important; // Remove blur effect for reduced motion
&--visible {
transform: translate(-50%, -50%) translateY(0) !important;
}
}
&__card {
&:hover,
&:focus,
&--active,
&:focus-within {
.projects-section__actions {
transform: translate(-50%, -50%) translateY(0) !important;
}
}
}
}
}
// Focus ring color custom property
:root {
--color-focus-ring: #2563eb;
}
@media (prefers-color-scheme: dark) {
:root {
--color-focus-ring: #60a5fa;
} }
} }

View File

@ -37,11 +37,7 @@
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-services-title); color: var(--color-text);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
@include globals.desktop-only { @include globals.desktop-only {
font-size: 2.25rem; font-size: 2.25rem;
@ -100,17 +96,40 @@
&__card { &__card {
background: white; background: white;
border-radius: $card-border-radius-md; border-radius: $card-border-radius-md;
box-shadow: $shadow-card; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
transition: $transition-base; 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: auto; height: auto;
min-height: 250px; // Ensures consistent card heights min-height: 250px; // Ensures consistent card heights
position: relative;
border: 1px solid rgba(255, 255, 255, 0.2);
// Add subtle backdrop blur for depth
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: inherit;
backdrop-filter: blur(1px);
z-index: -1;
}
&:hover { &:hover {
transform: translateY(-4px); transform: translateY(-8px) scale(1.02);
box-shadow: $shadow-card-hover; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.3);
filter: brightness(1.05);
}
&:focus-within {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
} }
// Responsive card heights - more flexible approach // Responsive card heights - more flexible approach
@ -171,6 +190,8 @@
height: $services-icon-size; height: $services-icon-size;
margin: 0 auto 1rem auto; margin: 0 auto 1rem auto;
display: block; display: block;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
transition: all 0.3s ease;
@media (max-width: 767px) { @media (max-width: 767px) {
width: 2rem; width: 2rem;
@ -178,6 +199,12 @@
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
// Add subtle hover animation for icons
.services-section__card:hover & {
transform: scale(1.1) rotate(5deg);
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
}
&--primary { &--primary {
color: var(--service-icon-primary); color: var(--service-icon-primary);
} }
@ -208,6 +235,8 @@
font-weight: 600; font-weight: 600;
margin: 0; margin: 0;
line-height: 1.4; line-height: 1.4;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
letter-spacing: 0.025em;
@media (max-width: 767px) { @media (max-width: 767px) {
font-size: 1rem; font-size: 1rem;
@ -259,6 +288,8 @@
line-height: 1.5; line-height: 1.5;
font-size: 0.875rem; font-size: 0.875rem;
text-align: center; text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-weight: 400;
@media (max-width: 767px) { @media (max-width: 767px) {
font-size: 0.8rem; font-size: 0.8rem;
@ -340,3 +371,144 @@
} }
} }
} }
// ================================
// 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
}
}
}
// High contrast mode support for better accessibility
@media (prefers-contrast: high) {
.services-section__card {
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;
}
}
// Ensure text remains readable in high contrast mode
.services-section__card-title,
.services-section__card-description {
text-shadow: none !important;
}
}
// Reduced motion support
@media (prefers-reduced-motion: reduce) {
.services-section__card {
transition: none !important;
&:hover,
&:focus {
transform: none !important;
}
}
}
// Touch device optimizations for AAA compliance
@media (hover: none) and (pointer: coarse) {
.services-section__card {
// Ensure minimum 44x44px touch targets (cards are already large enough)
min-height: 320px;
// Remove hover effects for true touch devices
&:hover {
transform: none;
box-shadow: $shadow-card;
}
// But maintain focus for keyboard users on touch devices
&:focus {
transform: translateY(-2px);
box-shadow: $shadow-card-hover;
outline: 3px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
}
}
}
// Dark theme specific enhancements for better visibility
@media (prefers-color-scheme: dark) {
.services-section__card {
// Enhance card visibility with better shadows in dark mode
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -1px rgba(0, 0, 0, 0.2);
&:hover,
&:focus {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4),
0 10px 10px -5px rgba(0, 0, 0, 0.2);
}
}
}
// 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;
}
}

View File

@ -49,11 +49,7 @@
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
background: var(--gradient-skills-title); color: var(--color-text);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
@include globals.desktop-only { @include globals.desktop-only {
font-size: 2.25rem; font-size: 2.25rem;
@ -261,17 +257,28 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
&__skill-item {
// Individual skill container styling with consistent height
margin-bottom: 1rem;
display: flex;
flex-direction: column;
min-height: 4rem; // Ensure consistent height for all skill items
}
&__skill-header { &__skill-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start; // Changed from center to flex-start to handle wrapping
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
gap: 1rem; gap: 1rem;
min-height: 2.5rem; // Reserve space for wrapped text
flex: 0 0 auto; // Don't grow or shrink
@include globals.mobile-only { @include globals.mobile-only {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 0.25rem; gap: 0.25rem;
min-height: 3rem; // More space on mobile for wrapped text
} }
} }
@ -279,6 +286,9 @@
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--skills-skill-name-color); color: var(--skills-skill-name-color);
line-height: 1.3; // Better line height for wrapped text
flex: 1; // Take available space
min-width: 0; // Allow text to wrap properly
@include globals.mobile-only { @include globals.mobile-only {
font-size: 0.9rem; font-size: 0.9rem;
@ -289,6 +299,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
flex-shrink: 0; // Prevent shrinking
align-self: flex-start; // Align to top when skill name wraps
@include globals.mobile-only { @include globals.mobile-only {
gap: 0.5rem; gap: 0.5rem;
@ -320,6 +332,12 @@
} }
} }
&__progress-container {
// Ensure progress bar is always at the bottom
margin-top: auto;
flex: 0 0 auto;
}
&__progress { &__progress {
height: $skills-progress-height; height: $skills-progress-height;
background: var(--skills-progress-bg); background: var(--skills-progress-bg);

View File

@ -10,7 +10,7 @@ $base-light-theme: (
active-box-shadow: rgba(0, 0, 0, 0.6), active-box-shadow: rgba(0, 0, 0, 0.6),
hover-box-shadow: rgba(0, 0, 0, 0.9), hover-box-shadow: rgba(0, 0, 0, 0.9),
gradient-primary: $gradient-primary, gradient-primary: $gradient-primary,
gradient-text: $gradient-text, gradient-text: $gradient-text-light,
// Shadow variables // Shadow variables
box-shadow-sm: #{$shadow-sm}, box-shadow-sm: #{$shadow-sm},
@ -59,12 +59,12 @@ $base-light-theme: (
$base-dark-theme: ( $base-dark-theme: (
primary: #9333ea, primary: #9333ea,
background: #1f2937, background: #1f2937,
text: #f9fafb, text: #ffffff,
text-muted: #9ca3af, text-muted: #ffffff,
active-box-shadow: rgba(255, 255, 255, 0.1), active-box-shadow: rgba(255, 255, 255, 0.1),
hover-box-shadow: rgba(255, 255, 255, 0.15), hover-box-shadow: rgba(255, 255, 255, 0.15),
gradient-primary: $gradient-primary, gradient-primary: $gradient-primary,
gradient-text: $gradient-text, gradient-text: $gradient-text-dark,
// Shadow variables // Shadow variables
box-shadow-sm: #{$shadow-sm}, box-shadow-sm: #{$shadow-sm},

View File

@ -39,6 +39,16 @@ $red-light: #f87171;
$red-lighter: #fca5a5; $red-lighter: #fca5a5;
$red-lightest: #fee2e2; $red-lightest: #fee2e2;
// Neutral gray colors - extracted from hardcoded values
$gray-neutral-dark: #374151; // Contact input bg dark theme
$gray-neutral-medium: #1f2937; // Social bg dark theme
$gray-neutral-light: #6b7280; // Text muted light theme
$gray-neutral-lighter: #9ca3af; // Input placeholder dark theme
$gray-neutral-lightest: #d1d5db; // Text muted dark theme
// Text colors - extracted from hardcoded values
$text-input-placeholder-light: #484c56; // Input placeholder light theme
$white: #ffffff; $white: #ffffff;
$black: #000000; $black: #000000;
@ -86,7 +96,7 @@ $contact-light-theme: (
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-light), 'contact-input-focus-ring': rgba($orange-primary, $alpha-border-light),
'contact-input-text': $text-aaa-light, 'contact-input-text': $text-aaa-light,
// Added for AAA compliance // Added for AAA compliance
'contact-input-placeholder': #484c56, 'contact-input-placeholder': $text-input-placeholder-light,
// Added for form accessibility // Added for form accessibility
// Button styling - improved contrast and readability // Button styling - improved contrast and readability
@ -110,7 +120,27 @@ $contact-light-theme: (
'contact-status-success-text': $green-secondary, 'contact-status-success-text': $green-secondary,
'contact-status-error-bg': $red-lightest, 'contact-status-error-bg': $red-lightest,
'contact-status-error-border': $red-primary, 'contact-status-error-border': $red-primary,
'contact-status-error-text': $red-secondary 'contact-status-error-text': $red-secondary,
// Text colors for AAA compliance
'color-text': $text-aaa-light,
// Near black for AAA contrast
'color-text-muted': $gray-neutral-light,
// Gray for secondary text
'primary-color': $indigo-primary,
// Indigo primary
'text-primary': $text-aaa-light,
// Main text color
'text-secondary': $gray-neutral-light,
// Secondary text color
// Enhanced social button colors for AAA compliance
'social-icon-email': $text-aaa-light,
// Dark for better contrast
'social-icon-github': $text-aaa-light,
// Dark for better contrast
'social-icon-linkedin': $text-aaa-light,
// Dark for better contrast
); );
$contact-dark-theme: ( $contact-dark-theme: (
@ -134,13 +164,13 @@ $contact-dark-theme: (
'contact-form-shadow': $shadow-dark, 'contact-form-shadow': $shadow-dark,
// Input styling (dark theme) - FIXED for AAA contrast // Input styling (dark theme) - FIXED for AAA contrast
'contact-input-bg': #374151, 'contact-input-bg': $gray-neutral-dark,
// Changed to neutral dark gray // Changed to neutral dark gray
'contact-input-border': rgba($orange-primary, $alpha-border-heavy), 'contact-input-border': rgba($orange-primary, $alpha-border-heavy),
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-medium), 'contact-input-focus-ring': rgba($orange-primary, $alpha-border-medium),
'contact-input-text': $text-aaa-dark, 'contact-input-text': $text-aaa-dark,
// White text for AAA contrast // White text for AAA contrast
'contact-input-placeholder': #9ca3af, 'contact-input-placeholder': $gray-neutral-lighter,
// Light gray for placeholder // Light gray for placeholder
// Button styling (dark theme) - improved contrast // Button styling (dark theme) - improved contrast
@ -150,12 +180,12 @@ $contact-dark-theme: (
'contact-button-hover-bg': 'contact-button-hover-bg':
linear-gradient(135deg, $blue-lighter 0%, $blue-light 100%), linear-gradient(135deg, $blue-lighter 0%, $blue-light 100%),
// Social button styling (dark theme) - FIXED for AAA contrast // Social button styling (dark theme) - FIXED for AAA contrast
'contact-social-bg': #1f2937, 'contact-social-bg': $gray-neutral-medium,
// Changed to neutral dark background // Changed to neutral dark background
'contact-social-text': $text-aaa-dark, 'contact-social-text': $text-aaa-dark,
// White text for AAA contrast // White text for AAA contrast
'contact-social-border': $orange-secondary, 'contact-social-border': $orange-secondary,
'contact-social-hover-bg': #374151, 'contact-social-hover-bg': $gray-neutral-dark,
// Slightly lighter on hover // Slightly lighter on hover
'contact-social-hover-border': $orange-primary, 'contact-social-hover-border': $orange-primary,
@ -165,5 +195,25 @@ $contact-dark-theme: (
'contact-status-success-text': $green-lighter, 'contact-status-success-text': $green-lighter,
'contact-status-error-bg': rgba($red-light, $alpha-low), 'contact-status-error-bg': rgba($red-light, $alpha-low),
'contact-status-error-border': $red-light, 'contact-status-error-border': $red-light,
'contact-status-error-text': $red-lighter 'contact-status-error-text': $red-lighter,
// Text colors for AAA compliance - ALL WHITE in dark mode
'color-text': $text-aaa-dark,
// White for AAA contrast on dark
'color-text-muted': $text-aaa-dark,
// Changed to white for consistency
'primary-color': $indigo-light,
// Lighter indigo for dark mode
'text-primary': $text-aaa-dark,
// Main text color
'text-secondary': $text-aaa-dark,
// Changed to white for consistency
// Secondary text color
// Enhanced social button colors for AAA compliance
'social-icon-email': $text-aaa-dark,
// White for better contrast
'social-icon-github': $text-aaa-dark,
// White for better contrast
'social-icon-linkedin': $text-aaa-dark,
// White for better contrast
); );

View File

@ -2,16 +2,16 @@
// Color Variables - Change once, use everywhere // Color Variables - Change once, use everywhere
// ================================ // ================================
// Light theme colors // Light theme colors - Darker for AAA compliance against colorful backgrounds
$light-primary-color: #0f172a; // Much darker blue (was #18338c) $light-primary-color: #0c4a6e; // Dark blue for blue gradient
$light-secondary-color: #0f2419; // Much darker green (was #13582e) $light-secondary-color: #064e3b; // Dark green for green gradient
$light-tertiary-color: #2d1b69; // Much darker purple (was #551fad) $light-tertiary-color: #581c87; // Dark purple for purple gradient
$light-quaternary-color: #042f2e; // Much darker teal (was #073c39) $light-quaternary-color: #134e4a; // Dark teal for teal gradient
$light-quinary-color: #451a01; // Much darker orange (was #6f2406) $light-quinary-color: #9a3412; // Dark orange for orange gradient
$light-senary-color: #7f1d1d; // Much darker red (was #b91c1c) $light-senary-color: #991b1b; // Dark red for red gradient
// Alternative: Use neutral dark colors for guaranteed AAA compliance // Alternative: Use universal dark colors for guaranteed AAA compliance
$light-text-primary: #111827; // Near black $light-text-primary: #111827; // Near black - universal fallback
$light-text-secondary: #1f2937; // Dark gray $light-text-secondary: #1f2937; // Dark gray
$light-text-tertiary: #374151; // Medium dark gray $light-text-tertiary: #374151; // Medium dark gray
@ -30,57 +30,118 @@ $dark-quaternary-title: #5eead4;
$dark-quinary-title: #fdba74; $dark-quinary-title: #fdba74;
$dark-senary-title: #fca5a5; $dark-senary-title: #fca5a5;
$dark-description-color: #ffffff; // Pure white instead of #f1f5f9 // Universal description color for dark theme // Background gradients - Enhanced for visual appeal
$light-services-bg: linear-gradient(
135deg,
#f8fafc 0%,
#e2e8f0 50%,
#cbd5e1 100%
);
$dark-services-bg: linear-gradient(
135deg,
#0f172a 0%,
#1e293b 50%,
#334155 100%
);
// Background gradients $light-title-gradient: linear-gradient(90deg, #0c4a6e, #1e40af, #3b82f6);
$light-services-bg: linear-gradient(135deg, #e0e7ef 0%, #91c5e0 100%); $dark-title-gradient: linear-gradient(90deg, #60a5fa, #3b82f6, #2563eb);
$dark-services-bg: linear-gradient(135deg, #78350f 0%, #a78bfa 100%);
$light-title-gradient: linear-gradient(90deg, #0f172a, #1e293b, #334155); // Card background gradients - Vibrant and diverse with AAA compliance
$dark-title-gradient: linear-gradient(90deg, #f1f5f9, #e2e8f0, #cbd5e1);
// Card background gradients
$light-card-primary-bg: linear-gradient( $light-card-primary-bg: linear-gradient(
135deg, 135deg,
#f8fafc, #dbeafe 0%,
#f1f5f9 #bfdbfe 50%,
); // Lighter backgrounds #93c5fd 100%
$light-card-secondary-bg: linear-gradient(135deg, #f9fafb, #f3f4f6); ); // Ocean Blue gradient
$light-card-tertiary-bg: linear-gradient(135deg, #faf9ff, #f5f3ff); $light-card-secondary-bg: linear-gradient(
$light-card-quaternary-bg: linear-gradient(135deg, #f0fdfa, #ecfdf5); 135deg,
$light-card-quinary-bg: linear-gradient(135deg, #fffbeb, #fef3c7); #d1fae5 0%,
$light-card-senary-bg: linear-gradient(135deg, #fef2f2, #fee2e2); #a7f3d0 50%,
#6ee7b7 100%
); // Emerald Green gradient
$light-card-tertiary-bg: linear-gradient(
135deg,
#e9d5ff 0%,
#ddd6fe 50%,
#c4b5fd 100%
); // Purple Violet gradient
$light-card-quaternary-bg: linear-gradient(
135deg,
#ccfbf1 0%,
#99f6e4 50%,
#5eead4 100%
); // Teal Cyan gradient
$light-card-quinary-bg: linear-gradient(
135deg,
#fed7aa 0%,
#fdba74 50%,
#fb923c 100%
); // Warm Orange gradient
$light-card-senary-bg: linear-gradient(
135deg,
#fecaca 0%,
#fca5a5 50%,
#f87171 100%
); // Coral Red gradient
// Dark theme card gradients - Rich and atmospheric
$dark-card-primary-bg: linear-gradient( $dark-card-primary-bg: linear-gradient(
135deg, 135deg,
rgba(30, 58, 138, 0.6), rgba(30, 58, 138, 0.8) 0%,
rgba(37, 99, 235, 0.5) rgba(37, 99, 235, 0.7) 50%,
); rgba(59, 130, 246, 0.6) 100%
); // Deep Ocean Blue
$dark-card-secondary-bg: linear-gradient( $dark-card-secondary-bg: linear-gradient(
135deg, 135deg,
rgba(22, 101, 52, 0.6), rgba(22, 101, 52, 0.8) 0%,
rgba(34, 197, 94, 0.5) rgba(34, 197, 94, 0.7) 50%,
); rgba(74, 222, 128, 0.6) 100%
); // Forest Emerald
$dark-card-tertiary-bg: linear-gradient( $dark-card-tertiary-bg: linear-gradient(
135deg, 135deg,
rgba(91, 33, 182, 0.6), rgba(91, 33, 182, 0.8) 0%,
rgba(124, 58, 237, 0.5) rgba(124, 58, 237, 0.7) 50%,
); rgba(147, 51, 234, 0.6) 100%
); // Royal Purple
$dark-card-quaternary-bg: linear-gradient( $dark-card-quaternary-bg: linear-gradient(
135deg, 135deg,
rgba(17, 94, 89, 0.6), rgba(17, 94, 89, 0.8) 0%,
rgba(20, 184, 166, 0.5) rgba(20, 184, 166, 0.7) 50%,
); rgba(45, 212, 191, 0.6) 100%
); // Mystic Teal
$dark-card-quinary-bg: linear-gradient( $dark-card-quinary-bg: linear-gradient(
135deg, 135deg,
rgba(154, 52, 18, 0.6), rgba(154, 52, 18, 0.8) 0%,
rgba(249, 115, 22, 0.5) rgba(249, 115, 22, 0.7) 50%,
); rgba(251, 146, 60, 0.6) 100%
); // Sunset Orange
$dark-card-senary-bg: linear-gradient( $dark-card-senary-bg: linear-gradient(
135deg, 135deg,
rgba(153, 27, 27, 0.6), rgba(153, 27, 27, 0.8) 0%,
rgba(239, 68, 68, 0.5) rgba(239, 68, 68, 0.7) 50%,
); rgba(248, 113, 113, 0.6) 100%
); // Crimson Fire
// ================================
// Additional variables needed for theme maps
// ================================
// Dark theme icon colors (brighter for better contrast)
$dark-primary-icon: #60a5fa; // Light blue
$dark-secondary-icon: #34d399; // Light green
$dark-tertiary-icon: #a78bfa; // Light purple
$dark-quaternary-icon: #5eead4; // Light teal
$dark-quinary-icon: #fbbf24; // Light orange
$dark-senary-icon: #f87171; // Light red
// Dark theme title colors (medium contrast)
$dark-primary-title: #93c5fd; // Medium blue
$dark-secondary-title: #6ee7b7; // Medium green
$dark-tertiary-title: #c4b5fd; // Medium purple
$dark-quaternary-title: #7dd3fc; // Medium teal
$dark-quinary-title: #fed7aa; // Medium orange
$dark-senary-title: #fca5a5; // Medium red
// ================================ // ================================
// Theme Maps - Using variables above // Theme Maps - Using variables above
@ -100,28 +161,28 @@ $services-light-theme: (
service-card-senary-bg: $light-card-senary-bg, service-card-senary-bg: $light-card-senary-bg,
// Icons - using same color for consistency // Icons - using same color for consistency
service-icon-primary: $light-primary-color, service-icon-primary: $light-text-primary,
service-icon-secondary: $light-secondary-color, service-icon-secondary: $light-text-primary,
service-icon-tertiary: $light-tertiary-color, service-icon-tertiary: $light-text-primary,
service-icon-quaternary: $light-quaternary-color, service-icon-quaternary: $light-text-primary,
service-icon-quinary: $light-quinary-color, service-icon-quinary: $light-text-primary,
service-icon-senary: $light-senary-color, service-icon-senary: $light-text-primary,
// Titles - using same color as icons // Titles - using same color as icons
service-title-primary: $light-primary-color, service-title-primary: $light-text-primary,
service-title-secondary: $light-secondary-color, service-title-secondary: $light-text-primary,
service-title-tertiary: $light-tertiary-color, service-title-tertiary: $light-text-primary,
service-title-quaternary: $light-quaternary-color, service-title-quaternary: $light-text-primary,
service-title-quinary: $light-quinary-color, service-title-quinary: $light-text-primary,
service-title-senary: $light-senary-color, service-title-senary: $light-text-primary,
// Descriptions - using same color as icons and titles // Descriptions - using same color as icons and titles
service-description-primary: $light-primary-color, service-description-primary: $light-text-primary,
service-description-secondary: $light-secondary-color, service-description-secondary: $light-text-primary,
service-description-tertiary: $light-tertiary-color, service-description-tertiary: $light-text-primary,
service-description-quaternary: $light-quaternary-color, service-description-quaternary: $light-text-primary,
service-description-quinary: $light-quinary-color, service-description-quinary: $light-text-primary,
service-description-senary: $light-senary-color service-description-senary: $light-text-primary
); );
$services-dark-theme: ( $services-dark-theme: (
@ -137,27 +198,27 @@ $services-dark-theme: (
service-card-quinary-bg: $dark-card-quinary-bg, service-card-quinary-bg: $dark-card-quinary-bg,
service-card-senary-bg: $dark-card-senary-bg, service-card-senary-bg: $dark-card-senary-bg,
// Icons - using distinct colors for better contrast // Icons - all using pure white
service-icon-primary: $dark-primary-icon, service-icon-primary: #ffffff,
service-icon-secondary: $dark-secondary-icon, service-icon-secondary: #ffffff,
service-icon-tertiary: $dark-tertiary-icon, service-icon-tertiary: #ffffff,
service-icon-quaternary: $dark-quaternary-icon, service-icon-quaternary: #ffffff,
service-icon-quinary: $dark-quinary-icon, service-icon-quinary: #ffffff,
service-icon-senary: $dark-senary-icon, service-icon-senary: #ffffff,
// Titles - using medium contrast colors // Titles - all using pure white
service-title-primary: $dark-primary-title, service-title-primary: #ffffff,
service-title-secondary: $dark-secondary-title, service-title-secondary: #ffffff,
service-title-tertiary: $dark-tertiary-title, service-title-tertiary: #ffffff,
service-title-quaternary: $dark-quaternary-title, service-title-quaternary: #ffffff,
service-title-quinary: $dark-quinary-title, service-title-quinary: #ffffff,
service-title-senary: $dark-senary-title, service-title-senary: #ffffff,
// Descriptions - all using the same high-contrast color // Descriptions - all using pure white
service-description-primary: $dark-description-color, service-description-primary: #ffffff,
service-description-secondary: $dark-description-color, service-description-secondary: #ffffff,
service-description-tertiary: $dark-description-color, service-description-tertiary: #ffffff,
service-description-quaternary: $dark-description-color, service-description-quaternary: #ffffff,
service-description-quinary: $dark-description-color, service-description-quinary: #ffffff,
service-description-senary: $dark-description-color service-description-senary: #ffffff
); );

View File

@ -184,10 +184,10 @@ $skills-dark-theme: (
linear-gradient(90deg, $blue-light, $green-light, $purple-light), linear-gradient(90deg, $blue-light, $green-light, $purple-light),
skills-category-bg: rgba($slate-800, $alpha-bg-card), skills-category-bg: rgba($slate-800, $alpha-bg-card),
skills-category-border: rgba($slate-600, $alpha-heavy), skills-category-border: rgba($slate-600, $alpha-heavy),
skills-category-title-color: $slate-100, skills-category-title-color: #ffffff,
skills-skill-name-color: $slate-300, skills-skill-name-color: #ffffff,
skills-skill-level-color: $slate-400, skills-skill-level-color: #ffffff,
skills-skill-percentage-color: $slate-200, skills-skill-percentage-color: #ffffff,
skills-progress-bg: rgba($slate-700, $alpha-bg-card), skills-progress-bg: rgba($slate-700, $alpha-bg-card),
// Dark theme card backgrounds // Dark theme card backgrounds
skills-category-bg-primary: skills-category-bg-primary:

View File

@ -16,7 +16,6 @@ $border-radius-sm: 0.375rem;
$border-radius-md: 0.5rem; $border-radius-md: 0.5rem;
$border-radius-lg: 1rem; $border-radius-lg: 1rem;
$border-radius-xl: 1.5rem; $border-radius-xl: 1.5rem;
$border-radius-full: 9999px;
// Spacing variables // Spacing variables
$spacing-xs: 0.5rem; $spacing-xs: 0.5rem;
@ -64,13 +63,21 @@ $gradient-primary: linear-gradient(
$color-secondary 0%, $color-secondary 0%,
$color-primary 100% $color-primary 100%
); );
$gradient-text: linear-gradient(
// Light theme gradient (keep original)
$gradient-text-light: linear-gradient(
to right, to right,
$color-secondary, $color-secondary,
$color-primary, $color-primary,
#0d9488 #0d9488
); );
// Dark theme gradient (white to light purple for hero title)
$gradient-text-dark: linear-gradient(to right, #ffffff, #c4b5fd, #a78bfa);
// Default gradient for backwards compatibility
$gradient-text: $gradient-text-light;
// Services Section Variables // Services Section Variables
$services-card-padding: 1.5rem; $services-card-padding: 1.5rem;
$services-card-border-radius: 0.75rem; $services-card-border-radius: 0.75rem;