19 — Go Further

🏁 Chapter 19 — Go Further

Lumen Studio is finished — a fully responsive, animated, accessible design-agency landing page built one real technique at a time. This closing chapter gathers the whole build into one reference, recaps what each chapter taught, and hands you five challenges to extend the project entirely on your own.

What you built — a recap

Eight chapters, one continuous page. Here's what each one added:

Ch.TitleWhat it added
12FoundationProject brief, wireframe, file structure, the full HTML skeleton, custom properties, reset, utility classes, the button system
13Navigation & HeroSticky glassmorphism nav, two-column hero with floating @keyframes cards
14Content BandsClient logo strip, services grid with an animated ::before top-bar, auto-fit/minmax() responsive layout
15About SectionTwo-column grid, image wrap with an absolutely-positioned stat badge, a checklist with custom ::before checkmarks
16TestimonialsAuto-fit card grid, decorative quote-mark ::before, avatar initials, the equal-height margin-top: auto trick
17CTA & FooterGradient CTA band with blurred glow circles, the site footer's link grid and hover transitions
18Bringing It to LifeThe .reveal scroll-in animation, IntersectionObserver, the nav scroll shadow, prefers-reduced-motion
19Go FurtherThis chapter — full reference + 5 independent challenges
Techniques carried the whole way through: CSS custom properties as design tokens, clamp() for fluid spacing/type, repeat(auto-fit, minmax()) for grids that reflow without a single media query, and ::before/::after for decoration that never touches your HTML.

Complete Lumen Studio CSS

Every rule from Chapters 12–18, in the order the browser sees them — a single searchable reference for when you're extending the project later.

▾ Full project CSS (all 13 build steps)

Step 1 — Custom properties & reset

/* ── Design tokens ── */ :root { --clr-bg: #ffffff; --clr-bg-alt: #f7f8fc; --clr-dark: #0d1117; --clr-text: #1f2937; --clr-muted: #6b7280; --clr-border: #e5e7eb; --clr-accent: #4f8ef7; --radius: 10px; --container: 1200px; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html { scroll-behavior: smooth; } body { font-family: var(--font); color: var(--clr-text); background: var(--clr-bg); }

Step 2 — Container helper & typography

.container { max-width: var(--container); margin-inline: auto; padding-inline: clamp(1rem, 4vw, 2rem); } h1 { font-size: clamp(2rem, 5vw, 3.25rem); } .section-label { font-size: .75rem; text-transform: uppercase; color: var(--clr-accent); }

Step 3 — Sticky nav with glassmorphism

.site-header { position: sticky; top: 0; z-index: 100; background: rgba(255, 255, 255, .85); backdrop-filter: blur(12px); transition: box-shadow .3s ease; } .site-header.scrolled { box-shadow: 0 4px 20px rgba(0,0,0,.08); } @media (max-width: 699px) { .nav-links { display: none; } }

Step 4 — Button component

.btn { display: inline-flex; padding: .7rem 1.5rem; border-radius: var(--radius); transition: background .2s, transform .2s; } .btn-primary { background: var(--clr-accent); color: #fff; } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(79,142,247,.35); } .btn-outline { background: transparent; border: 2px solid var(--clr-border); }

Step 5 — Hero with floating cards

@keyframes floatUp { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } .hero-inner { display: flex; gap: clamp(2rem, 5vw, 4rem); flex-wrap: wrap; } .hero-card.card-1 { position: absolute; animation: floatUp 3s ease-in-out infinite; }

Step 6 — Services grid with animated top-bar

.services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.5rem; } .s-card::before { content: ''; position: absolute; height: 4px; background: var(--clr-accent); transform: scaleX(0); transform-origin: left; transition: transform .3s; } .s-card:hover::before { transform: scaleX(1); }

Steps 7–8 — About section

.about-inner { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2.5rem, 6vw, 5rem); } .about-badge { position: absolute; bottom: -1.1rem; right: -1.1rem; } .about-checklist li::before { content: '✓'; border-radius: 50%; transition: background .2s; }

Steps 9–10 — Testimonials

.testi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } .t-card blockquote::before { content: '\201C'; font-size: 4rem; opacity: .15; } .t-author { display: flex; margin-top: auto; }

Step 11 — CTA with glow circles

.cta-section { background: linear-gradient(135deg, var(--clr-dark) 0%, var(--clr-navy) 60%); position: relative; } .cta-section::before { width: 500px; opacity: .1; filter: blur(80px); } .cta-inner { position: relative; z-index: 1; }

Steps 12–13 — Footer

.site-footer { background: var(--clr-dark); padding-top: clamp(3rem, 6vw, 5rem); } .footer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); } .footer-col a:hover { color: #fff; transform: translateX(4px); }

Step 13 — Scroll reveal & nav shadow (JS)

// CSS .reveal { opacity: 0; transform: translateY(24px); } .reveal.visible { opacity: 1; transform: translateY(0); } // JS const revealObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); revealObserver.unobserve(entry.target); } }); }, { threshold: 0.15 });

Go Further — 5 Challenges

The Lumen Studio page is complete. These challenges extend it with real-world features — each one teaches something new beyond the 13 steps you've already built.

1
Dark mode toggle
CSS custom properties data-theme attribute JS toggle

Add a ☀/☾ button to the nav that switches the entire page between light and dark mode by toggling a data-theme="dark" attribute on <html>. Override the :root custom properties inside [data-theme="dark"] — every component updates automatically because they all read from var(--clr-bg) etc.

🎨 CSS pattern
[data-theme="dark"] { --clr-bg: #0d1117; --clr-text: #e6edf3; --clr-border: #30363d; }
🎨 style.css
/* ── Dark mode overrides ── */ [data-theme="dark"] { --clr-bg: #0d1117; --clr-bg-alt: #161b22; --clr-text: #e6edf3; --clr-border: #30363d; --clr-muted: #8b949e; } .theme-btn { background: none; border: 1px solid var(--clr-border); border-radius: var(--radius); cursor: pointer; }
⚡ main.js
const themeBtn = document.querySelector('.theme-btn'); themeBtn.addEventListener('click', () => { const html = document.documentElement; const isDark = html.dataset.theme === 'dark'; html.dataset.theme = isDark ? '' : 'dark'; themeBtn.textContent = isDark ? '☾' : '☀'; localStorage.setItem('theme', html.dataset.theme); }); // Persist across page loads const saved = localStorage.getItem('theme'); if (saved) document.documentElement.dataset.theme = saved;

localStorage keeps the choice across page reloads.

2
Hamburger menu for mobile
flexbox JS classList toggle responsive design

Currently .nav-links is simply hidden below 700px with display: none. Add a hamburger button (≡) that appears at that breakpoint and toggles a dropdown nav.

Add a .nav-open class to .site-header when the burger is clicked, then show the nav links in a column when that class is present:

@media (max-width: 699px) { .site-header.nav-open .nav-links { display: flex; flex-direction: column; } }
🎨 style.css
.burger { display: none; background: none; border: none; font-size: 1.4rem; } @media (max-width: 699px) { .burger { display: block; } .site-header.nav-open .nav-links { display: flex; flex-direction: column; position: absolute; top: 100%; left: 0; right: 0; background: rgba(255,255,255,.97); } }
📄 index.html — inside .nav-inner
<button class="burger" aria-label="Toggle menu">☰</button>
⚡ main.js
document.querySelector('.burger').addEventListener('click', () => { document.querySelector('.site-header').classList.toggle('nav-open'); });
3
Coloured service-card top-bar on hover
::before pseudo-element CSS variables nth-child

The .s-card::before accent bar from Chapter 14 is always var(--clr-accent) blue. Make each card reveal a different colour on hover: card 1 → blue, card 2 → purple, card 3 → green, card 4 → orange.

Add a --bar-color custom property per card. The ::before reads var(--bar-color, var(--clr-accent)) so it falls back to the default when the variable isn't set.

🎨 style.css
.s-card::before { /* change this line: */ background: var(--bar-color, var(--clr-accent)); } /* Per-card colours */ .services-grid .s-card:nth-child(2) { --bar-color: #7c5cbf; } .services-grid .s-card:nth-child(3) { --bar-color: #0d9488; } .services-grid .s-card:nth-child(4) { --bar-color: #f59e0b; }
4
Portfolio / work gallery section
CSS Grid aspect-ratio overlay on hover new section

Add an "Our Work" section between Services and About. Display 6 project thumbnails in a responsive grid. On hover, each card should darken and reveal a project title overlay using a semi-transparent ::before.

Use a position: relative card with overflow: hidden. The image fills aspect-ratio: 4/3 and scales on hover. The overlay starts at opacity: 0 and fades in on hover.

.work-card::before { content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity .3s; } .work-card:hover::before { opacity: 1; }
📄 index.html
<section class="portfolio"> <div class="container"> <h2>Projects we're proud of</h2> <div class="work-grid"> <div class="work-card"> <div class="work-img">🏠</div> <div class="work-overlay"><span>Brand Identity</span></div> </div> <!-- repeat for each card --> </div> </div> </section>
🎨 style.css
.work-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } .work-card { position: relative; overflow: hidden; cursor: pointer; } .work-img { aspect-ratio: 4/3; transition: transform .4s; } .work-card:hover .work-img { transform: scale(1.06); } .work-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,.75), transparent); display: flex; align-items: flex-end; opacity: 0; transition: opacity .3s; } .work-card:hover .work-overlay { opacity: 1; }
5
Styled contact form
form styling :focus-visible :invalid pseudo-class

Replace the two CTA buttons from Chapter 17 with a short contact form (name, email, message, submit). Style it to look clean on the dark CTA background, with clear :focus-visible rings and a subtle red border on :invalid inputs.

Use :focus-visible instead of :focus so keyboard users see the ring but mouse users don't get a flicker on click. Combine :invalid with :not(:placeholder-shown) so the red border doesn't show on an untouched empty field.

.contact-input:not(:placeholder-shown):invalid { border-color: #f85149; }
📄 index.html — inside .cta-inner
<form class="contact-form" novalidate> <div class="form-row"> <input class="contact-input" type="text" placeholder="Your name" required> <input class="contact-input" type="email" placeholder="Your email" required> </div> <textarea class="contact-input" rows="4" placeholder="Your message" required></textarea> <button type="submit" class="btn btn-white">Send message</button> </form>
🎨 style.css
.contact-form { display: flex; flex-direction: column; max-width: 540px; } .form-row { display: grid; grid-template-columns: 1fr 1fr; } .contact-input { width: 100%; border: 1px solid rgba(255,255,255,.15); background: rgba(255,255,255,.07); color: #fff; outline: none; } .contact-input:focus-visible { border-color: var(--clr-accent); box-shadow: 0 0 0 3px rgba(79,142,247,.25); } .contact-input:not(:placeholder-shown):invalid { border-color: #f85149; }
🎉 Lumen Studio — complete

Eight chapters, thirteen build steps, one fully responsive, animated, accessible landing page — built entirely from CSS you now understand line by line. Take these five challenges as far as you like; every one of them reuses a technique already covered somewhere in Chapters 12–18.