π οΈ Weekend Project: CSS
Time to stop reading CSS and start shipping it. Over one focused weekend you'll build a small multi-page site β a mini "problem-solving" microsite β that puts every Module 5 technique to work: layout, glassmorphism, 3D card flips, transitions, filters, and a scroll-reactive nav. You'll build it in milestones, check yourself against a clear bar, and finish with something portfolio-worthy.
π― Learning Objectives
By the end of this build, you will be able to:
- Assemble a multi-page site with a shared, sticky, scroll-reactive navigation bar
- Compose advanced visual effects β glassmorphism, gradient text, layered shadows, and CSS filters β into a cohesive look
- Build an interactive 3D flip-card component with
transform-style: preserve-3dandbackface-visibility - Apply a mobile-first, responsive grid and honor
prefers-reduced-motionfor accessibility - Ship in milestones and self-assess against a "what good looks like" bar
Estimated Time: One weekend (6β10 hours) β’ Difficulty: Intermediate
Hands-on: This entire lesson is the exercise β build the site milestone by milestone using the starter code provided.
In This Lesson
The Brief
Your client is you. Build a small, good-looking educational microsite that explains a four-step problem-solving method β George PΓ³lya's classic framework from his 1945 book How to Solve It. The topic is just a vehicle: it gives you exactly four repeatable content blocks (perfect for a card grid) plus a home page, so you can focus your energy on the CSS.
π Why PΓ³lya?
PΓ³lya's method β Understand β Plan β Execute β Review β happens to be the same loop you'll use to build this site. Four steps means four flip cards. It's tidy content that shows off your layout skills without you having to invent a product.
Minimum pages to produce:
| Page | Purpose | Star CSS technique |
|---|---|---|
| index.html | Hero + overview of the four steps | Glassmorphism hero, gradient text |
| steps.html | The four steps as interactive cards | 3D flip cards |
| apply.html | How the method maps to web dev | Responsive card grid + hover filters |
| about.html | Who PΓ³lya was; further reading | Typography & layered shadows |
β οΈ Scope guard
Four pages, one shared stylesheet. Resist the urge to add a fifth page or a JS framework. The grade here is craft on a small surface, not size. If you finish early, deepen the polish β don't widen the scope.
The Build Roadmap
You'll ship in five milestones. Each one leaves you with something that works in the browser, so you're never more than one milestone away from a demoable site. Build in this order β later milestones lean on the design tokens and layout you set up first.
Notice the dotted line back to the start: that's PΓ³lya's Review step. After a first pass, loop back and raise the weakest area rather than piling on new features.
π‘ Work mobile-first
Write your base styles for a narrow phone screen, then add complexity at wider breakpoints with min-width media queries. It's far easier to grow a simple layout than to cram a desktop one into a phone.
Milestone 1 β Scaffold & Design Tokens
Goal: four HTML files that share one stylesheet, and a set of CSS custom properties (design tokens) so every color, radius, and shadow is defined in one place. Tokens are what make a site feel cohesive instead of assembled from random values.
Folder structure
polya-site/
βββ index.html
βββ steps.html
βββ apply.html
βββ about.html
βββ css/
βββ style.css
Design tokens (top of style.css)
Define everything as variables on :root. Bonus: a dark-mode override costs almost nothing once your colors are tokens.
:root {
/* Brand */
--brand: #0070f3;
--brand-2: #00c8ff;
--gradient: linear-gradient(45deg, var(--brand), var(--brand-2));
/* Surface & text */
--bg: #f4f7fb;
--surface: #ffffff;
--ink: #1e293b;
--ink-soft: #64748b;
/* Shape & depth */
--radius: 1rem;
--shadow-sm: 0 5px 15px rgb(0 0 0 / 0.10);
--shadow-lg: 0 20px 40px rgb(0 0 0 / 0.15);
/* Rhythm */
--space: clamp(1rem, 2vw, 2rem);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--surface: #1e293b;
--ink: #e2e8f0;
--ink-soft: #94a3b8;
--shadow-sm: 0 5px 15px rgb(0 0 0 / 0.40);
--shadow-lg: 0 20px 40px rgb(0 0 0 / 0.50);
}
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink);
background: var(--bg);
line-height: 1.6;
}
β Milestone 1 is done whenβ¦
All four pages open, link to the same css/style.css, share the same background and font, and you can change the whole site's accent color by editing a single --brand value.
Milestone 2 β Navigation & Hero
Goal: a shared sticky navigation bar that condenses on scroll, plus a glassmorphism hero on the home page with gradient-text heading.
Sticky, scroll-reactive nav
Put this markup at the top of every page (identical, so the site feels like one place). The .scrolled class is toggled by a tiny bit of JS.
<nav class="site-nav">
<a class="site-brand" href="index.html">How to Solve It</a>
<ul class="site-links">
<li><a href="index.html">Home</a></li>
<li><a href="steps.html">The Steps</a></li>
<li><a href="apply.html">Apply It</a></li>
<li><a href="about.html">About</a></li>
</ul>
</nav>
.site-nav {
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1rem var(--space);
background: rgb(255 255 255 / 0.8);
backdrop-filter: blur(10px);
box-shadow: var(--shadow-sm);
transition: padding 0.3s ease;
}
.site-nav.scrolled { padding-block: 0.4rem; }
.site-links {
display: flex;
gap: 1.5rem;
list-style: none;
margin: 0;
padding: 0;
}
.site-links a {
position: relative;
color: var(--ink);
text-decoration: none;
font-weight: 500;
}
/* underline that grows from the left */
.site-links a::after {
content: "";
position: absolute;
left: 0;
bottom: -4px;
height: 2px;
width: 0;
background: var(--gradient);
transition: width 0.3s ease;
}
.site-links a:hover::after,
.site-links a[aria-current="page"]::after { width: 100%; }
// tiny progressive enhancement β condense the nav after scrolling
const nav = document.querySelector('.site-nav');
window.addEventListener('scroll', () => {
nav.classList.toggle('scrolled', window.scrollY > 40);
}, { passive: true });
Glassmorphism hero + gradient text
.hero {
min-height: 70vh;
display: grid;
place-items: center;
padding: var(--space);
background: var(--gradient); /* swap for an image once you have one */
background-size: cover;
background-position: center;
}
.hero-card {
max-width: 40rem;
padding: clamp(1.5rem, 4vw, 3rem);
text-align: center;
border-radius: var(--radius);
background: rgb(255 255 255 / 0.15);
backdrop-filter: blur(10px);
border: 1px solid rgb(255 255 255 / 0.25);
box-shadow: var(--shadow-lg);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.hero-card:hover {
transform: translateY(-10px);
box-shadow: 0 30px 60px rgb(0 0 0 / 0.25);
}
.hero-title {
margin: 0 0 0.5rem;
font-size: clamp(2rem, 6vw, 3.5rem);
/* gradient text: paint the gradient, then clip it to the glyphs */
background: var(--gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
β οΈ Gradient-text gotcha
You need both -webkit-background-clip: text and the standard background-clip: text, plus color: transparent. Miss the transparent color and the text paints solid over the gradient β you'll see nothing special.
β Milestone 2 is done whenβ¦
The nav is identical on all pages, sticks to the top, and visibly shrinks when you scroll. The hero card looks frosted, the title shows the gradient, and the current page's nav link keeps its underline.
Milestone 3 β The 3D Flip Cards
Goal: the steps.html page shows PΓ³lya's four steps as cards that flip in 3D to reveal detail on the back. This is the showpiece β get it crisp.
Three properties do the heavy lifting: perspective on the container gives depth, transform-style: preserve-3d lets the two faces live in the same 3D space, and backface-visibility: hidden stops the back from showing through the front.
<div class="flip" tabindex="0">
<div class="flip-inner">
<div class="flip-face flip-front">
<span class="flip-num">1</span>
<h3>Understand</h3>
</div>
<div class="flip-face flip-back">
<p>Name what's known, unknown, and the goal β in your own words.</p>
</div>
</div>
</div>
.flip {
width: 100%;
aspect-ratio: 3 / 4;
perspective: 1000px; /* depth of the 3D scene */
}
.flip-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* flip on hover AND keyboard focus for accessibility */
.flip:hover .flip-inner,
.flip:focus-visible .flip-inner { transform: rotateY(180deg); }
.flip-face {
position: absolute;
inset: 0;
display: grid;
place-content: center;
gap: 0.5rem;
padding: 1.5rem;
border-radius: var(--radius);
backface-visibility: hidden;
box-shadow: var(--shadow-sm);
}
.flip-front { background: var(--surface); }
.flip-back {
background: var(--brand);
color: #fff;
transform: rotateY(180deg); /* pre-rotate so it faces us after the flip */
}
.flip-num {
font-size: 4rem;
font-weight: 800;
line-height: 1;
background: var(--gradient);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
π‘ Why tabindex="0" and :focus-visible?
A hover-only interaction is invisible to keyboard and touch users. Making the card focusable and flipping it on focus means anyone can reveal the back. Small change, big accessibility win.
β Milestone 3 is done whenβ¦
Four cards sit in a responsive row, each flips smoothly on hover and when tabbed to, the back face is readable (no mirrored text bleeding through), and there's no flicker at the halfway point of the flip.
Milestone 4 β Content Grid & Effects
Goal: the apply.html page uses an auto-fitting responsive grid of example cards, each with a lift-on-hover, a gradient top-bar that wipes in, and an image that de-saturates until you hover it.
Responsive grid with zero media queries
auto-fit plus minmax() gives you a grid that reflows on its own β one column on a phone, several on a wide screen β without a single breakpoint.
.examples {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: var(--space);
padding: var(--space);
}
.example {
position: relative;
overflow: hidden;
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow-sm);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.example:hover {
transform: translateY(-10px) scale(1.02);
box-shadow: var(--shadow-lg);
}
/* accent bar that wipes in from the left */
.example::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 5px;
background: var(--gradient);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.example:hover::before { transform: scaleX(1); }
/* filter: desaturate until hover */
.example img {
width: 100%;
height: 12rem;
object-fit: cover;
filter: grayscale(60%);
transition: filter 0.3s ease;
}
.example:hover img { filter: grayscale(0%); }
β οΈ Animate cheap properties
Prefer animating transform and opacity β the browser can offload them to the GPU. Animating width, height, top, or margin forces expensive layout recalculation ("layout thrash") and stutters on lower-end devices.
β Milestone 4 is done whenβ¦
Cards reflow from one column on mobile to three-plus on desktop with no horizontal scroll, each card lifts and reveals its accent bar on hover, and images snap from muted to full color. Resize the window to prove the grid adapts.
Milestone 5 β Polish, A11y & Ship
Goal: take the working site from "it functions" to "it's finished." This is PΓ³lya's Review step β the difference between a demo and a portfolio piece.
Respect reduced motion
Some users get motion sickness from big transitions. Honor their OS setting β this one media query is the single most important accessibility upgrade for an effects-heavy site.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}
Provide fallbacks for backdrop-filter
Where a browser lacks backdrop-filter, a plain translucent background still reads fine. Detect support and enhance rather than assume.
.hero-card { background: rgb(255 255 255 / 0.6); } /* readable fallback */
@supports (backdrop-filter: blur(10px)) {
.hero-card {
background: rgb(255 255 255 / 0.15);
backdrop-filter: blur(10px);
}
}
Final polish pass
- Check color contrast β body text should hit WCAG AA (4.5:1). Gradient text over a busy image is the usual offender.
- Tab through every page with the keyboard: can you reach and trigger every interactive element? Is the focus outline visible?
- Run Lighthouse and WAVE; fix anything red.
- Test at 360 px, 768 px, and 1280 px widths.
β Milestone 5 is done whenβ¦
The site is fully keyboard-navigable, honors reduced motion, degrades gracefully without backdrop-filter, passes a Lighthouse accessibility check, and looks intentional at three screen widths. Now it's shippable.
What Good Looks Like
Use this bar to grade yourself honestly. "Good" isn't "used the most effects" β it's restraint plus polish. A calm site with three effects done cleanly beats a noisy one with ten done sloppily.
| Area | Needs work | Good | Excellent |
|---|---|---|---|
| Cohesion | Random colors & spacing per page | Shared tokens; consistent look | One accent change re-themes the whole site |
| Effects | Effects fight for attention | Each effect supports the content | Effects feel invisible β they just work |
| Responsive | Horizontal scroll on mobile | Reflows cleanly at all widths | Layout improves, not just survives, on wide screens |
| Accessibility | Hover-only, low contrast | Keyboard-usable, AA contrast | Honors reduced motion + graceful fallbacks |
| Code | Copy-pasted values everywhere | Tokens + clear class names | Commented, DRY, easy to extend |
π‘ The one-sentence test
Show the site to someone for five seconds, then hide it and ask what it was about. If they can answer, your design served the content. If they only remember "it was flashy," dial the effects back.
Ship Checklist
Before you call it done, walk this list top to bottom. Each item maps to a milestone above.
π Final checklist
- β Four pages share one stylesheet and one set of
:roottokens - β Sticky nav is identical on every page and condenses on scroll
- β Current page is marked with
aria-current="page" - β Hero uses glassmorphism + gradient text with a readable fallback
- β Four flip cards work on hover and keyboard focus
- β Example grid reflows via
auto-fit/minmax()β no horizontal scroll - β Animations use
transform/opacity, not layout properties - β
prefers-reduced-motionis respected - β Body text meets WCAG AA contrast (4.5:1)
- β Lighthouse accessibility score is green
- β Verified at 360 / 768 / 1280 px widths
π― Quick Quiz
Question 1: Which trio of CSS properties makes a 3D flip card work?
Question 2: Why prefer animating transform over width or margin?
Question 3: What's the single most important accessibility upgrade for this effects-heavy site?
Summary & Quiz
π Key Takeaways
- Ship in milestones β every step leaves you a working, demoable site.
- Design tokens on
:rootare what make a multi-page site feel like one product. - The flip-card trio β
perspective,preserve-3d,backface-visibilityβ is the reusable pattern behind most 3D UI. auto-fit+minmax()gives responsive grids with no media queries.- Polish is accessibility: keyboard support, contrast, fallbacks, and
prefers-reduced-motion. - Good design is restraint β effects should serve the content, never upstage it.
π Further Reading
- MDN β CSS reference
- CSS-Tricks Almanac
- PΓ³lya β How to Solve It (overview)
- web.dev β prefers-reduced-motion
π What's Next?
You've squeezed everything out of hand-tuned CSS layout and effects. Next module we graduate to a modern layout engine built for exactly this kind of one-dimensional arrangement: Flexbox Layout Concepts. Much of the nav and card work you just did by hand gets dramatically simpler with it.
π You shipped a site!
Keep this project in your portfolio β it's concrete proof you can turn CSS knowledge into a polished, accessible, real thing.