Build a Portfolio Website
Build and deploy a professional portfolio site with HTML and CSS.
Introduction
30 Second Summary
When a recruiter looks you up online, the first thing they see shapes their impression. A personal portfolio website is the fastest way to stand out from other applicants.
In this project, you will build a professional portfolio website from scratch using HTML and CSS, then deploy it live so anyone can visit it with a single link.
What You'll Build
A single-page portfolio site featuring a hero section, a responsive project card grid, skills badges, an experience timeline, and a contact section with a CV download button.
By the end of this project, you'll have:
- A live, professional portfolio website hosted on GitHub Pages that you can link on applications.
- A scalable card-based project grid built with CSS Grid that stays clean as you add more projects over time.
- A solid understanding of how HTML structure and CSS styling work together to create web pages.
- Secret Mission: Add a dark/light mode toggle using CSS variables and a few lines of JavaScript.
Are there any prerequisites?
None. This project teaches HTML and CSS from scratch. You just need VS Code, GitHub Desktop, and a GitHub Pages repository already set up.
Set Up Your Project Files
Before writing any code, you need a clean project structure in place. Every website starts with an HTML file for content and a CSS file for styling.
In this step, you will create those two files, connect a professional font, and confirm your local server displays the page correctly.
In this step, get ready to:
- Create your project files and folder structure.
- Write the HTML boilerplate that connects your stylesheet and font.
- Start your local server and confirm the page loads.
Create your project files
Your portfolio site needs three things: an HTML file for the page content, a CSS file for the visual design, and a folder to hold your CV PDF.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select your GitHub Pages repository folder (the one connected to GitHub Desktop), then click Open.
- In the Explorer sidebar on the left, click the New File icon (a page with a plus sign).
- Type index.html and press Enter.
- Click the New File icon again and create a file called style.css.
- Click the New Folder icon (a folder with a plus sign) and name it assets.
- Place your CV or resume PDF inside the assets folder and rename it to cv.pdf.
No CV ready yet?
Create any blank PDF as a placeholder for now. You can replace it with your real CV later before deploying.
- Confirm your files exist by checking the Explorer sidebar.
You should see index.html, style.css, and an assets folder containing cv.pdf in the sidebar.
Don't see the Explorer sidebar?
Press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle the Explorer panel.
If your files are missing, make sure you created them at the top level of your repository folder, not inside a subfolder. Help me find my project files in VS Code.
Write the HTML boilerplate
Every web page starts with a standard skeleton that tells the browser what language the page uses, how to scale it on mobile, and which external files to load. This boilerplate connects your stylesheet and loads the Google Fonts family Inter for a clean, professional look.
- Open index.html by clicking it in the Explorer sidebar, then paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Name - Mechatronics Engineering & Finance</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
</html>
What does this code do?
- <!DOCTYPE html> tells the browser this is a modern HTML5 page.
- The viewport meta tag makes the page scale correctly on phones and tablets.
- The two preconnect links tell the browser to start connecting to Google Fonts early, which speeds up font loading.
- The Google Fonts link loads the Inter font in four weights (400, 500, 600, 700).
- The last link connects your style.css file so any styles you write there apply to this page.
- & is the HTML way to write an ampersand (&) character inside a tag.
- Replace Your Name in the <title> tag with your actual name.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing a white dot on the file tab?
A white dot on the tab name means the file has unsaved changes. Press Cmd+S (macOS) or Ctrl+S (Windows) to save. The dot disappears once the file is saved.
Help me save my file in VS Code.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before continuing.
ⓧ I'd like to double check the full code
Here is the complete index.html file at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Name - Mechatronics Engineering & Finance</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
</html>
Your style.css file should be empty for now.
Start your local server and verify
A local server lets your browser load the page as if it were a real website. This is important because some features (like font loading) only work when a page is served over HTTP.
- Start your Python local server from your project folder.
- Open your browser and navigate to the address your server displays (typically http://localhost:8000).
You should see a completely blank page. Check the browser tab at the top. Your name should appear there (from the <title> tag you set).
- Open DevTools by pressing F12 (or Cmd+Option+I on macOS).
- Click the Console tab and confirm there are no red error messages.
What should I see?
The page itself is blank because the <body> tag is empty. The browser tab shows your name because that comes from the <title> element in <head>.
No console errors means your stylesheet link and font links are all valid.
Seeing errors in the Console?
If you see an error mentioning style.css, make sure the file exists at the same level as index.html (not inside the assets folder).
If the page does not load at all, confirm your Python server is still running in the terminal. Help me troubleshoot my local server.
Your project structure is ready and your files are connected. Next up, you will fill in the HTML structure with your actual portfolio content.
Build the HTML Structure
Your HTML file is connected and loading in the browser, but the page is completely blank. To turn it into a portfolio, you need to add the actual content structure inside the <body> tags.
HTML provides special elements that describe what each part of a page means. Instead of using generic containers for everything, you will use elements like <header>, <main>, <section>, and <footer> that communicate the purpose of each area to browsers and screen readers.
In this step, get ready to:
- Build the page skeleton with a hero header, four content sections, and a footer.
- Create a project card grid with three example project cards.
- Add your skills, experience, and contact details with a CV download button.
Create the page skeleton with semantic HTML
Semantic HTML elements tell the browser what role each piece of content plays on the page. A <header> introduces the page, <main> wraps the primary content, each <section> groups a topic, and <footer> closes the page.
- In index.html, find the empty <body> tags.
- Replace the empty body with the following page skeleton:
<header class="hero">
<h1>Your Name</h1>
<p class="tagline">Mechatronics Engineering & Finance Student</p>
<p class="pitch">I build systems that bridge hardware, software, and financial analysis.</p>
<div class="hero-links">
<a href="https://github.com/your-username" target="_blank">GitHub</a>
<a href="https://linkedin.com/in/your-username" target="_blank">LinkedIn</a>
</div>
</header>
<main>
<section class="projects" id="projects">
<h2>Projects</h2>
</section>
<section class="skills" id="skills">
<h2>Skills</h2>
</section>
<section class="experience" id="experience">
<h2>Experience</h2>
</section>
<section class="contact" id="contact">
<h2>Contact Me</h2>
</section>
</main>
<footer>
<p>Built from scratch with HTML and CSS.</p>
</footer>
What does this code do?
The <header> with class hero is your introduction area. It holds your name in an <h1> (the most important heading on the page), a tagline, a pitch sentence, and links to your GitHub and LinkedIn profiles.
The <main> element wraps four <section> elements. Each section has an id attribute so you can link to it later, and a class for styling. The <footer> sits outside <main> because it is not part of the primary content.
The target="_blank" attribute on the GitHub and LinkedIn links opens them in a new browser tab so visitors do not leave your portfolio.
- Save index.html and refresh your browser.
- You should see your name as a large heading, your tagline, pitch, the two links, four section headings (Projects, Skills, Experience, Contact Me), and the footer text at the bottom.
Don't see any text on the page?
- Make sure you pasted the code between the opening <body> and closing </body> tags, not outside them.
- Check that your Python local server is still running in the terminal.
- Open DevTools with F12 and check the Console tab for any red error messages.
Still stuck? Help me figure out why my HTML content is not showing in the browser.
Build the project cards
Each project on your portfolio is its own self-contained card. The <article> element is perfect for this because it represents a standalone piece of content that could exist on its own.
- In index.html, find the Projects section:
<section class="projects" id="projects">
<h2>Projects</h2>
</section>
- Replace it with the full Projects section containing three project cards:
<section class="projects" id="projects">
<h2>Projects</h2>
<div class="project-grid">
<article class="project-card">
<h3>Project Title One</h3>
<p>Brief description of what you built and what you learned. Explain the problem it solves and your role.</p>
<div class="tags">
<span class="tag">Python</span>
<span class="tag">Arduino</span>
<span class="tag">CAD</span>
</div>
<a href="https://github.com/your-username/project-one" target="_blank" class="project-link">View on GitHub</a>
</article>
<article class="project-card">
<h3>Project Title Two</h3>
<p>Brief description of what you built and what you learned. Explain the problem it solves and your role.</p>
<div class="tags">
<span class="tag">Excel</span>
<span class="tag">Financial Modeling</span>
<span class="tag">Data Analysis</span>
</div>
<a href="https://github.com/your-username/project-two" target="_blank" class="project-link">View on GitHub</a>
</article>
<article class="project-card">
<h3>Project Title Three</h3>
<p>Brief description of what you built and what you learned. Explain the problem it solves and your role.</p>
<div class="tags">
<span class="tag">Robotics</span>
<span class="tag">C++</span>
<span class="tag">Sensors</span>
</div>
<a href="https://github.com/your-username/project-three" target="_blank" class="project-link">View on GitHub</a>
</article>
</div>
</section>
What does this code do?
The project-grid div is a container that will become a responsive grid when you add CSS later. Inside it, each <article> with class project-card represents one project.
Each card has an <h3> title, a <p> description, technology tags wrapped in <span> elements, and a link to the repository. The <span> elements will become pill-shaped badges once you style them in CSS.
- Save index.html and refresh your browser.
- You should see three project titles, their descriptions, technology names, and "View on GitHub" links listed as plain text under the Projects heading.
Only see the Projects heading with no cards?
- Make sure you replaced the entire original <section class="projects"> block, not just pasted inside it (which would leave a duplicate <h2> tag).
- Check that every opening tag has a matching closing tag. The section should end with </div> then </section>.
Help me find why my project cards are not appearing.
Add skills, experience, and contact sections
The remaining three sections follow a similar pattern. Each one has a heading, a container, and content elements with classes you will style later.
- Find the Skills section in your file:
<section class="skills" id="skills">
<h2>Skills</h2>
</section>
- Replace it with the full Skills section containing categorized skill badges:
<section class="skills" id="skills">
<h2>Skills</h2>
<div class="skills-category">
<h3>Engineering</h3>
<div class="skills-list">
<span class="skill-badge">Python</span>
<span class="skill-badge">C/C++</span>
<span class="skill-badge">MATLAB</span>
<span class="skill-badge">Arduino</span>
<span class="skill-badge">SolidWorks</span>
<span class="skill-badge">Circuit Design</span>
</div>
</div>
<div class="skills-category">
<h3>Finance</h3>
<div class="skills-list">
<span class="skill-badge">Financial Modeling</span>
<span class="skill-badge">Excel</span>
<span class="skill-badge">Data Analysis</span>
<span class="skill-badge">Risk Assessment</span>
</div>
</div>
</section>
What does this code do?
Skills are grouped into categories using skills-category divs. Each category has a subheading (<h3>) and a skills-list container full of <span> elements. These spans will become pill-shaped badges with CSS later.
- Save index.html and refresh your browser.
- You should see "Engineering" and "Finance" subheadings with skill names listed as plain text below them.
- Next, find the Experience section:
<section class="experience" id="experience">
<h2>Experience</h2>
</section>
- Replace it with the full Experience section containing two timeline entries:
<section class="experience" id="experience">
<h2>Experience</h2>
<div class="experience-list">
<div class="experience-item">
<div class="experience-header">
<h3>Role Title</h3>
<span class="experience-date">Jan 2025 - Present</span>
</div>
<p class="experience-org">Organization Name</p>
<p>One-line description of what you did and the impact you made.</p>
</div>
<div class="experience-item">
<div class="experience-header">
<h3>Role Title</h3>
<span class="experience-date">Jun 2024 - Dec 2024</span>
</div>
<p class="experience-org">Organization Name</p>
<p>One-line description of what you did and the impact you made.</p>
</div>
</div>
</section>
What does this code do?
Each experience-item groups a role together with its date and organization. The experience-header div holds the role title and date side by side (once styled). The experience-org paragraph highlights where you worked.
- Save index.html and refresh. You should see role titles, dates, organization names, and descriptions listed under the Experience heading.
- Finally, find the Contact section:
<section class="contact" id="contact">
<h2>Contact Me</h2>
</section>
- Replace it with the full Contact section containing a CV download button and profile links:
<section class="contact" id="contact">
<h2>Contact Me</h2>
<p class="contact-intro">Interested in working together? Reach out or download my CV below.</p>
<a href="assets/cv.pdf" download="YourName-CV.pdf" class="cv-button">Download CV (PDF)</a>
<div class="contact-links">
<a href="mailto:your-email@example.com">Email</a>
<a href="https://github.com/your-username" target="_blank">GitHub</a>
<a href="https://linkedin.com/in/your-username" target="_blank">LinkedIn</a>
</div>
</section>
What does the download attribute do?
The download attribute on the CV link tells the browser to save the file instead of opening it in the browser tab. The value YourName-CV.pdf suggests a clean filename for the downloaded file.
This works reliably because your PDF is stored in the same repository (same-origin). The mailto: link opens the visitor's default email app with your address pre-filled.
- Save index.html and refresh your browser.
- You should see the "Contact Me" heading, your intro message, a "Download CV (PDF)" link, and three contact links (Email, GitHub, LinkedIn) at the bottom of the page.
- Click the "Download CV (PDF)" link to confirm it triggers a download (or shows a 404 if you used a placeholder file).
CV link opens the PDF in the browser instead of downloading?
- Make sure you typed download="YourName-CV.pdf" as an attribute on the <a> tag, not as part of the href value.
- Check that the href path assets/cv.pdf matches the actual file location in your project folder.
- Some browsers override the download attribute for certain file types. This will work correctly once deployed to GitHub Pages.
Help me fix my CV download link.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html and all your content is visible in the browser.
ⓧ I'd like to double check the full code
Here is the complete index.html file at this point. Compare your file against this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Name - Mechatronics Engineering & Finance</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="hero">
<h1>Your Name</h1>
<p class="tagline">Mechatronics Engineering & Finance Student</p>
<p class="pitch">I build systems that bridge hardware, software, and financial analysis.</p>
<div class="hero-links">
<a href="https://github.com/your-username" target="_blank">GitHub</a>
<a href="https://linkedin.com/in/your-username" target="_blank">LinkedIn</a>
</div>
</header>
<main>
<section class="projects" id="projects">
<h2>Projects</h2>
<div class="project-grid">
<article class="project-card">
<h3>Project Title One</h3>
<p>Brief description of what you built and what you learned. Explain the problem it solves and your role.</p>
<div class="tags">
<span class="tag">Python</span>
<span class="tag">Arduino</span>
<span class="tag">CAD</span>
</div>
<a href="https://github.com/your-username/project-one" target="_blank" class="project-link">View on GitHub</a>
</article>
<article class="project-card">
<h3>Project Title Two</h3>
<p>Brief description of what you built and what you learned. Explain the problem it solves and your role.</p>
<div class="tags">
<span class="tag">Excel</span>
<span class="tag">Financial Modeling</span>
<span class="tag">Data Analysis</span>
</div>
<a href="https://github.com/your-username/project-two" target="_blank" class="project-link">View on GitHub</a>
</article>
<article class="project-card">
<h3>Project Title Three</h3>
<p>Brief description of what you built and what you learned. Explain the problem it solves and your role.</p>
<div class="tags">
<span class="tag">Robotics</span>
<span class="tag">C++</span>
<span class="tag">Sensors</span>
</div>
<a href="https://github.com/your-username/project-three" target="_blank" class="project-link">View on GitHub</a>
</article>
</div>
</section>
<section class="skills" id="skills">
<h2>Skills</h2>
<div class="skills-category">
<h3>Engineering</h3>
<div class="skills-list">
<span class="skill-badge">Python</span>
<span class="skill-badge">C/C++</span>
<span class="skill-badge">MATLAB</span>
<span class="skill-badge">Arduino</span>
<span class="skill-badge">SolidWorks</span>
<span class="skill-badge">Circuit Design</span>
</div>
</div>
<div class="skills-category">
<h3>Finance</h3>
<div class="skills-list">
<span class="skill-badge">Financial Modeling</span>
<span class="skill-badge">Excel</span>
<span class="skill-badge">Data Analysis</span>
<span class="skill-badge">Risk Assessment</span>
</div>
</div>
</section>
<section class="experience" id="experience">
<h2>Experience</h2>
<div class="experience-list">
<div class="experience-item">
<div class="experience-header">
<h3>Role Title</h3>
<span class="experience-date">Jan 2025 - Present</span>
</div>
<p class="experience-org">Organization Name</p>
<p>One-line description of what you did and the impact you made.</p>
</div>
<div class="experience-item">
<div class="experience-header">
<h3>Role Title</h3>
<span class="experience-date">Jun 2024 - Dec 2024</span>
</div>
<p class="experience-org">Organization Name</p>
<p>One-line description of what you did and the impact you made.</p>
</div>
</div>
</section>
<section class="contact" id="contact">
<h2>Contact Me</h2>
<p class="contact-intro">Interested in working together? Reach out or download my CV below.</p>
<a href="assets/cv.pdf" download="YourName-CV.pdf" class="cv-button">Download CV (PDF)</a>
<div class="contact-links">
<a href="mailto:your-email@example.com">Email</a>
<a href="https://github.com/your-username" target="_blank">GitHub</a>
<a href="https://linkedin.com/in/your-username" target="_blank">LinkedIn</a>
</div>
</section>
</main>
<footer>
<p>Built from scratch with HTML and CSS.</p>
</footer>
</body>
</html>
All of your content is on the page. Next up, you will transform this wall of text into a polished layout using CSS.
Style the Layout with CSS
Your HTML is complete, but right now everything appears as plain, unstyled text. CSS is the language that controls how that HTML looks in the browser.
In this step, you will build the visual foundation of your portfolio: a color system, a centered hero section, and a responsive project grid. By the end, your page will look like a real website instead of a wall of text.
In this step, get ready to:
- Define a reusable color palette with CSS custom properties.
- Style the hero section with Flexbox to center your name and links.
- Build a responsive project card grid with CSS Grid.
Set up your color palette and base reset
CSS custom properties (also called CSS variables) let you define colors and spacing values in one place. When you want to change a color later, you update one line instead of hunting through your entire stylesheet.
- Switch back to style.css in VS Code.
- Add the following code to define your color palette, a universal reset, and base body styles:
/* Define reusable color and spacing values */
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-light: #555555;
--color-accent: #2563eb;
--color-card-bg: #f8f9fa;
--color-card-border: #e2e8f0;
--color-badge-bg: #e8f0fe;
--color-badge-text: #1a56db;
--color-hero-bg: #f0f4ff;
--spacing-section: 4rem;
--max-width: 1000px;
--radius: 8px;
}
/* Remove default margins and set consistent box-sizing */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Set the base font and colors for the entire page */
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.6;
}
/* Tighten heading line-height so multi-line titles look clean */
h1, h2, h3 {
line-height: 1.2;
}
/* Style all links with the accent color */
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
What does this code do?
The :root selector targets the entire document. Any variable defined here (prefixed with --) can be used anywhere in your CSS with var(--name).
The universal reset (* selector) removes browser-default margins and padding that create inconsistent spacing. Setting box-sizing: border-box means padding is included inside an element's width, which makes sizing predictable.
What are CSS custom properties?
Think of them as named containers for values. Instead of writing #2563eb in 10 different places, you write it once as --color-accent and reference it everywhere. Change the value in one spot and your entire site updates.
- Save style.css and refresh your browser.
- You should see the text change to the Inter font, default margins disappear, and links turn blue. The page still looks basic, but the spacing is tighter and cleaner.
Page looks the same as before?
- Make sure you saved style.css. Unsaved files show a dot next to the filename in the VS Code tab.
- Check that your index.html has <link rel="stylesheet" href="style.css"> in the <head>. This tag connects your CSS to your HTML.
- Open your browser's DevTools (F12) and check the Console tab for errors mentioning the stylesheet.
Why isn't my CSS affecting the page even though I linked the stylesheet?
Style the hero section with Flexbox
Flexbox is a CSS layout system that makes it easy to center and space items. You will use it to center your name, tagline, and links in the hero area at the top of the page.
- In style.css, add the following code below the a:hover rule you just wrote:
/* Hero Section */
.hero {
background-color: var(--color-hero-bg);
padding: var(--spacing-section) 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.hero h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.tagline {
font-size: 1.2rem;
color: var(--color-text-light);
margin-bottom: 0.75rem;
}
.pitch {
font-size: 1rem;
color: var(--color-text-light);
max-width: 500px;
margin-bottom: 1.5rem;
}
.hero-links {
display: flex;
gap: 1.5rem;
}
.hero-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.hero-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
What does this code do?
- display: flex with flex-direction: column stacks children vertically. Combined with align-items: center, everything centers horizontally.
- The .hero-links container uses gap: 1.5rem to space the GitHub and LinkedIn buttons evenly without manual margins.
- The transition property on the links creates a smooth color change on hover instead of an instant swap.
- Save style.css and refresh your browser.
- You should see your name large and centered at the top, with a soft blue background behind it. The GitHub and LinkedIn links appear as outlined buttons side by side. Hover over them to see the smooth fill animation.
Hero not centering?
- Make sure the class in your HTML is class="hero" and in your CSS the selector is .hero. Class selectors always start with a dot.
- Check that you have both display: flex and align-items: center on the .hero rule. You need both for horizontal centering.
Help me debug why my Flexbox centering is not working on the hero section.
Build the project card grid with CSS Grid
CSS Grid is designed for two-dimensional layouts like card grids. The pattern you are about to use automatically creates as many columns as fit on screen and reflows them when the window size changes. This means you can add more project cards in the future without touching your CSS.
- In style.css, add the following code below the .hero-links a:hover rule:
/* Main Content */
main {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
}
section {
padding: var(--spacing-section) 0;
}
section h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
/* Projects Grid */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.project-card {
background-color: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
What does this code do?
- margin: 0 auto on main centers the content area within the browser window. The max-width stops it from stretching too wide on large monitors.
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) tells the browser: "create as many columns as fit, each at least 280px wide, sharing leftover space equally." This is the magic that makes the grid responsive without any media queries.
- The :hover rule lifts the card upward by 4px and adds a shadow, creating a subtle "floating" effect when you mouse over it.
Why CSS Grid instead of Flexbox here?
Flexbox works best for one-dimensional layouts (a single row or column). CSS Grid excels at two-dimensional layouts where you want rows and columns at the same time. A card grid needs both, so Grid is the right tool.
The auto-fill keyword means the grid never needs manual updates. Add a fourth or fifth project card to your HTML and the grid automatically places it.
- Save style.css and refresh your browser.
- Your project cards should appear in a neat grid (3 columns on a wide screen). Resize your browser window narrower to see the cards reflow to 2 columns, then 1 column. Hover over a card to see it lift with a shadow.
Cards stacking in a single column on a wide screen?
- Check that your CSS selector is .project-grid (with a dot) and matches the class="project-grid" in your HTML exactly.
- Make sure you wrote display: grid (not display: flex) on the .project-grid rule.
- Verify there are no typos in grid-template-columns. A missing closing parenthesis will break the rule silently.
Why are my project cards not displaying in a grid layout?
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and your page shows a centered hero section and a responsive card grid.
ⓧ I'd like to double check the full code
Here is the complete style.css at this point. Compare yours against it:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-light: #555555;
--color-accent: #2563eb;
--color-card-bg: #f8f9fa;
--color-card-border: #e2e8f0;
--color-badge-bg: #e8f0fe;
--color-badge-text: #1a56db;
--color-hero-bg: #f0f4ff;
--spacing-section: 4rem;
--max-width: 1000px;
--radius: 8px;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Hero Section */
.hero {
background-color: var(--color-hero-bg);
padding: var(--spacing-section) 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.hero h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.tagline {
font-size: 1.2rem;
color: var(--color-text-light);
margin-bottom: 0.75rem;
}
.pitch {
font-size: 1rem;
color: var(--color-text-light);
max-width: 500px;
margin-bottom: 1.5rem;
}
.hero-links {
display: flex;
gap: 1.5rem;
}
.hero-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.hero-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
/* Main Content */
main {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
}
section {
padding: var(--spacing-section) 0;
}
section h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
/* Projects Grid */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.project-card {
background-color: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
Your layout foundation is in place. The hero draws the eye, and the project grid handles itself no matter how many cards you add. Next up, you will polish the remaining sections and make the whole page feel cohesive.
Polish the Design
Your project card grid is taking shape, but the content inside each card is still unstyled. The skills, experience, and contact sections need attention too.
In this step, you will finish styling the project cards, turn your skills into neat pill badges, give experience entries a timeline feel, and make the contact section stand out with a prominent CV download button.
In this step, get ready to:
- Style skills and experience sections with Flexbox pill badges and a timeline layout.
- Style the contact section with a prominent CV download button and link row.
- Add a footer with a subtle border and consistent section spacing.
Style the skills and experience sections
Right now, the text inside your project cards and the skills section are plain and unformatted. You will use Flexbox with flex-wrap: wrap to display technology tags as inline pill-shaped badges that flow neatly across lines.
- In style.css, find the closing brace of your .project-card:hover rule.
- Add the following styles directly below it to finish the card interior and create pill-shaped tags:
.project-card h3 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
}
.project-card p {
color: var(--color-text-light);
font-size: 0.95rem;
margin-bottom: 1rem;
flex-grow: 1;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tag {
background-color: var(--color-badge-bg);
color: var(--color-badge-text);
font-size: 0.8rem;
font-weight: 500;
padding: 0.25rem 0.75rem;
border-radius: 100px;
}
.project-link {
font-weight: 500;
font-size: 0.9rem;
}
What does this code do?
These rules style the elements inside each project card.
- flex-grow: 1 on the paragraph pushes the tags and link to the bottom of the card, keeping cards aligned even when descriptions are different lengths.
- border-radius: 100px creates the "pill" shape. Any large value rounds the ends into semicircles regardless of the element's height.
- flex-wrap: wrap on .tags lets tags overflow onto a new line instead of squishing together.
- Save style.css and refresh your browser.
- Your project cards now show pill-shaped technology tags in a light blue background, and card descriptions are neatly spaced.
Tags not showing as pills?
- Check that the class name in your CSS (.tag) matches the class in your HTML (class="tag"). CSS class names are case-sensitive.
- Make sure you placed this code AFTER the .project-card:hover block, not inside it.
Why aren't my project card tags styled as pill badges?
Now apply the same pill-badge pattern to your Skills section. The badges use the exact same CSS variables, just with slightly different sizing.
- Add the following Skills section styles below your .project-link rule:
/* Skills Section */
.skills-category {
margin-bottom: 1.5rem;
}
.skills-category h3 {
font-size: 1.1rem;
margin-bottom: 0.75rem;
color: var(--color-text-light);
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-badge {
background-color: var(--color-badge-bg);
color: var(--color-badge-text);
font-size: 0.85rem;
font-weight: 500;
padding: 0.4rem 0.9rem;
border-radius: 100px;
}
What does this code do?
- .skills-list uses flex-wrap: wrap so badges flow onto new lines when they run out of horizontal space.
- .skill-badge reuses the same badge color variables as the project tags. Consistent colors tie the sections together visually.
- Save style.css and refresh your browser.
- Your Skills section now shows neat rows of pill-shaped badges grouped under "Engineering" and "Finance" subheadings.
Skills badges not wrapping?
- Confirm you used .skills-list (not .skill-list) to match the class name in your HTML.
- Check that display: flex is on the .skills-list container, not on the individual badges.
Help me debug my skills badges layout.
Next, style the Experience section as a clean vertical list with a colored left border that creates a timeline feel.
- Add the following Experience section styles below your .skill-badge rule:
/* Experience Section */
.experience-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.experience-item {
padding-left: 1.5rem;
border-left: 3px solid var(--color-accent);
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 0.5rem;
}
.experience-header h3 {
font-size: 1.1rem;
}
.experience-date {
font-size: 0.85rem;
color: var(--color-text-light);
}
.experience-org {
font-weight: 500;
color: var(--color-accent);
margin-bottom: 0.25rem;
}
.experience-item > p:last-child {
color: var(--color-text-light);
font-size: 0.95rem;
}
What does this code do?
- border-left: 3px solid var(--color-accent) creates a colored left border on each experience entry, giving the section a timeline appearance.
- justify-content: space-between on the header pushes the role title to the left and the date to the right.
- .experience-item > p:last-child targets only the description paragraph (the last <p> inside each item) and gives it a lighter color for visual hierarchy.
- Save style.css and refresh your browser.
- Each experience entry now has a blue left border, with the role title on the left and the date on the right. The organization name appears in the accent color below.
Date not sitting on the right side?
- Make sure display: flex and justify-content: space-between are on .experience-header, not on .experience-item.
- Check your HTML has the <h3> and <span class="experience-date"> both inside the experience-header div.
Help me fix my experience section layout.
Style the contact section
The contact section is where recruiters decide whether to reach out. The CV download link needs to stand out as a clear call-to-action button.
- Add the following Contact section styles below your .experience-item > p:last-child rule:
/* Contact Section */
.contact {
text-align: center;
}
.contact-intro {
color: var(--color-text-light);
margin-bottom: 1.5rem;
}
.cv-button {
display: inline-block;
background-color: var(--color-accent);
color: #ffffff;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
font-size: 1rem;
transition: opacity 0.2s;
margin-bottom: 1.5rem;
}
.cv-button:hover {
opacity: 0.9;
text-decoration: none;
}
.contact-links {
display: flex;
justify-content: center;
gap: 1.5rem;
}
.contact-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.contact-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
What does this code do?
- .cv-button turns your download link into a solid blue button. The display: inline-block is needed because <a> elements are inline by default and ignore padding/margin without it.
- .contact-links a gives the Email, GitHub, and LinkedIn links a bordered button style that fills with color on hover.
- transition properties on both buttons create a smooth color shift when the cursor enters, instead of an instant snap.
- Save style.css and refresh your browser.
- The Contact section is now centered with a prominent blue "Download CV" button. Below it, Email, GitHub, and LinkedIn appear as bordered buttons that fill blue on hover.
CV button not looking like a button?
- Make sure your HTML link has class="cv-button" on it. Without the class, the CSS rule has nothing to target.
- Check for a missing semicolon on the line above display: inline-block. A missing semicolon breaks all rules that follow it in the same selector.
Why doesn't my CV download link look like a button?
Add the footer
A subtle footer gives the page a clean bottom edge. The top border separates it from the content above without competing for attention.
- Add the following footer styles below your .contact-links a:hover rule:
/* Footer */
footer {
text-align: center;
padding: 2rem 1.5rem;
border-top: 1px solid var(--color-card-border);
color: var(--color-text-light);
font-size: 0.85rem;
}
What does this code do?
- border-top: 1px solid var(--color-card-border) adds a thin line that visually separates the footer from the last section.
- The lighter color and smaller font size keep the footer understated. It should be present but not distract from your main content.
- Save style.css and refresh your browser.
- Scroll to the bottom of the page. You should see a small centered footer line with a subtle top border above it.
Footer border not showing?
- Confirm you used border-top (not border) to only show the line on the top edge.
- If the border color is the same as your background, it will be invisible. Check that --color-card-border is defined in your :root block.
Help me debug my footer styling.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and that your browser reflects all the styling changes.
ⓧ I'd like to double check the full code
Here is the complete style.css file as it should look at the end of this step:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-light: #555555;
--color-accent: #2563eb;
--color-card-bg: #f8f9fa;
--color-card-border: #e2e8f0;
--color-badge-bg: #e8f0fe;
--color-badge-text: #1a56db;
--color-hero-bg: #f0f4ff;
--spacing-section: 4rem;
--max-width: 1000px;
--radius: 8px;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Hero Section */
.hero {
background-color: var(--color-hero-bg);
padding: var(--spacing-section) 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.hero h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.tagline {
font-size: 1.2rem;
color: var(--color-text-light);
margin-bottom: 0.75rem;
}
.pitch {
font-size: 1rem;
color: var(--color-text-light);
max-width: 500px;
margin-bottom: 1.5rem;
}
.hero-links {
display: flex;
gap: 1.5rem;
}
.hero-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.hero-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
/* Main Content */
main {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
}
section {
padding: var(--spacing-section) 0;
}
section h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
/* Projects Grid */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.project-card {
background-color: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.project-card h3 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
}
.project-card p {
color: var(--color-text-light);
font-size: 0.95rem;
margin-bottom: 1rem;
flex-grow: 1;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tag {
background-color: var(--color-badge-bg);
color: var(--color-badge-text);
font-size: 0.8rem;
font-weight: 500;
padding: 0.25rem 0.75rem;
border-radius: 100px;
}
.project-link {
font-weight: 500;
font-size: 0.9rem;
}
/* Skills Section */
.skills-category {
margin-bottom: 1.5rem;
}
.skills-category h3 {
font-size: 1.1rem;
margin-bottom: 0.75rem;
color: var(--color-text-light);
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-badge {
background-color: var(--color-badge-bg);
color: var(--color-badge-text);
font-size: 0.85rem;
font-weight: 500;
padding: 0.4rem 0.9rem;
border-radius: 100px;
}
/* Experience Section */
.experience-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.experience-item {
padding-left: 1.5rem;
border-left: 3px solid var(--color-accent);
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 0.5rem;
}
.experience-header h3 {
font-size: 1.1rem;
}
.experience-date {
font-size: 0.85rem;
color: var(--color-text-light);
}
.experience-org {
font-weight: 500;
color: var(--color-accent);
margin-bottom: 0.25rem;
}
.experience-item > p:last-child {
color: var(--color-text-light);
font-size: 0.95rem;
}
/* Contact Section */
.contact {
text-align: center;
}
.contact-intro {
color: var(--color-text-light);
margin-bottom: 1.5rem;
}
.cv-button {
display: inline-block;
background-color: var(--color-accent);
color: #ffffff;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
font-size: 1rem;
transition: opacity 0.2s;
margin-bottom: 1.5rem;
}
.cv-button:hover {
opacity: 0.9;
text-decoration: none;
}
.contact-links {
display: flex;
justify-content: center;
gap: 1.5rem;
}
.contact-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.contact-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
/* Footer */
footer {
text-align: center;
padding: 2rem 1.5rem;
border-top: 1px solid var(--color-card-border);
color: var(--color-text-light);
font-size: 0.85rem;
}
Your portfolio site now looks polished and professional. Next up, you will make it responsive for mobile screens and deploy it live to GitHub Pages.
Make It Responsive and Deploy
Your portfolio looks great on a wide desktop screen. But recruiters often check links from their phones first.
In this step, you will add media queries so the layout adapts to smaller screens. Then you will push everything to GitHub Pages so your site is live on the internet with a real URL.
In this step, get ready to:
- Add a responsive media query for mobile screens.
- Test the layout at phone width.
- Commit and deploy your site to GitHub Pages.
Add a media query for small screens
A media query tells the browser to apply different styles when the screen meets a certain condition. You will target screens narrower than 768px, which covers most phones and small tablets.
Why no changes to the project grid?
Your .project-grid already uses auto-fill with minmax(280px, 1fr). This means the grid automatically collapses from three columns to two to one as the screen shrinks. No extra media query rules needed for the cards.
- In style.css, scroll to the very bottom of the file (after the footer rule).
- Add the responsive media query by pasting this code at the end of the file:
/* Responsive Design */
@media (max-width: 768px) {
.hero h1 {
font-size: 2rem;
}
.tagline {
font-size: 1rem;
}
section h2 {
font-size: 1.4rem;
}
.experience-header {
flex-direction: column;
gap: 0.25rem;
}
.contact-links {
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
}
What does this code do?
Everything inside the @media (max-width: 768px) block only applies when the browser window is 768 pixels wide or narrower.
- .hero h1 and .tagline get smaller font sizes so they do not overflow on narrow screens.
- section h2 shrinks from 1.75rem to 1.4rem for a better fit.
- .experience-header switches from a horizontal row (role title on left, date on right) to a vertical stack so neither gets cut off.
- .contact-links stacks vertically so each link button has enough tap space on a phone screen.
- Save style.css.
- Resize your browser window to about half its width. You should see the hero heading shrink and the contact links stack into a vertical column.
Contact links still in a row?
- Make sure you saved style.css after pasting the media query.
- Check that the @media block is outside (below) the closing brace of the footer rule. Nesting a media query inside another rule breaks it.
- Make sure your browser window is actually narrower than 768px. Drag it smaller until the change triggers.
Why aren't my media query styles applying?
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete style.css file including the new media query at the bottom.
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-text-light: #555555;
--color-accent: #2563eb;
--color-card-bg: #f8f9fa;
--color-card-border: #e2e8f0;
--color-badge-bg: #e8f0fe;
--color-badge-text: #1a56db;
--color-hero-bg: #f0f4ff;
--spacing-section: 4rem;
--max-width: 1000px;
--radius: 8px;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Hero Section */
.hero {
background-color: var(--color-hero-bg);
padding: var(--spacing-section) 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.hero h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.tagline {
font-size: 1.2rem;
color: var(--color-text-light);
margin-bottom: 0.75rem;
}
.pitch {
font-size: 1rem;
color: var(--color-text-light);
max-width: 500px;
margin-bottom: 1.5rem;
}
.hero-links {
display: flex;
gap: 1.5rem;
}
.hero-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.hero-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
/* Main Content */
main {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
}
section {
padding: var(--spacing-section) 0;
}
section h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
/* Projects Grid */
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.project-card {
background-color: var(--color-card-bg);
border: 1px solid var(--color-card-border);
border-radius: var(--radius);
padding: 1.5rem;
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.project-card h3 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
}
.project-card p {
color: var(--color-text-light);
font-size: 0.95rem;
margin-bottom: 1rem;
flex-grow: 1;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tag {
background-color: var(--color-badge-bg);
color: var(--color-badge-text);
font-size: 0.8rem;
font-weight: 500;
padding: 0.25rem 0.75rem;
border-radius: 100px;
}
.project-link {
font-weight: 500;
font-size: 0.9rem;
}
/* Skills Section */
.skills-category {
margin-bottom: 1.5rem;
}
.skills-category h3 {
font-size: 1.1rem;
margin-bottom: 0.75rem;
color: var(--color-text-light);
}
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.skill-badge {
background-color: var(--color-badge-bg);
color: var(--color-badge-text);
font-size: 0.85rem;
font-weight: 500;
padding: 0.4rem 0.9rem;
border-radius: 100px;
}
/* Experience Section */
.experience-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.experience-item {
padding-left: 1.5rem;
border-left: 3px solid var(--color-accent);
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 0.5rem;
}
.experience-header h3 {
font-size: 1.1rem;
}
.experience-date {
font-size: 0.85rem;
color: var(--color-text-light);
}
.experience-org {
font-weight: 500;
color: var(--color-accent);
margin-bottom: 0.25rem;
}
.experience-item > p:last-child {
color: var(--color-text-light);
font-size: 0.95rem;
}
/* Contact Section */
.contact {
text-align: center;
}
.contact-intro {
color: var(--color-text-light);
margin-bottom: 1.5rem;
}
.cv-button {
display: inline-block;
background-color: var(--color-accent);
color: #ffffff;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
font-size: 1rem;
transition: opacity 0.2s;
margin-bottom: 1.5rem;
}
.cv-button:hover {
opacity: 0.9;
text-decoration: none;
}
.contact-links {
display: flex;
justify-content: center;
gap: 1.5rem;
}
.contact-links a {
font-weight: 500;
padding: 0.5rem 1rem;
border: 2px solid var(--color-accent);
border-radius: var(--radius);
transition: background-color 0.2s, color 0.2s;
}
.contact-links a:hover {
background-color: var(--color-accent);
color: #ffffff;
text-decoration: none;
}
/* Footer */
footer {
text-align: center;
padding: 2rem 1.5rem;
border-top: 1px solid var(--color-card-border);
color: var(--color-text-light);
font-size: 0.85rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.hero h1 {
font-size: 2rem;
}
.tagline {
font-size: 1rem;
}
section h2 {
font-size: 1.4rem;
}
.experience-header {
flex-direction: column;
gap: 0.25rem;
}
.contact-links {
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
}
Test the responsive layout
Before you deploy, you want to make sure nothing looks broken on a phone-sized screen.
- Drag your browser window to its narrowest width (around 375px, the width of a typical phone screen).
- Scroll through the entire page from top to bottom.
- Check that all text is readable and no content overflows horizontally (no sideways scrollbar).
- Confirm the project cards have collapsed to a single column.
- Confirm the CV download button is large enough to tap easily at mobile size.
What should I see?
At phone width, your hero heading is smaller but still readable. The project cards stack in one column. The experience entries show the role title above the date instead of side by side. The contact links are stacked vertically with comfortable spacing between each button.
Commit and push with GitHub Desktop
Your site is ready to go live. GitHub Desktop makes it easy to commit your files and push them to your repository, which triggers GitHub Pages to publish your site.
- Open GitHub Desktop.
- In the left panel, you should see your changed files listed: index.html, style.css, and the assets folder (containing your CV PDF).
- Make sure all files are checked (ticked) so they are included in the commit.
- In the Summary field at the bottom left, type Add portfolio site.
- Click Commit to main.
- Click Push origin in the top bar to send your commit to GitHub.
How long until my site is live?
GitHub Pages typically takes 1-2 minutes to build and deploy after a push. You can check the status by visiting your repository on GitHub and looking at the small dot next to your latest commit. An orange dot means it is building. A green tick means it is live.
- Wait 1-2 minutes, then visit your GitHub Pages URL in a new browser tab.
- Confirm the full site loads with all sections visible.
- Click the Download CV (PDF) button and confirm the file downloads.
- Click the GitHub and LinkedIn links in the hero and contact sections to verify they point to the correct destinations.
Seeing a 404 page?
- Your entry file must be named exactly index.html (lowercase) at the root of your repository. GitHub Pages will not find it if it is in a subfolder or has a different name.
- Check that the push actually completed in GitHub Desktop. If you see Push origin still available, click it again.
- Give it another minute. The first deployment can sometimes take slightly longer.
My GitHub Pages site shows a 404. How do I fix it?
CV download not working on the live site?
- Make sure the assets folder and your PDF file were included in the commit. Check GitHub Desktop to see if they appear in the list of changed files.
- Verify the filename in your href matches the actual file path exactly, including uppercase and lowercase letters.
Why does my CV download link work locally but not on GitHub Pages?
Secret mission
Add Dark/Light Mode Toggle
Your portfolio looks great, but what if visitors prefer dark mode? In this secret mission, you'll add a toggle button that lets users switch between light and dark themes. You'll learn how CSS variables make theming possible and write your first lines of JavaScript.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally and deploys to GitHub Pages for free, so there are no ongoing costs.
Resources you used:
- Project files (index.html, style.css, assets/cv.pdf) in your GitHub Pages repository.
- GitHub Pages live site serving your portfolio.
- Python local server (running process in your terminal).
Keep everything running
No action needed. Choose this if you plan to keep updating your portfolio site with new projects and experience.
- Your GitHub Pages site stays live at your URL.
- Your local files remain in your repository folder, ready for edits.
- The Python local server may still be running in your terminal. You can stop it anytime by pressing Ctrl+C in the terminal window where it is running.
Pause - I'll come back to this later
Stop the running process to free up your terminal. Your files and live site stay intact for when you return.
- Switch to the terminal running your Python local server.
- Press Ctrl+C to stop the server.
- Your GitHub Pages site remains live. Your local files are unchanged and ready for future edits.
Delete - I don't want to use this again
Remove your local project files and take down the live site.
- Stop the Python local server by pressing Ctrl+C in the terminal where it is running.
- Delete your local project files by running the following command from your repository folder:
macOS
rm index.html style.css
rm -rf assets
Windows
del index.html, style.css
rmdir /s /q assets
- To take down your live GitHub Pages site, go to your repository on GitHub.
- Click Settings.
- Scroll down to the Danger Zone section at the bottom of the page.
- Click Delete this repository and follow the confirmation prompts.
This is permanent
Deleting the repository removes your live site and all commit history. Make sure you have a local backup of any files you want to keep before deleting.
Nice Work!
Nice Work!
Nice work! You just built a professional portfolio website from scratch using only HTML and CSS, and deployed it live to the internet.
What you learned:
- Built a complete website using semantic HTML elements (header, main, section, article, footer) and styled it with CSS custom properties, Flexbox, and CSS Grid.
- Created a responsive project card grid using auto-fill that automatically reflows as you add more projects over time.
- Deployed a live site to GitHub Pages with a CV download button and profile links that recruiters can access from a single URL.
- Secret Mission: Added a dark/light mode toggle using CSS variables and minimal JavaScript, with localStorage persistence.
Ready to quiz yourself?