From Nothing to Pressing Room: A Git Tutorial

Prerequisites, and Step 0 — from nothing

You need: a terminal, git installed, a text editor, and a browser. Optionally, ImageMagick (convert) for the image-optimization step — skip it and just use unoptimized images if you don't want to install it, the page still works.

This guide gives you the literal, complete contents of every file, in the order you'd naturally write them. Copy each code block into the file named just above it. At the end of every step there's a git commit — run it before moving on, so your history matches the steps below one-to-one.

Start from a truly empty folder:

mkdir pressing-room && cd pressing-room
git init
mkdir styles images assets

That's it for Step 0 — there's nothing to commit yet since there's nothing to add. The next step creates your first real file.

Step 1 — Design tokens: styles/design-system.css

This is the foundation everything else builds on: every color, spacing value, radius, and font as a CSS custom property, plus a small library of reusable classes (.btn, .tag, .card, .input…) built on top of them. Create styles/design-system.css with exactly this:

/* Modernist — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@400;600;800&display=swap');

:root {
  --color-bg: #f3f2f2;
  --color-surface: #eae9e9;
  --color-text: #201e1d;
  --color-accent: #ec3013;
  --color-accent-2: #e15b47;
  --color-divider: color-mix(in srgb, #201e1d 40%, transparent);

  --color-neutral-100: #f8f4f4;
  --color-neutral-200: #eae7e7;
  --color-neutral-300: #d7d3d3;
  --color-neutral-400: #bab6b6;
  --color-neutral-500: #9b9797;
  --color-neutral-600: #7d7979;
  --color-neutral-700: #605d5d;
  --color-neutral-800: #444141;
  --color-neutral-900: #2d2b2b;

  --color-accent-100: #fff2ef;
  --color-accent-200: #ffe0d9;
  --color-accent-300: #ffc4b8;
  --color-accent-400: #ff9783;
  --color-accent-500: #ff563c;
  --color-accent-600: #dd2b0f;
  --color-accent-700: #ae1800;
  --color-accent-800: #7c1405;
  --color-accent-900: #4d170e;

  --color-accent-2-100: #fff2ef;
  --color-accent-2-200: #ffe0da;
  --color-accent-2-300: #ffc4b9;
  --color-accent-2-400: #ff9784;
  --color-accent-2-500: #ef6853;
  --color-accent-2-600: #c94b39;
  --color-accent-2-700: #9e3526;
  --color-accent-2-800: #71261b;
  --color-accent-2-900: #471d16;

  --font-heading: "Archivo", system-ui, sans-serif;
  --font-heading-weight: 800;
  --font-body: "Archivo", system-ui, sans-serif;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  --radius-sm: 0;
  --radius-md: 0;
  --radius-lg: 0;

  --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
}
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }

.grayscale{filter:grayscale(1) contrast(1.08)}

*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-size: 0.9375rem; line-height: 1.55; font-weight: 400; }
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
}
h1 { font-size: 2.625rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5625rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1rem; }
h6 { font-size: 0.8125rem; }
h6, .heading-label { letter-spacing: 0.11em; text-transform: uppercase; }
.heading-label { font-size: 0.6875rem; }
p { margin: 0 0 var(--space-3); }
a { color: var(--color-accent); text-underline-offset: 3px; }
img { display: block; max-width: 100%; }
figure { margin: 0; }
figcaption {
  font-size: 0.6875rem; margin-top: var(--space-1);
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }

.hr {
  height: 2px; border: 0; margin: var(--space-4) 0;
  background: var(--color-divider);
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem;
  cursor: pointer; text-decoration: none;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 0.875rem; line-height: 1.2; color: var(--color-text);
  background: transparent; border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
.btn svg { display: block; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary { background: var(--color-accent); color: var(--color-bg); }
.btn-primary:hover { background: var(--color-accent-600); }
.btn-primary:active { background: var(--color-accent-700); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
.btn-icon { width: 36px; height: 36px; padding: 0; }
.btn-block { width: 100%; margin-top: var(--space-2); justify-content: flex-start; text-align: left; }

.field > label {
  display: block; font-size: 0.75rem; margin-bottom: 0.3125rem;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.input {
  width: 100%; min-height: 36px; padding: 0.375rem 0.625rem; font: inherit;
  font-size: 0.875rem; color: var(--color-text); caret-color: var(--color-accent);
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
textarea.input { min-height: 90px; resize: vertical; }
.radio { display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.875rem; }
.radio input, .seg-opt input {
  position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
}
.radio .dot {
  width: 16px; height: 16px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--color-divider);
}
.radio:hover .dot { border-color: var(--color-accent); }
.radio input:checked + .dot {
  border-color: var(--color-accent); background: var(--color-accent);
  box-shadow: inset 0 0 0 4px var(--color-bg);
}
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.seg {
  display: inline-flex; overflow: hidden;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.seg-opt {
  display: inline-flex; align-items: center; gap: 0.375rem;
  padding: 0.4375rem 0.75rem; font-size: 0.8125rem; cursor: pointer;
}
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt:has(input:checked) { background: var(--color-accent); color: var(--color-bg); }
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

.card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-surface);
}
.card-kicker { font-size: 0.625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
.card-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 1.0625rem; line-height: 1.2;
}
.card-body { margin: 0; font-size: 0.8125rem; opacity: 0.8; flex: 1; }
.card-meta {
  display: flex; align-items: center; gap: 0.375rem; font-size: 0.6875rem;
  color: color-mix(in srgb, var(--color-text) 50%, transparent);
}
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

.tag {
  display: inline-flex; align-items: center; font-size: 0.6875rem;
  letter-spacing: 0.02em; padding: 0.1875rem 0.625rem;
  border-radius: calc(var(--radius-md) * 0.75);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }

.nav {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: 2px solid var(--color-divider);
}
.nav-brand {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 1.125rem; margin-right: auto;
}
.nav a { color: inherit; text-decoration: none; font-size: 0.875rem; }
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }

.table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.table th {
  text-align: left; font-size: 0.6875rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  padding: var(--space-2); border-bottom: 2px solid var(--color-divider);
}
.table td {
  padding: var(--space-2);
  border-bottom: 1px solid var(--color-divider);
}
.table tbody tr:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }

.dialog-backdrop {
  position: fixed; inset: 0; display: grid; place-items: center;
  padding: var(--space-4);
  background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
}
.dialog {
  width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-lg);
}
.dialog-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 1.25rem;
}
.dialog-body { font-size: 0.875rem; opacity: 0.85; }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }

A few things worth noticing before you move on:

Commit it:

git add styles/design-system.css
git commit -m "feat: design-token foundation and base component classes"

Step 2 — Global reset and the logo mark

Create styles/base.css:

/* — global page setup — */
body { margin: 0; }
a { color: var(--color-text); text-decoration: none; }
a:hover { color: var(--color-accent-700); }

.page {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  min-height: 100vh;
}

This is deliberately tiny — it's the handful of rules that apply to the whole page and don't belong to any one section, kept separate from design-system.css (which is tokens and reusable components, not page setup).

Next, the brand mark itself: two small SVGs, one for light backgrounds and one for dark. Create assets/logo-mark.svg:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
  <rect width="64" height="64" fill="#ec3013"></rect>
  <circle cx="32" cy="32" r="19" fill="none" stroke="#f3f2f2" stroke-width="10"></circle>
  <circle cx="32" cy="32" r="4" fill="#f3f2f2"></circle>
</svg>

And assets/logo-mark-reversed.svg — same shape, inverted colors, for the dark footer:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
  <rect width="64" height="64" fill="#f3f2f2"></rect>
  <circle cx="32" cy="32" r="19" fill="none" stroke="#201e1d" stroke-width="10"></circle>
  <circle cx="32" cy="32" r="4" fill="#ec3013"></circle>
</svg>

The mark itself is simple on purpose: a filled square (the record sleeve), a ring (the record), a dot (the spindle hole). If you're hand-typing SVGs for the first time — viewBox="0 0 64 64" defines a 64×64 coordinate space; width/height on the tag itself just set its default display size, and CSS can override that later wherever it's used.

Commit both:

git add styles/base.css assets/
git commit -m "feat: global reset and brand mark SVGs"

Step 3 — The document shell and the header

Create index.html. This is the whole <head>, plus the opening of the page and the header — every later step appends more markup right before the final </div></body></html>, which you won't add until Step 9. Browsers render unclosed HTML fine, so you can preview after every step; just don't treat the page as finished until Step 9's closing tags are in.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pressing Room — Analog sound, digital ease</title>
<link rel="stylesheet" href="styles/design-system.css">
<link rel="stylesheet" href="styles/base.css">
<link rel="stylesheet" href="styles/header.css">
</head>
<body>
<div class="page">

  <header class="site-header">
    <div class="site-header__inner">
      <a href="#" class="brand">
        <img class="brand__icon" src="assets/logo-mark.svg" alt="" width="26" height="26">
        <span class="brand__name">Pressing Room</span>
      </a>
      <label class="site-search">
        <span class="visually-hidden">Search</span>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.6-3.6"></path></svg>
        <input class="input" placeholder="Search artists, albums, labels">
      </label>
      <nav class="site-nav" aria-label="Main">
        <a href="#">Home</a>
        <a href="#">About</a>
        <a href="#" class="is-active" aria-current="page">Shop</a>
        <a href="#">Help</a>
      </nav>
      <a class="btn btn-primary" href="#">
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="20" r="1.4"></circle><circle cx="18" cy="20" r="1.4"></circle><path d="M2 3h2.2l2.6 12h11.4l2.2-8H6"></path></svg>
        Your Cart (2)
      </a>
    </div>
  </header>

Notice <img class="brand__icon" ... alt=""> — the alt is deliberately empty, not missing. The logo sits right next to the visible text "Pressing Room", so a screen reader announcing the image's name too would just repeat it. aria-hidden="true" on the two <svg> icons does the same job for icons that are purely decorative next to their own label. <nav aria-label="Main"> names this navigation region distinctly, since a page can have more than one <nav> (the footer will have four more, later).

Now styles/header.css:

/* — header — */
.site-header {
  position: sticky; top: 0; z-index: 20;
  background: var(--color-bg);
  border-bottom: 2px solid var(--color-divider);
}
.site-header__inner {
  max-width: 1200px; margin: 0 auto; padding: 0.875rem 2rem;
  display: flex; align-items: center; gap: 2rem; flex-wrap: wrap;
}
.brand {
  display: flex; align-items: center; gap: 0.625rem;
  margin-right: auto; color: var(--color-text);
}
.brand__icon { width: 26px; height: 26px; flex: none; display: block; }
.brand__name {
  font-family: var(--font-heading); font-weight: 700; font-size: 1.0625rem;
  letter-spacing: -0.01em; text-transform: uppercase;
}

.site-search {
  display: flex; align-items: center; gap: 0.5625rem;
  min-width: 190px; flex: 1 1 190px; max-width: 320px;
  border: 2px solid var(--color-divider); padding: 0.375rem 0.75rem;
}
.site-search svg { flex: none; }
.site-search .input { border: 0; background: transparent; min-height: auto; padding: 0; font-size: 0.8125rem; }

.site-nav {
  display: flex; align-items: center; gap: 1.625rem;
  font-size: 0.75rem; letter-spacing: .08em; text-transform: uppercase;
}
.site-nav a { color: var(--color-text); }
.site-nav a.is-active {
  color: var(--color-accent-700); border-bottom: 2px solid var(--color-accent); padding-bottom: 2px;
}

/* — responsive — */
@media (max-width: 768px) {
  .site-header__inner { padding: 0.75rem 1.25rem; gap: 1rem; }
  .site-search { order: 3; max-width: none; flex-basis: 100%; }
  .site-nav { order: 4; flex-basis: 100%; justify-content: center; gap: 1rem; }
}

margin-right: auto on .brand is doing real layout work — in a flex row, one auto margin eats all the leftover space on that side, which is what pushes the search box, nav, and cart button to the right without a fixed width anywhere. The @media block at the bottom is the first real responsive behavior in this project: under 768px, the search box and nav each claim a full row of their own (flex-basis: 100%) via CSS order, instead of squeezing into one cramped line.

Open index.html directly in a browser now (double-click the file, or drag it into a tab) — you should see a working sticky header, even though the rest of the page is blank.

git add index.html styles/header.css
git commit -m "feat: document shell and header section"

Step 4 — Real images, sourced and optimized

The finished page uses 11 real album covers. You'll need your own copies — covers aren't something this guide can hand you as text, and if you're building a public portfolio piece rather than a private exercise, use art you have the right to use (your own photos of your own records, or covers from an artist/label's official press kit) rather than assuming any download is fair game.

Save each one into images/ under this exact filename — every later step's HTML points at these paths:

Filename Artist — Album
bruce-springsteen-born-in-the-usa.jpeg Bruce Springsteen — Born in the U.S.A.
pink-floyd-dark-side-of-the-moon.jpeg Pink Floyd — The Dark Side of the Moon
radiohead-ok-computer.jpeg Radiohead — OK Computer
beatles-1967-1970.jpeg The Beatles — 1967-1970
frank-ocean-blond.jpg Frank Ocean — Blond
tyler-the-creator-igor.jpg Tyler, The Creator — IGOR
bon-iver-22-a-million.jpg Bon Iver — 22, A Million
john-mayer-born-and-raised.jpg John Mayer — Born and Raised
hozier-unreal-unearth-unending.jpg Hozier — Unreal Unearth: Unending
frank-sinatra-nothing-but-the-best.jpeg Frank Sinatra — Nothing but the Best
ralphie-choo-damor-traficante.jpeg Ralphie Choo — D'Amor Traficante

Every slot on this page is a square, so a square (or near-square) source image looks best — most official cover art already is. Don't worry about matching resolution exactly.

Now the part that actually matters for a real repo: resize and compress before you commit. A cover straight off the internet is often 1500–2500px square and several hundred KB — the page never displays one larger than a few hundred pixels, so most of that is pure download weight. With ImageMagick installed:

cd images
for f in *.jpg *.jpeg; do
  [ -f "$f" ] || continue
  convert "$f" -resize '1000x1000>' -strip -sampling-factor 4:2:0 -quality 82 -interlace JPEG "$f"
done
cd ..

What each flag does: -resize '1000x1000>' shrinks anything bigger than 1000px on its long edge and leaves smaller images alone (the trailing > means "only shrink, never enlarge"); -strip removes embedded metadata (camera info, color profiles) the browser doesn't need; -quality 82 is JPEG compression that's very hard to see at normal viewing sizes; -interlace JPEG makes it render progressively (a blurry-then-sharp preview) instead of top-to-bottom on a slow connection. On the real version of this project, that one command cut total image weight by about two-thirds with no visible loss.

No ImageMagick and don't want to install it? Skip this and commit the images as-is — the page still works, it just ships more bytes than it needs to.

git add images/
git commit -m "feat: add real album cover images"

Step 5 — The hero

Add <link rel="stylesheet" href="styles/hero.css"> to index.html's <head>, right after the header.css line. Then, right after </header> (still inside <div class="page">), add:

  <section class="hero">
    <div class="hero__split">
      <div class="hero__panel-text">
        <p class="tag tag-accent">New arrivals every Friday</p>
        <div class="hero__headline">
          <div class="hero__headline-rule" aria-hidden="true"></div>
          <h1 class="hero__title">Analog sound<span class="hero__punct">,</span><br><span class="hero__band">digital ease.</span></h1>
        </div>
        <div class="hr"></div>
        <p class="hero__copy">Fourteen thousand records on the shelves. Graded by hand, played before they ship.</p>
        <div class="hero__actions">
          <a class="btn btn-primary" href="#">Shop now</a>
          <a class="btn btn-secondary" href="#">This week's picks</a>
        </div>
      </div>
      <div class="hero__panel-art">
        <div class="hero__frame">
          <img class="hero__frame-cover" src="images/bruce-springsteen-born-in-the-usa.jpeg" alt="Born in the U.S.A. album cover" loading="lazy">
        </div>
      </div>
    </div>
  </section>

That <h1> is the only one on the whole page — every page needs exactly one, and it's what identifies this as the heading for the document. Its markup is more elaborate than plain text for a reason: .hero__punct colors just the comma a lighter weight, and .hero__band wraps "digital ease." in a solid highlight — both are pure visual flourish, so they're <span>s inside the heading, not separate elements that would confuse a screen reader's reading of it as one sentence.

Now styles/hero.css:

/* — hero (Split panel) — */
.hero { border-bottom: 2px solid var(--color-divider); }
.hero__split {
  max-width: 1200px; margin: 0 auto;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.hero__panel-text { padding: 4.5rem 2rem; border-right: 2px solid var(--color-divider); }
.hero__headline {
  display: grid; grid-template-columns: auto 1fr; gap: 0 1.375rem; margin: 0 0 1.25rem;
}
.hero__headline-rule { width: 2px; background: var(--color-accent); }
.hero__title {
  font-size: clamp(2.375rem, 6vw, 4.625rem); line-height: 1.12; margin: 0; letter-spacing: -0.03em;
}
.hero__punct { color: var(--color-accent); font-weight: 400; font-size: 1.2em; line-height: 0; }
.hero__band {
  background: var(--color-accent); color: var(--color-bg); padding: 0 0.12em;
  box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
.hero__split .hr { margin: 1.625rem 0; }
.hero__copy { margin: 0 0 1.625rem; max-width: 40ch; font-size: 0.9375rem; }
.hero__actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.hero__actions .btn { font-size: 0.9375rem; }
.hero__actions .btn-primary { padding: 0.75rem 1.625rem; }
.hero__actions .btn-secondary { padding: 0.75rem 1.375rem; }

.hero__panel-art {
  padding: 4.5rem 2rem; display: grid; place-items: center; background: var(--color-neutral-200);
}
.hero__frame {
  position: relative; width: 100%; max-width: 380px; aspect-ratio: 1;
  border: 2px solid var(--color-divider); overflow: hidden; background: var(--color-bg);
}
.hero__frame-cover {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform .4s ease;
}
.hero__frame:hover .hero__frame-cover { transform: scale(1.04); }

/* — responsive — */
@media (max-width: 768px) {
  .hero__panel-text, .hero__panel-art { padding: 2.75rem 1.25rem; }
}
@media (max-width: 680px) {
  .hero__panel-text { border-right: 0; border-bottom: 2px solid var(--color-divider); }
}

Two patterns worth understanding here, because they repeat in every section that follows:

Commit:

git add index.html styles/hero.css
git commit -m "feat: hero section"

Add <link rel="stylesheet" href="styles/featured.css"> to the <head>, after hero.css. Then add this right after </section> that closes the hero:

  <section class="featured">
    <div class="featured__inner">
      <div class="featured__head">
        <h2 class="featured__title">Featured pressings</h2>
        <div class="featured__nav">
          <button class="btn btn-secondary btn-icon" aria-label="Previous"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m14 6-6 6 6 6"></path></svg></button>
          <button class="btn btn-secondary btn-icon" aria-label="Next"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m10 6 6 6-6 6"></path></svg></button>
        </div>
      </div>
      <div class="hr"></div>
      <div class="featured__grid">
        <article class="feature-card">
          <div class="feature-card__art">
            <img class="feature-card__cover" src="images/pink-floyd-dark-side-of-the-moon.jpeg" alt="The Dark Side of the Moon album cover" loading="lazy">
          </div>
          <div>
            <span class="feature-card__artist">Pink Floyd</span>
            <h3 class="feature-card__title">The Dark Side of the Moon</h3>
            <div class="feature-card__meta-row">
              <span class="tag tag-outline">Prog rock</span>
              <span class="feature-card__meta">LP · 1973</span>
              <span class="feature-card__price">$42</span>
            </div>
          </div>
        </article>
        <article class="feature-card">
          <div class="feature-card__art">
            <img class="feature-card__cover" src="images/radiohead-ok-computer.jpeg" alt="OK Computer album cover" loading="lazy">
          </div>
          <div>
            <span class="feature-card__artist">Radiohead</span>
            <h3 class="feature-card__title">OK Computer</h3>
            <div class="feature-card__meta-row">
              <span class="tag tag-outline">Alt rock</span>
              <span class="feature-card__meta">LP · 1997</span>
              <span class="feature-card__price">$38</span>
            </div>
          </div>
        </article>
      </div>
    </div>
  </section>

Each card title is <h3> — not <h4>, even though it's visually smaller than you might expect. The page's heading order so far is h1 (hero) → h2 ("Featured pressings") → h3 (each card). Levels only ever step down by one; jumping straight to h4 here would skip a level for no reason other than "it looked about that size," which is exactly the mistake to avoid — font size is CSS's job, heading level is structure's job, and they're allowed to disagree.

styles/featured.css:

/* — featured pressings — */
.featured { border-bottom: 2px solid var(--color-divider); }
.featured__inner { max-width: 1200px; margin: 0 auto; padding: 3.5rem 2rem; }
.featured__head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1.25rem; flex-wrap: wrap; margin-bottom: 0.75rem;
}
.featured__title { font-size: clamp(1.625rem, 3.4vw, 2.5rem); margin: 0; letter-spacing: -0.02em; }
.featured__nav { display: flex; gap: 0.5rem; }
.featured .hr { margin-bottom: 0; }

.featured__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.feature-card {
  display: grid; grid-template-columns: 130px 1fr; gap: 1.375rem; align-items: start;
}
.feature-card:first-child { padding: 2rem 2rem 0 0; border-right: 2px solid var(--color-divider); }
.feature-card:last-child { padding: 2rem 0 0 2rem; }
.feature-card__art {
  position: relative; aspect-ratio: 1; background: var(--color-neutral-200); overflow: hidden;
}
.feature-card__cover {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform .4s ease;
}
.feature-card:hover .feature-card__cover { transform: scale(1.05); }
.feature-card__artist { font-size: 0.6875rem; letter-spacing: .1em; text-transform: uppercase; color: var(--color-accent-700); }
.feature-card__title { margin: 0.25rem 0 0.75rem; font-size: 1.5rem; letter-spacing: -0.01em; }
.feature-card__meta-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.feature-card__meta { font-size: 0.75rem; letter-spacing: .06em; text-transform: uppercase; }
.feature-card__price { margin-left: auto; font-family: var(--font-heading); font-weight: 700; font-size: 1.25rem; }

/* — responsive — */
@media (max-width: 768px) {
  .featured__inner { padding: 2.75rem 1.25rem; }
}
@media (max-width: 680px) {
  .feature-card:first-child {
    padding: 2rem 1.25rem 1.5rem; border-right: 0; border-bottom: 2px solid var(--color-divider);
  }
  .feature-card:last-child { padding: 1.5rem 1.25rem 2rem; }
}

:first-child and :last-child give the two cards their asymmetric padding (a gap between them, flush against the section's own edges on the outside) without needing extra classes — and the same 680px stacking fix from Step 5 shows up again, for the same reason.

git add index.html styles/featured.css
git commit -m "feat: featured pressings section"

Step 7 — Newsletter

Add <link rel="stylesheet" href="styles/newsletter.css"> to the <head>, after featured.css. Then, right after the featured section's closing </section>:

  <section class="newsletter">
    <div class="newsletter__inner">
      <div>
        <h2 class="newsletter__title">The Friday list</h2>
        <p class="newsletter__copy">One email a week: what came in, what's left, and the three records we'd take home ourselves.</p>
      </div>
      <form class="newsletter__form">
        <label class="visually-hidden" for="newsletter-email">Email address</label>
        <input id="newsletter-email" name="email" type="email" class="input newsletter__input" placeholder="Email address" required>
        <button class="btn btn-primary newsletter__submit" type="submit">Subscribe</button>
      </form>
    </div>
  </section>

The <label class="visually-hidden"> is worth pausing on: the input already has a placeholder, so why add a label too? Because a placeholder isn't a real accessible name — it disappears the moment you start typing, and some assistive tech never reads it as a label in the first place. .visually-hidden (defined back in design-system.css, Step 1) hides it visually without hiding it from screen readers: sighted users see the placeholder text doing the same job, screen reader users get a real, permanent "Email address" label. This form doesn't actually submit anywhere in this project — that's fine, it's a mockup — but the markup is written as if it will.

styles/newsletter.css:

/* — newsletter — */
.newsletter { border-bottom: 2px solid var(--color-divider); }
.newsletter__inner {
  max-width: 1200px; margin: 0 auto; padding: 2.75rem 2rem;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2.25rem; align-items: center;
}
.newsletter__title { font-size: clamp(1.5rem, 3vw, 2.125rem); margin: 0 0 0.5rem; letter-spacing: -0.02em; }
.newsletter__copy { margin: 0; max-width: 46ch; font-size: 0.9375rem; }
.newsletter__form { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.newsletter__input { flex: 1 1 200px; min-height: 44px; }
.newsletter__submit { min-height: 44px; padding-inline: 1.5rem; font-size: 0.875rem; }

/* — responsive — */
@media (max-width: 768px) {
  .newsletter__inner { padding: 2rem 1.25rem; }
}

This is the shortest section so far, and it's shortest for a reason: it reuses .input and .btn-primary straight from Step 1 almost unchanged. That's the design-tokens investment paying off again — a form that looks consistent with the rest of the site cost about six lines of section-specific CSS.

git add index.html styles/newsletter.css
git commit -m "feat: newsletter section"

Step 8 — In the racks

Add <link rel="stylesheet" href="styles/racks.css"> to the <head>, after newsletter.css. Then, right after the newsletter section's closing </section>:

  <section class="racks">
    <div class="racks__inner">
      <div class="racks__head">
        <h2 class="racks__title">In the racks</h2>
        <div class="racks__filters">
          <span class="tag tag-accent">All</span>
          <span class="tag tag-outline">Vinyl</span>
          <span class="tag tag-outline">CD</span>
          <span class="tag tag-outline">Cassette</span>
        </div>
      </div>
      <div class="hr"></div>

      <div class="racks__grid">
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/beatles-1967-1970.jpeg" alt="The Beatles 1967-1970 album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">The Beatles</span>
            <h3 class="record-card__title">1967-1970</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">2×LP · 1973</span>
              <span class="record-card__price">$36</span>
            </div>
            <span class="tag tag-outline record-card__genre">Rock</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/frank-ocean-blond.jpg" alt="Frank Ocean Blond album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">Frank Ocean</span>
            <h3 class="record-card__title">Blond</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP · 2016</span>
              <span class="record-card__price">$45</span>
            </div>
            <span class="tag tag-outline record-card__genre">R&amp;B</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/tyler-the-creator-igor.jpg" alt="Tyler, The Creator IGOR album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">Tyler, The Creator</span>
            <h3 class="record-card__title">IGOR</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP · 2019</span>
              <span class="record-card__price">$34</span>
            </div>
            <span class="tag tag-outline record-card__genre">Hip-hop</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/bon-iver-22-a-million.jpg" alt="Bon Iver 22, A Million album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">Bon Iver</span>
            <h3 class="record-card__title">22, A Million</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP · 2016</span>
              <span class="record-card__price">$30</span>
            </div>
            <span class="tag tag-outline record-card__genre">Indie</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/john-mayer-born-and-raised.jpg" alt="John Mayer Born and Raised album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">John Mayer</span>
            <h3 class="record-card__title">Born &amp; Raised</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP · 2012</span>
              <span class="record-card__price">$28</span>
            </div>
            <span class="tag tag-outline record-card__genre">Folk rock</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/hozier-unreal-unearth-unending.jpg" alt="Hozier Unreal Unearth: Unending album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">Hozier</span>
            <h3 class="record-card__title">Unreal Unearth: Unending</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP · 2024</span>
              <span class="record-card__price">$32</span>
            </div>
            <span class="tag tag-outline record-card__genre">Alt folk</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/frank-sinatra-nothing-but-the-best.jpeg" alt="Frank Sinatra Nothing But the Best album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">Frank Sinatra</span>
            <h3 class="record-card__title">Nothing but the Best</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP · 2008</span>
              <span class="record-card__price">$27</span>
            </div>
            <span class="tag tag-outline record-card__genre">Swing</span>
          </div>
        </article>
        <article class="record-card">
          <div class="record-card__art">
            <img class="record-card__cover" src="images/ralphie-choo-damor-traficante.jpeg" alt="Ralphie Choo D'Amor Traficante album cover" loading="lazy">
          </div>
          <div class="record-card__body">
            <span class="record-card__artist">Ralphie Choo</span>
            <h3 class="record-card__title">D'Amor Traficante</h3>
            <div class="record-card__meta-row">
              <span class="record-card__meta">LP</span>
              <span class="record-card__price">$33</span>
            </div>
            <span class="tag tag-outline record-card__genre">Alt pop</span>
          </div>
        </article>
      </div>
    </div>
  </section>

Eight cards, one repeated shape — type the first one carefully, then it's mechanical copy-and-edit for the rest (artist, title, format, year, price, genre, image path, alt text). Same <h3> rule as Step 6: these titles sit under the same <h2>In the racks</h2>, so they're <h3>, not <h4>.

styles/racks.css:

/* — in the racks — */
.racks { border-bottom: 2px solid var(--color-divider); }
.racks__inner { max-width: 1200px; margin: 0 auto; padding: 3.5rem 2rem; }
.racks__head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1.25rem; flex-wrap: wrap; margin-bottom: 0.75rem;
}
.racks__title { font-size: clamp(1.625rem, 3.4vw, 2.5rem); margin: 0; letter-spacing: -0.02em; }
.racks__filters { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.racks .hr { margin-bottom: 0; }

.racks__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  border-left: 2px solid var(--color-divider);
}
.record-card {
  padding: 1.5rem;
  border-right: 2px solid var(--color-divider); border-bottom: 2px solid var(--color-divider);
}
.record-card__art {
  position: relative; aspect-ratio: 1; background: var(--color-neutral-200); overflow: hidden;
}
.record-card__cover {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform .35s ease;
}
.record-card:hover .record-card__cover { transform: scale(1.06); }
.record-card__artist {
  display: block; margin-top: 1.125rem; font-size: 0.65625rem;
  letter-spacing: .11em; text-transform: uppercase; color: var(--color-accent-700);
}
.record-card__title { margin: 0.3125rem 0 0.75rem; font-size: 1.1875rem; letter-spacing: -0.01em; }
.record-card__meta-row { display: flex; align-items: baseline; gap: 0.625rem; }
.record-card__meta { font-size: 0.71875rem; letter-spacing: .06em; text-transform: uppercase; }
.record-card__price { margin-left: auto; font-family: var(--font-heading); font-weight: 700; font-size: 1.125rem; }
.record-card__genre { margin-top: 0.875rem; }

/* — responsive — */
@media (max-width: 768px) {
  .racks__inner { padding: 2.75rem 1.25rem; }
  .racks__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  .racks__grid { grid-template-columns: 1fr; }
}

Notice padding: 1.5rem lives on .record-card itself, not on a sub-element — that's deliberate: the cover image and the caption text below it need to share one padded box so the whole card reads as a single, evenly-inset cell. Putting the padding on the wrong element here is an easy, purely-visual mistake (the image would bleed to the card's edge while the text stayed inset) that nothing will error on — it'll just look subtly wrong, which is exactly why it's worth typing this one carefully and comparing against a live preview.

Also notice the ruled-grid trick: the grid container gets a border-left, and every card gets its own border-right + border-bottom — no :last-child exceptions needed. That works at any column count the responsive rules produce (3 across, 2 across, 1 across), which is more robust than the :first-child/:last-child approach Step 6 used for exactly two cards.

git add index.html styles/racks.css
git commit -m "feat: in the racks section"

Last one. Add <link rel="stylesheet" href="styles/about.css"> and <link rel="stylesheet" href="styles/footer.css"> to the <head>, after racks.css. Then, right after the racks section's closing </section>, add the about section, the footer, and the closing tags that finish the document — this is the step where the file finally becomes fully valid HTML:

  <section class="about">
    <div class="about__inner">
      <div>
        <h2 class="about__title">About the shop</h2>
        <p class="about__copy">Pressing Room started as two crates behind a coffee counter on Harrison Street. We now grade, clean and ship about four hundred records a week, and we still play every one before it goes out.</p>
        <a class="btn btn-secondary" href="#">Read our story</a>
      </div>
      <div class="about__stats">
        <div class="about__stat">
          <span class="about__stat-value">14,200</span>
          <span class="about__stat-label">records on the shelves</span>
        </div>
        <div class="about__stat">
          <span class="about__stat-value">2009</span>
          <span class="about__stat-label">opened on Harrison St</span>
        </div>
        <div class="about__stat">
          <span class="about__stat-value">1 day</span>
          <span class="about__stat-label">order to doorstep</span>
        </div>
      </div>
    </div>
  </section>

  <footer class="site-footer">
    <div class="site-footer__grid">
      <div>
        <div class="site-footer__brand-row">
          <img class="site-footer__icon" src="assets/logo-mark-reversed.svg" alt="" width="26" height="26">
          <span class="site-footer__brand-name">Pressing Room</span>
        </div>
        <p class="site-footer__address">1717 Harrison St, San Francisco<br>CA 94103, USA</p>
      </div>
      <nav class="site-footer__col" aria-label="Main menu">
        <h3 class="heading-label">Main menu</h3>
        <a href="#">Home</a>
        <a href="#">About</a>
        <a href="#">Shop</a>
        <a href="#">Help</a>
      </nav>
      <nav class="site-footer__col" aria-label="Company">
        <h3 class="heading-label">Company</h3>
        <a href="#">The company</a>
        <a href="#">Careers</a>
        <a href="#">Press</a>
      </nav>
      <nav class="site-footer__col" aria-label="Discover">
        <h3 class="heading-label">Discover</h3>
        <a href="#">Grading guide</a>
        <a href="#">Our history</a>
        <a href="#">Trade-ins</a>
      </nav>
      <nav class="site-footer__col" aria-label="Find us on">
        <h3 class="heading-label">Find us on</h3>
        <a href="#">Instagram</a>
        <a href="#">Discogs</a>
        <a href="#">Bandcamp</a>
      </nav>
    </div>
    <div class="site-footer__bottom">
      <span>© 2026 Pressing Room</span>
      <span class="site-footer__bottom-spacer">Shipping / Returns / Privacy</span>
    </div>
  </footer>

</div>
</body>
</html>

Two small, deliberate choices here:

styles/about.css:

/* — about — */
.about { border-bottom: 2px solid var(--color-divider); }
.about__inner {
  max-width: 1200px; margin: 0 auto; padding: 3.75rem 2rem;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 2.75rem;
}
.about__title { font-size: clamp(1.625rem, 3.6vw, 2.625rem); margin: 0 0 1rem; letter-spacing: -0.02em; }
.about__copy { max-width: 52ch; font-size: 0.9375rem; margin: 0 0 1.375rem; }
.about__stats { display: grid; gap: 0; align-content: start; }
.about__stat {
  display: flex; align-items: baseline; gap: 1.25rem; padding: 0.875rem 0;
  border-top: 2px solid var(--color-divider);
}
.about__stat:last-child { border-bottom: 2px solid var(--color-divider); }
.about__stat-value {
  font-family: var(--font-heading); font-weight: 700; font-size: 2.125rem;
  color: var(--color-accent-700); min-width: 8.125rem;
}
.about__stat-label { font-size: 0.75rem; letter-spacing: .06em; text-transform: uppercase; }

/* — responsive — */
@media (max-width: 768px) {
  .about__inner { padding: 2.5rem 1.25rem; }
  .about__stat-value { min-width: 5.5rem; }
}

styles/footer.css:

/* — footer — */
.site-footer { background: var(--color-text); color: var(--color-neutral-200); padding: 3.5rem 2rem 1.375rem; }
.site-footer__grid {
  max-width: 1200px; margin: 0 auto;
  display: grid; grid-template-columns: minmax(200px, 1.3fr) repeat(auto-fit, minmax(130px, 1fr)); gap: 2.5rem;
}
.site-footer__brand-row { display: flex; align-items: center; gap: 0.625rem; }
.site-footer__icon { width: 26px; height: 26px; flex: none; display: block; }
.site-footer__brand-name {
  font-family: var(--font-heading); font-weight: 700; font-size: 1.0625rem;
  text-transform: uppercase; color: var(--color-neutral-100);
}
.site-footer__address { margin: 1.25rem 0 0; font-size: 0.8125rem; line-height: 1.7; }
.site-footer__col { display: flex; flex-direction: column; gap: 0.5625rem; }
.site-footer__col .heading-label { margin: 0 0 0.375rem; color: var(--color-accent-300); }
.site-footer__col a { color: inherit; font-size: 0.8125rem; }
.site-footer__bottom {
  max-width: 1200px; margin: 2.75rem auto 0; padding-top: 1.125rem;
  border-top: 2px solid var(--color-neutral-700);
  display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.75rem; letter-spacing: .05em; text-transform: uppercase;
}
.site-footer__bottom-spacer { margin-left: auto; }

/* — responsive — */
@media (max-width: 768px) {
  .site-footer { padding: 2.75rem 1.25rem 1.25rem; }
  .site-footer__grid { grid-template-columns: 1fr 1fr; gap: 2rem; }
}
@media (max-width: 480px) {
  .site-footer__grid { grid-template-columns: 1fr; }
  .site-footer__bottom-spacer { margin-left: 0; }
}
git add index.html styles/about.css styles/footer.css
git commit -m "feat: about section and footer, closing out the page"

Wrap-up

Check it properly now, served rather than opened as a bare file (some things, like relative paths, behave slightly differently over file://):

python3 -m http.server 8000
# then open http://localhost:8000 in a browser

You should see the full page: sticky header, split-panel hero with the red-band headline, two featured pressings, the newsletter strip, eight records in a bordered grid, the about stats, and the footer — matching the finished site this guide was written from.

git log --oneline should now read as ten honest, in-order commits:

# Commit message
1 feat: global reset and brand mark SVGs
2 feat: design-token foundation and base component classes
3 feat: document shell and header section
4 feat: add real album cover images
5 feat: hero section
6 feat: featured pressings section
7 feat: newsletter section
8 feat: in the racks section
9 feat: about section and footer, closing out the page

(Nine, not ten — Steps 1 and 2 as written above share one commit each with their own step, and Step 0 had nothing to commit. Your actual count may differ slightly depending on how you split things up, and that's fine — the point isn't hitting an exact number, it's that every commit is one understandable unit of work.)

A few things to try next, now that the whole thing exists: