From 584d39db3dece9e9a3abd6710815f12d10decef3 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 30 Oct 2025 16:18:13 +0100 Subject: [PATCH] more WCAG compliance changes: contrast and aria added --- src/components/sections/AboutSection.tsx | 94 +++-- .../sections/CertificationsSection.tsx | 90 ++++- src/components/sections/ContactSection.tsx | 169 +++++--- src/components/sections/HeroSection.tsx | 84 +++- src/components/sections/ProjectsSection.tsx | 279 +++++++++++-- src/components/sections/ServicesSection.tsx | 69 +++- src/components/sections/SkillsSection.tsx | 184 ++++++--- src/config/texts.ts | 6 +- src/data/Project.ts | 3 +- src/scss/App.scss | 2 +- src/scss/globals.scss | 113 +++++- src/scss/sections/about-section.scss | 19 +- src/scss/sections/certifications-section.scss | 6 +- src/scss/sections/contact-section.scss | 88 +++- src/scss/sections/hero-section.scss | 3 +- src/scss/sections/projects-section.scss | 379 +++++++++++++++++- src/scss/sections/services-section.scss | 190 ++++++++- src/scss/sections/skills-section.scss | 30 +- src/scss/themes/_base.scss | 8 +- src/scss/themes/_contact.scss | 64 ++- src/scss/themes/_services.scss | 225 +++++++---- src/scss/themes/_skills.scss | 8 +- src/scss/variables.scss | 11 +- 23 files changed, 1769 insertions(+), 355 deletions(-) diff --git a/src/components/sections/AboutSection.tsx b/src/components/sections/AboutSection.tsx index 8f967b3..d6b96d7 100644 --- a/src/components/sections/AboutSection.tsx +++ b/src/components/sections/AboutSection.tsx @@ -49,28 +49,37 @@ export default function AboutSection(props: AboutSectionProps = {}) { } ] } = props; + return ( -
+
{/* Section Header */} -
-

+
+

{title}

{subtitle}

+
+ + {/* Skip link for keyboard navigation */} + {/* Personal Photo and Bio */} -
+
{`${name} -
+
@@ -88,28 +97,57 @@ export default function AboutSection(props: AboutSectionProps = {}) {
{/* Feature Cards Grid */} -
- {featureCards.map((card, index) => { - const IconComponent = card.icon; - return ( -
-
- -

- {card.title} -

-
-
-

- {card.description} -

-
-
- ); - })} +
+

My Areas of Expertise

+ +
+ {featureCards.map((card, index) => { + const IconComponent = card.icon; + return ( +
+
+
+
+

+ {card.description} +

+
+ + {/* Status indicator for screen readers */} +
+ Expertise area {index + 1} of {featureCards.length} +
+
+ ); + })} +

diff --git a/src/components/sections/CertificationsSection.tsx b/src/components/sections/CertificationsSection.tsx index 7107914..4fab98b 100644 --- a/src/components/sections/CertificationsSection.tsx +++ b/src/components/sections/CertificationsSection.tsx @@ -48,50 +48,112 @@ 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 Header */} -
-

+
+

{title}

{subtitle}

+
+ + {/* Skip link for keyboard navigation */} + + + {/* Certifications sorting info for screen readers */} +
+

+ Certifications are sorted by year, showing the most recent first. + Total: {sortedCertifications.length} certifications and qualifications. +

{/* Certifications Grid */} -
+
{sortedCertifications.map((cert, index) => ( -
-
+
+
{cert.icon ? ( {cert.issuer} ) : ( - +
+

+ {cert.name} +

+

+ {cert.issuer} +

+ +
- + {cert.year}
-
+ + {/* Hidden details for screen readers */} +
+ Certification {index + 1} of {sortedCertifications.length}. + {cert.name} from {cert.issuer}, completed in {cert.year}. +
+ ))}
+ + {/* Certifications summary for screen readers */} +
+

+ 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. +

+

); -} +} \ No newline at end of file diff --git a/src/components/sections/ContactSection.tsx b/src/components/sections/ContactSection.tsx index eaf061e..07d4b26 100644 --- a/src/components/sections/ContactSection.tsx +++ b/src/components/sections/ContactSection.tsx @@ -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 Header */} -
-

+
+

{title}

{subtitle}

+
+ + {/* Skip link for keyboard navigation */} + {/* Contact Content Grid */} -
+
{/* Let's Connect Card */} -
+
- +
{/* Contact Info Sidebar */} -
+ - {/* Social Contact Methods - Below connect card but same width */} -
-

{socialTitle}

-
-
+ {/* Social Contact Methods */} +
+

{socialTitle}

+
+ +
- {getObfuscatedEmail()} + + {getObfuscatedEmail()} +
-
+ +
- {personalConfig.social.github.username} + + {personalConfig.social.github.username} +
-
+ +
- {personalConfig.social.linkedin.username} + + {personalConfig.social.linkedin.username} +
diff --git a/src/components/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx index 54a06ef..675ae69 100644 --- a/src/components/sections/HeroSection.tsx +++ b/src/components/sections/HeroSection.tsx @@ -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 ( -
+
-

{title}

-

- {description} -

-
+ {/* Skip link for keyboard navigation */} + + +
+

{title}

+

+ {description} +

+
+ + + + {/* Hidden descriptions for screen readers */} +
+ This will download my current resume in PDF format
-
+ +
); diff --git a/src/components/sections/ProjectsSection.tsx b/src/components/sections/ProjectsSection.tsx index 37b00d4..e9e3124 100644 --- a/src/components/sections/ProjectsSection.tsx +++ b/src/components/sections/ProjectsSection.tsx @@ -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(null); + const [focusedActionIndex, setFocusedActionIndex] = useState(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, 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, 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 (
@@ -61,62 +194,144 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {

+ {/* Keyboard navigation instructions */} +
+

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.

+
+ + {/* Global keyboard shortcuts info - MOVED BEFORE CARDS */} +
+
+ Keyboard Navigation Help +
+
    +
  • Tab - Navigate between cards and actions
  • +
  • Enter or Space - Select/activate a card
  • +
  • Escape - Close selected card
  • +
  • - Move between cards when one is selected
  • +
+
+
+
+ + {/* Skip link for keyboard navigation help */} + + {/* Projects Grid */} -
- {projects.map((project) => ( -
-
- {project.title} -
-
+
+ {projects.map((project, cardIndex) => { + const actions = getProjectActions(project); + const isActive = activeCardIndex === cardIndex; + + return ( +
{ 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)} + > +
+ {`Screenshot + + {/* Add this overlay div */} + + + {/* Conditionally visible action buttons */} +
{project.github && ( { 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)} > - - {texts.codeButtonText} + )} {project.live && ( { 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)} > - - {texts.liveButtonText} + )}
-
-
-
-

{project.title}

-

{project.description}

-
+
+
+

{project.title}

+

+ {project.description} +

-
- {project.technologies.map((tech) => ( - - {tech} - - ))} + {/* Add project metadata for screen readers */} +
+ Project {cardIndex + 1} of {projects.length}. + {actions.length > 0 ? `${actions.length} actions available.` : 'No actions available.'} + {project.year && `Created in ${project.year}.`} +
+
+ +
+ {project.technologies.map((tech, index) => ( + + {tech} + + ))} +
+ + {/* Enhanced status indicator for active card */} + {isActive && ( +
+ {project.title} card selected. {actions.length} actions available. + Use Tab to navigate actions, Escape to close, Arrow keys to move between cards. +
+ )}
-
-
- ))} + + ); + })}
diff --git a/src/components/sections/ServicesSection.tsx b/src/components/sections/ServicesSection.tsx index 13cff4f..d02824c 100644 --- a/src/components/sections/ServicesSection.tsx +++ b/src/components/sections/ServicesSection.tsx @@ -54,37 +54,84 @@ export default function ServicesSection(props: ServicesSectionProps = {}) { } ] } = props; + return ( -
+
-
-

+ {/* Section Header */} +
+

{title}

{subtitle}

+
+ + {/* Skip link for keyboard navigation */} + -
+ + {/* Services Grid */} +
{services.map((service, index) => { const IconComponent = service.icon; return ( -
-
- -

+
+
+

+ +
-

+

{service.description}

-
+ + {/* Status indicator for screen readers */} +
+ Service {index + 1} of {services.length} +
+ ); })}
+ + {/* Services summary for screen readers */} +
+

+ Services overview: {services.length} professional services available. + Use Tab to navigate between service cards for detailed information. +

+

); diff --git a/src/components/sections/SkillsSection.tsx b/src/components/sections/SkillsSection.tsx index d454789..9f16c2f 100644 --- a/src/components/sections/SkillsSection.tsx +++ b/src/components/sections/SkillsSection.tsx @@ -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 Header */} +
+

{title}

{subtitle}

+
+ + {/* Skip link for keyboard navigation */} + -
- {skillCategories.map((category, categoryIndex) => ( -
-

- {category.title} -

-
- {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 */} +
+ {skillCategories.map((category, categoryIndex) => { + const colorClass = getColorByPosition(categoryIndex); + return ( +
+
+

+ {category.title} +

+
+ Skill category {categoryIndex + 1} of {skillCategories.length}. + Contains {category.skills.length} skills. +
+
- return ( -
-
- - {skillItem.skill} - -
- - {displayLevel} - - +
+ {category.skills.map((skillItem, skillIndex) => { + const displayLevel = calculateSkillLevel(skillItem.value); + return ( +
+
+

+ {skillItem.skill} +

+
+ + {displayLevel} + + +
+
+ +
+
+ +
+ + {/* Screen reader description */} +
+ Skill {skillIndex + 1} of {category.skills.length} in {category.title}. + Proficiency level: {displayLevel} at {skillItem.value} percent. +
-
-
-
-
- ); - })} -
-
- ))} + ); + })} +
+
+ ); + })} +
+ + {/* Skills summary for screen readers */} +
+

+ 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. +

diff --git a/src/config/texts.ts b/src/config/texts.ts index 5cef05f..966b765 100644 --- a/src/config/texts.ts +++ b/src/config/texts.ts @@ -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', }, diff --git a/src/data/Project.ts b/src/data/Project.ts index 335cf57..69dc89c 100644 --- a/src/data/Project.ts +++ b/src/data/Project.ts @@ -7,4 +7,5 @@ export interface Project { technologies: string[]; github?: string; live?: string; -} \ No newline at end of file + year?: string; +} diff --git a/src/scss/App.scss b/src/scss/App.scss index 66a3c7b..345151f 100644 --- a/src/scss/App.scss +++ b/src/scss/App.scss @@ -40,7 +40,7 @@ body { section { min-height: 100vh; - padding: 1rem 1rem; + padding: 3rem 1rem; margin: 0; // Remove any section margins } diff --git a/src/scss/globals.scss b/src/scss/globals.scss index 16aa09a..35c618f 100644 --- a/src/scss/globals.scss +++ b/src/scss/globals.scss @@ -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}) { diff --git a/src/scss/sections/about-section.scss b/src/scss/sections/about-section.scss index 5726eac..58d322a 100644 --- a/src/scss/sections/about-section.scss +++ b/src/scss/sections/about-section.scss @@ -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); } diff --git a/src/scss/sections/certifications-section.scss b/src/scss/sections/certifications-section.scss index 6f2bd85..1deee41 100644 --- a/src/scss/sections/certifications-section.scss +++ b/src/scss/sections/certifications-section.scss @@ -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; diff --git a/src/scss/sections/contact-section.scss b/src/scss/sections/contact-section.scss index 28498ce..da926d0 100644 --- a/src/scss/sections/contact-section.scss +++ b/src/scss/sections/contact-section.scss @@ -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; + } + } +} diff --git a/src/scss/sections/hero-section.scss b/src/scss/sections/hero-section.scss index 6a6bbd3..1ea32aa 100644 --- a/src/scss/sections/hero-section.scss +++ b/src/scss/sections/hero-section.scss @@ -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 { diff --git a/src/scss/sections/projects-section.scss b/src/scss/sections/projects-section.scss index 8bb74c5..673ed55 100644 --- a/src/scss/sections/projects-section.scss +++ b/src/scss/sections/projects-section.scss @@ -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; } } diff --git a/src/scss/sections/services-section.scss b/src/scss/sections/services-section.scss index 6919989..9c97d5f 100644 --- a/src/scss/sections/services-section.scss +++ b/src/scss/sections/services-section.scss @@ -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; + } +} diff --git a/src/scss/sections/skills-section.scss b/src/scss/sections/skills-section.scss index 2de813f..af43fb0 100644 --- a/src/scss/sections/skills-section.scss +++ b/src/scss/sections/skills-section.scss @@ -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); diff --git a/src/scss/themes/_base.scss b/src/scss/themes/_base.scss index 523e781..f1616fb 100644 --- a/src/scss/themes/_base.scss +++ b/src/scss/themes/_base.scss @@ -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}, diff --git a/src/scss/themes/_contact.scss b/src/scss/themes/_contact.scss index 4c61353..8a7adc3 100644 --- a/src/scss/themes/_contact.scss +++ b/src/scss/themes/_contact.scss @@ -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 ); diff --git a/src/scss/themes/_services.scss b/src/scss/themes/_services.scss index 87a0ebe..3f76b04 100644 --- a/src/scss/themes/_services.scss +++ b/src/scss/themes/_services.scss @@ -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 ); diff --git a/src/scss/themes/_skills.scss b/src/scss/themes/_skills.scss index 03687ff..c34f5f8 100644 --- a/src/scss/themes/_skills.scss +++ b/src/scss/themes/_skills.scss @@ -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: diff --git a/src/scss/variables.scss b/src/scss/variables.scss index 4b96bc4..dca2df8 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -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;