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;
return (
<section id="about" className="about-section">
<section id="about" className="about-section" aria-labelledby="about-title">
<div className="about-section__container">
{/* Section Header */}
<div className="about-section__header">
<h2 className="about-section__title">
<header className="about-section__header">
<h2 id="about-title" className="about-section__title">
{title}
</h2>
<p className="about-section__subtitle">
{subtitle}
</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>
{/* Personal Photo and Bio */}
<div className="about-section__content">
<div className="about-section__content" role="main">
<div className="about-section__image-wrapper">
<img
src={profileImage}
alt={`${name} - Web Developer`}
alt={`Professional headshot of ${name}, Web Developer and Software Engineer`}
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 className="about-section__bio-section">
@ -88,28 +97,57 @@ export default function AboutSection(props: AboutSectionProps = {}) {
</div>
{/* Feature Cards Grid */}
<div className="about-section__features">
{featureCards.map((card, index) => {
const IconComponent = card.icon;
return (
<div
key={index}
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
>
<div className="about-section__feature-header">
<IconComponent className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`} />
<h3 className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}>
{card.title}
</h3>
</div>
<div className="about-section__feature-content">
<p className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}>
{card.description}
</p>
</div>
</div>
);
})}
<div
id="features"
className="about-section__features"
role="region"
aria-labelledby="features-heading"
>
<h3 id="features-heading" className="sr-only">My Areas of Expertise</h3>
<div className="about-section__features-grid">
{featureCards.map((card, index) => {
const IconComponent = card.icon;
return (
<article
key={index}
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
tabIndex={0}
role="group"
aria-labelledby={`feature-${index}-title`}
aria-describedby={`feature-${index}-description`}
>
<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>
</section>

View File

@ -48,49 +48,111 @@ export default function CertificationsSection(props: CertificationsSectionProps
}
]
} = props;
// Sort certifications by year in descending order (newest first)
const sortedCertifications = [...certifications].sort((a, b) => {
return parseInt(b.year) - parseInt(a.year);
});
return (
<section id="certifications" className="certifications-section">
<section id="certifications" className="certifications-section" aria-labelledby="certifications-title">
<div className="certifications-section__container">
{/* Section Header */}
<div className="certifications-section__header">
<h2 className="certifications-section__title">
<header className="certifications-section__header">
<h2 id="certifications-title" className="certifications-section__title">
{title}
</h2>
<p className="certifications-section__subtitle">
{subtitle}
</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>
{/* Certifications Grid */}
<div className="certifications-section__grid">
<div
className="certifications-section__grid"
role="region"
aria-label="Professional certifications and qualifications"
>
{sortedCertifications.map((cert, index) => (
<div key={index} className="certifications-section__card">
<div className="certifications-section__card-header">
<article
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 ? (
<img
src={cert.icon}
alt={cert.issuer}
alt={`${cert.issuer} logo`}
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>
<p className="certifications-section__card-issuer">{cert.issuer}</p>
</div>
<h3
id={`cert-${index}-title`}
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">
<span className="certifications-section__card-badge">
<span
className="certifications-section__card-badge"
aria-label={`Completed in ${cert.year}`}
>
{cert.year}
</span>
</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>
{/* 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>
</section>
);

View File

@ -18,11 +18,9 @@ interface ContactSectionProps {
locationValue?: string;
}
// TODO Create contact form with backend access
export default function ContactSection(props: ContactSectionProps = {}) {
const texts = getTexts().contact;
// Use centralized texts as defaults, allow props to override
const {
title = texts.title,
subtitle = texts.subtitle,
@ -39,40 +37,81 @@ export default function ContactSection(props: ContactSectionProps = {}) {
locationValue = texts.locationValue,
} = props;
// Create email link with pre-filled subject and body
// Enhanced email handler with better accessibility
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,");
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 parts = ['freelancer', 'sascha-bach', 'de'];
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 (
<section id="contact" className="contact-section">
<section id="contact" className="contact-section" aria-labelledby="contact-title">
<div className="contact-section__container">
{/* Section Header */}
<div className="contact-section__header">
<h2 className="contact-section__title">
<header className="contact-section__header">
<h2 id="contact-title" className="contact-section__title">
{title}
</h2>
<p className="contact-section__subtitle">
{subtitle}
</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>
{/* Contact Content Grid */}
<div className="contact-section__grid">
<div className="contact-section__grid" role="main">
{/* 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">
<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">
<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">
{connectDescription}
</p>
@ -80,80 +119,102 @@ export default function ContactSection(props: ContactSectionProps = {}) {
</div>
<button
onClick={() => {
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}`;
}}
onClick={handleEmailClick}
className="contact-section__email-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">
{buttonText}
</span>
</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>
{/* 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__availability">
<div className="contact-section__status-indicator"></div>
<h3 id="info-heading" className="sr-only">Contact Information</h3>
<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>
</div>
<div className="contact-section__quick-facts">
<div className="contact-section__fact">
<span className="contact-section__fact-label">{responseTimeLabel}</span>
<span className="contact-section__fact-value">{responseTimeValue}</span>
</div>
<div className="contact-section__fact">
<span className="contact-section__fact-label">{preferredContactLabel}</span>
<span className="contact-section__fact-value">{preferredContactValue}</span>
</div>
<div className="contact-section__fact">
<span className="contact-section__fact-label">{locationLabel}</span>
<span className="contact-section__fact-value">{locationValue}</span>
</div>
<dl className="contact-section__fact-list">
<div className="contact-section__fact">
<dt className="contact-section__fact-label">{responseTimeLabel}</dt>
<dd className="contact-section__fact-value">{responseTimeValue}</dd>
</div>
<div className="contact-section__fact">
<dt className="contact-section__fact-label">{preferredContactLabel}</dt>
<dd className="contact-section__fact-value">{preferredContactValue}</dd>
</div>
<div className="contact-section__fact">
<dt className="contact-section__fact-label">{locationLabel}</dt>
<dd className="contact-section__fact-value">{locationValue}</dd>
</div>
</dl>
</div>
</div>
</div>
</aside>
{/* Social Contact Methods - Below connect card but same width */}
<div className="contact-section__social-card">
<h4 className="contact-section__social-title">{socialTitle}</h4>
<div className="contact-section__social-list">
<div className="contact-section__detail-item">
{/* Social Contact Methods */}
<div className="contact-section__social-card" role="region" aria-labelledby="social-heading">
<h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4>
<div className="contact-section__social-list" role="list">
<div className="contact-section__detail-item" role="listitem">
<button
className="contact-section__social-button contact-section__social-button--email"
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>
<span className="contact-section__detail-text">{getObfuscatedEmail()}</span>
<span className="contact-section__detail-text" aria-label="Email address">
{getObfuscatedEmail()}
</span>
</div>
<div className="contact-section__detail-item">
<div className="contact-section__detail-item" role="listitem">
<button
className="contact-section__social-button contact-section__social-button--github"
onClick={() => window.open(personalConfig.social.github.url, '_blank')}
aria-label="GitHub Profile"
onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
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>
<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 className="contact-section__detail-item">
<div className="contact-section__detail-item" role="listitem">
<button
className="contact-section__social-button contact-section__social-button--linkedin"
onClick={() => window.open(personalConfig.social.linkedin.url, '_blank')}
aria-label="LinkedIn Profile"
onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
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>
<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>

View File

@ -29,12 +29,29 @@ export default function HeroSection(props: HeroSectionProps = {}) {
const location = useLocation();
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 = () => {
announceToScreenReader('Navigating to projects section');
scrollToProjects(location.pathname, navigate);
};
const handleDownloadResume = () => {
try {
announceToScreenReader('Starting resume download');
// Create a temporary anchor element
const link = document.createElement('a');
link.href = resumePDF;
@ -49,19 +66,23 @@ export default function HeroSection(props: HeroSectionProps = {}) {
document.body.removeChild(link);
console.log('✅ Resume download initiated');
announceToScreenReader('Resume download started successfully');
} catch (error) {
console.error('❌ Error downloading resume:', error);
announceToScreenReader('Resume download failed, opening in new tab');
// Fallback: open in new tab if download fails
try {
window.open(resumePDF, '_blank');
} catch (fallbackError) {
console.error('❌ Fallback method also failed:', fallbackError);
announceToScreenReader('Unable to access resume file');
}
}
};
const handleGetInTouch = () => {
announceToScreenReader('Navigating to contact section');
const contactSection = document.querySelector('.contact-section');
if (contactSection) {
contactSection.scrollIntoView({
@ -72,41 +93,80 @@ export default function HeroSection(props: HeroSectionProps = {}) {
};
return (
<section className="hero-section">
<section className="hero-section" role="main" aria-labelledby="hero-title">
<div className="hero-section__container">
<h1 className="hero-section__title">{title}</h1>
<p className="hero-section__description">
{description}
</p>
<div className="hero-section__buttons">
{/* Skip link for keyboard navigation */}
<div className="hero-section__skip">
<a href="#about" className="sr-only sr-only-focusable">
Skip to about section
</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
className="hero-section__button hero-section__button--primary"
onClick={handleViewWork}
aria-label="View my work and projects"
type="button"
>
{primaryButtonText}
</button>
<button
className="hero-section__button hero-section__button--secondary"
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}
</button>
<button
className="hero-section__button hero-section__button--tertiary"
onClick={handleGetInTouch}
aria-label="Navigate to contact section to get in touch"
type="button"
>
{tertiaryButtonText}
</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 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) => (
<div key={item.label} className={`hero-section__stat-item hero-section__stat-item--${index + 1}`}>
<span className="hero-section__stat-value">{item.value}</span>
<span className="hero-section__stat-label">{item.label}</span>
<div
key={item.label}
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>
</aside>
</div>
</section>
);

View File

@ -1,4 +1,5 @@
import { Github, ExternalLink } from 'lucide-react';
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
import { personalConfig } from '../../config/personal';
import { getTexts } from '../../config/texts';
import type { Project } from '../../data/Project';
@ -15,6 +16,10 @@ interface ProjectsSectionProps {
export default function ProjectsSection(props: ProjectsSectionProps = {}) {
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
const {
@ -27,6 +32,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
description: "A personal portfolio website to showcase my skills, projects, and experience.",
image: portfolioImage,
technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"],
year: "2025",
github: personalConfig.gitProjects.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.",
image: ergoVRImage,
technologies: ["Unity3D", "C#", "Oculus SDK"],
year: "2015",
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.",
image: icaraceImage,
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
year: "2018",
github: personalConfig.gitProjects.ergoVR
}
]
} = 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 (
<section id="projects" className="projects-section">
<div className="projects-section__container">
@ -61,62 +194,144 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
</p>
</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 */}
<div className="projects-section__grid">
{projects.map((project) => (
<div key={project.id} className="projects-section__card">
<div className="projects-section__image-container">
<img
src={project.image || "/api/placeholder/400/200"}
alt={project.title}
className="projects-section__image"
/>
<div className="projects-section__overlay">
<div className="projects-section__actions">
<div className="projects-section__grid" role="grid" aria-label="Projects grid">
{projects.map((project, cardIndex) => {
const actions = getProjectActions(project);
const isActive = activeCardIndex === cardIndex;
return (
<article
key={project.id}
className={`projects-section__card ${isActive ? 'projects-section__card--active' : ''}`}
ref={(el) => { cardRefs.current[cardIndex] = el; }}
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 && (
<a
ref={(el) => { actionRefs.current[cardIndex * 2] = el }}
href={project.github}
target="_blank"
rel="noopener noreferrer"
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" />
{texts.codeButtonText}
<Github className="projects-section__action-icon" aria-hidden="true" />
<span className="projects-section__action-text">{texts.codeButtonText}</span>
</a>
)}
{project.live && (
<a
ref={(el) => { actionRefs.current[cardIndex * 2 + 1] = el }}
href={project.live}
target="_blank"
rel="noopener noreferrer"
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" />
{texts.liveButtonText}
<ExternalLink className="projects-section__action-icon" aria-hidden="true" />
<span className="projects-section__action-text">{texts.liveButtonText}</span>
</a>
)}
</div>
</div>
</div>
<div className="projects-section__content">
<div className="projects-section__header-content">
<h3 className="projects-section__card-title">{project.title}</h3>
<p className="projects-section__card-description">{project.description}</p>
</div>
<div className="projects-section__content">
<div className="projects-section__header-content">
<h3 className="projects-section__card-title">{project.title}</h3>
<p
id={`project-${project.id}-description`}
className="projects-section__card-description"
>
{project.description}
</p>
<div className="projects-section__technologies">
{project.technologies.map((tech) => (
<span key={tech} className="projects-section__tech-badge">
{tech}
</span>
))}
{/* Add project metadata for screen readers */}
<div className="projects-section__metadata sr-only">
Project {cardIndex + 1} of {projects.length}.
{actions.length > 0 ? `${actions.length} actions available.` : 'No actions available.'}
{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>
</section>

View File

@ -54,37 +54,84 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
}
]
} = props;
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__header">
<h2 className="services-section__title">
{/* Section Header */}
<header className="services-section__header">
<h2 id="services-title" className="services-section__title">
{title}
</h2>
<p className="services-section__subtitle">
{subtitle}
</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 className="services-section__grid">
{/* Services Grid */}
<div
className="services-section__grid"
role="region"
aria-label="Professional services offered"
>
{services.map((service, index) => {
const IconComponent = service.icon;
return (
<div key={index} className={`services-section__card services-section__card--${service.colorClass}`}>
<div className="services-section__card-header">
<IconComponent className={`services-section__icon services-section__icon--${service.colorClass}`} />
<h3 className={`services-section__card-title services-section__card-title--${service.colorClass}`}>
<article
key={index}
className={`services-section__card services-section__card--${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}
</h3>
</div>
</header>
<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}
</p>
</div>
</div>
{/* Status indicator for screen readers */}
<div className="sr-only">
Service {index + 1} of {services.length}
</div>
</article>
);
})}
</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>
</section>
);

View File

@ -48,99 +48,171 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
skills: [
{ skill: "Accessibility (a11y) Standards", value: 90 },
{ skill: "SCSS/Sass", value: 95 },
{ skill: "CSS3", value: 90 },
{ skill: "Bootstrap", value: 65 }
{ skill: "CSS3", value: 95 },
{ skill: "Bootstrap", value: 80 }
]
},
{
title: "Backend Development",
skills: [
{ skill: "Node.js", value: 65 },
{ skill: "Express.js", value: 25 },
{ skill: "REST APIs", value: 70 },
{ skill: "MongoDB", value: 50 }
{ skill: "Node.js", value: 75 },
{ skill: "Express.js", value: 70 },
{ skill: "REST APIs", value: 85 },
{ skill: "Database Design", value: 70 }
]
},
{
title: "Development Tools",
skills: [
{ skill: "Git & GitHub", value: 90 },
{ skill: "Visual Studio Code", value: 75 },
{ skill: "Vite", value: 80 },
{ skill: "NPM", value: 85 }
{ skill: "VS Code", value: 95 },
{ skill: "npm/yarn", value: 85 },
{ skill: "Webpack/Vite", value: 75 }
]
},
{
title: "Testing & Quality",
skills: [
{ skill: "Cypress", value: 85 },
{ skill: "Jasmine/Karma", value: 75 },
{ skill: "ESLint", value: 90 },
{ skill: "Prettier", value: 95 }
{ skill: "Unit Testing", value: 80 },
{ skill: "E2E Testing", value: 70 },
{ skill: "Code Review", value: 85 },
{ skill: "Performance Optimization", value: 75 }
]
},
{
title: "AI-Tools",
skills: [
{ skill: "GitHub Copilot", value: 85 },
{ skill: "Prompt Engineering", value: 45 },
{ skill: "Prototyping / MVP-Building with AI", value: 45 }
{ skill: "GitHub Copilot", value: 95 },
{ skill: "ChatGPT", value: 90 },
{ skill: "Claude", value: 85 },
{ skill: "AI-Assisted Development", value: 90 }
]
}
]
} = props;
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__header">
<h2 className="skills-section__title">
{/* Section Header */}
<header className="skills-section__header">
<h2 id="skills-title" className="skills-section__title">
{title}
</h2>
<p className="skills-section__subtitle">
{subtitle}
</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 className="skills-section__grid">
{skillCategories.map((category, categoryIndex) => (
<div key={categoryIndex} className="skills-section__category">
<h3 className="skills-section__category-title">
{category.title}
</h3>
<div className="skills-section__skills">
{category.skills.map((skillItem, skillIndex) => {
// Use provided level or calculate from value
const displayLevel = skillItem.level || calculateSkillLevel(skillItem.value);
// Use provided color or get color based on position in array
const colorClass = skillItem.color || getColorByPosition(skillIndex);
{/* Skills Grid */}
<div
className="skills-section__grid"
role="region"
aria-label="Technical skills organized by category"
>
{skillCategories.map((category, categoryIndex) => {
const colorClass = getColorByPosition(categoryIndex);
return (
<article
key={category.title}
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 key={skillIndex} className="skills-section__skill">
<div className="skills-section__skill-header">
<span className="skills-section__skill-name">
{skillItem.skill}
</span>
<div className="skills-section__skill-info">
<span className="skills-section__skill-level">
{displayLevel}
</span>
<span className="skills-section__skill-percentage" aria-hidden="true">
{skillItem.value}%
</span>
<div
className="skills-section__skills-list"
role="list"
aria-label={`${category.title} skills`}
>
{category.skills.map((skillItem, skillIndex) => {
const displayLevel = calculateSkillLevel(skillItem.value);
return (
<div
key={skillItem.skill}
className="skills-section__skill-item"
role="listitem"
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 className="skills-section__progress">
<div
className={`skills-section__progress-bar skills-section__progress-bar--${colorClass}`}
style={{ width: `${skillItem.value}%` }}
></div>
</div>
</div>
);
})}
</div>
</div>
))}
);
})}
</div>
</article>
);
})}
</div>
{/* Skills summary for screen readers */}
<div className="skills-section__summary sr-only" aria-live="polite">
<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>
</section>

View File

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

View File

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

View File

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

View File

@ -3,13 +3,63 @@
@forward 'variables';
@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 {
position: absolute;
top: -40px;
left: 6px;
background: var(--color-primary);
color: white;
color: var(--color-text-aaa-dark);
padding: 8px;
text-decoration: none;
z-index: 9999;
@ -19,7 +69,7 @@
&:focus {
top: 6px;
outline: 2px solid var(--color-secondary);
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
}
@ -36,6 +86,65 @@
@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)
@mixin mobile-only {
@media (max-width: #{$mobile-breakpoint - 1px}) {

View File

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

View File

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

View File

@ -23,11 +23,7 @@
font-size: 1.875rem;
font-weight: bold;
margin-bottom: 1rem;
background: var(--gradient-contact-title);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
color: var(--color-text);
@include globals.desktop-only {
font-size: 2.25rem;
@ -98,11 +94,6 @@
font-weight: 700;
color: var(--color-text);
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 {
@ -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;
color: var(--color-text-muted);
max-width: 600px;
margin: 0;
margin-left: auto;
margin-right: auto;
}
&__buttons {

View File

@ -23,11 +23,7 @@
font-size: 1.875rem;
font-weight: bold;
margin-bottom: 1rem;
background: var(--gradient-projects-title);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
color: var(--color-text);
@include globals.desktop-only {
font-size: 2.25rem;
@ -74,6 +70,57 @@
.projects-section__overlay {
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;
align-items: 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 {
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
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 {
@ -117,6 +185,8 @@
text-decoration: none;
transition: all 0.2s ease;
border: 1px solid transparent;
min-width: 120px; // Ensure consistent button sizes
justify-content: center; // Center content within button
&--primary {
background: var(--projects-button-primary-bg);
@ -139,6 +209,15 @@
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 {
@ -185,6 +264,83 @@
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
@include globals.mobile-only {
&__card {
@ -202,11 +358,222 @@
&__actions {
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
// Adjust transform for mobile
transform: translate(-50%, -50%) translateY(15px);
&--visible {
transform: translate(-50%, -50%) translateY(0);
}
}
&__action-button {
justify-content: center;
min-width: 140px; // Slightly wider on mobile for touch targets
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-weight: bold;
margin-bottom: 1rem;
background: var(--gradient-services-title);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
color: var(--color-text);
@include globals.desktop-only {
font-size: 2.25rem;
@ -100,17 +96,40 @@
&__card {
background: white;
border-radius: $card-border-radius-md;
box-shadow: $shadow-card;
transition: $transition-base;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
display: flex;
flex-direction: column;
height: auto;
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 {
transform: translateY(-4px);
box-shadow: $shadow-card-hover;
transform: translateY(-8px) scale(1.02);
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
@ -171,6 +190,8 @@
height: $services-icon-size;
margin: 0 auto 1rem auto;
display: block;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
transition: all 0.3s ease;
@media (max-width: 767px) {
width: 2rem;
@ -178,6 +199,12 @@
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 {
color: var(--service-icon-primary);
}
@ -208,6 +235,8 @@
font-weight: 600;
margin: 0;
line-height: 1.4;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
letter-spacing: 0.025em;
@media (max-width: 767px) {
font-size: 1rem;
@ -259,6 +288,8 @@
line-height: 1.5;
font-size: 0.875rem;
text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-weight: 400;
@media (max-width: 767px) {
font-size: 0.8rem;
@ -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-weight: bold;
margin-bottom: 1rem;
background: var(--gradient-skills-title);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
color: var(--color-text);
@include globals.desktop-only {
font-size: 2.25rem;
@ -261,17 +257,28 @@
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 {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start; // Changed from center to flex-start to handle wrapping
margin-bottom: 0.5rem;
gap: 1rem;
min-height: 2.5rem; // Reserve space for wrapped text
flex: 0 0 auto; // Don't grow or shrink
@include globals.mobile-only {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
min-height: 3rem; // More space on mobile for wrapped text
}
}
@ -279,6 +286,9 @@
font-size: 1rem;
font-weight: 600;
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 {
font-size: 0.9rem;
@ -289,6 +299,8 @@
display: flex;
align-items: center;
gap: 0.75rem;
flex-shrink: 0; // Prevent shrinking
align-self: flex-start; // Align to top when skill name wraps
@include globals.mobile-only {
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 {
height: $skills-progress-height;
background: var(--skills-progress-bg);

View File

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

View File

@ -39,6 +39,16 @@ $red-light: #f87171;
$red-lighter: #fca5a5;
$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;
$black: #000000;
@ -86,7 +96,7 @@ $contact-light-theme: (
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-light),
'contact-input-text': $text-aaa-light,
// Added for AAA compliance
'contact-input-placeholder': #484c56,
'contact-input-placeholder': $text-input-placeholder-light,
// Added for form accessibility
// Button styling - improved contrast and readability
@ -110,7 +120,27 @@ $contact-light-theme: (
'contact-status-success-text': $green-secondary,
'contact-status-error-bg': $red-lightest,
'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: (
@ -134,13 +164,13 @@ $contact-dark-theme: (
'contact-form-shadow': $shadow-dark,
// Input styling (dark theme) - FIXED for AAA contrast
'contact-input-bg': #374151,
'contact-input-bg': $gray-neutral-dark,
// Changed to neutral dark gray
'contact-input-border': rgba($orange-primary, $alpha-border-heavy),
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-medium),
'contact-input-text': $text-aaa-dark,
// White text for AAA contrast
'contact-input-placeholder': #9ca3af,
'contact-input-placeholder': $gray-neutral-lighter,
// Light gray for placeholder
// Button styling (dark theme) - improved contrast
@ -150,12 +180,12 @@ $contact-dark-theme: (
'contact-button-hover-bg':
linear-gradient(135deg, $blue-lighter 0%, $blue-light 100%),
// Social button styling (dark theme) - FIXED for AAA contrast
'contact-social-bg': #1f2937,
'contact-social-bg': $gray-neutral-medium,
// Changed to neutral dark background
'contact-social-text': $text-aaa-dark,
// White text for AAA contrast
'contact-social-border': $orange-secondary,
'contact-social-hover-bg': #374151,
'contact-social-hover-bg': $gray-neutral-dark,
// Slightly lighter on hover
'contact-social-hover-border': $orange-primary,
@ -165,5 +195,25 @@ $contact-dark-theme: (
'contact-status-success-text': $green-lighter,
'contact-status-error-bg': rgba($red-light, $alpha-low),
'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
// ================================
// Light theme colors
$light-primary-color: #0f172a; // Much darker blue (was #18338c)
$light-secondary-color: #0f2419; // Much darker green (was #13582e)
$light-tertiary-color: #2d1b69; // Much darker purple (was #551fad)
$light-quaternary-color: #042f2e; // Much darker teal (was #073c39)
$light-quinary-color: #451a01; // Much darker orange (was #6f2406)
$light-senary-color: #7f1d1d; // Much darker red (was #b91c1c)
// Light theme colors - Darker for AAA compliance against colorful backgrounds
$light-primary-color: #0c4a6e; // Dark blue for blue gradient
$light-secondary-color: #064e3b; // Dark green for green gradient
$light-tertiary-color: #581c87; // Dark purple for purple gradient
$light-quaternary-color: #134e4a; // Dark teal for teal gradient
$light-quinary-color: #9a3412; // Dark orange for orange gradient
$light-senary-color: #991b1b; // Dark red for red gradient
// Alternative: Use neutral dark colors for guaranteed AAA compliance
$light-text-primary: #111827; // Near black
// Alternative: Use universal dark colors for guaranteed AAA compliance
$light-text-primary: #111827; // Near black - universal fallback
$light-text-secondary: #1f2937; // Dark gray
$light-text-tertiary: #374151; // Medium dark gray
@ -30,57 +30,118 @@ $dark-quaternary-title: #5eead4;
$dark-quinary-title: #fdba74;
$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-services-bg: linear-gradient(135deg, #e0e7ef 0%, #91c5e0 100%);
$dark-services-bg: linear-gradient(135deg, #78350f 0%, #a78bfa 100%);
$light-title-gradient: linear-gradient(90deg, #0c4a6e, #1e40af, #3b82f6);
$dark-title-gradient: linear-gradient(90deg, #60a5fa, #3b82f6, #2563eb);
$light-title-gradient: linear-gradient(90deg, #0f172a, #1e293b, #334155);
$dark-title-gradient: linear-gradient(90deg, #f1f5f9, #e2e8f0, #cbd5e1);
// Card background gradients
// Card background gradients - Vibrant and diverse with AAA compliance
$light-card-primary-bg: linear-gradient(
135deg,
#f8fafc,
#f1f5f9
); // Lighter backgrounds
$light-card-secondary-bg: linear-gradient(135deg, #f9fafb, #f3f4f6);
$light-card-tertiary-bg: linear-gradient(135deg, #faf9ff, #f5f3ff);
$light-card-quaternary-bg: linear-gradient(135deg, #f0fdfa, #ecfdf5);
$light-card-quinary-bg: linear-gradient(135deg, #fffbeb, #fef3c7);
$light-card-senary-bg: linear-gradient(135deg, #fef2f2, #fee2e2);
#dbeafe 0%,
#bfdbfe 50%,
#93c5fd 100%
); // Ocean Blue gradient
$light-card-secondary-bg: linear-gradient(
135deg,
#d1fae5 0%,
#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(
135deg,
rgba(30, 58, 138, 0.6),
rgba(37, 99, 235, 0.5)
);
rgba(30, 58, 138, 0.8) 0%,
rgba(37, 99, 235, 0.7) 50%,
rgba(59, 130, 246, 0.6) 100%
); // Deep Ocean Blue
$dark-card-secondary-bg: linear-gradient(
135deg,
rgba(22, 101, 52, 0.6),
rgba(34, 197, 94, 0.5)
);
rgba(22, 101, 52, 0.8) 0%,
rgba(34, 197, 94, 0.7) 50%,
rgba(74, 222, 128, 0.6) 100%
); // Forest Emerald
$dark-card-tertiary-bg: linear-gradient(
135deg,
rgba(91, 33, 182, 0.6),
rgba(124, 58, 237, 0.5)
);
rgba(91, 33, 182, 0.8) 0%,
rgba(124, 58, 237, 0.7) 50%,
rgba(147, 51, 234, 0.6) 100%
); // Royal Purple
$dark-card-quaternary-bg: linear-gradient(
135deg,
rgba(17, 94, 89, 0.6),
rgba(20, 184, 166, 0.5)
);
rgba(17, 94, 89, 0.8) 0%,
rgba(20, 184, 166, 0.7) 50%,
rgba(45, 212, 191, 0.6) 100%
); // Mystic Teal
$dark-card-quinary-bg: linear-gradient(
135deg,
rgba(154, 52, 18, 0.6),
rgba(249, 115, 22, 0.5)
);
rgba(154, 52, 18, 0.8) 0%,
rgba(249, 115, 22, 0.7) 50%,
rgba(251, 146, 60, 0.6) 100%
); // Sunset Orange
$dark-card-senary-bg: linear-gradient(
135deg,
rgba(153, 27, 27, 0.6),
rgba(239, 68, 68, 0.5)
);
rgba(153, 27, 27, 0.8) 0%,
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
@ -100,28 +161,28 @@ $services-light-theme: (
service-card-senary-bg: $light-card-senary-bg,
// Icons - using same color for consistency
service-icon-primary: $light-primary-color,
service-icon-secondary: $light-secondary-color,
service-icon-tertiary: $light-tertiary-color,
service-icon-quaternary: $light-quaternary-color,
service-icon-quinary: $light-quinary-color,
service-icon-senary: $light-senary-color,
service-icon-primary: $light-text-primary,
service-icon-secondary: $light-text-primary,
service-icon-tertiary: $light-text-primary,
service-icon-quaternary: $light-text-primary,
service-icon-quinary: $light-text-primary,
service-icon-senary: $light-text-primary,
// Titles - using same color as icons
service-title-primary: $light-primary-color,
service-title-secondary: $light-secondary-color,
service-title-tertiary: $light-tertiary-color,
service-title-quaternary: $light-quaternary-color,
service-title-quinary: $light-quinary-color,
service-title-senary: $light-senary-color,
service-title-primary: $light-text-primary,
service-title-secondary: $light-text-primary,
service-title-tertiary: $light-text-primary,
service-title-quaternary: $light-text-primary,
service-title-quinary: $light-text-primary,
service-title-senary: $light-text-primary,
// Descriptions - using same color as icons and titles
service-description-primary: $light-primary-color,
service-description-secondary: $light-secondary-color,
service-description-tertiary: $light-tertiary-color,
service-description-quaternary: $light-quaternary-color,
service-description-quinary: $light-quinary-color,
service-description-senary: $light-senary-color
service-description-primary: $light-text-primary,
service-description-secondary: $light-text-primary,
service-description-tertiary: $light-text-primary,
service-description-quaternary: $light-text-primary,
service-description-quinary: $light-text-primary,
service-description-senary: $light-text-primary
);
$services-dark-theme: (
@ -137,27 +198,27 @@ $services-dark-theme: (
service-card-quinary-bg: $dark-card-quinary-bg,
service-card-senary-bg: $dark-card-senary-bg,
// Icons - using distinct colors for better contrast
service-icon-primary: $dark-primary-icon,
service-icon-secondary: $dark-secondary-icon,
service-icon-tertiary: $dark-tertiary-icon,
service-icon-quaternary: $dark-quaternary-icon,
service-icon-quinary: $dark-quinary-icon,
service-icon-senary: $dark-senary-icon,
// Icons - all using pure white
service-icon-primary: #ffffff,
service-icon-secondary: #ffffff,
service-icon-tertiary: #ffffff,
service-icon-quaternary: #ffffff,
service-icon-quinary: #ffffff,
service-icon-senary: #ffffff,
// Titles - using medium contrast colors
service-title-primary: $dark-primary-title,
service-title-secondary: $dark-secondary-title,
service-title-tertiary: $dark-tertiary-title,
service-title-quaternary: $dark-quaternary-title,
service-title-quinary: $dark-quinary-title,
service-title-senary: $dark-senary-title,
// Titles - all using pure white
service-title-primary: #ffffff,
service-title-secondary: #ffffff,
service-title-tertiary: #ffffff,
service-title-quaternary: #ffffff,
service-title-quinary: #ffffff,
service-title-senary: #ffffff,
// Descriptions - all using the same high-contrast color
service-description-primary: $dark-description-color,
service-description-secondary: $dark-description-color,
service-description-tertiary: $dark-description-color,
service-description-quaternary: $dark-description-color,
service-description-quinary: $dark-description-color,
service-description-senary: $dark-description-color
// Descriptions - all using pure white
service-description-primary: #ffffff,
service-description-secondary: #ffffff,
service-description-tertiary: #ffffff,
service-description-quaternary: #ffffff,
service-description-quinary: #ffffff,
service-description-senary: #ffffff
);

View File

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

View File

@ -16,7 +16,6 @@ $border-radius-sm: 0.375rem;
$border-radius-md: 0.5rem;
$border-radius-lg: 1rem;
$border-radius-xl: 1.5rem;
$border-radius-full: 9999px;
// Spacing variables
$spacing-xs: 0.5rem;
@ -64,13 +63,21 @@ $gradient-primary: linear-gradient(
$color-secondary 0%,
$color-primary 100%
);
$gradient-text: linear-gradient(
// Light theme gradient (keep original)
$gradient-text-light: linear-gradient(
to right,
$color-secondary,
$color-primary,
#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-card-padding: 1.5rem;
$services-card-border-radius: 0.75rem;