Lesson 24
Capstone: Product Landing Page
This capstone project exercises 14+ features from the course. The HTML is complete below - your job is to build the CSS section by section, applying what you've learned across all lessons.
Features used in this project
@layer(structure)- Custom properties - three-tier tokens
- oklch + relative colour syntax (derived palette)
- Dark/light theming via data-theme attribute
- Nesting (native CSS)
- Logical properties
- Grid (named areas, responsive rearrangement)
- Flexbox (nav, button groups)
- Container queries (feature cards)
- clamp() for fluid type and spacing
- Scroll-driven animations (progress bar, section reveals)
- Transitions (hover states, toggle)
- @scope (testimonial region)
- Anchor positioning (tooltip)
The project skeleton
Below is the complete HTML and starter CSS. The CSS uses @layer to structure everything, with placeholder comments where you'll build each challenge.
Ad
<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Capstone: Horizon — Product Landing Page</title>
<style>
/* ============================================================
CAPSTONE PROJECT: "Horizon" — a product landing page
This page exercises 14+ features from the course:
1. @layer (structure)
2. Custom properties — three-tier tokens
3. oklch + relative colour syntax (derived palette)
4. Dark/light theming via data-theme attribute
5. Nesting (native CSS)
6. Logical properties
7. Grid (named areas, responsive rearrangement)
8. Flexbox (nav, button groups)
9. Container queries (feature cards)
10. clamp() for fluid type and spacing
11. Scroll-driven animations (progress bar, section reveals)
12. Transitions (hover states, toggle)
13. @scope (testimonial region)
14. Anchor positioning (tooltip)
STATUS: Skeleton — HTML complete, CSS minimal.
Challenges will build the CSS section by section.
============================================================ */
/* === LAYER ORDER === */
@layer reset, tokens, layout, components, utilities;
/* === RESET (provided) === */
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
body {
min-height: 100dvh;
}
img,
picture,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
ul,
ol {
list-style: none;
padding: 0;
}
}
/* === TOKENS (Challenge 1: you build this) === */
@layer tokens {
:root {
/* TODO: Primitives, semantics, component tokens */
/* Placeholder so the page isn't invisible */
--color-surface: #fefefe;
--color-text: #1a1a2e;
--color-primary: #3a86ff;
--font-body:
-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-heading: 'Georgia', serif;
--font-mono: 'SF Mono', 'Fira Code', monospace;
--gray-50: oklch(99.41% 0.001 0);
--gray-100: oklch(96.43% 0.007 256);
--gray-800: oklch(18.27% 0.036 263);
--gray-900: oklch(10.16% 0.029 264);
--brand-hue: 250;
--brand-chroma: 0.15;
--brand-base: oklch(0.55 var(--brand-chroma) var(--brand-hue));
--space-2: 0.5rem;
--space-4: 1rem;
--space-6: 1.5rem;
--radius-sm: 4px;
--radius-md: 8px;
}
}
/* === LAYOUT (Challenge 2+) === */
@layer layout {
body {
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-surface);
line-height: 1.7;
}
}
/* === COMPONENTS (Challenge 3+) === */
@layer components {
/* Built up through challenges */
}
/* === UTILITIES === */
@layer utilities {
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
}
</style>
</head>
<body>
<!-- PROGRESS BAR (scroll-driven) -->
<div class="progress-bar" aria-hidden="true"></div>
<!-- HEADER / NAV -->
<header class="site-header">
<nav class="nav">
<a href="#" class="nav-logo">Horizon</a>
<ul class="nav-links">
<li><a href="#features">Features</a></li>
<li><a href="#pricing">Pricing</a></li>
<li><a href="#testimonials">Testimonials</a></li>
</ul>
<button
class="theme-toggle"
aria-label="Toggle theme"
onclick="toggleTheme()"
>
<span class="toggle-icon"></span>
</button>
</nav>
</header>
<!-- HERO -->
<section class="hero">
<div class="hero-content">
<h1>Build faster.<br />Ship with confidence.</h1>
<p class="hero-subtitle">
The modern platform for teams who refuse to compromise on quality.
</p>
<div class="hero-actions">
<a href="#" class="btn btn-primary">Get Started</a>
<a href="#" class="btn btn-secondary">Learn More</a>
</div>
</div>
</section>
<!-- FEATURES (container query cards) -->
<section class="features" id="features">
<h2 class="section-title">Features</h2>
<div class="features-grid">
<div class="feature-card-wrapper">
<article class="feature-card">
<div class="feature-icon">⚡</div>
<h3>Lightning Fast</h3>
<p>Sub-second builds with intelligent caching.</p>
</article>
</div>
<div class="feature-card-wrapper">
<article class="feature-card">
<div class="feature-icon">🔒</div>
<h3>Secure by Default</h3>
<p>Automatic dependency scanning and zero-trust networking.</p>
</article>
</div>
<div class="feature-card-wrapper">
<article class="feature-card">
<div class="feature-icon">📊</div>
<h3>Observable</h3>
<p>Traces, metrics, and logs unified in one view.</p>
</article>
</div>
<div class="feature-card-wrapper">
<article class="feature-card">
<div class="feature-icon">🔄</div>
<h3>GitOps Native</h3>
<p>Every change is a pull request. Rollback to any commit.</p>
</article>
</div>
</div>
</section>
<!-- PRICING (grid layout) -->
<section class="pricing" id="pricing">
<h2 class="section-title">Pricing</h2>
<div class="pricing-grid">
<div class="pricing-card">
<h3>Starter</h3>
<p class="price"><span class="price-amount">$0</span>/month</p>
<ul class="pricing-features">
<li>3 projects</li>
<li>1 GB storage</li>
<li>Community support</li>
</ul>
<a href="#" class="btn btn-secondary">Start Free</a>
</div>
<div class="pricing-card pricing-featured">
<h3>Pro</h3>
<p class="price"><span class="price-amount">$29</span>/month</p>
<ul class="pricing-features">
<li>Unlimited projects</li>
<li>50 GB storage</li>
<li>Priority support</li>
<li>Custom domains</li>
</ul>
<a href="#" class="btn btn-primary">Go Pro</a>
</div>
<div class="pricing-card">
<h3>Enterprise</h3>
<p class="price"><span class="price-amount">Custom</span></p>
<ul class="pricing-features">
<li>Everything in Pro</li>
<li>SSO / SAML</li>
<li>Dedicated support</li>
<li>SLA guarantee</li>
</ul>
<a href="#" class="btn btn-secondary">Contact Sales</a>
</div>
</div>
</section>
<!-- TESTIMONIALS (scoped styles) -->
<section class="testimonials" id="testimonials" data-theme="dark">
<h2 class="section-title">What people say</h2>
<div class="testimonial-grid">
<blockquote class="testimonial">
<p>"Horizon cut our deploy time from 12 minutes to 45 seconds."</p>
<footer>— Sarah Chen, CTO at Fintrack</footer>
</blockquote>
<blockquote class="testimonial">
<p>"The observability features alone justified the switch."</p>
<footer>— Marcus Rivera, SRE Lead</footer>
</blockquote>
<blockquote class="testimonial">
<p>"Our team of 40 engineers adopted it in a week."</p>
<footer>— Priya Sharma, VP Engineering</footer>
</blockquote>
</div>
</section>
<!-- FOOTER -->
<footer class="site-footer">
<p>Built with pure CSS. No frameworks. No JavaScript (except the theme toggle).</p>
</footer>
<!-- TOOLTIP (anchor positioning) -->
<span class="tooltip" id="tooltip-pro" popover>
Most popular plan — chosen by 73% of teams
</span>
<script>
function toggleTheme() {
const root = document.documentElement;
const current = root.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
}
</script>
</body>
</html>
Challenges
Challenge 1: Token architecture
Build the @layer tokens section:
- Define primitive tokens (palette using oklch, spacing scale, radii)
- Define semantic tokens that map primitives to purposes (
--color-surface,--color-text,--color-primary, etc.) - Add a
[data-theme="dark"]block that swaps the semantic layer - Use relative colour syntax to derive hover/light/muted variants from
--brand-base
Challenge 2: Layout with grid
- Make the page a pancake stack (header auto, main 1fr, footer auto)
- Use
clamp()for fluid section padding - Create the pricing grid using named grid areas or
repeat(auto-fill, minmax(...)) - Use logical properties throughout (
padding-inline,margin-block)
Challenge 3: Navigation with flexbox
- Style the nav using flexbox with space-between
- Add transitions on link hover states
- Style the theme toggle button
Challenge 4: Hero section
- Use
clamp()for fluid heading size - Style the CTA buttons with component tokens (
--btn-bg,--btn-text) - Add hover transitions on buttons
Challenge 5: Feature cards with container queries
- Make
.feature-card-wrappera container (container-type: inline-size) - Default: cards stacked vertically
- At
@container (min-width: 350px): switch to horizontal layout (icon left, text right) - Use the auto-fill grid pattern for the features grid
Challenge 6: Scroll-driven animations
- Add a progress bar that grows with page scroll (
animation-timeline: scroll()) - Add reveal animations on feature cards and testimonials using
animation-timeline: view() - Use
animation-range: entryto trigger only during entry
Challenge 7: Testimonials with @scope
- The testimonials section has
data-theme="dark"- use scoped token overrides - Add
@scope (.testimonials) to (.site-footer)for structural styles that shouldn't leak - Style blockquotes with borders and italic text
Challenge 8: Anchor positioning (tooltip)
- Position the tooltip relative to the "Pro" pricing card using anchor positioning
- Use the
popoverattribute for show/hide - Style with an arrow and appropriate z-index
Challenge 9: Responsive collapse
- Use a media query to collapse the nav to a hamburger pattern on small viewports
- Make the pricing grid stack to a single column on narrow containers
- Ensure all clamp() values have sensible min/max for mobile
The goal: After completing all challenges, you should have a fully-styled, dark-mode-aware, responsive product landing page using only the CSS features from this course. No frameworks, no build tools, no JavaScript (except the theme toggle).