Project: style a complete webpage from scratch
🏗 Chapter 12 — Project: Lumen Studio
This is the capstone chapter of the CSS Beginner course. Rather than introducing new properties, we apply everything from Chapters 1–11 to build a complete, professional, responsive landing page for a fictional creative agency called Lumen Studio. By the end of all three parts you will have a page you can deploy as-is or adapt into your own portfolio.
The project is split across three parts to keep each lesson manageable:
- Part A (this file) — Project intro, HTML structure, and CSS Steps 1–3: foundation variables, reset, utilities, and buttons.
- Part B — CSS Steps 4–8: navigation, hero, clients strip, services grid, and stats bar.
- Part C — CSS Steps 9–13: about section, testimonials, CTA, footer, JavaScript helpers, and "Go Further" challenges.
1 — What We're Building
Lumen Studio is a single-page marketing site for a creative design agency. It uses a clean light theme (white background, dark navy text, blue accent) with a dark navy footer and stats section for contrast. Every major layout and styling technique from the course appears somewhere on the page.
h1 · clamp() · flex actions
position:absolute badge
::before checkmarks
Skills used — chapter map
| Page section | CSS technique | Taught in |
|---|---|---|
| Entire page | CSS custom properties (:root variables) | Ch.9 |
| All text | Typography, clamp(), rem, line-height | Ch.5 Ch.11 |
| All spacing | Box model: margin, padding, gap | Ch.3 |
| Colors & gradients | Custom properties, rgba(), linear-gradient() | Ch.4 |
| Buttons | display: inline-flex, transition, transform | Ch.6 Ch.10 |
| Navigation | position: sticky, Flexbox, backdrop-filter | Ch.7 Ch.8 |
| Hero | CSS Grid 2-col, @keyframes float animation | Ch.9 Ch.10 |
| Services, Testimonials | Grid auto-fit / minmax() | Ch.9 |
| Service card top bar | ::before pseudo-element + transform: scaleX() | Ch.10 |
| About badge | position: absolute on a position: relative parent | Ch.7 |
| CTA circles | ::before / ::after decorative elements | Ch.7 |
| Footer | Grid auto-fit, Flexbox footer-bottom | Ch.9 Ch.8 |
| Responsive layout | Viewport meta, clamp(), @media (min-width) | Ch.11 |
| Scroll reveal | IntersectionObserver + CSS opacity / transform transition | Ch.10 |
2 — File Structure
The project uses three files. Keep them in the same folder so the relative paths work without any configuration.
index.html → "Open with Live Server"). It auto-refreshes the browser every time you save — essential for a CSS-heavy project like this.
3 — The HTML
Build this first. The HTML is the skeleton — we add no CSS yet. Every class name here will be targeted in Parts A, B, and C. Read the comments carefully; they are your map through the CSS walkthrough that follows.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lumen Studio — Creative Design Agency</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- ── NAVIGATION ─────────────────────────────────── -->
<header class="site-header">
<nav class="nav container">
<a href="#" class="nav-logo">Lumen<span>.</span></a>
<ul class="nav-links">
<li><a href="#services">Services</a></li>
<li><a href="#about">About</a></li>
<li><a href="#work">Work</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<a href="#contact" class="btn btn-primary btn-sm">Start a project</a>
<button class="hamburger" aria-label="Open menu">☰</button>
</nav>
</header>
<!-- ── HERO ───────────────────────────────────────── -->
<section class="hero">
<div class="container hero-inner">
<div class="hero-text">
<span class="hero-eyebrow">Creative Studio</span>
<h1>We design brands that <em>stand out</em></h1>
<p>Award-winning design and development for companies
that want to be remembered.</p>
<div class="hero-actions">
<a href="#work" class="btn btn-primary btn-lg">See our work</a>
<a href="#about" class="btn btn-ghost">Learn more</a>
</div>
</div>
<div class="hero-visual" aria-hidden="true">
<div class="h-card h-card--a">
<div class="h-card-icon">✦</div>
<div class="h-card-title">Brand Strategy</div>
<div class="h-card-sub">Identity · Positioning · Voice</div>
</div>
<div class="h-card h-card--b">
<div class="h-card-icon">◈</div>
<div class="h-card-title">UI / UX Design</div>
<div class="h-card-sub">Web · Mobile · Systems</div>
</div>
<div class="h-card h-card--c">
<div class="h-card-icon">⬡</div>
<div class="h-card-title">Development</div>
<div class="h-card-sub">React · Next.js · CMS</div>
</div>
</div>
</div>
</section>
<!-- ── CLIENTS STRIP ──────────────────────────────── -->
<div class="clients">
<div class="container clients-inner">
<span class="clients-label">Trusted by</span>
<div class="clients-logos">
<span class="client-logo">Meridian</span>
<span class="client-logo">Vantage</span>
<span class="client-logo">Halcyon</span>
<span class="client-logo">Prism</span>
<span class="client-logo">Solaris</span>
</div>
</div>
</div>
<!-- ── SERVICES ───────────────────────────────────── -->
<section class="services" id="services">
<div class="container">
<div class="services-header reveal">
<span class="section-label">What we do</span>
<h2 class="section-title">Everything you need to grow</h2>
<p class="section-sub">From brand discovery through to launch and beyond.</p>
</div>
<div class="services-grid">
<div class="s-card reveal"><div class="s-icon">🎯</div><h3>Brand Strategy</h3>
<p>We help you find and own your place in the market.</p></div>
<div class="s-card reveal"><div class="s-icon">🎨</div><h3>UI / UX Design</h3>
<p>Intuitive interfaces that guide users to what matters.</p></div>
<div class="s-card reveal"><div class="s-icon">⚡</div><h3>Web Development</h3>
<p>Fast, accessible builds in React, Next.js or your stack.</p></div>
<div class="s-card reveal"><div class="s-icon">📈</div><h3>Growth & SEO</h3>
<p>Content strategy that turns visitors into customers.</p></div>
<div class="s-card reveal"><div class="s-icon">📱</div><h3>Mobile Design</h3>
<p>Native-feeling experiences with craft users expect.</p></div>
<div class="s-card reveal"><div class="s-icon">🔧</div><h3>Ongoing Support</h3>
<p>Monthly retainers so your presence keeps evolving.</p></div>
</div>
</div>
</section>
<!-- ── STATS ───────────────────────────────────────── -->
<section class="stats">
<div class="container stats-grid">
<div class="stat reveal"><span class="stat-num">140+</span><span class="stat-label">Projects delivered</span></div>
<div class="stat reveal"><span class="stat-num">98%</span><span class="stat-label">Client satisfaction</span></div>
<div class="stat reveal"><span class="stat-num">12</span><span class="stat-label">Industry awards</span></div>
<div class="stat reveal"><span class="stat-num">8yr</span><span class="stat-label">In business</span></div>
</div>
</section>
<!-- ── ABOUT ───────────────────────────────────────── -->
<section class="about" id="about">
<div class="container about-inner">
<div class="about-image-wrap reveal">
<div class="about-img">🎨</div>
<div class="about-badge">
<span class="badge-num">8+</span> Years of craft
</div>
</div>
<div class="about-text reveal">
<span class="section-label">About us</span>
<h2>A small studio with big ambitions</h2>
<p>We're a tight-knit team of designers, developers, and strategists
who believe thoughtful craft and commercial results reinforce each other.</p>
<p>Every project is a partnership. We ask the awkward questions
and sweat the small details most agencies skip.</p>
<ul class="about-checklist">
<li>Dedicated project lead on every engagement</li>
<li>Weekly progress calls and shared Figma access</li>
<li>Fixed-price packages — no surprise invoices</li>
<li>Post-launch support included as standard</li>
</ul>
<a href="#contact" class="btn btn-primary">Work with us</a>
</div>
</div>
</section>
<!-- ── TESTIMONIALS ────────────────────────────────── -->
<section class="testimonials">
<div class="container">
<div class="testi-header reveal">
<span class="section-label">Testimonials</span>
<h2 class="section-title">What our clients say</h2>
<p class="section-sub">We let the work speak — then let the clients speak for the work.</p>
</div>
<div class="testi-grid">
<div class="t-card reveal">
<div class="t-stars">★★★★★</div>
<blockquote>The best creative decision we made this year.
On time, on budget, beyond expectation.</blockquote>
<div class="t-author">
<div class="t-avatar">SA</div>
<div class="t-author-info">
<strong>Sarah Adeyemi</strong><span>CEO, Meridian Labs</span>
</div>
</div>
</div>
<div class="t-card reveal">
<div class="t-stars">★★★★★</div>
<blockquote>They redesigned how we think about our digital
presence. Conversion rates are up 34%.</blockquote>
<div class="t-author">
<div class="t-avatar">JK</div>
<div class="t-author-info">
<strong>James Kowalski</strong><span>Head of Product, Vantage</span>
</div>
</div>
</div>
<div class="t-card reveal">
<div class="t-stars">★★★★★</div>
<blockquote>Extraordinary attention to detail. Every
micro-interaction felt considered.</blockquote>
<div class="t-author">
<div class="t-avatar">ML</div>
<div class="t-author-info">
<strong>Maria Leclaire</strong><span>Design Director, Halcyon</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── CTA ────────────────────────────────────────── -->
<section class="cta-section" id="contact">
<div class="container cta-inner reveal">
<span class="section-label"
style="color: rgba(255,255,255,0.5)">Get started</span>
<h2>Ready to build something great?</h2>
<p>Tell us about your project. We'll reply within one business day.</p>
<a href="mailto:hello@lumen.studio"
class="btn btn-white btn-lg">Start a project →</a>
</div>
</section>
<!-- ── FOOTER ─────────────────────────────────────── -->
<footer class="site-footer">
<div class="container footer-grid">
<div class="footer-brand">
<a href="#" class="nav-logo">Lumen<span>.</span></a>
<p>Creative studio in London, working worldwide.</p>
</div>
<div class="footer-col">
<h4>Services</h4>
<ul>
<li><a href="#">Brand Strategy</a></li>
<li><a href="#">UI / UX Design</a></li>
<li><a href="#">Web Development</a></li>
<li><a href="#">Growth & SEO</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Company</h4>
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Work</a></li>
<li><a href="#">Careers</a></li>
<li><a href="#">Blog</a></li>
</ul>
</div>
<div class="footer-col">
<h4>Contact</h4>
<ul>
<li><a href="mailto:hello@lumen.studio">hello@lumen.studio</a></li>
<li><a href="#">+44 20 7946 0000</a></li>
<li><a href="#">London, UK</a></li>
</ul>
</div>
</div>
<div class="container footer-bottom">
<p>© 2026 Lumen Studio Ltd.</p>
<div><a href="#">Privacy</a> <a href="#">Terms</a></div>
</div>
</footer>
<script src="main.js"></script>
</body>
</html>
reveal class is added to elements that will fade in as the user scrolls. It does nothing until JavaScript activates it in Part C — so leave it on now and the page will look fine without the JS, just without scroll animations.index.html in your browser. You'll see unstyled plain text. That's correct. Every CSS step in Parts A–C progressively improves what you see, which is the best way to understand what each block of CSS actually does.
The first block in style.css does two things: it defines all colours, fonts, and sizes as CSS variables (so changing one value updates the whole page), and it strips the browser's built-in default styles so we start from a clean baseline.
/* ── Custom properties ───────────────────────────── */
:root {
/* Colours */
--clr-bg: #ffffff;
--clr-bg-alt: #f7f8fc;
--clr-dark: #0d1117;
--clr-navy: #111827;
--clr-text: #1f2937;
--clr-muted: #6b7280;
--clr-border: #e5e7eb;
--clr-accent: #4f8ef7;
--clr-accent-dark: #3a7ae0;
/* Font */
--font: system-ui, -apple-system, sans-serif;
/* Reused values */
--radius: 10px;
--container: 1200px;
}
/* ── Reset ───────────────────────────────────────── */
*,
*::before,
*::after {
box-sizing: border-box; /* padding/border inside width — always */
margin: 0;
padding: 0;
}
img,
video {
max-width: 100%;
height: auto;
display: block;
}
ul { list-style: none; }
a { text-decoration: none; color: inherit; }
button { font-family: inherit; cursor: pointer; }
body {
font-family: var(--font);
color: var(--clr-text);
background-color: var(--clr-bg);
line-height: 1.6;
-webkit-font-smoothing: antialiased; /* crisper text on Mac/iOS */
}
:root variables means you can completely rebrand the page by changing 9 values. Try swapping --clr-accent to #e85d3f and watch the entire page update.These three classes are reused throughout the page. Defining them once here prevents repetition across every section. .container is especially important — it applies the max-width + margin: auto centering pattern from Chapter 11 to every section consistently.
/* ── Container — centered, max-width, fluid padding ─ */
.container {
max-width: var(--container); /* 1200px — never wider */
margin: 0 auto; /* horizontally centered */
padding: 0 clamp(1rem, 4vw, 2.5rem); /* fluid side padding */
}
/* ── Section typography helpers ──────────────────── */
.section-label {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--clr-accent);
margin-bottom: 0.6rem;
}
.section-title {
font-size: clamp(1.6rem, 3.5vw, 2.5rem);
font-weight: 700;
color: var(--clr-dark);
margin-bottom:0.6rem;
line-height: 1.2;
}
.section-sub {
color: var(--clr-muted);
max-width: 55ch;
line-height:1.7;
}
Buttons appear in the nav, hero, about section, and CTA. Defining a base .btn class plus modifier classes (.btn-primary, .btn-ghost, .btn-white, .btn-lg, .btn-sm) means any element can become any button variant just by adding HTML classes — no duplicated CSS.
/* ── Base button ─────────────────────────────────── */
.btn {
display: inline-flex; /* icon + text alignment */
align-items: center;
gap: 0.4em;
padding: 0.65em 1.4em;
border-radius: 6px;
font-weight: 600;
font-size: 0.95rem;
border: 2px solid transparent;
white-space: nowrap;
transition: background 0.2s ease,
transform 0.15s ease,
box-shadow 0.2s ease,
border-color 0.2s ease,
color 0.2s ease;
}
.btn:hover { transform: translateY(-2px); }
/* ── Variants ────────────────────────────────────── */
.btn-primary {
background: var(--clr-accent);
color: #fff;
}
.btn-primary:hover {
background: var(--clr-accent-dark);
box-shadow: 0 6px 20px rgba(79, 142, 247, 0.35);
}
.btn-ghost {
background: transparent;
border-color: var(--clr-border);
color: var(--clr-text);
}
.btn-ghost:hover {
border-color: var(--clr-accent);
color: var(--clr-accent);
}
.btn-white {
background: #fff;
color: var(--clr-accent);
}
.btn-white:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
/* ── Size modifiers ──────────────────────────────── */
.btn-lg { padding: 0.8em 2em; font-size: 1.05rem; }
.btn-sm { padding: 0.45em 1em; font-size: 0.85rem; }
inline-flex on a button? It keeps the button as an inline element (so it sits in text flow and sizes to its content) while enabling Flexbox alignment internally. This is how you get an icon and label perfectly vertically centred without extra positioning tricks — a pattern used in almost every real-world component library.
The nav uses position: sticky so it stays at the top of the screen as the user scrolls (Chapter 7). The inner .nav element is a Flexbox row that pushes the logo to the left and the links+button to the right with justify-content: space-between (Chapter 8). On mobile the nav links are hidden with display: none and replaced by a hamburger button — a single @media query at 768px reverses this (Chapter 11). The backdrop-filter: blur() creates the frosted-glass effect as page content scrolls beneath the bar.
/* ── Sticky header shell ─────────────────────────── */
.site-header {
position: sticky;
top: 0;
z-index: 100; /* sits above all page content */
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px); /* frosted-glass effect */
-webkit-backdrop-filter: blur(12px); /* Safari prefix */
border-bottom: 1px solid var(--clr-border);
transition: box-shadow 0.2s;
}
/* Added by JavaScript once the user scrolls — deeper shadow */
.site-header.scrolled {
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08);
}
/* ── Nav row — Flexbox ───────────────────────────── */
.nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
height: 68px; /* fixed height — consistent feel */
}
/* ── Logo ────────────────────────────────────────── */
.nav-logo {
font-weight: 800;
font-size: 1.3rem;
color: var(--clr-dark);
letter-spacing: -0.02em;
}
.nav-logo span { color: var(--clr-accent); } /* blue dot */
/* ── Nav links — hidden on mobile ────────────────── */
.nav-links {
display: none; /* hidden until 768px breakpoint */
align-items:center;
gap: 1.75rem;
}
.nav-links a {
font-size: 0.9rem;
font-weight:500;
color: var(--clr-muted);
transition:color 0.2s;
}
.nav-links a:hover { color: var(--clr-text); }
/* ── Hamburger — shown on mobile only ────────────── */
.hamburger {
display: flex;
align-items: center;
justify-content:center;
background: none;
border: 1px solid var(--clr-border);
border-radius:6px;
padding: 0.4em 0.6em;
font-size: 1.1rem;
color: var(--clr-text);
transition: border-color 0.2s;
}
.hamburger:hover { border-color: var(--clr-accent); }
/* ── Responsive: tablet+ shows links, hides hamburger */
@media (min-width: 768px) {
.nav-links { display: flex; }
.hamburger { display: none; }
}
.scrolled class is applied by a 3-line JavaScript snippet in Part C. Without it the nav looks fine — the border-bottom alone separates it from the page. The JS shadow is a polish detail, not a requirement for the layout.z-index: 100 guarantees the nav always wins without needing to guess what other stacking contexts exist on the page.
The hero is the most complex section — it combines several techniques at once. The outer .hero has a subtle diagonal gradient background and generous clamp() padding that scales from comfortable on mobile to dramatic on desktop. Inside, .hero-inner is a CSS Grid that stacks on mobile and splits into two columns (text left, cards right) at 900px. The three floating cards use @keyframes animations with different durations and alternate direction to create a natural, unsynchronised floating effect.
/* ── Section shell ───────────────────────────────── */
.hero {
padding: clamp(5rem, 12vw, 10rem) 0 clamp(4rem, 8vw, 7rem);
background: linear-gradient(160deg, #eef3ff 0%, #ffffff 55%);
overflow: hidden; /* card animations can't escape the section */
}
/* ── Two-column grid ─────────────────────────────── */
.hero-inner {
display: grid;
gap: 3rem;
align-items: center;
}
@media (min-width: 900px) {
.hero-inner { grid-template-columns: 1.15fr 1fr; }
}
/* ── Eyebrow pill ────────────────────────────────── */
.hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5em;
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--clr-accent);
background: rgba(79, 142, 247, 0.1);
padding: 0.35em 0.9em;
border-radius: 999px;
margin-bottom: 1.25rem;
}
.hero-eyebrow::before { /* small blue dot */
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--clr-accent);
}
/* ── Heading ─────────────────────────────────────── */
.hero-text h1 {
font-size: clamp(2.5rem, 5.5vw, 4.25rem);
font-weight: 800;
line-height: 1.08;
color: var(--clr-dark);
letter-spacing:-0.03em;
margin-bottom: 1.25rem;
}
.hero-text h1 em {
font-style: normal;
color: var(--clr-accent); /* blue highlight word */
}
/* ── Subheading ──────────────────────────────────── */
.hero-text > p {
font-size: clamp(1rem, 2vw, 1.2rem);
color: var(--clr-muted);
max-width: 48ch;
line-height: 1.7;
margin-bottom:2rem;
}
/* ── CTA button row ──────────────────────────────── */
.hero-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap; /* buttons wrap on very narrow screens */
}
/* ── Floating cards grid ─────────────────────────── */
.hero-visual {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
padding: 1rem;
}
.h-card {
background: #fff;
border: 1px solid var(--clr-border);
border-radius:12px;
padding: 1.25rem 1.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transition: transform 0.35s ease, box-shadow 0.35s ease;
}
.h-card:hover {
transform: translateY(-5px);
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.1);
}
/* Card A spans full width (both columns) */
.h-card--a {
grid-column: 1 / -1;
animation: floatUp 5s ease-in-out infinite alternate;
}
.h-card--b { animation: floatDown 6s ease-in-out infinite alternate; }
.h-card--c { animation: floatUp 4.5s 0.5s ease-in-out infinite alternate; }
.h-card-icon { font-size: 1.5rem; margin-bottom: 0.5rem; }
.h-card-title { font-weight: 700; font-size: 0.95rem; color: var(--clr-dark); margin-bottom: 0.2rem; }
.h-card-sub { font-size: 0.78rem; color: var(--clr-muted); }
/* ── Float keyframes ─────────────────────────────── */
@keyframes floatUp {
from { transform: translateY(0); }
to { transform: translateY(-10px); }
}
@keyframes floatDown {
from { transform: translateY(0); }
to { transform: translateY(8px); }
}
We design brands that stand out
Award-winning design and development for companies that want to be remembered.
How the float animation works
Three cards, three separate animations — different durations (5s, 6s, 4.5s) and different offsets (card C has a 0.5s delay) — mean they never move in perfect sync. alternate makes the animation reverse direction at the end of each cycle instead of jumping back to the start, creating a smooth infinite loop. ease-in-out makes each float feel organic rather than mechanical.
A simple trust signal between the hero and services. One Flexbox row with the "Trusted by" label on the left and logo names on the right. The logo names are rendered in the border colour (--clr-border, a light grey) rather than actual images — a common placeholder technique during build. They lighten to muted grey on hover, suggesting interactivity without being distracting.
.clients {
padding: 2.5rem 0;
border-top: 1px solid var(--clr-border);
border-bottom: 1px solid var(--clr-border);
}
.clients-inner {
display: flex;
align-items: center;
gap: 2.5rem;
flex-wrap: wrap;
}
.clients-label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--clr-muted);
white-space: nowrap;
}
.clients-logos {
display: flex;
gap: 2rem;
flex-wrap: wrap;
align-items: center;
}
.client-logo {
font-weight: 800;
font-size: 1rem;
color: var(--clr-border); /* very light — placeholder look */
letter-spacing: -0.02em;
transition: color 0.2s;
}
.client-logo:hover { color: var(--clr-muted); }
<span> elements with <img> tags (SVG logos, filter: grayscale(1), opacity: 0.4) when you have real client logos. The CSS structure stays identical — you're just swapping text for images.The services section uses repeat(auto-fit, minmax(260px, 1fr)) — the same responsive grid pattern from Chapter 9. No media queries needed: the browser works out how many columns fit, moving from 1 to 2 to 3 columns automatically as the viewport grows. Each card has a hidden ::before pseudo-element — a 3px blue line at the top — that slides in from the left on hover using transform: scaleX(). This is the same performance-friendly animation technique from Chapter 10.
.services {
padding: clamp(4rem, 8vw, 7rem) 0;
background: var(--clr-bg-alt); /* slight off-white — visual break */
}
.services-header { margin-bottom: 3rem; }
/* ── Responsive auto-fit grid ────────────────────── */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
/* ── Card — with hidden top-border pseudo-element ─── */
.s-card {
background: #fff;
border: 1px solid var(--clr-border);
border-radius:var(--radius);
padding: 2rem;
position: relative; /* ::before is positioned relative to this */
overflow: hidden; /* clips ::before when scaleX(0) */
transition: transform 0.25s ease,
box-shadow 0.25s ease,
border-color 0.25s ease;
}
/* Top-edge reveal bar — hidden by default */
.s-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--clr-accent);
transform: scaleX(0); /* invisible: scaled to zero width */
transform-origin: left; /* grows from the left edge */
transition: transform 0.3s ease;
}
.s-card:hover {
transform: translateY(-5px);
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.08);
border-color: rgba(79, 142, 247, 0.3);
}
.s-card:hover::before {
transform: scaleX(1); /* slides in on hover */
}
.s-icon {
width: 48px;
height: 48px;
border-radius: 10px;
background: rgba(79, 142, 247, 0.1);
display: flex;
align-items: center;
justify-content:center;
font-size: 1.4rem;
margin-bottom: 1.25rem;
}
.s-card h3 {
font-size: 1.05rem;
font-weight: 700;
color: var(--clr-dark);
margin-bottom:0.6rem;
}
.s-card p {
color: var(--clr-muted);
font-size: 0.9rem;
line-height: 1.65;
}
Brand Strategy
From positioning to visual identity. We help you own your place in the market.
UI / UX Design
Intuitive interfaces that guide users to the actions that matter most.
Web Development
Fast, accessible builds in React, Next.js or your stack of choice.
overflow: hidden on the card? The ::before pseudo-element starts at scaleX(0) — technically zero width, but it still occupies space at its full width at the top of the card. Without overflow: hidden, this invisible element could extend outside the card's border-radius corners. Clipping it inside the card keeps the animation clean.
The stats bar provides a strong visual contrast between the light services section above and the light about section below. Its dark navy background (--clr-navy) paired with the blue accent number creates a focused, high-impact moment. The grid uses the same auto-fit / minmax() pattern as the services — four stats on a wide screen, two-by-two on tablet, stacked on mobile with no media queries required.
.stats {
background: var(--clr-navy);
padding: clamp(3rem, 6vw, 5rem) 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 2rem;
text-align: center;
}
.stat-num {
display: block;
font-size: clamp(2.25rem, 5vw, 3.5rem);
font-weight: 800;
color: var(--clr-accent);
line-height: 1;
margin-bottom:0.4rem;
}
.stat-label {
font-size: 0.82rem;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 0.07em;
}
--clr-accent on --clr-navy meets WCAG AA contrast (4.5:1 for normal text). The values in this project pass — blue #4f8ef7 on navy #111827 gives a ratio of ~5.2:1. If you change the accent colour, verify contrast with a tool like WebAIM Contrast Checker.
What we're building
The About section is a classic image + text layout. On desktop it sits side by side; on mobile it stacks. This substep wires up the outer section and the two-column grid — no image styling or badges yet, just the grid scaffold.
Here's the HTML we're targeting (already written in Part A):
<section class="about" id="about">
<div class="container about-inner">
<!-- LEFT: image block (styled in C2 + C3) -->
<div class="about-image-wrap reveal">
<div class="about-img">🎨</div>
<div class="about-badge">
<span class="badge-num">8+</span> Years of craft
</div>
</div>
<!-- RIGHT: text block -->
<div class="about-text reveal">
<p class="section-label">Our story</p>
<h2 class="section-title">Crafted with intention</h2>
<p>We've spent eight years turning complex briefs...</p>
<ul class="about-checklist">...</ul>
<a class="btn btn-primary">Meet the team</a>
</div>
</div>
</section>
Two direct children inside .about-inner: the image wrap (left) and the text block (right). Grid will place them automatically.
The CSS — outer section
First we style the .about section itself — background colour and generous vertical padding. We use clamp() so spacing breathes on all screen sizes:
/* ── Step 9 · About ──────────────────────────────── */
.about {
padding: clamp(4rem, 10vw, 7rem) 0;
background: var(--clr-bg); /* white — contrasts with the dark stats bar above */
}
--clr-dark (#0d1117). A white About section creates a strong contrast break that signals a new page "chapter" to the reader.
The CSS — two-column grid
.about-inner gets a two-column grid. Both columns are equal width (1fr 1fr) and vertically centered with align-items: center so the text doesn't float to the top when the image is taller.
.about-inner {
display: grid;
grid-template-columns: 1fr 1fr; /* image | text, equal halves */
gap: clamp(2.5rem, 6vw, 5rem);
align-items: center;
}
/* Mobile: stack columns */
@media (max-width: 899px) {
.about-inner {
grid-template-columns: 1fr; /* image above, text below */
}
}
The gap uses clamp() — wider on large screens where the columns have more room to breathe, narrower on tablet.
Why align-items: center?
Without it, grid children stretch to the tallest cell height by default. If the text column is taller than the image, the image would pin to the top — misaligned. center keeps both columns vertically centered regardless of which side is taller.
gap on the grid container is cleaner than adding margin-right to one column. Gap disappears automatically when the grid collapses to a single column — no media query needed to clear the gap.
Live demo — grid structure
Toggle the grid outlines on to see exactly how the two columns sit. Toggle "Mobile" to watch the single-column stack.
Our story
Crafted with intention
We've spent eight years turning complex briefs into clear, purposeful design — for brands that mean business.
Meet the teamWhat this gives us
- Two equal columns on desktop — image left, text right
- Fluid gap that scales with viewport width via
clamp() - Vertical centering so neither column floats to the top
- A single-column stack on screens narrower than 900 px
.about-image-wrap and .about-img elements. The image wrap becomes position: relative — the foundation the absolute-positioned badge will attach to in C3.
The key concept: position: relative as a containing block
The most important line in this substep is one you won't see change anything visually — yet. Setting position: relative on the image wrap creates a containing block for the badge that's coming in C3.
When you set position: absolute on a child element, the browser walks up the DOM tree looking for the nearest ancestor that has a position other than static. That ancestor becomes the child's reference frame for top, right, bottom, and left. If there's no positioned ancestor, the badge anchors to the viewport — almost certainly not what we want.
In C3 we'll set bottom: -1rem; right: -1rem on this badge — those values will be relative to the image wrap, not the page.
The CSS
/* ── Step 9 · About image wrap ───────────────────── */
.about-image-wrap {
position: relative; /* containing block for the badge */
border-radius: var(--radius);
}
No overflow: hidden here — we need the badge to be able to peek outside the wrap boundary in C3.
.about-img {
aspect-ratio: 4 / 3; /* locks the proportions */
border-radius: var(--radius);
background: var(--clr-bg-alt); /* light grey when no real photo */
overflow: hidden; /* clips a real <img> to the border-radius */
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(3rem, 8vw, 5rem); /* scales the emoji placeholder */
transition: transform 0.4s ease; /* subtle zoom on hover */
}
.about-img:hover {
transform: scale(1.03); /* gentle zoom — image stays clipped */
}
overflow: hidden on .about-img (not the wrap) clips the zoom effect and any real photo to the rounded corners.
Why aspect-ratio: 4 / 3?
Without a fixed height or aspect ratio, a grid cell with no content height would collapse. aspect-ratio tells the browser: "make the height 75% of the width, always." This works for both the emoji placeholder and a real <img> tag — you swap the background for an image later without touching the layout.
.about-img {
background: #f0f0f0;
/* no height set → collapses to 0 */
}
.about-img {
aspect-ratio: 4 / 3;
background: #f0f0f0;
/* height derived from width ✓ */
}
<img src="team.jpg" alt="..."> and add object-fit: cover; width: 100%; height: 100%; to the img. The aspect-ratio and overflow: hidden on the parent handle the rest.
Live demo
Hover the image to see the scale transition. Toggle the annotation to label the key CSS properties.
Our story
Crafted with intention
Eight years turning complex briefs into clear, purposeful design — for brands that mean business.
Meet the teamWhat overflow: hidden does — and where to put it
This is a subtle but important detail that trips up beginners:
.about-imggetsoverflow: hidden— this clips the zoom effect and a real photo to the rounded corners of the image itself..about-image-wrapdoes NOT getoverflow: hidden— if it did, the badge we're adding in C3 (which will peek outside the wrap boundary) would be clipped and invisible.
overflow: hidden to the wrap to "fix" the border-radius — then wondering why the absolutely-positioned badge disappears. Clip on the image, not the container.
What we have so far
.about— section padding + white background ✓.about-inner— 2-col grid, centered, fluid gap ✓.about-image-wrap—position: relative(containing block ready) ✓.about-img— aspect ratio locked, hover zoom, rounded corners ✓
.about-badge (position: absolute anchored to the wrap) and the .about-checklist with ::before checkmark pseudo-elements.